On Blahfe

HerokuとGAEのCIをDockerとパイプラインから構成されたWerckerで管理する

blog/backendwerckerdockerherokugoogle-app-engine

Continuous Integration (CI) が徐々にDockerに対応し始める機運です。先行してWerckerがDocker対応を始めたので、その流れに乗るべくWerckerをDocker化してみました。

PROBLEM

  • パフォーマンス改善のための開発環境がいけてない
  • 別PaaSへ移行するための開発環境が汎用化できてない、つらい

SOLUTION

というわけで、まずはCI上のDockerに載せてから次の手(GAEあたり)を考えることにしました。CIはWerckerを使用。以前から使っていたのですが、今回はボックスがDockerになったのでそちらに対応しました。

まず、Werckerは「Docker」「環境変数」による環境管理、「パイプライン」によるワークフロー管理を行っています。

  1. Dockerで環境を管理。 今回は対応していないですが、GAEのコンテナ(gcr.io/google_appengine/ruby:xxx)と共通化することもできます。ただし、HerokuのHobby Dynosはプロセス数に制限があるのでコンテナ運用は工夫が必要です。
  2. 異なるサービス間のネットワークをWerckerが生成する環境変数で管理。 Dockerのネットワーク設定の煩雑さを解消します。
  3. タスクをワークフローとしてパイプラインで条件付け管理。 パイプラインごとにコンテナを立ち上げているので、同じDocker環境でもパイプラインごとに環境変数を分けることが可能です。Herokuのパイプラインでもいいですが、今後別PaaSに移行する可能性を考えてCI管理にbetしました。

次に、Werckerのふるまいを定義するwercker.ymlは、下記シークエンス図のようにパイプラインごとに記述されています。今回は各パイプラインの詳細を見ていくことにします。

thumbnail

devパイプライン

devパイプラインは wercker dev コマンドをローカルでたたく際に使います。下記の例だとRSpec走らせているだけなのでおまけ程度。ただ、ローカル開発でDockerを使うことになったらこういう提案もありだと思います。プロジェクトレポジトリすべてをDockerにしてローカル開発するペイン、所謂git-dockerのバージョン管理問題があるので代替案として。

yaml
box: ruby:2.3.1
services:
  - postgres:9.6.1
  - redis:3.0.3

dev:
  steps:
    - bundle-install
    - script:
        name: Install ImageMagick
        code: |
          apt-get update
          apt-get install -y nodejs imagemagick
    - script:
        name: Setup database
        code: |
          RAILS_ENV=test bundle exec rake db:create db:migrate
    - internal/watch:
        name: Run rspec
        code: |
          RAILS_ENV=test bundle exec rake spec
        reload: true

buildパイプライン

buildパイプラインもdevパイプラインと同じDockerボックスを使っています。やっていることはdevパイプラインと変わらず、すべてのブランチで走ります。

yaml
build:
  steps:
    - bundle-install
    - script:
        name: Install ImageMagick
        code: |
          apt-get update
          apt-get install -y nodejs imagemagick
    - script:
        name: Echo Ruby information
        code: |
          env
          echo "ruby version $(ruby --version) running!"
          echo "from location $(which ruby)"
          echo -p "gem list: $(gem list)"
    - script:
        name: Setup database
        code: |
          RAILS_ENV=test bundle exec rake db:create db:migrate
    - script:
        name: Run rspec
        code: |
          RAILS_ENV=test bundle exec rake spec

deploy-stageパイプライン

deploy-stageパイプラインはステージング環境用。現在Herokuを本番環境で利用しているので、デプロイごとにそれをフォークして環境構築しています。また、Railsのアセットプリコンパイルの時間短縮はほかのCIと同様にキャッシュを利用しています。

他のPaaSに移った場合に現在行っている本番環境のフォークをどうするかが検討課題となります。

