Ubuntu14.04 LTS root 日本語入力

Ubuntu 14.04 LTS をインストールし、root で自動ログイン(/etc/lightdm/lightdm.conf 修正)なんてしちゃってる危険な諸君!

[SeatDefaults]
autologin-guest=false
autologin-user=root
autologin-user-timeout=0
autologin-session=lightdm-autologin

root で自動ログインしてしまうと日本語入力ができなくて涙目になっていらっしゃる方もいるのではないでしょうか(Mozc は root で動かない)。


そういうときは、Mozc を諦めて Anthy にしましょう。
世の中、妥協も必要です(妥協すんのそっちかよって野暮なツッコミはなしでお願いします)。


Ubuntu・フレーバーの日本語化

apt install fcitx-mozc fcitx-anthy

╭( ・ㅂ・)و ̑̑ グッ !

Google Container Engine(GKE) を使ってみよう

年末に C87 という比較的大規模なイベントがありまして、代表者ではありませんでしたがメンバーの一員としてサークル参加をしていました。
そして、サークルのホームページを GKE を用いて kubernetes 1 マスター、nginx 1 マスター、1 レプリケーションの 3 サーバーを Google HTTP Load Balancing させて運用しておりました(運用開始後一ヶ月経った今大体 $31.39 程度の課金、無駄に豪華!)。


触ってみないと気づきにくいのですが、Google のロードバランサは Network Load Balancing(L4) と HTTP Load Balancing(L7) があります(ネットワーク負荷分散と、HTTP 負荷分散)。
Network Load Balancing の方はセッションアフィニティーが設定できるため、現実的にはこちらを使うことになると思います。
HTTP Load Balancing の方は、現時点ではスティッキーセッションにはできないようです(Google さんはよ!)。
サークルのホームページとして構築したクラスターは単なる静的ファイルの塊を nginx で捌くだけなので、スティッキーでなくても問題ないため、HTTP Load Balancing の方を(無駄に)利用していました。
残念なのが、ノード間での共有ディスクは読み取り専用となってしまうことです。クラスタリングしつつストレージを共有しなければならないとき、共有はできても書き込みはできないため、使えません。そのような場合には、Google Cloud Storage を使用するしかなさそうです。
s3fs 等を利用しマウントによるストレージの共有を試してみたりしましたが、はっきりいって遅すぎて使い物になりませんでした。
(Compute Engine の中で s3fs を利用して Cloud Storage をマウントしても、実用に耐えうるスピードは得られませんでした)
ノード間で書き込みも可能な共有ディスクがいつか接続できるようになるといいですね。


GKE ですが、様々な方が記事にされていますが、公式のチュートリアルが一番素直です。

Hello Wordpress の方ですと、設定ファイルを用意せずにサクっと作れるチュートリアルになっており、Guestbook ですと、基本的な概念は一通り押えるチュートリアルになっています。
この二つをやってみるのがいいと思います。
日本語の記事ですと、私はこの方の記事がとても分かりやすかったです。まずはこちらを読み、その後 Googleチュートリアルを両方試してみるとほとんどつかめると思います。


Google Compute Engine(GCE) の方ですと、GUI からある程度(ssh やりだすとやっぱり CLI...で ssh はかなり使うよね...)はできるのですが、GKE の方ですと、CLI がもう前提です。
というわけで、Google Cloud SDK をまずはインストールしましょう。


GKE では、基本的に gcloud preview container で始まるコマンドを使います。で、以下をご覧ください。
Known issues with Google Container Engine
> Windows is not currently supported. The gcloud preview container command is built on top of the Kubernetes client’s kubecfg binary, which is not yet available on Windows.
Windows ユーザなら誰もが通る道だと思いますが、WindowsSDK をインストールしても今は GKE を使えません!
GKE を使いたい場合、Linux 環境を用意して、そこに SDK をインストールしましょう。


というわけで、以下は Ubuntu 14.04 の利用を前提に説明します。

  • Google Cloud SDK のインストール
    1. apt-get install -y curl
    2. curl https://sdk.cloud.google.com | bash
    3. (私は mkdir -p /opt/google してこの中にに入れました)
    4. source ~/.bashrc (zsh とか使ってる人には釈迦に説法ですよね?)
    5. gcloud components update preview
    6. さらに認証を行います
