Rails アプリケーションを Fargate に移行した

これが

f:id:kirikiriyamama:20200630225315p:plain

こうなった

f:id:kirikiriyamama:20200704190805p:plain

インフラ構成

リバースプロキシをなくした

リバースプロキシを採用する場合、主な役割としては以下が想定される。

  1. SSL/TLS 終端
  2. リクエストのキューイング
  3. 静的ファイルの配信

このうち 1 と 2 については ALB で代替することができる。現に移行前の時点でこれらは CLB でも行われていた。また 3 については CDN のほうがよりうまく扱えるはずだ。

そこで、リバースプロキシ (今回は nginx) でやっていた処理を Rails に移植してリバースプロキシをなくすことにした。Rails でやることは増えるが、構成をシンプルにできるメリットのほうが大きいと判断した。また、複数のコンテナを協調させる難易度が高いということも理由として挙げられる。

移植した処理内容については後述する。

静的ファイルはコンテナに含める

静的ファイルを S3 等のストレージから配信する場合、ポータビリティの低下といった問題が挙げられる。アプリケーションが Docker イメージだけで完結しなくなり、またそれに伴いデバッグが複雑になってしまう。そこで、CloudFront を前段に配置し、静的ファイルは Rails のコンテナに持たせることにした。Rails で静的ファイルを配信することのパフォーマンス面については後述する。

CloudFront は Rails とは別ドメインにする。Railsexample.com とすると CloudFront が assets.example.com というような構成だ。静的ファイルは assets.example.com を介して、その他は example.com から取得する。このとき CloudFront のオリジンは example.com になる。全てのリクエストを CloudFront 経由にするパターンもあるが、CDN の設定をなるべくシンプルにしておきたいことから採用はしなかった。少し話が逸れてしまうがポータビリティの観点から CDN はコンテンツ配信に専念させたいと思っていて、アプリケーションの動作に CDN の機能が必須になってしまうような状況は避けたい。

一応、動的ページに CloudFront 経由でアクセスされることも想定されるが、RailsCache-Control: max-age=0, private, must-revalidate を返す (つまり CF にはキャッシュされない) ため特には問題にならないと考えている。この場合、検索エンジンにクロールされると重複コンテンツと判定される恐れがあるため view に <link rel="canonical"> を追加しておく。

また、静的ファイルを別ドメインから配信する場合は CORS を有効にする必要がある。今回は rack-cors を用いた。

リバースプロキシでやっていた処理の移植

静的ファイルの配信

CloudFront を前段に配置することを前提に、以下の要件で配信をしたい。

  • /assets 以下はキャッシュする (fingerprint があるため)
  • それ以外は更新がないか常に確認させる

Rails 標準の仕組み (config.public_file_server) ではリクエストパスに応じて Cache-Control を制御するといったことができず、要件を満たすことができない。そのため config.public_filer_server を参考に Rack middleware を作成した。

class Rack::MyStatic
  def initialize(app, root: 'public')
    @assets = ActionDispatch::Static.new(
      app, root, headers: { 'Cache-Control' => 'public, max-age=315360000' }
    )
    @public = ActionDispatch::Static.new(
      app, root, headers: { 'Cache-Control' => 'no-cache' }
    )
  end

  def call(env)
    req = Rack::Request.new(env)

    case
    when req.path.start_with?('/assets/')
      @assets.call(req.env)
    else
      @public.call(req.env)
    end
  end
end

Rails へのリクエスト数は増えるが、静的ファイルの大多数を占める /assets 以下は CloudFront でキャッシュすることができる。実際、この変更の前後に New Relic を確認したところ、特にパフォーマンスに影響はなかった。

また、移行前の構成では nginx が gzip 圧縮をしていたがこれは CloudFront で行う。

www ドメインを naked ドメインにリダイレクトする

www.example.comexample.com にリダイレクトするやつ。rack-rewrite を用いた。

Dockerfile

base と呼ばれる、依存パッケージや gem をインストールしたイメージを起点に、development や production のイメージを作成する。イメージを共通にしたいところではあるが、development と production は必要になるパッケージも異なるため完全に共通にすることは難しいと考えている。例えば development では E2E テストに使用するため Chrome が必要になる。

また、今回移行したアプリケーションには RAILS_ENV=staging の環境が存在しているのだが、staging と production でも Dockerfile を分けている。CloudFront から静的ファイルを取得するために config.action_controller.asset_host を指定していて、その値が静的ファイルに埋め込まれるためだ (つまりRAILS_ENV によって静的ファイルに差分がある)1。これはなんとかしたいと思っている。ジャストアイデアだが全てのリクエストが CloudFront を介するようにすれば asset_host を指定する必要もなくなるし RAILS_ENV=staging もなくせるのかもしれない。

