仕事面で 2017 年を振り返ると、いろいろやったけど自分でなんか作ったというのはほとんどない。 人のふんどしで相撲をとっていた一年(転職してからは半年強)だと言える。SaaS として提供されているツールを導入したり、 OSS の分析ツールを導入・構築したり、会社の仕組みを調整したりしてただけだった。各ライブラリを作ってくれた人には感謝しかない 🙏🏻

組織方面

  • チーム横断の定例 MTG 働きかけ
    • 人が増えて「あの人何やってるかわからない」「仕事を横からいきなり依頼される」などの問題が出てきたため、チーム横断の定例ミーティングを開催してお互いの状況を確認したり依頼しそうなことがあれば前もって共有するように
  • 全体ミーティングフォーマット整え&司会業
    • かつては社長が考えていることを聞くだけの場だったが、チームごとに資料を作ってみんなで発表し、議論をする場に変えた
  • Slack 導入
    • Slack に変えるまでも別のチャットツール使ってたけど、平アカウントでは大したことできず、窮屈な感じがした
    • Slack は平アカウントでも外部ツール連携したり API 使ってなんかやったりできて便利
    • ベンチャーには平社員でも必要なことをやれるようなシステムの方が向いてると思う。 Slack はデザインだけじゃなくてそういうところが優れている。フラットで雰囲気が明るい。使うのが楽しくなる。
  • Kibela 導入
    • Wiki と Blog 、 Board ( Group )の概念がちょうどよい。 Qiita:Team で厳しかったところが解消されている。
    • Kibela 導入以前、情報共有は Issue Tracker に何でも書いてる感じだったが、 Issue Tracker は Issue Tracker なので close することができない問題を扱うのに向いていない
    • タスクには落とし込めないけど社内で見解を表明しておきたい事柄を社内ブログに書いて問題意識をみんなと共有する文化を構築できた
    • Kibela は PlantUML で図を書けるのがとにかくすばらしい。込み入った処理フローをシーケンス図にすることで設計・実装がはかどる。
  • HRT について説く
  • OKR 導入
    • OKR を設定してやっていきましょうという風にした
      • とはいえ自分は HR の専門家ではないのでちゃんと運用して行くにはそういう人に入ってもらわないと厳しいと思ってる

エンジニアリング方面

  • t_wada (テスト文化根付かせ)業
    • No Test, No Merge
  • CI が回る仕組み構築業
    • テストコードは別の人が書いてたけど回せてなくて fail しっ放しになってたので気合いで通るようにして CircleCI で Pull Request ごとにビルドするようにした✌🏻
  • Pull Request テンプレート導入
    • どんな問題を解決する Pull Request なのか、何をやったのか、完了条件を明記する✅
  • Pull Request レビューフォーマット提案
  • gitignore されていた Gemfile.lock をリポジトリに突っ込み業
    • Gemfile でバージョンが固定されてた😢
  • Embulk で分析用データ書き出し業
  • autodoc で API ドキュメント自動生成の仕組み構築
  • Git Flow から GitHub Flow へブランチ戦略変更
    • 1日に何回もデプロイするような製品はこっちの方が向いてる
  • Rubocop 導入& .rubocop.yml 番人業
    • 👮🏻‍♂️
  • CircleCI から勝手に deploy される仕組み導入
  • docker-compose 導入
    • Docker は使われてたがクラスターの管理は手運用だったので docker-compose 使うようにした
  • AWS ECS 導入
  • 社内 Gyazo 導入
  • Redash 導入
    • 経営陣しか数値に関心を持ってなかったので全員が見るように毎朝 Slack に KPI を通知するようにした
    • 複雑なクエリを組んでテーブルごとに値を集計しているだけでは見えてこない値を追えるようになった
    • 独自に KPI/KGI を設定して Growth Hack に取り組むエンジニアも
  • リードレプリカ作ってデータ分析がやりやすくなる仕組み導入
    • RDS で Multi AZ にはなっていたがリードレプリカがなく重いクエリを投げられなかった
    • 複雑な JOIN クエリも書けるようになりデータ分析し放題
    • 来年は BigQuery とかも使えるようにしてさらに分析が捗るようにしたい
  • Itamae でプロビジョニング( Linux アカウントの管理)
    • Itamae 一発で Linux アカウント追加できるようにしてサーバーサイドのエンジニアしか DB にクエリを投げられない状況を改善
  • cronbot 導入
    • 他人が作ったスケジュールも更新できて便利
    • KPI 通知は redashbot と cronbot を組み合わせて実現
  • iOS と Android のダウンロード数自動取得
    • iOS 側はタイムゾーンがずれる、 Android 側は更新が異常に遅いという問題があるものの、ある程度の目安となる数値が毎朝自動で Slack に通知されるように
  • お問い合わせがあったときに Slack に通知する仕組み導入
    • お問い合わせはカスタマーサポートの人が一手に引き受ける感じだったけどみんなが関心を持って見るようになった
    • カスタマーサポートの人からエスカレーションされる前にエンジニアが回答
    • 不具合あったときはいち早く対応可能に
  • Ruby app の前段に CloudFront 導入
    • app サーバーへのリクエストが半減
    • Nginx でキャッシュしきれてなかった静的ファイルを CloudFront でキャッシュするようになり爆速に
  • サイト全面 HTTPS 化
    • CSS/JS が並列で配信されるようになり爆速に

