PROBLEM

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

-

SOLUTION

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

Wercker3つの特徴

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

Werckerのふるまいを定義するwercker.ymlは、下記のようにパイプラインごとに記述されている。

plantuml

dev

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

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パイプラインと変わらず。すべてのブランチで走る。

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に移った場合に現在おこなっている本番環境のフォークをどうするかが検討課題となる。

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パイプラインと同じ。

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を参照。

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をつけてる。

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

-

以上