CI/CD

CircleCI をつかっている

テスト

先述の development のイメージを用いてテストを実行している。都度イメージをビルドしているのだが、ここで考える必要があるのがキャッシュによるビルド高速化だ。今回は CircleCI の Docker Layer Caching を採用した。正直高いとは思うが、手っ取り早い。また、DLC に加えてビルドしたイメージを ECR に push/pull している。DLC は状況によっては効かない可能性があり (例えば大量のブランチを同時に push した場合など2)、保険としてビルド済みのイメージを --cache-from に渡している。イメージの push はブランチ単位で行い、pull はカレントブランチか存在しなければ master のイメージで行う。CircleCI の save_cache/restore_cache に近いイメージだ。

docker image push name:current
docker image pull name:current || docker image pull name:master
docker image build --cache-from name:current --cache-from name:master

このキャッシュ戦略はあくまでレイヤーキャッシュを用いているため、 以下のような Dockerfile で Gemfile に変更があった場合、以降のレイヤーではキャッシュを使うことができず bundle install をやり直すことになってしまう。

COPY Gemfile Gemfile.lock ./
RUN bundle install

この問題は、Buildkit の Cache Mount をつかって gem のキャッシュをすることで軽減できる気がしている。

デプロイ

https://github.com/eagletmt/hako を使っている。db:migrate のような処理は hako oneshot (RunTask) で実行する。

hako の環境は Docker イメージで用意していて3、これにリポジトリに存在する hako の definition file を渡して実行する。順当にやるとボリュームをマウントすることになると思うが CircleCI の docker executor では使えないため4、Dockerfile を用意して COPY するか docker cp をつかって回避する。

秘匿値

SSM Parameter Store で管理していて、Task Definition の secrets パラメータを用いて環境変数に展開している。

rails console

Fargate の場合、SSH ログインができないため rails console 等つかうことができない。そもそも本番環境で rails console なんてつかわないほうがいいとは思うが、やんごとなき理由で rails console が必要なときのために SSM Session Manager (エージェントを介してシェルアクセスを提供する機能) で Fargate インスタンスにログインできるようにした。大まかな流れは以下となる。

  1. SSM Activation を作成する
  2. hako oneshot で ECS Task を作成する
  3. aws ssm start-session でログインして作業をする
  4. 後処理
    • ECS Task の終了
    • SSM Managed Instance の削除
    • SSM Activation の削除

SSM Agent はイメージに含めていないためアドホックにインストールする。今回は ECS Task を作成する際に CMD に SSM Agent をインストール/起動するスクリプトを指定するようにした。

注意点として、SSM Activation は毎回作成する必要がある。Activation には有効期限を設定しなければならないのだが、設定できる値は最長で 30 日後となっている。つまり Activation を 30 日以上使いまわしたい場合は再作成の必要がある。期限の管理をするのも大変なので都度作成するようにしたが、もっといい方法がある気もしている。

余談だが Session Manager は Session の出力内容 (操作ログ) を CloudWatch Logs や S3 に転送することもできる。script(1) を使ってやっていたようなことが簡単に実現できて非常に便利だ。

ログ

CloudWatch Logs に転送している。ログを確認するときは CloudWatch Logs Insights や https://github.com/knqyf263/utern5 を使う。

終わりに

移行にあたっては、Route53 の Weighted Routing をつかって新環境に段階的にトラフィックを流すようにした。いわゆるカナリアリリースである。幸いにも移行は成功して、その後もトラブルなく運用できている。

Fargate というよりはほとんど Docker と ECS の話だったが、なにはともあれ Fargate によってインスタンスの管理から解放された。

Rails に初コントリビュートした

Rails::Application#config_for merges shared configuration deeply by kirikiriyamama · Pull Request #37913 · rails/rails · GitHub

うれしい。


Rails::Application#config_forRails.env に応じて YAML ファイルを Hash として読み込むメソッドである。

# config/example.yml
development:
  key: val
config_for(:example)[:key] #=> "val"

また、各環境を通して共通の設定を記述することができる。

# config/example.yml
shared:
  foo: foo
development:
  bar: bar
config_for(:example) #=> {:foo=>"foo", :bar=>"bar"}

これまで、この共通値は shallow merge されていたが、これを deep merge するようにした。


非互換な変更になるため、PR にあたってはユースケースベースで話すことを意識した (表参道.rb でめっちゃ相談した、各位ありがとうございます)。 具体的には他の gem を使って設定値の管理をしているが Rails Way に移行したい、また共通の設定値が深くネストしているみたいな話をした。