yaml
deploy-stage-heroku:
  steps:
    - bundle-install
    - script:
        name: Install NodeJS
        code: |
          apt-get update
          apt-get install -y nodejs
    - nabinno/heroku-install:
        key: $HEROKU_KEY
        user: $HEROKU_USER
        app-name: $HEROKU_APP_NAME
    - script:
        name: Fork Application - destroy application
        code: |
          heroku apps:destroy --app $HEROKU_APP_NAME --confirm $HEROKU_APP_NAME
    - script:
        name: Fork Application - fork
        code: |
          heroku fork --from $FROM_HEROKU_APP_NAME --to $HEROKU_APP_NAME
    - script:
        name: Fork Application - setup addons of rediscloud
        code: |
          heroku addons:create rediscloud:30 --app $HEROKU_APP_NAME
    - script:
        name: Fork Application -change dynos
        code: |
          heroku ps:scale web=1:Free worker=1:Free --app $HEROKU_APP_NAME
    - script:
        name: Fork Application - change environment variables
        code: |
          _rediscloud_url=$(heroku run 'env | grep -e REDISCLOUD_.*_URL' --app $HEROKU_APP_NAME | awk -F= '{print $2}')
          heroku config:set \
            S3_BUCKET=$S3_BUCKET \
            HEROKU_APP=$HEROKU_APP_NAME \
            REDISCLOUD_URL=$_rediscloud_url \
            --app $HEROKU_APP_NAME
    - script:
        name: Assets Precompile - restore assets cache
        code: |
          [ -e $WERCKER_CACHE_DIR/public/assets ] && cp -fr $WERCKER_CACHE_DIR/public/assets $WERCKER_SOURCE_DIR/public || true
          mkdir -p $WERCKER_SOURCE_DIR/tmp/cache
          [ -e $WERCKER_CACHE_DIR/tmp/cache/assets ] && cp -fr $WERCKER_CACHE_DIR/tmp/cache/assets $WERCKER_SOURCE_DIR/tmp/cache || true
    - script:
        name: Assets Precompile - main process
        code: |
          RAILS_ENV=production bundle exec rake assets:precompile --trace
    - script:
        name: Assets Precompile - store assets cache
        code: |
          mkdir -p $WERCKER_CACHE_DIR/public/assets
          cp -fr $WERCKER_SOURCE_DIR/public/assets $WERCKER_CACHE_DIR/public
          mkdir -p $WERCKER_CACHE_DIR/tmp/cache/assets
          cp -fr $WERCKER_SOURCE_DIR/tmp/cache/assets $WERCKER_CACHE_DIR/tmp/cache
    - add-ssh-key:
        host: github.com
        keyname: GITHUB
    - add-to-known_hosts:
        hostname: github.com
        fingerprint: 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48
    - script:
        name: Assets Precompile - git commit
        code: |
          {
            git add public/assets/.sprockets-manifest-*.json
            git commit -m 'Run `rake assets:precompile` on Wercker.'
          } || {
            echo 'Skip: keep precompiled assets manifest.'
          }
    - heroku-deploy:
        key: $HEROKU_KEY
        user: $HEROKU_USER
        app-name: $HEROKU_APP_NAME
    - script:
        name: DB Migrate
        code: |
          heroku run 'bundle exec rake db:migrate --trace' --app $HEROKU_APP_NAME
  after-steps:
    - wantedly/pretty-slack-notify:
        webhook_url: ${SLACK_WEBHOOK_URL}
        channel: general

deploy-prod-herokuパイプライン

deploy-prod-herokuパイプラインは本番環境へのリリース用。環境変数以外はdeploy-stageパイプラインと同じものです。

yaml
deploy-prod-heroku:
  steps:
    - bundle-install
    - script:
        name: Install NodeJS
        code: |
          apt-get update
          apt-get install -y nodejs
    - script:
        name: Assets Precompile - restore assets cache
        code: |
          [ -e $WERCKER_CACHE_DIR/public/assets ] && cp -fr $WERCKER_CACHE_DIR/public/assets $WERCKER_SOURCE_DIR/public || true
          mkdir -p $WERCKER_SOURCE_DIR/tmp/cache
          [ -e $WERCKER_CACHE_DIR/tmp/cache/assets ] && cp -fr $WERCKER_CACHE_DIR/tmp/cache/assets $WERCKER_SOURCE_DIR/tmp/cache || true
    - script:
        name: Assets Precompile - main process
        code: |
          RAILS_ENV=production bundle exec rake assets:precompile --trace
    - script:
        name: Assets Precompile - store assets cache
        code: |
          mkdir -p $WERCKER_CACHE_DIR/public/assets
          cp -fr $WERCKER_SOURCE_DIR/public/assets $WERCKER_CACHE_DIR/public
          mkdir -p $WERCKER_CACHE_DIR/tmp/cache/assets
          cp -fr $WERCKER_SOURCE_DIR/tmp/cache/assets $WERCKER_CACHE_DIR/tmp/cache
    - add-ssh-key:
        host: github.com
        keyname: GITHUB
    - add-to-known_hosts:
        hostname: github.com
        fingerprint: 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48
    - script:
        name: Assets Precompile - git commit
        code: |
          {
            git add public/assets/.sprockets-manifest-*.json
            git commit -m 'Run `rake assets:precompile` on Wercker.'
          } || {
            echo 'Skip: keep precompiled assets manifest.'
          }
    - script:
        name: Add git-tag
        code: |
          _tag=$(date -u -d '9 hours' +%Y-%m-%d-%H-%M-%S)
          git config --global user.email 'wercker@blahfe.com'
          git config --global user.name 'Wercker Bot'
          git tag -a $_tag master -m 'wercker deploy'
          git push origin $_tag
    - heroku-deploy:
        key: $HEROKU_KEY
        user: $HEROKU_USER
        app-name: $HEROKU_APP_NAME
        install-toolbelt: true
    - script:
        name: DB Migrate
        code: |
          heroku run 'bundle exec rake db:migrate --trace' --app $HEROKU_APP_NAME
  after-steps:
    - wantedly/pretty-slack-notify:
        webhook_url: ${SLACK_WEBHOOK_URL}
        channel: general