gcloud auth login
  • Google API プロジェクトの作成
    1. Google Developers Console より適当にプロジェクトを作成します(名前やIDは何でもいいです)
    2. プロジェクト作成後画面上部に表示されるプロジェクト番号をメモします。
    3. ここではプロジェクト番号を 1234567891234 とします
    4. [API と認証] - [API] より、Google Container Engine APIGoogle Compute Engine を ON にします(GKE のノードは結局 GCE で動きます)
  • gcloud にデフォルトのプロジェクト番号、ゾーンを設定
    • gcloud に毎度毎度プロジェクト番号等を渡すのは面倒くさいので、最初に設定してあげます。ゾーンはアジアにします。今のところアジアは三箇所(a, b, c)有りますが、アジアのどこにあるのかは中の人しか分かりません。
gcloud config set project 1234567891234
gcloud config set compute/zone asia-east1-a
  • Kubernetes クラスタを作成します。唯一これだけは画面からできますが、CLI でやっちゃいましょう
gcloud preview container clusters create test --num-nodes 1 --machine-type f1-micro
    • クラスタ名を test とし、マシンタイプは一番安いヤーツ(f1-micro)にしています。--num-nodes が 1 なので、Kubernetes マスターと合わせて 2 台構成になります(--num-nodes にはマスターを除く台数を指定します)
    • しばらくすると処理が終わります。その後、[計算処理] - [Container Engine] に
test 	asia-east1-a 	1 	1 vCPUs 	0.6 GB 	xxx.yyy.zzz.www

と表示されるはずです。同様に、
[計算処理] - [Compute Engine] - [VM インスタンス] に

k8s-test-master asia-east1-a k8s-test-master default xxx.yyy.zzz.www
k8s-test-node-1 asia-east1-a k8s-test-node-1 default www.xxx.yyy.zzz

と表示されるはずです。

gcloud preview container clusters list
gcloud preview container clusters describe test

などとすることで、クラスターの一覧を表示したり、詳細を見たりすることが可能です。
作成された k8s-test-master や k8s-test-node-1 は単に Google Compute Engine のインスタンスでしかないので、ssh で適当に入って Web サーバーを建てたり等普通の仮想環境としても利用できます。

  • Pod を作成
    1. Pod を作りましょう。Pod とはコンテナの集合です。折角ですから、Dockerfile を作ってみよう で作成したコンテナを利用してみましょう(イメージを yourdockerhub/mynginx として push しているとします)。俺はもっと簡単に試したいんだ!という方は、yourdockerhub/mynginx を単に nginx に置換して進めてください。
    2. 以下のファイルを /root/test-pod.json として置いておきます
{
  "id": "test",
  "kind": "Pod",
  "apiVersion": "v1beta1",
  "desiredState": {
    "manifest": {
      "version": "v1beta1",
      "id": "test",
      "containers": [{
        "name": "test",
        "image": "yourdockerhub/mynginx",
        "cpu": 100,
        "ports": [
          {
            "name": "test-http",
            "containerPort": 80,
            "hostPort": 80
          }
        ]
      }]
    }
  },
  "labels": {
    "name": "test",
    "role": "master"
  }
}

containerPort や hostPort は docker -p と同様ですね。名前や ID は適当に付ければ OK です。ラベルはちょっと重要ですが、使ってると後で分かってきます。まずはやってみましょう!
"kind": "Pod", "apiVersion": "v1beta1", "version": "v1beta1" は固定です。今は必ずこれを指定しなければいけないと Google のドキュメントに書かれているので、言われたとおりに指定します。
毎回コマンドにクラスター名を指定するのが面倒なので、最初のステップと同様にデフォルトとして設定しておきます(--cluster-name test を毎回付けるか、このコマンドを一回だけ実行しておくかの違い)。

gcloud config set container/cluster test

いよいよ Pod を作成します。

gcloud preview container pods create --config-file /root/test-pod.json