自分でまともな OSS を作れないことにコンプレックスを感じていた時期もあったが( OSS コミュニティでの活動が評価軸となるような職場では全然評価されない)、自分が作れなくても他の人が作ってくれるので、それをいかに組み合わせて有効活用し、価値を生み出せるかに注力すればいいかなぁと思うようになった。

もちろん、 OSS 使っててバグを見つけたり不便なところあったら改善する Pull Request なんかは出していきたいと思ってる。ただ自分は頭がよくないし、抽象的な思考は苦手で個別具体的なコードを書くことしかできないので、自分で OSS を生み出すことは諦めて個別具体的な事象に特化してやっていく方が自分的にも世の中的にも幸せだよね、という風に割り切れるようになった。

こういう割り切りができるようになったのは Kaizen Platform で仕事する機会を得たからだよなぁと思う。 OSS への考え方に限らず、コードを書く部分以外で組織を変革したりだとかオペレーションの仕組みを変えたりだとかは全部 Kaizen Platform で学んだ気がする。1年11ヶ月と短い期間だったけれど、いまの自分の血となり肉となっていると思う。

Kaizen を辞めたときの記事で以下のように書いてたけど、いまのところ失敗を糧にしていい方向に向かってるのではないかと思う。

Kaizen でのリモートワーク失敗経験をどう今後の人生に生かすか。以下のツイートを繰り返し眺めながら悔い改めていきたいと思う。

Kaizen Platform という会社について - portal shit!

というわけでいまは YAMAP という会社で働いています。元同僚の pyama86 さんに比べたら知名度では全然負けててミジンコみたいなもんだと思うけど、そのうち逆転できるようにプロダクトの完成度を高めていって pyama86 さんの方が YAMAP のパクりであるような雰囲気を醸成していきたい。今後ともよろしくお願いいたします🙏🏻

YAMAP

DSC_4054

以下の文章は正月に「2017 年の Lokka へのコントリビュート目標」というタイトルで書いたまま下書きになってたものです。もう 2017 年も終わりそうだけど公開しておきます。


RubyKaigi 2016 で komagata さんと Lokka についてしゃべったのだけど、 Lokka の開発も停滞してしまっていて(ブログとしては大体の機能そろっていて完成しているとも言える)、 "lokka" でググると JavaScript 製の GraphQL クライアントがヒットして、 GitHub のスター数ではこっちの方が多かったりする。

kadirahq/lokka: Simple JavaScript Client for GraphQL

このままで紛らわしいからその名前こっちに寄越せ、とか言われかねない。もっと Lokka を盛り上げていきたい。

なので一度仕切り直しで今後の方針とかをどうするかを決めた方がよいのではないかと思っている。まずは Issue の棚卸が必要ではないかと思う。

加えて自分でも結構いろいろ lokka-plugin を作っているのだけど、個人のリポジトリに適当に上げてあるだけだとユーザーとしては利用しづらいと思う。そこで GitHub の Lokka org に lokka-plugins というリポジトリを作って、とりあえずそこにコードを集約するようにしたらどうかと思う。前に Rebuild で Jenkins の川口さんが話していたやり方。

OSS 、コードが素晴らしいことも大事だけど、利用しやすくないとユーザーに使ってもらえなくて盛り上がって行かないと思う。プラグインを使いやすい、作りやすいようにして裾野を広げていきたい。

ほかにも手軽に使ってもらうためにはいくつかやらないといけないことあると思う。 gem 化は是非とも必要だと思う。 Lokka 動かすために本体のソースコードごと管理しなければいけないのはやっぱり結構敷居が高いと思う。ディレクトリ作って Gemfile に gem 'lokka' と書いて bundle install し、 theme と db config さえ置けば動くようになるのがよさそう。ファイルをアップロードする仕組みについてもどうにかしたい。 Heroku 運用が前提のためファイルシステムを使うことができず本体にそういう仕組みがなかったのだと思う。 Amazon S3 や Google Cloud Storage 、 Dropbox 使えるようにするとかやり方を考えたい。