deploy-prod-gaeパイプライン

deploy-prod-gaeパイプラインはdeploy-prod-herokuパイプラインと同じく本番環境へのリリース用。GAEにいつでも移行できるように走らせています。

GAEのデプロイは癖があって、gcloud app deployコマンドをつかってDockerビルドを走らせますが、その時にDocker内に外部から環境変数を設定することができません。そのため、アセットプリコンパイルのビルドの際、asset_syncを使っていると別サーバーへ同期に失敗します。また、パイプライン上の別ステップに環境変数を当てて行うことはできるが、gcloudのデプロイステップとアセットプリコンパイルが重複して適切なダイジェストを発行できません。従って、GAEをつかう場合は ./public ディレクトリをつかうのが現状の正解です。HerokuのSlugの取り扱い方針と違うので注意が必要です。

GAEのコンテナの中身は、gcloud beta app gen-config --runtime=ruby --custom で出力されるDockerfileを参照ください。

yaml
deploy-prod-gae:
  steps:
    - bundle-install
    - script:
        name: Install ImageMagick
        code: |
          apt-get update
          apt-get install -y nodejs imagemagick
    - script:
        name: Echo Ruby information
        code: |
          env
          echo "ruby version $(ruby --version) running!"
          echo "from location $(which ruby)"
          echo -p "gem list: $(gem list)"
    - script:
        name: DB Migrate
        code: |
          RAILS_ENV=production \
            DATABASE_URL=${DATABASE_URL} \
            bundle exec rake db:create db:migrate --trace
    - script:
        name: Install gcloud
        code: |
          curl https://sdk.cloud.google.com | bash
          source ~/.bashrc
    - script:
        name: Authenticate gcloud
        code: |
          gcloud config set project utagaki-v2
          openssl aes-256-cbc -k ${DECRYPT_KEY} -d -in ./gcloud.json.encrypted -out ./gcloud.json
          gcloud auth activate-service-account --key-file ./gcloud.json
    - script:
        name: Deploy app to Google App Engine
        code: |
          gcloud app deploy ./app.yaml --promote --stop-previous-version
  after-steps:
    - wantedly/pretty-slack-notify:
        webhook_url: ${SLACK_WEBHOOK_URL}
        channel: general

post-deployパイプライン

post-deployパイプラインは本番環境にデプロイした後の後処理用です。参考程度に git tag をつけています。

yaml
post-deploy:
  steps:
    - add-ssh-key:
        host: github.com
        keyname: GITHUB
    - add-to-known_hosts:
        hostname: github.com
        fingerprint: 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48
    - script:
        name: Add git-tag
        code: |
          _tag=$(date -u -d '9 hours' +%Y-%m-%d-%H-%M-%S)
          git remote add origin git@github.com:nabinno/utagaki.git
          git config --global user.email 'wercker@blahfe.com'
          git config --global user.name 'Wercker Bot'
          git tag -a $_tag master -m 'wercker deploy'
          git push origin $_tag
  after-steps:
    - wantedly/pretty-slack-notify:
        webhook_url: ${SLACK_WEBHOOK_URL}
        channel: general

WRAPUP

こうしてWerckerの設定ファイルを書いてみるに、どのCI、どの仮想環境も同じ書き味ということが分かります。当処懸念していたDocker化することによる嵌まり事はなく、すんなり移行することができました。

手軽さ、管理のしやすさから、今後はすべてのCIがDockerに移行するでしょう。

nabinno
Emacsianでアート好き、ランニング好きな@nabinnoが書いています
GitHub / X / LinkedIn / ネクイノ
blog/market

今後の成長分野:新たなテクノロジーの展望

テクノロジーの進化は、絶え間ない変化の中で私たちの日常を塗り替えてきました。時には経済的な危機が、新たな可能性を切り拓く契機となることもあります。そこで、過去のリセッション期に生まれたテクノロジーの足

market-trendrecession
blog/organization

ATKerneyの課題解決パターンの魅力的な探求

ATKerneyの課題解決パターン は、課題の本質を見極め、効果的な戦略的構造化を通じて解決策を導き出す手法にフォーカスしています。この冒険の旅は、解決者と協力者たちが心を一つにし、課題に立ち向かう様

problem-solvingatkerney
blog/market

就職氷河期とは何だったのか

私はいわゆる就職氷河期世代です。周囲から時折漏れ聞こえる不平のような言葉がありますが、それを単なる不平として片付けるのはもったいない気がします。できれば、その中に新しい視点を見つけ、次のチャンスへ繋げ

labor-economicsrecessionemployment-ice-age