しばらくすると作成されます。

gcloud preview container pods list
gcloud preview container pods describe test

などとすることで、pod のリストや詳細を表示することが可能です。

  • そして service の作成・・・
    • は必要ないんです!実はもう、あなたはほとんど GKE の試用を達成しているのです。他の記事ですと service の作成まで、あるいは Replication Controller の作成までを解説しているかも知れませんが、今回作成したのは 1 ノードしかない nginx です。service は裏にある「複数個」の Pod への接続、環境の受け渡し、あるいはロードバランスが必要なため作成します。今回は、Pod は 1 個しかない上に、何の情報もなくとも 80 番ポートで勝手に動作するため、不要なのです。
  • 残る手順・・それはポートの開放
    1. gcloud compute firewall-rules create コマンドでできるのですが、ポートの開放は GUI からでも色々できるようになっているため、GUI でやりましょう。
    2. [計算処理] - [Compute Engine] - [VM インスタンス] を表示し、[k8s-test-node-1] をクリックします。このノードの詳細が表示されるはずです。
    3. [HTTP トラフィックを許可する] というチェックボックスがあるので、これをチェックします。これで、このノードの nginx に外からつなげられるようになりました。HTTP, HTTPS 以外のポートについても、[Compute Engine] - [ネットワーク] の画面から開放が可能です。
  • k8s-test-node-1 の [外部IP] にアクセスしてみましょう。nginx が表示されたかと思います。


如何でしたでしょうか?
なるべく簡単な手順で GKE を紹介したため、使えることは使えたけど応用力にかける感想をお持ちかも知れません。
上記の例ですと、確かにその通りです。が、GKE には本当に様々な機能があります。そして、それは公式のチュートリアルや他の方の記事に詳細に書かれています。
本記事が、そのための足がかりとなり、より深い理解への一助となることを祈ります。


以上で本記事は終了です。
ありがとうございました。

Azure モバイル サービス

Azure のモバイル サービスは秀逸です。
Push 通知ですと Amazon SNS に目が行きがちですが、とても使いやすいサービスになっています。

  • Azure は MS のクラウドなので、当然のこととして SQL Server を持っており、極容易に連携が可能。
    • デバイストークンの管理用のテーブルや、ユーザ毎のデータ用のデータベースを独自に用意する必要がない
    • しかもスキーマレスであり、開発中などで後からカラムを追加することも容易(もちろんスキーマレスでなくすことも可能)
    • データを挿入、削除等のタイミングをフックでき、そのタイミングでの処理をブラウザから実装できる(このユーザにこのデータが挿入されたら通知する、等)
  • バックエンドの API(通知を行う、DB にデータを登録する、外部 URL へ通信を行う)といった API を、ブラウザ上から実装、設定可能(AWS Lambda と似てますね)
    • バックエンドはデフォルトは Node.js ですが、C# でも実装可能です。

Node.js なので、require から様々な機能が使えます。
Push はもちろんのこと、他システムへの WebHook フック(例えば post)も可能です。

require('request').post({
    uri: 'http://www.example.com/receiver',
    form: {
        data_a: 'data_a',
        data_b: 100,
        data_c: true
    }
}, function(err, response, body) {
    if (err) {
        console.log(err);
    }
});

DB アクセスは、基本は

request.service.tables.getTable('テーブル名').read({
    success: function(results) {
    }
});

などと専用の API で読み書きを行いますが

request.service.mssql.query('TRUNCATE TABLE テーブル名', {
    success: function(results) {
    },
    error: function(err) {
    }
});

などとして自由に SQL を流すことも可能です。

チュートリアルがまた分かりやすく、これを読むだけで iPhone, Android への通知機能を1〜2時間程度でゼロから作ることができます。
Add push notifications to your Mobile Services app(iOS)
Add push notifications to your Mobile Services app(Android)
他社のサービスの設定方法まで詳しく丁寧に書かれているのが、素敵ですね。