これらを推し進めるために、以下のことをやりたい。

  1. Lokka 開発者ミートアップを開催
  2. Slack にチームを作ってコミュニケーションできるようにする

1 は RubyKaigi のときに komagata さんに提案したけどそのあと動けてなかった。とりあえずは自分が komagta さんのところに会いに行くだけになるかもだけど、 Issue の棚卸と今後の方向性を固める会をやった方がよさげだと思う。仕事でも OSS でも意思や目的を共有しないと Project は先に進まないと思う。

2 に関しては Lokka は Lingr でコミュニケーションしていたが、いまは皆さん Slack を仕事で使っていると思うし Slack の方がコミュニケーションしやすいはず。というわけで Slack チームを立ち上げたい。1

という感じで 2017 年も Lokka の開発に関与していきたい。


転職して東京に行く機会がなくなり、結局 Lokka 開発者会議をやることはできなかった。プラグインの gem 化はおろかリポジトリへの集約も DataMapper => ActiveRecord への移行も手を付けられなかったが、それ以外で Lokka の改善は結構頑張ったと思う。

今年やった Lokka の改善

  • パーマリンク生成高速化 https://github.com/lokka/lokka/pull/220
    • Lokka はどのフォーマットで permalink を生成するかを DB に保存している
      • リンクを一つ生成する度に permalink のフォーマットを調べるためのクエリが流れる
    • ビュー内で各記事へのリンクは多数
      • めっちゃクエリが流れる 😱
    • request_store.gem を使ってリクエストごとに一回だけクエリが流れるようにする
      • テーマにもよるが記事一覧でパーマリンク生成のために 10 回くらい DB アクセスが発生していたところは 1/10 になる
      • 管理画面の記事一覧では 100 記事表示しているので 1/100 になる(爆速になった!!!、!)
  • 管理画面をスマートフォン最適化 https://github.com/lokka/lokka/pull/225
    • スマートフォンから管理画面が見やすくなるよう CSS を修正
    • 寝床からでもブログ書けるようになった!!!、!
  • ファイルアップロード機能を追加 https://github.com/lokka/lokka/pull/226
    • S3 にバケットを作ってもらいさえすれば GitHub のようにドラッグ&ドロップで画像をアップロード出来るように
    • めっちゃお手軽お気軽に画像アップロードできるようになって最高便利!!!、!
  • MySQL 絵文字対応 https://github.com/lokka/lokka/pull/230 😃
    • 今の時代、絵文字が使えないのはつらい 😅
  • Ruby 2.4 対応 https://github.com/lokka/lokka/pull/231
    • Lokka で Sinatra 、 DataMapper の次に依存度が高い PadrinoHelper のバージョンを上げることに成功(めっちゃ大変だった!!!、!)
      • 自分としては Rails 3 を Rails 4 に上げるくらいの働きをしたと思ってる😎

locale が i18n.gem がサポートしてるやつじゃないと 500 エラーになるという問題があって、こちらも自分のブログでは直してあるので修正する Pull Request を出したい。

P_BLOG のときもそうだったけど、どうも自分はユーザーの少なくなってきた CMS を細々と改造して使っていくのが好きみたいだ。このブログの開発・運用から学ぶことも多くて仕事にも役立っているので、まだしばらくは使い続けていきたいと思う。 Lokka は永遠に不滅です。


  1. 調べてみたらすでに lokka.slack.com は存在するみたいなんだけどこれって Lokka for CMS のやつですかね? 

Rails 5.1 から入った Encrypted Secrets というのがある。 OAuth の client_secret などパスワード的なやつを暗号化して保存する仕組み。この手のやつはこれまで環境変数などにして dotenv などの機能を使ってそれぞれの環境ごとに .env ファイルを置く、というのがベストプラクティスだったと思うけど、 Encrypted Secrets を使えば秘密情報も暗号化してリポリトリに放りこめるので管理対象が少なくなって便利になる。暗号化するときの鍵は RAILS_MASTER_KEY という環境変数に格納するか、 gitignore した上で config/secrets.yml.key という名前で配置すると Rails がいい感じに読み取ってくれる。

Rails のエコシステムには config (旧 rails_config )という gem もあって、こいつも設定系の情報を入れておく用途によく使う。秘密系の情報と設定系の情報でどちらに値が入っているかを意識するのがめんどい& Rails.application.secrets.foo_bar とか入力するのが長い& Encrypted Secrets は YAML をネストさせられないのがだるいので、 config.gem の config/settings.yml の中で以下のようにしたら便利ではないかと思ってやってみた。

foo:
  bar: <%= Rails.application.secrets.foo_bar %>

呼び出し側の before after はこんな感じ。

Before

bar = Rails.application.secrets.foo_bar