あと念のためオプションで挙動を切り替えることも提案したが、特に言及されることなくスッとマージされた。

YAMAHA WLX202 のバグを踏んだ

経緯

Wi-Fi の調整をしていて、そのときは問題ないのだが、ある程度の時間をおくと 5 GHz 帯だけ繋がらなくなる現象が発生していた。2.4 GHz を捨てようとしていたので 5 GHz が落ちるとわりと困る。AP を再起動するとなおるからしばらくはそれでお茶を濁していたのだが、ある日、一応ファームウェアのリリースノートを確認しにいくとこんな記述が。

PMF機能が有効な5GHz 無線インターフェースで、PMF対応の無線端末が以下のいずれかの動作をすると無線出力が停止してしまうことがあるバグを修正した。
- 同一サブネットに所属するAPにローミングしたとき
- WPA-EAP (RADIUS認証)を使用する環境で、RADIUSユーザーのセッションタイムアウト発生による再認証が失敗したとき

http://www.rtpro.yamaha.co.jp/RT/relnote/ap/Rev.16.00/relnote_16_00_09.txt より

PMF機能が有効な5GHz 無線インターフェースで、接続している無線端末との通信が300秒以上行われなかったことによる無線端末切断が行われたとき、無線出力が停止してしまうことがあるバグを修正した。

http://www.rtpro.yamaha.co.jp/RT/relnote/ap/Rev.16.00/relnote_16_00_10.txt より

これやんけ!!

ということでファームウェアを更新して解決した。まさか AP のバグとは思わないし、ある程度の時間をおかないと現象が発生しないということも相まって解決するまでにだいぶ時間を要してしまった。

まとめ

ファームウェアはちゃんとあげよう

オフィスのリモート会議の環境を整えた

これまで Revolabs FLX UC 500 をつかっていたが、人数が増えたり (10+) 会議室が広くなるにしたがって部屋の端にいる人たちの声を拾いにくくなってしまった 1。あと諸々あって以下の機材を導入した。

YAMAHA YVC-1000 (スピーカーフォン)

https://sound-solution.yamaha.com/products/uc/yvc-1000/index

この製品はマイクを 5 台まで数珠つなぎにすることができる。とりあえず 2 台で運用しているが 10 人強の会議ではまったく問題なく、将来もっと人数が増えたときにはマイクを増設することもできる。

Logicool PTZ Pro 2 (カメラ)

Logicool PTZ Pro 2 Video Conference Camera & Remote

リモコンでパンやチルト、ズームすることができる。

Zoom (ビデオ会議サービス)

Video Conferencing, Web Conferencing, Webinars, Screen Sharing - Zoom

機材ではないけど。他のサービスに比べて品質がよく感じる2。あと Speaker View がとても気に入っていて、これはなにかというと参加者全員の映像を同じサイズでタイル状に表示する機能だ。Zoom 以前は発言者のみが大きく表示され、発言者以外の様子がわからないという問題があった。

Shure SM58SE (ボーカルマイク)

www.shure.co.jp

YVC-1000 は音声入力端子 (RCA) を備えており、マイクを接続すると本体スピーカーで拡声しつつ Zoom でリモート参加者にも配信することができる。全社会議のような大きな会議で使用している。

余談だが、スイッチ付きのモデルにしたところ話者が知らないうちにスイッチを切ってしまうことが多々あり、慣れない人が使う場合はスイッチ無しにしたほうがいいのかもしれない。

Shure QLXD24/SM58-JB (ワイヤレスマイク)

www.shure.co.jp

大きな会議の質疑応答などで前まで出てきてもらうのは大変なので、ワイヤレスマイクを受け渡すようにした。

選定にあたっては、B 帯 (800 MHz 帯) のデジタルの機器で探すことにした。まず周波数帯だが、今回のようなユースケースでは B 帯と 2.4 GHz 帯の二択に絞られる3。2.4 GHz 帯は Wi-Fi 等と干渉する可能性があり、IT 企業においては厳しいと判断した。また、アナログは混信やノイズがはいりやすいといった問題がある4

ちなみに、YVC-1000 の音声入力はステレオなので R と L があり、それぞれにマイクをつなぐことで 2 本まではミキサーなしでミキシング (?) することができる。今回は有線マイクと無線マイクをこの方法で接続した。強引な気もするが、YVC-1000 のマニュアルに書いてあるのでそういうものらしい。


  1. この規模になるまではまったく問題なくて、とてもいい製品だった

  2. 具体的には誰かの回線が乱れたときに全員が巻き込まれないで済むとか

  3. DECT (1.9 GHz 帯) とかもあるにはあるが

  4. アナログのメリットは安いこと