というわけで、クライアントから Azure モバイル サービスに定義したバックエンド APIJava から HttpClient を用いて呼び出すプログラムを作成しました。
といっても簡単で、モバイルサービス名と API 名から決まる URL に対して、X-ZUMO-APPLICATION ヘッダーに API キーを設定しリクエストを投げるだけです。
FIXME になっている箇所はご自身の環境に合わせて修正しご利用くださいませ。

AzureUtils

public class AzureUtils {
    private static final String AZURE_API_KEY = "FIXME";

    private static final String AZURE_BASE_URL = "https://FIXME.azure-mobile.net";

    private static HttpResponse post(final String url, final UrlEncodedFormEntity parameters) throws IOException {
        final HttpClient client = new DefaultHttpClient();

        try {
            final HttpPost method = new HttpPost(url);

            method.setHeader("X-ZUMO-APPLICATION", AZURE_API_KEY);
            method.setEntity(parameters);

            return client.execute(method);
        } finally {
            client.getConnectionManager().shutdown();
        }
    }

    public static HttpResponse exec(final String apiName, final UrlEncodedFormEntity parameters) throws IOException {
        return post(AZURE_BASE_URL + "/api/" + apiName, parameters);
    }
}

UrlEncodedFormEntity に設定したパラメーター(例えば hoge)が、Azure の API で request.body.hoge として取れる寸法です。

Azure 仮想マシンリージョン間の移動

普通逆だと思いますが、東日本リージョンに建てている仮想マシンを、米国東部2リージョンに引っ越すことになりました(貧乏だからです・・・)
仮想マシンだけを引っ越すとなると、すべて GUI から可能です。

  1. 仮想マシンを選択
  2. 削除
  3. 接続されたディスクの保持

これで BLOB を保持したまま古い仮想マシンを消し、新しい仮想マシンを作成する際に、簡易作成ではなくギャラリーからを選び、[マイ ディスク] を選びましょう。
残しておいたディスクが出てくるので、それで作り直せば OK です(マイ ディスクに出てこなければ、一旦ログアウト、ログインしてみましょう)。


基本的に、静的IPを振るために仮想ネットワークに所属させたりする場合も、同じ手順を踏みます。
(仮想ネットワークに所属させていないインスタンスをネットワークに所属させたい場合、現時点では作り直すしか方法はないようです)
インスタンスを作成し直すため、エンドポイントの設定等仮想マシン作成後からの設定を無くさないように注意してください。


これだけだとディスクが東日本リージョンに残ってしまうため、次の手順を踏み米国東部2リージョンにストレージをコピーします。

  1. Azure SDK のセットアップ
  2. ストレージのコピー
    1. Add-AzureAccount あるいは、認証済みで複数アカウントが存在するのであれば Select-AzureSubscription "Visual Studio Professional with MSDN" などとします
    2. ストレージを表示し、リストの中から移行元となる東日本リージョンのディスクを選択します
    3. コンテナーから、移行元対象の BLOB の URL をコピーします
    4. [ダッシュボード]の[アクセス キーの管理]よりプライマリアクセスキーをコピーします
      • xxxxx だったとします。
    5. 同様に、移行先となるストレージを選択し、移行先のコンテナの URL をコピーします。
    6. [ダッシュボード]の[アクセス キーの管理]よりプライマリアクセスキーをコピーします
      • yyyyy だったとします。
    7. azcopy /Source:https://portalvhdsxxxxxxxxxxxxx.blob.core.windows.net/vhds /Dest:https://portalvhdsyyyyyyyyyyyyy.blob.core.windows.net/vhds /SourceKey:xxxxx /DestKey:yyyyy /S
    8. 移行先である https://portalvhdsyyyyyyyyyyyyy.blob.core.windows.net/vhds に xxxxxxxx.xxxxxxxxxxxxxxxxxfrom.vhd ができていることが、GUI から確認できます。
    9. 仮想マシンを選び、イメージタブからイメージの作成を選び、移行先にコピーした https://portalvhdsyyyyyyyyyyyyy.blob.core.windows.net/vhds/xxxxxxxx.xxxxxxxxxxxxxxxxxfrom.vhd よりイメージを作成します。
    10. 後は、仮想マシンの作成より、簡易作成ではなくギャラリーから作成し、[マイ イメージ]に前ステップで作成したイメージで仮想マシンを作成します。
    11. ╭( ・ㅂ・)و グッ