After

bar = Settings.foo.bar

「めっちゃ最高便利じゃん」と思っていたけど、これをやると副作用がでかい。なんと Rails.application.secretsfoo_bar が見つからなくなる! というか Rails.application.secrets がほぼほぼ空になる!!!、!

[3] pry(main)> Rails.application.secrets
=> {:secret_key_base=>"xxx", :secret_token=>nil}

config/settings.yml から Encrypted Secrets を参照しているコードを取り除くと見えるようになる。

結論

というわけで config/settings.yml の中に Rails の Encrypted Secrets を混ぜて使うと危険っぽいです ☢️

ecs-deploy-flow

仕事で開発中のシステムで、 master ブランチに Pull Request が Merge されると自動的に AWS ECS に構築した社内向けの確認環境にデプロイが行われるような仕組みを導入した。自動テスト、コンテナイメージのビルド、デプロイには CircleCI を利用している。 .circleci/config.yml は以下のような感じ。

version: 2

shared: &shared
  working_directory: ~/app
  docker:
    - image: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/organization/app
      environment:
        PGHOST: 127.0.0.1
        PGUSER: user
        RAILS_ENV: test
        REDIS_HOST: localhost
    - image: circleci/postgres:9.6-alpine
      environment:
        POSTGRES_USER: user
        POSTGRES_PASSWORD: password
    - image: redis:3.2-alpine

jobs:
  build:
    <<: *shared
    steps:
      - checkout
      # Restore bundle cache
      - &restore_cache
        type: cache-restore
        key: app-{{ arch }}-{{ checksum "Gemfile.lock" }}
      # Bundle install dependencies
      - &bundle_install
        run: bundle install -j4 --path vendor/bundle
      # Store bundle cache
      - &save_cache
        type: cache-save
        key: app-{{ arch }}-{{ checksum "Gemfile.lock" }}
        paths:
            - vendor/bundle
      # Database setup
      - &db_setup
        run:
          name: Database Setup
          command: |
            bundle exec rake db:create
            bundle exec rake db:structure:load
      - type: shell
        command: bundle exec rubocop
      # Run rspec in parallel
      - type: shell
        command: |
          mkdir coverage
          COVERAGE=1 bundle exec rspec --profile 10 \
                            --format RspecJunitFormatter \
                            --out /tmp/test-results/rspec.xml \
                            --format progress \
                            $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
      # Save artifacts
      - type: store_test_results
        path: /tmp/test-results
      - type: store_artifacts
        path: coverage

  generate-doc:
    <<: *shared
    steps:
      - run:
          name: Install dependencies
          command: |
            apk add --no-cache git openssh ca-certificates
      - checkout
      - *restore_cache
      - *bundle_install
      - *save_cache
      - *db_setup
      # Generate document
      - run:
          name: Generate API doc
          command: |
            AUTODOC=1 bundle exec rake spec:requests
      - run:
          name: Generate Schema doc
          command: |
            diff=$(git diff HEAD^ db)
            if [ -n diff ]; then
              bundle exec rake schema_doc:out > doc/schema.md
            fi
      - run:
          name: Setup GitHub
          command: |
            export USERNAME=$(git log --pretty=tformat:%an | head -1)
            export EMAIL=$(git log --pretty=tformat:%ae | head -1)
            git config --global user.email "${EMAIL}"
            git config --global user.name "${USERNAME}"
      - run:
          name: Push updated doc to GitHub
          command: |
            git add doc
            git commit --quiet -m "[ci skip] API document Update

            ${CIRCLE_BUILD_URL}"
            git push origin ${CIRCLE_BRANCH}

  deploy:
    docker:
      - image: docker:17.05.0-ce-git
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Install dependencies
          command: |
            apk add --no-cache \
              py-pip=9.0.0-r1 jq curl curl-dev bash
            pip install \
              docker-compose==1.12.0 \
              awscli==1.11.76
            curl https://raw.githubusercontent.com/silinternational/ecs-deploy/ac2b53cb358814ff2cdf753365cc0ea383d7b77c/ecs-deploy | tee -a /usr/bin/ecs-deploy \
              && chmod +x /usr/bin/ecs-deploy
      - restore_cache:
          keys:
            - v1-{{ .Branch }}
          paths:
            - /caches/app.tar
      - run:
          name: Load Docker image layer cache
          command: |
            set +o pipefail
            docker load -i /caches/app.tar | true
      - run:
          name: Build application Docker image
          command: |
            docker build --file=docker/app/Dockerfile --cache-from=app -t organization/app .
      - run:
          name: Save Docker image layer cache
          command: |
            mkdir -p /caches
            docker save -o /caches/app.tar organization/app
      - save_cache:
          key: v1-{{ .Branch }}-{{ epoch }}
          paths:
            - /caches/app.tar
      - run:
          name: Push application Docker image to ECR
          command: |
            login="$(aws ecr get-login --region ap-northeast-1)"
            ${login}
            docker tag organiation/app:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/organization/app:latest
            docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/organization/app:latest
      - run:
          name: Deploy container
          command: |
            ecs-deploy \
              --region ap-northeast-1 \
              --cluster app-dev \
              --service-name puma \
              --image xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/organization/app:latest \
              --timeout 300

