読者です 読者をやめる 読者になる 読者になる

GeekFactory

int128.hatenablog.com

Continuous deployment to App Engine from Circle CI by Gradle

CI gradle app engine

Circle CIを利用して、JVMベースのアプリをGoogle App Engineにデプロイする作業を自動化してみました。

うれしいこと

  • 誰でも簡単にデプロイできる
  • CIやPull Requestレビューを通過したコードのみデプロイできるように制限できる
  • ビルド環境の差異による影響を排除できる

基本的な考え方

ざっくりとした流れは以下になります。

  1. GitHubにmasterをpushする
  2. Circle CIでビルドが実行される
  3. ./gradlew build でビルドする
  4. ./gradlew appengineUpdate でデプロイする

App Engine SDKはgradle-appengine-pluginが自動的にダウンロードするように設定します。また、デプロイにはService Accountを利用するため、あらかじめ https://console.cloud.google.com/iam-admin/serviceaccounts でService Accountを作成して鍵ファイルを取得しておきます。

実装

まず、Circle CIのYAMLを設定します。ポイントは以下になります。

  • Java 7でビルドする
  • Dependencies CacheにApp Engine SDKを含めてCI時間を短縮する
  • 環境変数 APPENGINE_KEY を経由してService Accountの鍵ファイルを渡す(あらかじめbase64エンコードしておく)

circle.yml は以下のようになります。

machine:
  java:
    version: openjdk7

  environment:
    TERM: dumb
    GRADLE_OPTS: -Xmx1g -Xms1g

dependencies:
  override:
    - ./gradlew testClasses appengineDownloadSdk

test:
  override:
    - ./gradlew build

deployment:
  production:
    branch: master
    commands:
      - echo "$APPENGINE_KEY" | base64 --decode > build/appengine-key.json
      - ./gradlew appengineUpdate

次に、Gradleを設定します。App Engine SDKを自動的にダウンロードするようにします。また、appcfgにService Accountの鍵ファイルを指定します。

appengine {
    downloadSdk = true
    appcfg {
        oauth2 = true
        if (file('build/appengine-key.json').exists()) {
            extraOptions << '--service_account_json_key_file=build/appengine-key.json'
        }
    }
}

実装例

GaelykとReactのアプリをCircle CIからApp Engineにデプロイする例を下記に置いているので参考にどうぞ。

github.com

Circle CIでSSHサーバを利用する際の注意点

CI Circle CI

Circle CIでテストコードからローカルのSSHサーバを利用する際の注意点をメモします。

背景

SSHクライアントのテストでSSHサーバが必要なので、CI環境で用意したい。具体的には、Gradle SSH PluginのテストでCircle CIのSSHサーバを利用したい。

課題と対策

Circle CIではデフォルトでSSHサーバが起動しており、ubuntuユーザに対して公開鍵認証でログインできるようになっています。

私が試した範囲では以下の課題がありました。

  1. 一定時間ごとにホスト鍵が変わる
  2. ubuntuユーザへのログインに時間が掛かる

まず、一定時間(1〜2分?)ごとにSSHサーバのホスト鍵が変わるようです。これにより known_hosts に書かれたホスト鍵が古くなるため、Host Key Checkingに失敗します。デフォルトで起動しているsshdに問題があるようなので、以下のように別のsshdを起動したら解決しました。

test:
  pre:
    - sudo /usr/sbin/sshd -D -e -p 8022:
        background: true

追記: 別のsshdを起動する方法でもホスト鍵が変わる場合があるようです。メモリを大量に使用した場合かもしれません。対策はまだ見つかっていません。

また、CIを実行しているユーザ(ubuntu)へのログインは異常に時間がかかるため、スローテストの原因になります。おそらくubuntuユーザのログイン時に何かやっているためと思われます。これは別のユーザを作成したら解決しました。

# configure public key authentication
ssh-keygen -t rsa -N '' -f ~/.ssh/id_ext
sudo useradd -m tester
sudo -u tester -i mkdir -p -m 700 .ssh
sudo -u tester -i tee .ssh/authorized_keys < ~/.ssh/id_ext.pub
sudo -u tester -i chmod 600 .ssh/authorized_keys

# configure sudo without password
echo "tester ALL=(ALL) NOPASSWD: ALL" > /tmp/tester
sudo chmod 440 /tmp/tester
sudo chown 0.0 /tmp/tester
sudo mv /tmp/tester /etc/sudoers.d

なお、2の解決策を実施することでCIの所要時間が約8分から約5分に短縮されました。あくまでもGradle SSH Pluginの例なので参考ですが。

別の選択肢

以下の選択肢もあります。

  1. Dockerコンテナでsshdを実行してログインする
  2. EC2などにログインする

1はdocker pullでやや時間がかかります。2はレイテンシの影響を受けます。

Gradle SSH Plugin 2.4.0をリリースした

gradle groovy

Gradle SSH Plugin 2.4.0、Groovy SSH 2.4.0をリリースしました。

github.com

2.4.0の変更点

New features:

  • Host key checking for gateway access
  • Put files filtered by given closure
  • Get files filtered by given closure
  • Add ssh.runtime object in CLI

Bug fixes:

  • Skip lecture message from sudo result
  • Specify null as UserInfo to prevent changing known_hosts

Host key checking for gateway access

ゲートウェイサーバを経由して多段接続する場合でも known_hosts が使えるようになりました。例えば、localhost→A→Bのように踏み台Aを経由してBに接続する場合、Bは実際にはポートフォワードの接続先(localhost:xxxxx)に見えるため、ホスト鍵の検証に失敗する問題がありました。2.4.0からは known_hosts に含まれるBをポートフォワードの接続先に読み替える実装を追加しました。

Get/Put files filtered by given closure

これまで get put メソッドはすべてのファイルを再帰的に転送する機能しか提供していませんでしたが、2.4.0からはクロージャでフィルタする機能も提供するようになりました。

get from: '/remote/folder', into: buildDir, filter: { it.name =~ /\.xml$/ }

put from: buildDir, into: '/remote/folder', filter: { it.name =~ /\.xml$/ }

SFTP/SCP×GET/PUTのすべての組み合わせに手を入れる必要があったため、実装は随分と骨の折れる作業になりました。

Add ssh.runtime object in CLI

Groovy SSHスタンドアロンJARを利用する場合に ssh.runtime.jar でJAR自身を参照できるようになりました。これにより、リモートホストにJARを転送してスクリプトを実行する処理を簡単に記述できます。まあ、スタンドアロンJARのテストでうれしいだけかもしれませんが。

ssh.run {
    session(ssh.remotes.tester) {
        put from: ssh.runtime.jar, into: '.'
        execute 'java -jar gssh.jar --version'
        execute 'java -jar gssh.jar --help'
    }
}

Skip lecture message from sudo result

sudoコマンドを実行して最初にlecture messageが表示された場合に、コマンドの実行結果にlecture messageが含まれてしまう問題を修正しました。

振り返り

ここに書いている以外にもOS Integration TestをすべてEC2に追い出すとか、Circle CIへの移行に取り掛かるといった改善をやっていました。ビルドやCIの改善に手を付け始めるとあっという間に時間を使ってしまい、気が付けば2.4.0のリリースが月末になってしまいました。

現状ではCIに7〜8分を要しているので、以下の改善に手を付けたいです。

  • DockerのSSHコンテナを利用してOS Integration Testを実行する(EC2は廃止)
  • Plugin Integration Testの対象バージョンを1系に限定する
  • 効率的なキャッシュ