Amazon SNS + Google Cloud Messaging + Android

AWS SDK for JavaAndroid への Push 通知を考えている皆様!


iPhone アプリしか作ったことがない人にとってはかなりの衝撃の事実だと思いますが、Android では通知のためのデバイストークンがころころ変わります。

  • バージョンアップ
  • 再ンストール
  • その他色々

これらを契機にして registration id が変わりまくります。
Android への通知は Google Cloud Messaging を使うと思いますが、Amazon SNS と組み合わせて使用することも多いと思います。
そんなときには!

Amazon SNS のモバイルトークン管理についてのベストプラクティス
この記事に目を通した方がよいです(クラスメソッド様、いつも良記事お世話になっております)。
もっと言うと、この記事の元となった記事
Mobile token management with Amazon SNS


ポイントは createEndpoint メソッドです。
このように実装し、トークンを新しい物に更新していきましょう。
わざわざ正規表現でひっかける必要があるのであれば、例外クラスのプロパティとして組み込んでもらいたいところですが・・・
通知機能を一から悩んで作りこむよりも、まずこの記事を参考に実装した方がうまくいくこと間違い無しです!


Google Cloud Messaging が使用できない市場(国)もあるので、国際化対応する際には更に注意です

AppEngine について色々

AppEngine は無課金ではソケットが使えないため、そのままでは HttpClient が使えません。それで困ってる方は

  • GAEClientConnection
  • GAEConnectionManager

を使いましょう!

new DefaultHttpClient(new GAEConnectionManager());

といった感じで、コンストラクタに渡してあげます。これで GAE でも HttpClient が動作します。


さらに、GAEClientConnection.java の FetchOptions.Builder に、setDeadline(10.0) を追加してあげましょう。
デフォルトは 5 秒でタイムアウトです。限界の 10 秒に設定します。
デフォルトの 5 秒では、通信先や自分自身の状態によってはタイムアウトする可能性が普通にあります。


AppEngine で作るのが初めての方向きに、String => String のキーバリューユーティルを作ってみました。
永続化データを適当に放り込んだりする際の実装の一助になると思います。適当にご利用くださいませ。

EMF.java

import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public final class EMF {
    private static final EntityManagerFactory emfInstance = Persistence.createEntityManagerFactory("transactions-optional");

    private EMF() {
        throw new AssertionError();
    }

    public static EntityManagerFactory get() {
        return emfInstance;
    }
}

KeyValue.java

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "KEYVALUE")
public class KeyValue {
    private String key;

    private String value;

    public KeyValue(final String key, final String value) {
        this.key = key;
        this.value = value;
    }

    @Id
    @Column(name = "KEY")
    public String getKey() {
        return key;
    }

    public void setKey(final String key) {
        this.key = key;
    }

    @Column(name = "VALUE")
    public String getValue() {
        return value;
    }

    public void setValue(final String value) {
        this.value = value;
    }
}

KeyValueUtil.java

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

public class KeyValueUtil {
    public static String find(final String key) {
        final EntityManagerFactory emf = EMF.get();
        final EntityManager em = emf.createEntityManager();

        try {
            final EntityTransaction txn = em.getTransaction();

            txn.begin();

            try {
                final String result = find(em, key);

                txn.commit();

                return result;
            } finally {
                if (txn.isActive()) {
                    txn.rollback();
                }
            }
        } finally {
            if (em != null) {
                em.close();
            }
        }
    }

    public static String find(final EntityManager em, final String key) {
        if (em == null) {
            return find(key);
        } else {
            final KeyValue keyValue = em.find(KeyValue.class, key);

            return keyValue == null ? null : keyValue.getValue();
        }
    }