workflows:
  version: 2
  build-and-generate-doc:
    jobs:
      - build
      - generate-doc:
          requires:
            - build
          filters:
            branches:
              only:
                - master
      - deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - master
  1. master ブランチに対して出された Pull Request が Merge される
  2. CircleCI でテストが実行される
  3. テストが成功すると CircleCI 上からデプロイが行われる
    • コンテナイメージをビルド
    • ビルドしたイメージを AWS ECR にプッシュ
    • プッシュしたイメージを利用するタスクを AWS ECS に作成
      ecs-deploy 任せ
    • 古いコンテナから新しいコンテナに LB 切り替え
      こちらも ecs-deploy にやってもらってる
  4. CircleCI 上実行された Request Spec で自動生成された API ドキュメントを GitHub にプッシュ

コードが Merge されると勝手に確認環境にデプロイされるので、クライアントサイドの開発者からデプロイを頼まれて対応する必要がないし、クライアントサイドの人はいつでも最新の API ドキュメントを GitHub 上で確認できる。 API ドキュメントは手動更新ではなくテストから自動生成されるので、ドキュメントと実際の API の挙動が異なる、というありがちな問題も回避できる。

自分としては結構頑張ったつもりだったんだけど、「それ ECS でやる意味あるの? というか Docker じゃなくて普通の EC2 インスタンスに Capistrano でデプロイするのでよくね?」というツッコミが入った。デプロイフローで CircleCI への依存度が強すぎる、イメージのビルドとデプロイに時間がかかりすぎるし、ちょっとした typo の修正のためにイメージをビルドしたりとかあり得ない、 Docker を使うにしても ECS は使わず、 EC2 で Docker を動かし、コンテナがマウントしたディレクトリに Capistrano でデプロイするべき、という意見だった。このときぐぬぬとなってしまってあまりうまく答えられなかったので考えられるメリットを書き出してみる。

確かに Docker と ECS による環境を構築するのには時間がかかる。デプロイのためにそこそこでかいイメージをビルドしてプッシュするというのも大袈裟だ。加えて Production で運用するとなるとログの収集やデータベースのマイグレーションなど、考えなければならない問題がいくつかある1

ただコンテナベースのデプロイには以下のようなメリットがあると思う。

環境のポータビリティー

まず Ruby や Rails などのバージョンアップが容易になる。手元で試して確認した構成とほぼほぼ同じイメージをデプロイできる。デプロイ前にサーバーに新しいバージョンの Ruby をインストールしたりしなくて済むし、手元ではエラーにならなかったのに本番でエラーになった、というようなケースを減らすことができる。

サーバー構築手順のコード化

人数が少ない会社で専業のインフラエンジニアもいない状況だと Chef や Puppet でサーバーの構成管理をし、複数台あるサーバー群の管理をすることは難しい。 Dockerfile に手順を落とし込み、 Docker さえ入ってたらあとは何も考えなくて良いというのはとても助かる。少なくとも秘伝のタレ化しやすいサーバーの構築手順がコード化され、コードレビューのプロセスに載せることができる

迅速なスケール

AWS ECS のようなマネージドコンテナサービスと組み合わせて使えばスケールアウトが楽ちん極まりない。 AWS マネジメントコンソールか cli で操作するだけで簡単にスケールさせることができる。スケールに際して LB に組み込む前にプロビジョニングしたり最新のコードをデプロイしたりする必要もない。

デプロイ失敗が減る

Capistrano によるデプロイはデプロイ対象が増えてくると SSH が不安定になりデプロイに失敗することが増えてくる。 ECS のような AWS の仕組みに載せることで、イメージを ECR にプッシュさえできれば IaaS 側でよろしくやってくれるというのはとても良い。

以上のようなところだろうか。まだ Production に投入するところまでは持って行けてないので、今の自分の考察が正しいのかどうかをこれから検証していきたい。

関連してそうな記事

📝 CircleCI と autodoc で Rails API のドキュメントを自動更新


  1. いまは先人がいっぱいいるのでログの集約もマイグレーションも情報はいっぱいあると思う 

autodoc-generation-flow.png

