| @Mac/iPhone

Soulver と Calca というのを去年自分が作った Advent Calendar で書いた(ただし締め切りを二ヶ月過ぎてから)。数字混じりのテキストを入力するといい感じに内容を読み取って計算してくれるソフトの紹介記事だった。

Soulver は便利でよく使っているのだけど、何か計算をしたいときに一々新規ドキュメントを作成しなければならないのが煩わしい。結果を保存しなくていいような、本当にチラシの裏で筆算してあとは捨ててもいいような手軽な計算ができないものかと思っていた。例えば Alfred を起動してさっと計算するような。

実は Alfred にも計算機能はある。ランチャーウィンドウを呼び出して数字を入力すると計算してくれる。

Alfred Calculator

ただ Soulver にあるような 1 USD in JPY と入力して 113 JPY と通貨換算してくるような機能はない。ネットでデジタルコンテンツをコンシュームしたり海外の通販でブツをゲットしたりするデジタルネイティブの皆様におかれましては USD 決済が基本であり、お金を払う前に「日本円にしたらいくらくらいかな?」と考えることはよくあると思う。そういうときにさくっと Alfred で通貨換算してくれる方法があったら便利ですよね。あるんですよそういうのが。

Currency Converter\.alfredworkflow
http://cl.ly/MBJ8

ちょっと URL が怪しいけど CloudApp というサイトからダウンロードできる。中身はシェルスクリプトで、例えばドル円の換算だと次のようなシェルスクリプトが実行される。

AMOUNT={query};

RESULT=`curl -s "http://finance.google.com/finance/converter?a=$AMOUNT&from=USD&to=JPY" | awk '/<span/{print}' | sed -e 's/<[^>][^>]*>//g' -e '/^ *$/d'`

echo '<?xml version="1.0"?><items>
            <item uid="convert" arg="'$RESULT'">
                <title>'$RESULT'</title>
                <icon>icon.png</icon>
                <subtitle></subtitle>
            </item>
        </items>'

Google の Currency Converter サービスに curl で問い合わせて結果を Alfred で表示できるように XML に整形しているだけである。

たとえば USD 58 と入力すると以下のように日本円でいくらになるかを表示してくれる。

Currency Converter USD

ここで Enter を押すとクリップボードに結果を保存してくれる。

Notification

なお Google の Currency Converter の URL が最近変更されたため↑のリンクのやつをダウンロードしてインストールしただけでは動かないと思う。 curl する URL を変える必要がある。よくわからん人は僕が貼ってるシェルスクリプトをコピーして 「Alfred Preferences」 -> 「Workflows」 -> 「Currency Converter 」と進み、 Script Filter にペーストするとよい。

Alfred Workflow Preference

Alfred Workflow Script Filter

curl する URL のクエリパラメーターを調整することで EUR -> JPY や話題の BTC -> JPY の変換もできる。仮想通貨の相場が気になって気になって仕方がない方などは Alfred で効率的に計算してるふりしてレートが調べられて便利だと思うので是非お試し下さい。


この記事は Alfred 3 Advent Calendar 2017 - Adventar 7 日目の記事でしたが一日遅れで書いてます。 8 日目は誰も登録していません。


追記 2018-06-25

コメント欄で @gnrr さんにご指摘いただいてますが、 Google の通貨変換 API が機械的なアクセスを拒否するようになり、この記事に書いてあるやり方では通貨換算できなくなりました。 @gnrr さんが作成された以下の Alfred Extension だとこの機能が復活しますのでお困りの方はご利用下さい。

| @散財

Anker 製品には全幅の信頼を寄せているのだけど、購入したイヤフォンが立て続けに 1 年もたずに壊れてしまって安物買いの銭失い感を味わった。

↑で買った Anker のイヤフォン、イヤービースが外れやすくなってていつかなくなるだろうなぁと思ってたけど今日なくなった。 Anker のイヤフォンは二代目で、値段が安いわりに音はまぁまぁ良いから気に入っていた。二代目のやつはアーム式でジョギングでも快適に使えてた。ただ耐久性に難があると思う。先代は micro USB の充電ポート周辺のプラスチックが割れてぶっ壊れたし、イヤーピースも空港で外れてなくなった。イヤーピースは特殊形状なので他の部品を買って取り付けるわけにもいかないのが困る。値段や音質、機能には満足してるんだけど、もうちょい高いやつを買って使った方がよいのかなぁという気がする。

| @技術/プログラミング

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 に投入するところまでは持って行けてないので、今の自分の考察が正しいのかどうかをこれから検証していきたい。