    public static KeyValue findKeyValue(final String key) {
        final EntityManagerFactory emf = EMF.get();
        final EntityManager em = emf.createEntityManager();

        try {
            final EntityTransaction txn = em.getTransaction();

            txn.begin();

            try {
                final KeyValue result = findKeyValue(em, key);

                txn.commit();

                return result;
            } finally {
                if (txn.isActive()) {
                    txn.rollback();
                }
            }
        } finally {
            if (em != null) {
                em.close();
            }
        }
    }

    public static KeyValue findKeyValue(final EntityManager em, final String key) {
        if (em == null) {
            return findKeyValue(key);
        } else {
            return em.find(KeyValue.class, key);
        }
    }

    public static void merge(final KeyValue keyValue) {
        final EntityManagerFactory emf = EMF.get();
        final EntityManager em = emf.createEntityManager();

        try {
            final EntityTransaction txn = em.getTransaction();

            txn.begin();

            try {
                merge(em, keyValue);
                txn.commit();
            } finally {
                if (txn.isActive()) {
                    txn.rollback();
                }
            }
        } finally {
            if (em != null) {
                em.close();
            }
        }
    }

    public static void merge(final EntityManager em, final KeyValue keyValue) {
        if (em == null) {
            merge(keyValue);
        } else {
            em.merge(keyValue);
        }
    }

    public static void remove(final KeyValue keyValue) {
        final EntityManagerFactory emf = EMF.get();
        final EntityManager em = emf.createEntityManager();

        try {
            final EntityTransaction txn = em.getTransaction();

            txn.begin();

            try {
                remove(em, keyValue);
                txn.commit();
            } finally {
                if (txn.isActive()) {
                    txn.rollback();
                }
            }
        } finally {
            if (em != null) {
                em.close();
            }
        }
    }

    public static void remove(final EntityManager em, final KeyValue keyValue) {
        if (em == null) {
            remove(keyValue);
        } else {
            em.remove(keyValue);
        }
    }
}

Heroku+Quartz Scheduler という選択肢

AppEngine、便利ですよね。
特に秀逸なのが、cron.xml !!

<cron>
  <url>/cron/honki</url>
  <description>honki</description>
  <schedule>every monday,tuesday,wednesday,thursday,friday of month 00:00</schedule>
  <timezone>Asia/Tokyo</timezone>
</cron>
<cron>
  <url>/cron/weather</url>
  <description>weather forecast</description>
  <schedule>every monday,tuesday,wednesday,thursday,friday of month 08:00</schedule>
  <timezone>Asia/Tokyo</timezone>
</cron>
<cron>
  <url>/cron/fortune</url>
  <description>fortune</description>
  <schedule>every monday,tuesday,wednesday,thursday,friday of month 08:00</schedule>
  <timezone>Asia/Tokyo</timezone>
</cron>
<cron>
  <url>/cron/xxx/login</url>
  <description>XXX login</description>
  <schedule>every 168 hours</schedule>
  <timezone>Asia/Tokyo</timezone>
</cron>
<cron>
  <url>/cron/yyy/login</url>
  <description>YYY login</description>
  <schedule>every 720 hours</schedule>
  <timezone>Asia/Tokyo</timezone>
</cron>

こんな感じで、毎営業日、日付が変わったタイミングで同僚のグループウェアに「本気出す!」というスケジュールを登録したり、毎朝天気予報や占いを通知したり(Amazon SNS Topic で一括通知、便利ですよね)、一定時間ログインしないとメールでアカウント消すぞと脅してくる○○○プリントさんやら○○急便さんやらに HttpClient で定期的に自動ログインさせたり・・・色々捗りますよね。


で、Heroku です。
昔は AppEngine は Cloud SQL がなかったため、Heroku を使うことも多かったと思います(AppEngine は色々特殊で tie-in されますしね)。
Heroku には、AppEngine の cron.xml がない代わりに、Heroku Scheduler なるものがあります。

しかし、これは /1h, /10m, /1d 毎の起動しかできず、使いすぎると無料枠を突き抜け課金されてしまいます。
というわけで、僕的には絶対に使いたく有りません(ドケチ)。

そこで・・・


QUARTZ!!!!!!