autodoc を導入して Rails プロジェクトで Request Spec を書くと自動的にドキュメントが更新されるようにした。 autodoc 自体は前々職の頃から利用していて大変お世話になっていた。ただ最初の頃は手元で AUTODOC=1 bundle exec rake spec:requests して手動でドキュメント更新していた。ドキュメントが更新されるかどうかは担当者の心がけ次第なのでよくなかった。

前職では CircleCI を使っていて、デプロイや Asset Precompile など CI でいろいろやるのが当たり前だったので、 Pull Request が Merge されたタイミングでドキュメント生成するように .circleci.yml をカスタマイズしてた。

いま仕事しに行ってるところでは .circleci/config.yml を version 2 にしていて、 version 2 からは workflow の概念が導入されたので、頑張ってシェルスクリプトで条件分岐させたりする必要がなくなった。 .circleci/config.yml は以下のような感じになってる。

version: 2

shared: &shared
  working_directory: ~/app
  docker:
    - image: circleci/ruby:2.4.1-node
      environment:
        PGHOST: 127.0.0.1
        PGUSER: username
        RAILS_ENV: test
        REDIS_HOST: localhost
    - image: circleci/postgres:9.6-alpine
      environment:
        POSTGRES_USER: username
        POSTGRES_PASSWORD: pasword
    - image: redis:3.2-alpine

jobs:
  build:
    <<: *shared
    steps:
      - checkout
      # Restore bundle cache
      - &restore_cache
        type: cache-restore
        key: app-{{ checksum "Gemfile.lock" }}
      # Bundle install dependencies
      - &bundle_install
        run: bundle install -j4 --path vendor/bundle
      # Store bundle cache
      - &save_cache
        type: cache-save
        key: app-{{ checksum "Gemfile.lock" }}
        paths:
            - vendor/bundle
      # Database setup
      - &db_setup
        run:
          name: Database Setup
          command: |
            sudo apt install postgresql-client
            bundle exec rake db:create
            bundle exec rake db:structure:load
      - type: shell
        command: bundle exec rubocop
      # Run rspec in parallel
      - type: shell
        command: |
          mkdir coverage
          COVERAGE=1 bundle exec rspec --profile 10 \
            --format RspecJunitFormatter \
            --out /tmp/test-results/rspec.xml \
            --format progress \
            $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
      # Save artifacts
      - type: store_test_results
        path: /tmp/test-results
      - type: store_artifacts
        path: coverage

  generate-doc:
    <<: *shared
    steps:
      - checkout
      - *restore_cache
      - *bundle_install
      - *save_cache
      - *db_setup
      # Generate document
      - type: shell
        command: |
          AUTODOC=1 bundle exec rake spec:requests
      - run:
          name: Setup GitHub
          command: |
            export USERNAME=$(git log --pretty=tformat:%an | head -1)
            export EMAIL=$(git log --pretty=tformat:%ae | head -1)
            git config --global user.email "${EMAIL}"
            git config --global user.name "${USERNAME}"
      - run:
          command: |
            git add doc
            git commit --quiet -m "[ci skip] API document Update

            ${CIRCLE_BUILD_URL}"
            git push origin ${CIRCLE_BRANCH}

workflows:
  version: 2
  build-and-generate-doc:
    jobs:
      - build
      - generate-doc:
          requires:
            - build
          filters:
            branches:
              only:
                - master

master ブランチでのビルドのときだけ generate-doc という job が実行されるようになっている( master ブランチ以外のビルドではドキュメント生成がスキップされる)。buildgenerate-doc で重複してる部分が多いので YAML のアンカー機能を使って重複を整理しているけど結構長い。 generate-doc ジョブでドキュメントが生成されると勝手に GitHub の master ブランチに対して push する。このときコミットメッセージに [ci skip] という文字列を付けておけば、 CircleCI はビルドをスキップするので延々とドキュメントの自動更新ビルドが走り続けることはない。

おかげでいまはテストさえ書けば、実際の API と同じフォーマットのドキュメントが自動生成されるのでとても便利になったと思う。

ちなみに JSON Schema というのもあって、これは JSON に仕様を書くとドキュメントやらモックサーバーを作ってくれるものらしい。めっちゃ便利そうだけど、ちゃんと使うのにはそれなりに仕組みを整える必要がありそうで手を出していない。 autodoc の作者の r7kamura さんのブログにも書いてある通り、 autodoc の便利なところは以下だと思う。

実際にアプリが生成した内容からドキュメントを生成するため、実装とドキュメントの乖離が少なく抑えられる。 また、テストを書くことの見返りが増えるため開発者がテストを書くのを推進しやすい。

全てがJSONになる - ✘╹◡╹✘