[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 での後追いドキュメント自動生成で自分は事足りるかなという感じがしている。

| @労働

プログラマーの種類、いろいろあると思う。

  1. ハッカー
    プログラミングが楽しくて、コードさえ書ければあとは何でもいいという人。
  2. OSS プログラマー
    1 と似てるけど、 OSS が楽しくてコードを書いてる人。カンファレンスで登壇したり、技術ブログを熱心に書いたりする。
  3. プロダクト指向プログラマー
    開発だけでなく事業の方向性にも口を出したい人。社長が死んだらいつだって代替ができる、というような気概の人。
  4. サラリーマンプログラマー
    プログラミングは仕事のためと割り切っていて、余暇には一切コードを書かない人。

1 〜 4 のどれか一つに綺麗に分類できるというものではなく、 1 かつ 3 とか、 3 かつ 4 とか、複数にまたがる人もいる。

職業プログラマーになりたての頃、サラリーマンプログラマーしかいない環境で働いて早くそこを抜け出したいと思っていた。プログラマーは会社の最下層で、プロジェクトマネージャーや営業が偉かった。プログラマーの人たちは仕事のために仕方なくプログラミングしてた。

それが嫌で余暇でもコード書くような人たちがいる職場に移ったけど、そこではハッカーや OSS プログラマーがよいプログラマーの代表とされていた(と思う)。自分もそういうのを目指し OSS カンファレンスに登壇したり技術的に中身のあるブログ記事を書いたりしたいと思ったけど、なかなかよいコードは書けないし、結婚して所帯を持つと町内会の草むしりや廃品回収、町内の運動会のテント設営、審判にかり出され、またあるときは子どもの幼稚園に夫婦そろって呼び出されて「お前らは夫婦そろって人間の屑以下だ、心を入れ替えるか幼稚園を辞めるかどっちか選べ」と説教されるなど多忙を極め、勉強会で話したりブログ記事を書いたりということが難しくなった。

一時期は自分は凡人プログラマーなんだろうなぁ、かつて忌み嫌っていたサラリーマンプログラマーと同じなのかなぁ、と将来を悲観していた。この頃は技術的な伸びしろがなくなって学ぶことに対する意欲が失せてしまっていた。

ただ、仕事で失敗プロジェクトを担当してつらい目に遭うことが多く、チームで仕事することだとか、失敗からどう学ぶかとか、その手のことに関してはそこそこ知見が貯まっていった。どうすればチーム開発はうまくいくのか、どういう風に情報を共有すればいいのかなどなど。業務改善のためにちょっとしたスクリプト書いたり、ぶっ壊れて動かなくなってる hubot スクリプトを直したり、 CI のセットアップを頑張ったりと、エンジニアチーム全体の生産性を上げるような落ち穂拾い業務みたいなタスクも好きだった。

いまは自分はチーム開発プログラマーなんだろうなぁと思う。ハッカー気質は皆無じゃないけどそこそこ、 OSS へのコントリビュートも気が向いたときに、プロダクトのことを気にする気持ちもある。どれも中途半端で単独では生産性高くないけどチームで仕事をするときに効能を発揮するタイプ。こんな風に自分が適している役割が分かると仕事がやりやすくなるし、目指すべき方向性のようなものが明確になって将来のことを考える上でも役に立つ。

ベンチャー企業で働く場合は自分のマインドやスキルセットが会社のステージにフィットすることも大事だと思う。もともとはベンチャー企業でも成功して上場しているような会社だとデプロイ時にハンコリレーなどがあってやるべきことをやるべきときにババッとやるのが難しい。そういうのは自分には向いてない。一方で起業したばかりの小さなスタートアップも、自分のような最初から綺麗なコードを書こうとするプログラマーはきっとフィットしない。起業したばかりのスタートアップでは自分たちの事業が社会から求められるかどうか不透明なので、そんな状態でテストがどうだとか CI がどうだとかチーム体制がどうだとかを言うようなプログラマーは必要ない。汚くてもクソコードでもテストコードなんて存在しなくてもいいからとにかく金を生み出すコードを素早く書ける人が向いている。

その意味でいまいる会社は創業当初の価値仮説の検証は済んでいて、これからいかに事業を伸ばしていくかというステージに入っているので、ちゃんとテストを書いたり、 CI を導入して継続的インテグレーションに努めたり、デプロイをいい感じにしたり、情報共有ツールを導入したり、開発チームの組織体制を云々したりといった部分で価値を発揮しやすい。

一握りの本当に優秀なソフトウェアエンジニアってのは受託会社だろうが事業会社だろうがベンチャーだろうが大企業だろうがどこにいたって価値を発揮できるのかもしれない。しかし自分のようなすべてにおいて中途半端なプログラマーは、自分に何ができるのかを理解し自分を必要とする規模とステージの会社を見つけてそこの門を叩くのがよいと思う。もちろん、事業に共感できるとか、チームメンバーとの相性も大事だけど、事業に共感できてもチームのメンバーがいい人ばかりでも、会社の規模やステージが自分を必要としなければ価値を発揮することは難しい。

関連してそうな記事

| @技術/プログラミング

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 関連の記事には大変お世話になりました。