Quartz を使いましょう。Java で書かれたジョブスケジューラーです。組み込みで定義されている cron トリガーを使えば、秒単位でのきめ細やかなジョブ実行が可能です。
しかし、Heroku は1時間リクエストがないと idle 状態になってしまい、Quartz のスレッドも止まってしまいます。
なので、アプリケーションの起動時に、1時間毎に自分自身にリクエストを投げるジョブを登録してあげましょう。

これで、Heroku でも無課金できめ細やかなジョブ制御が可能です。

大ざっぱですが、以下のような感じで OK です(Jetty + PostgreSQL)。

Procfile

web:    java $JAVA_OPTS -cp target/classes:target/dependency/* test.Main

test.Main

public class Main {
    public static void main(final String[] args) throws Exception {
        final String webappDirLocation = "src/main/webapp/";

        System.setProperty("org.quartz.properties", webappDirLocation + "WEB-INF/classes/quartz.properties");

        // The port that we should run on can be set into an environment variable
        // Look for that variable and default to 8080 if it isn't there.
        String webPort = System.getenv("PORT");
        if (webPort == null || webPort.isEmpty()) {
            webPort = "8081";
        }

        final Server server = new Server(Integer.valueOf(webPort));
        final WebAppContext root = new WebAppContext();

        root.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
        root.setContextPath("/");
        root.setDescriptor(webappDirLocation + "/WEB-INF/web.xml");
        root.setResourceBase(webappDirLocation);
        root.setConfigurations(new Configuration[] { new WebInfConfiguration(), new WebXmlConfiguration(), new MetaInfConfiguration(), new FragmentConfiguration(), new EnvConfiguration(), new PlusConfiguration(), new JettyWebXmlConfiguration(), new TagLibConfiguration() });

        // Parent loader priority is a class loader setting that Jetty accepts.
        // By default Jetty will behave like most web containers in that it will
        // allow your application to replace non-server libraries that are part of the
        // container. Setting parent loader priority to true changes this behavior.
        // Read more here: http://wiki.eclipse.org/Jetty/Reference/Jetty_Classloading
        root.setParentLoaderPriority(true);

        server.setHandler(root);

        server.start();
        server.join();
    }
}

WEB-INF/classes/quartz.properties

org.quartz.scheduler.instanceName = HerokuScheduler
org.quartz.threadPool.threadCount = 4
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.tablePrefix = qrtz_
org.quartz.jobStore.dataSource = postgres
org.quartz.dataSource.postgres.jndiURL = java:comp/env/jdbc/postgres

QuartzSchedulerHolder

public class QuartzSchedulerHolder {
    public static final Scheduler SCHEDULER;

    static {
        try {
            SCHEDULER = StdSchedulerFactory.getDefaultScheduler();
        } catch (final SchedulerException e) {
            throw new RuntimeException(e.getLocalizedMessage(), e);
        }
    }
}

ServletContextListenerImpl

public class ServletContextListenerImpl implements ServletContextListener {
    @Override
    public void contextInitialized(final ServletContextEvent event) {
        try {
            QuartzSchedulerHolder.SCHEDULER.start();

            final String id = String.format("%018d", System.nanoTime());
            final JobDetail job = JobBuilder.newJob(KeepHerokuAlive.class).withIdentity(id, "KeepAlive").build();
            final Trigger trigger = TriggerBuilder.newTrigger().withIdentity(id, "KeepAlive").startAt(new Date(FIXME)).endAt(new Date(FIXME)).withSchedule(CronScheduleBuilder.cronSchedule("0 0/55 * * * ?").inTimeZone(TimeZone.getTimeZone("Asia/Tokyo"))).build();

            QuartzSchedulerHolder.SCHEDULER.scheduleJob(job, trigger); // 55分毎に自分自身に HTTP リクエストを送信
        } catch (final SchedulerException e) {
            throw new RuntimeException(e.getLocalizedMessage(), e);
        }
    }

    @Override
    public void contextDestroyed(final ServletContextEvent event) {
        try {
            QuartzSchedulerHolder.SCHEDULER.shutdown();
        } catch (final SchedulerException e) {
            throw new RuntimeException(e.getLocalizedMessage(), e);
        }
    }
}