autodoc で master ブランチへの Merge をトリガーにしてドキュメントを自動生成するというポリシーでは、 B/E 側の作業中に F/E の人が API のドキュメントを見られなくて不便だという問題は確かに存在する。しかし JSON Schema で事前に仕様を固めて実装前にモックサーバーやドキュメントを提供できたとして、果たして事前に決めたとおりに B/E も F/E も実装できるのだろうか。きっと作っていく途中で「やっぱりアレはコレに変えたい」みたいのが双方から出てくると思う。

↑の r7kamura さんの記事では他に外の API をモックするダミーサーバーを JSON Schema で作ったりしてる。確かにすでに仕様が固まった外部の何かをモックするサーバーのセットアップなどには便利なのかもしれない。ただいまのところは autodoc での後追いドキュメント自動生成で自分は事足りるかなという感じがしている。

ECS 化していたブログをさくら VPS に戻した。理由としてはお金が高かった。普段、 S3 と Route 53 に払ってる金額の 20 倍くらいの金額になって少ない小遣いでは払えないと思ったので VPS に戻した。ただ AWS で運用して勉強になった点もあった。

まず ECS での運用について知見が得られたのがよかった。 deploy するためには docker builddocker push が必要で、最初はちょっとした修正のためこれやるのは大げさだと思ったが、慣れればそんなでもなかった。ただイメージのビルドをどこでやるかは考えといた方がよさげ。自宅だと高速な光回線があるからよいけど、実家やどこか旅行に行ったときに image をビルドして ECR にプッシュするのはつらい。 GitHub に git push する度に CircleCI でビルドされるような体制を構築する必要があると思った。

CloudFront をウェブアプリケーションの前段に挟む、というのもやってみたけどこれもよかった。 Rails なら Asset Precompile によって静的なファイルへのリクエストはウェブアプリケーションまで届かないようにするのを良くやると思うけど、 Lokka は Sass や CoffeeScript を動的にコンパイルしてクライアントに返すので、 CSS や JS などへのリクエストにも puma のプロセスが消費されてエコではなかった。 VPS 運用時には Nginx を前段に入れて、画像やコンパイル済みの JS / CSS ファイルへのリクエストは puma にプロキシせず直接返していたが、 ECS 化したときに Nginx を挟まなくなったので CloudFront を入れて画像や JS / CSS へのリクエストをキャッシュするようにしてみた。

cloudfront-ecs-image.png

すると puma の負荷が低下し、 New Relic の Appdex が 0.95 前後だったのが 0.99 になった。 ECS で利用していた EC2 コンテナは t2.micro でしょぼいのでサーバーのスペックアップで改善されたわけではない。ということで VPS に戻すときにも Nginx の proxy_cache を使って Sass や CoffeeScript から動的にコンパイルされる CSS / JS をキャッシュするようにしてみた。さすがに CDN のような配信の最適化は実現できないが、以前よりかはかなりましになるはず。

なおセッションを有効にした Rack アプリで Sass や CoffeeScript を動的にコンパイルすると Set-Cookie ヘッダーがセットされてしまう。 Nginx の proxy_cacheSet-Cookie ヘッダーがセットされてるとキャッシュをしないので、

proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;

などとしてやる必要がある。

今後仕事でも ECS 化を行う予定があるので得られた知見を有効活用していきたい。

docker-on-ecs.png

ブログを Docker 化して AWS ECS で運用するようにした。

なぜ Docker 化したか

  • 仕事で Docker を使う機会が増え知見がたまってきた
  • 仕事では Production 投入はできていないので個人ブログで Production 投入して知見を得ておきたかった

どうやったか

ローカルセットアップ編

  • Dockerfile & docker-compose.yml を作成した
    • Alpine Linux を使ってなるべくイメージを小さくする
  • Gem::LoadError 問題
    • Lokka の Gemfile には動的な読み込みを行っている部分があるため、 Dockerfile で単純に COPY するだけでは Gem::LoadError になってしまう。
      • Lokka のプラグインは Gem 化されておらずリポジトリ内に含める形式
      • プラグイン側で必要な gem はプラグイン内に Gemfile を配置して宣言する形式
      • Lokka 本体の Gemfile には Dir["public/plugin/lokka-*/Gemfile"].each {|path| eval(open(path) {|f| f.read }) } のようなコードがあって強引に eval で内容を取り込んでいる
    • 対策
      • Gemfile.docker を用意する
      • Gemfile.docker を生成するためのシェルスクリプトを用意して実行する
      • Dockerfile の COPY は以下のようにする
      • COPY Gemfile.docker /app/Gemfile
  • 他、 MySQL のコンテナを追加して手元でアプリが起動するところまでは確認済み