Slack でチャンネルが作成されたら通知する

いつの間にか新しいチャンネルがつくられていること、ままある。

仕組み

  • Slack の Events API (Event Subscriptions) で channel_created を GAS に通知する
  • GAS で Slack に投稿する

同じやりかたで emoji を通知したりもできる。

GAS のスクリプト

var webhookUrl = 'https://hooks.slack.com/services/xxxxx/xxxxx/xxxxx';

function doPost(e) {
  var data = JSON.parse(e.postData.getDataAsString());
  
  if (data.type === 'url_verification') {
    var content = { 'challenge': data.challenge };
    return ContentService.createTextOutput(JSON.stringify(content)).setMimeType(ContentService.MimeType.JSON);
  }
  
  if (data.type === 'event_callback' && data.event.type === 'channel_created') {
    var payload = {
      'text': 'New channel <#' + data.event.channel.id + '> was created!'
    };
    UrlFetchApp.fetch(webhookUrl, {
      'method': 'post',
      'contentType': 'application/json',
      'payload': JSON.stringify(payload)
    });
  }
}

同僚と開発環境共有会をした

弊チームは全員がリモートで働いていて、日常的に同僚の開発環境を見ることがないので、極稀に発生するメンバーが物理的に集う機会に開発環境の共有会 (という名の自慢会) をした。

git push --force の代わりに --force-with-lease を使うと安心ということを学んだ。あと、VSCode と RubyMine が便利そうだった、多分 Vim 断ちできないんだろうけど。

自分が提供できた情報は以下。

tmux のウィンドウ名を自動でカレントディレクトリ名にする

set-window-option -g automatic-rename on
set-window-option -g automatic-rename-format "#{?pane_in_mode,[tmux],#{b:pane_current_path}}#{?pane_dead,[dead],}

Vim の 0 レジスタを覚えると便利

Vim でコピペするときの Tips - 反省はしても後悔はしない

C-{p,n} で入力中の文字列からはじまるコマンド履歴を表示する

autoload -Uz history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey '^P' history-beginning-search-backward-end
bindkey '^N' history-beginning-search-forward-end

macOS のドキュメントビューア

Dash for macOS - API Documentation Browser, Snippet Manager - Kapeli

2018年

家のネットワークを Cisco にした

AP が1台しかないのになぜか WLC がある。

キャンプにいくようになった

某アニメの影響。3月末に飯能で初キャンプに挑むが寒すぎて全く眠れず太陽のありがたみを知る。

f:id:kirikiriyamama:20190110000600j:plain
成田でキャンプしたときの様子

結婚した

紙切れ一枚で人の名字が変わってビビる。

こじんまりと、親兄弟だけで式も挙げた。以前から一緒に暮らしていたので籍をいれても特になにも変わらなかったのだが、式を挙げたら結婚した実感がわいてきた。挙げる前は正直めんどくさかったけど、親も喜んでいたし結果としては挙げてよかった。

新婚旅行にいった

フィンランドエストニアにいった。

3日で洋食に飽きてフィンランドの和食を探し始める。フィンランドの和食事情にとても詳しくなった。

食洗機を買った

最高。みんな買ったほうがいい。

RubyKaigi でスタッフをした

ネットワーク班で、無限にケーブルの敷設などした。Wi-Fi は筋肉。

1000 assoc を超える規模のネットワークに携わることができて楽しかった。

f:id:kirikiriyamama:20190110205522j:plain
無限はんぺん

転職した

スマービー という、ママ向けの EC サイトを運営する会社に転職した。

フルリモートで働いているのだが、コミュニケーションが基本的にオンラインで完結するようになっていて感動した。これで場所に縛られない生活になったのでそろそろ田舎に引越したい。

Apple を見限った

これ以上ハードウェアで振り回されたくないので (e.g. Touch Bar)。PC は Ubuntu on ThinkPad にした。

総合的な体験でいえば Apple のほうがよかったけれど、ThinkPad のハードウェアや Linux デスクトップ環境はとても気に入っている。

友人の結婚式でアプリをつくった

高専時代の友人が結婚した。彼らしい、とてもいい式だった。

何人かで結婚式で撮った写真を共有するためのウェブアプリをつくった。結婚式の写真、各々が撮ってそれで終わりなイメージがあるので、みんなが撮った写真を集めて届けることができてよかった。

少しだけ技術的な話をするとサーバは DB に状態を持たせないことを意識してつくった。なんかちょっとこじらせた設計になった気もするけど趣味コードということでひとつ。

github.com