Production セットアップ編

  • ECR にリポジトリを作成し image を push (公式のチュートリアル通りにやればできる)
  • ECS にサービスやターゲットグループ、タスクの作成なども指示通りに行う
    • 土台となる EC2 インスタンスは手動で作るのではなく、 ECS の画面でポチポチやると勝手に作られる
    • 詳細コンテナ設定でエントリポイントを入力する欄に、 Dockerfile と同じように文字列で書いていたらコンテナが起動せずハマった
      • カンマ区切りで書かないといけないらしい
      • puma を起動したかったら bundle exec puma ではなく、 bundle,exec,puma というように書かないといけない スクリーンショット 2017-08-19 10.50.09.png
  • 諸々設定を済ませたらロードバランサー( ALB )を EC2 のパネルで作成してターゲットに Docker コンテナが動いている EC2 インスタンスを指定する
    • ECS の用語やサービス構成に慣れるのに時間がかかるが、歯を食いしばってがんばるしかない
  • DB に関しては RDS を使うことにした
    • 稼働中の VPS サーバーで mysqldump -ufoo -p db_name | mysql -ubar -p db_name -h foo.bar.ap-northeast-1.rds.amazonaws.com みたいな感じで雑に流し込んで移行する
  • ECS は VPC でしか使えないので、 VPC に慣れてない人は VPC に慣れるところから頑張るしかない
  • セキュリティグループの設定なども必要になるので頑張って下さい
  • Nginx を利用しないので SSL の復号を ALB で行う必要がある
    • ACM で無料で証明書を発行できるのを知らず、 Let's Encrypt の証明書を取り込んで使う
  • ここまでで一旦公開

運用して気づいた問題点

  • サイトが 503 や 504 になる
    • Docker コンテナがすぐ死ぬ
    • ALB から切り離されることしばしば
    • VPS 時代は Nginx に静的ファイルの配信をまかせていたが、 Nginx を挟まなくなったので puma が担当することになりアプリの負荷が高まったのではと推察
      • CloudFront を挟んでいい感じにキャッシュしてもらい、静的ファイルの配信は CloudFront にまかせることに

CloudFront 導入編

  • ALB で使っているのとは別に SSL 証明書を取得する必要がある
    • CloudFront <-> ALB 間の通信を HTTPS で行うため
    • Route53 で ALB に割り当てている A レコードをサブドメイン付きの別のものに変更
    • ALB 用にはワイルドカード証明書を使う(無料で証明書取得できる ACM 最高)
    • Let's Encrypt の証明書を使うのはやめ、ルートドメインの証明書も ACM で取得して CloudFront に設定
  • 動的コンテンツ( HTML など)はキャッシュしないようにしないといけない
    スクリーンショット 2017-08-19 11.32.04.png
    • 当初、設定がうまくいっておらず、以下のような問題が発生
      • POST, PUT, DELETE できない
      • Cookie が origin に転送されずセッションが維持できない
      • クエリストリングが無視されてしまい、ページ検索などができない

所感

  • 体感的にサイトの読み込みがチョッパヤになった
  • CloudFront 導入したが、まだ 503 にはなる
    • そもそもインスタンスを良いやつに変えないとダメなのかもしれない
    • タスク数を増やしてクラスタリングするなどいろいろ試してみる
      • クラスタリングするためには Cookie セッションではなく Redis や Memcached などをセッションストレージに使う必要が出てくる…
  • Deploy だるい問題
    • cap deploy しなくなり、イメージをビルドして push する感じになる
      • Alpine Linux でもそこそこイメージサイズはでかくなるので貧弱な回線では docker push にめっちゃ時間かかる
    • ECS 側でもサービスを更新するなどの作業が発生
      • Blue / Green Deployment できるがポチポチ作業が発生するのがだるい
      • Rails を運用する場合は migration なども発生するのでうまいことやる必要あり
    • git commit しなくても作りかけのコードの状態で docker-compose build してしまいがちになり、リポジトリのコードと動いてるコンテナイメージの間に差分が発生してしまいそう
      • ちゃんと CircleCI などを導入してイメージのビルドとプッシュは CI サービスでやる、というような運用にしないと破綻しそう
  • 手順書問題
    • こんな風にブログを書いて雑な手順書を作成するようではいけない。 Terraform 化しないと破滅する。
  • Lokka は CMS for Cloud です
    • git push heroku master するだけで使えることが売りの Lokka を AWS のガチな構成で運用するという皮肉
  • お金高い
    • 毎月 3000 円くらいかかる感じになりそう。 VPS は年払いで 16000 円くらいなのでだいぶ高い。払えなくなったら VPS に戻しそう。

謝辞

r7kamura さんの amakan Docker 化の一連の記事と Classmethod 社の ECS 関連の記事には大変お世話になりました。