はてなブログをECSに移行してリリース頻度も改善した話

この記事ははてなエンジニア Advent Calendar 2022の26日目のエントリです。

こんにちは id:cohalz です。はてなブログでは2022年7月にインフラをAmazon EC2からAWS ECS(AWS Fargate)に移行するプロジェクトが完了しました。

プロジェクトは2021年9月から始まったので約10ヶ月間という大きなプロジェクトでした。

プロジェクト完了までに行ってきたことのうち、特に面白かったところなどをこの記事で実施した順に振り返ってみます。

はてなブログのインフラのこれまで

はてなブログはEC2上でchefによるプロビジョニングを行い、デプロイはCapistranoを利用していました。

これらは社内に古くからある仕組みを利用していましたが、古くなっているゆえにチーム内にメンテナンスできる人が少なくなってリードタイムが長くなったり、オートスケールに対応していなかったりと使いづらくなってきました。

稼働している言語やミドルウェアのバージョンは数年前に一度大きく上げていたためさほど古いわけではありませんでしたが、バージョンアップにかかる作業は大変であるため放置されがちだったため、ここを将来的に改善しやすくする意図もありました。

とは言え今回の変更では差分を少なくし移行時に変わる部分を減らすことを優先してバージョンアップは行いませんでした。

はてなブログチームで運用しているサービスはいくつかありますが、はてなブログ本体のみECSに移行できていない状態でした。

過去の移行のうち、はてなフォトライフの移行に関してはエンジニアセミナーで発表を行なっていますのでそちらもご確認ください。

アプリケーションを動かせるようにする

ALBを追加する

まず最初にDockerfileをWebアプリケーション向けに調整した後、実際にECSで検証環境で動かすようにしました。

またこのタイミングでプロキシ(nginx)やvarnishとアプリケーションへの間にALBを追加しました。

追加前後の構成は下の図のようになります。(移行に関係ない部分は省略しています)

移行前の構成

ALB追加後の構成

ALBではなくCloud Mapなどを使うというのも考えられましたが、EC2でもALBのリスナールールを使って加重ルーティングを行えるようになる他メトリックも取れるようになるのでこちらを採用しました。

また、今まではアプリケーションサーバの入れ替えはプロキシの設定ファイルの変更が必要で手間だったのがターゲットグループの変更だけで済むようになECS移行する前からのメリットもありました。

しかしALBはHTTPのロードバランスをする関係でいくつかヘッダを設定するため、元々設定していた X-Forwarded-ProtoX-Forwarded-For などのヘッダをALBが上書きしまうことによって動作しなくなるということがありそのままでは動きませんでした。

docs.aws.amazon.com

これを回避するためにプロキシからは別名のヘッダで送るようにしアプリケーションもそのヘッダを見るような変更をする対応をしました。

検証環境を用意だけしておく

staging環境とは別の環境が存在しており、そちらと同等の環境をECSで別で作成しました。

この段階でECSでのリリースフローも既存のECS化されたサービスに合わせて構築しましたが、本番の移行はしませんでした。

その理由としては移行対象はwebアプリケーションに限らず、非同期処理を行うコンポーネント(worker)とバッチを実行するコンポーネントがあるためです。

ここでwebアプリケーションだけ先に移行したとしても他のコンポーネントはEC2のままであり、デプロイ手順が増えたりデプロイタイミングのことを考えないといけないなど二重管理によるまた別の問題が発生します。

そのため二重管理をする期間は短くするために、他のコンポーネントもスムーズに移行できるように必要なものを終わらせて一気に変更する方針をとっています。

とはいえ早めに動作確認できるようにしたおかげで、動かなかった部分を直すタスクを並行して別の人に任せるなどがしやすくなりました。

プロキシの設定埋め込み

プロキシはnginxを動かしていましたが、こちらもコンテナにするにあたっていくつか工夫をしました。

まず、nginxの公式イメージでは環境変数の埋め込みができるようになるためこれを利用しようとしましたが、環境変数の埋め込みはnginx自体がサポートしているわけではありません。

そのためEC2では環境変数が使えないため、EC2とECSで両方動く設定を作るにはひと工夫が必要でした。

解決方法としては単純ですがEC2側の方はCIで無理やり設定ファイルを書き換える形でブランチ名を埋め込むようにして対応しました。

はてなブログではgit flowによるブランチ戦略を取っていたのでブランチと環境を紐づけることができたのでこれで目的の手段が達成されました。

set $branch dummy_branch_name;
- name: rename proxy branch
  run: |
    sed -i config/nginx.conf \
      -e "s@dummy_branch_name@{{ github.ref_name }}@";

証明書の配信

はてなブログでは独自ドメインを配信するためにTLS終端をnginxから行っています。

コンテナ環境では証明書ファイルを更新するという手段は難しいので他の方法にする必要がありました。

最初はイメージに証明書を含めることを検討しましたが、最終的には起動時に最新の証明書を保存しているS3からダウンロードするようにしました。

これにより証明書の有効期限が切れそうになったタイミングではコンテナを再起動すれば良くなるほかセキュリティ上の問題も解決されました。

TLS周りでは他にもnginxのチケットキー(ssl_session_ticket_key)だけは各コンテナで同一にしつつ簡単に再生成できるようにコンテナイメージに含めて、再ビルドしたら必ずマスターキーが再生成されるようにしています。

アクセスログを配送できるようにする

今まではLTSV形式でファイルに書き出したアクセスログをcronでS3に配送していました。

S3に保存されたアクセスログはAthenaで検索できるようにしている他、GCSにエクスポートしてBigQueryの方でも検索できるようになっていました。

またこれらは発信者情報開示請求の際に利用するログであるため、欠損が起こらないような仕組みにする必要がありました。

これらをECSに移行しても動くようにする必要がありましたがいくつか課題がありました。

アクセスログの形式を新しくする

今回ではサイドカーのFluent Bitがアクセスログを配送するという仕組みを取りましたが、Fluent Bitでは入力としてLTSVは扱えるものの、出力は必ずJSONになってしまうのでLTSV形式でS3に保存が不可能という問題がありました。

そのため、S3にはJSONの形式で保存することをゴールとして構成を考えました。

最終的にJSONでログを保存するならLTSVのままログを出す必要もないのでまずアクセスログの形式をJSONに変更しました。

ログが欠損しないようにLTSVのログもJSONのログも両方出すようにして、今までログ配送に使っていたツールもJSONに対応して使うことにしました。

github.com

ここからAthenaのテーブルおよびBigQueryに配送する先も変更して問題ないことを確認しました。

EC2でもFirehoseを経由するように

ここまででログの形式はJSONになりましたが、 cronでログを転送しているものとFirehose経由でS3に置くファイルを同一の形式にするのは難しいことや、もし移行の際にプロキシ自体の動作とFirehoseの動作の2つを気にする必要がある事から事前にEC2でもFirehoseを使ったログ配送を行うようにしました。

EC2側でもfluentdが動いていたのでこちらでもあらかじめJSONのアクセスログを同様にKinesis Firehoseに流すように変更しました。

アクセスログ配送の変遷

タイムゾーンをUTCに統一

以前はアクセスログの時刻を始めS3のパーティションも全てタイムゾーンはJSTでした。

しかしFirehoseはUTCでログを扱っているので実際のログの時刻に対してパーティションがずれるという問題がありました。

これを回避するためにそもそものログを全てUTCにする変更をしました。

Kinesis FirehoseのLambdaでJSTに変換するということも可能ではありましたが、ALBなど他のログはUTCであり、ログ配送から分析までのさまざまな経路でタイムゾーンを考えるのは大変なので結果UTCに変えることにしました。

とは言えタイムゾーンの切り替えのタイミングではアクセスログの感じがずれてしまうのは避けられないので、データ分析を行なっている人に連絡し、切り替え日を連絡しタイムゾーンが変わることを伝える対応をしました。

FirehoseのLambda変換とAthenaでPartition Projectionで分析しやすいテーブルに

FirehoseのLambdaを使ったデータ変換を用い、アクセスログをそのまま分析用途に使えるようなパーティション分割をしました。

またAthenaのPartition Projection機能を使いパーティションキーが自動で認識されるようになったので、ニアリアルタイムで分析が可能になりました。

Athenaのパーティションは今まで日単位だけしかありませんでしたが、もっと細かくパーティションを切るようにしたり、用途別のドメインでグルーピングしたパーティションキーも追加したことでAthenaで高速な集計を行えるようになりました。

この改善により移行に必要になったさまざまな分析もスムーズに行えるようになりました。

モニタリングをコンテナ環境に対応させる

今まではファイルの書き出されたアクセスログからMackerelにレイテンシやステータスコードのメトリックを投稿していました。

これもコンテナになるとファイルに書き込みできなくなるので別の方法でメトリックを取得できるようにすることが必要でした。

プロキシの後ろにALBがあるのでそちらでもアプリケーションのモニタリングは可能ですが、ALBの前にはさらにvarnishによるHTTPキャッシュがあり、キャッシュから返したリクエストに関しては対象外になってしまうので全部のリクエストの様子を確認することはできません。

ALBでは一部のメトリックしか取れない

上の図にあるようにプロキシ(nginx)の前はNLBであるためHTTPレイヤのメトリックは取れないため、一番ユーザに近いHTTPレイヤでのメトリックとしてnginxから引き続き同等のメトリックを取得できるような方法を考えました。

結論としては以下の2種類の方法で代替することにしました。

  • アクセスログをAthenaで集計してMackerelに投稿する
  • nginx-module-vtsのエンドポイントを叩いてMackerelに投稿する

社内にはMackerelプラグインでnginx-module-vtsを用いたレイテンシやステータスコードのメトリックを取得するプラグインや、Athenaに対してクエリを実行してその結果を投稿するプラグインが存在していたのでこちらを利用することでEC2でもECSでもメトリックを取得して比較できるようになりました。

コンテナ移行のタイミングで代替として利用し始めたプラグインですが、ブログでは用途別にいくつもドメインがあり、移行もドメイン単位で行ったのでここで細かくメトリックが取れるようになったことで問題が起きているかすぐ確認できるようになりました。

一番気に入っているのはドメイン別のエラー率のグラフで、障害が起きたときにどのドメインでどのくらいの割合で起きているのか1つのグラフでわかるようになり、影響範囲や原因の特定がとてもしやすくなりました。

エラー率のグラフ

og:imageを動的に生成してる部分をマイクロサービス化

はてなブログでは2019年からog:imageを自動で生成する仕組みが動いています。

staff.hatenablog.com

これはアプリケーション上でHeadless Chromeを実行する形で実現していました。

このエンドポイントはbot向けであるためアクセス傾向も他と違い、またメモリ使用量なども他に比べて大きかったため、他のアプリケーションサーバとは別のサーバで動かしていました。

これを同じイメージでコンテナに移行した場合、Dockerfileにheadless chromeを入れることになりイメージサイズやビルド時間の増加、それを回避するために別のイメージにする場合も管理コストの観点から避けたい状態でした。

他のエンドポイントに比べて変更頻度も低いのでこのタイミングでマイクロサービスとして切り出すことにしました。

こちらはAPI Gateway + Lambdaで実行する形に置き換え管理コストの低い形に落ち着きました。

複数バージョンの静的ファイルを配信できるようにする

ECS移行にあたって今まではプロキシ(nginx)で配信していた静的ファイルの配信の仕組みを変更する対応も行いました。

今までのデプロイではアプリケーションサーバとプロキシのサーバの静的ファイルは同時に更新されていたため、複数バージョンのことを考える必要はありませんでした。

これがコンテナになりアプリケーションとプロキシのデプロイタイミングを同時にすることは難しくなります。

もしアプリケーションが先に更新されると古い静的ファイルが新しいURLで配信されてしまう問題が発生してしまうのでこれは避けないといけません。

このため、静的ファイルを複数バージョンで正しく配信できるようにする仕組みが必要でした。

もしこれが解決していない場合プロキシを先にコンテナにするにしてもアプリケーションを先にコンテナにするにしても起きてしまう問題なので、コンテナ移行する前に行う必要がありました。

ブログ固有の静的ファイル配信の難しさ

はてなブログにはユーザがヘッダやデザイン設定に自由にHTMLやCSSをかけるという特徴があります。

ユーザがコピペ可能なデザイン設定を公開してそれが多く利用されているということがありますが、その中にはてなが提供している静的ファイルが参照されているということも多くある状態でした。

これによってはてなブログが配信している静的ファイルのパスを変えることは非常に難しい状態でした。

法人も含めたたくさんのユーザへの影響を考えると変更する場合時間がかかってしまうので、今回はユーザ側の操作は必要としないように既存のパスは変えずに静的ファイル配信の問題を解決しました。

できる限りエラーにならずにS3から配信する仕組み

これを解決するためにコミットハッシュなどを元にしたプレフィックスでビルドした静的ファイルをS3に置いておきそれをプロキシ経由で配信するという形にしました。

nginxを経由してS3から静的ファイルを配信

最終的には下のようなルールで配信する静的ファイルをS3から配信するような形にしました。

リクエストURL S3のkey
/js/hatenablog.js branch/master/js/hatenablog.js
/js/hatenablog.js?version=deadbeef revision/deadbeef/js/hatenablog.js
/js/hatenablog.js?version=v0.0.1 branch/master/js/hatenablog.js

コミットハッシュだけでなくブランチ名ベースでのprefixも併用することで、上の表の1番目のようなversionパラメータがついていない場合でも、3番目のようにversionパラメータがついていたとしても可能な限り最新のファイルを見にいくようにフォールバックしてエラーを返さないようにしていることです。

フォールバック先のブランチ名に関してもnginxの設定は環境変数とmapをうまく使い、ECSでもEC2でもブランチ名をうまく解決できるようにしました。

実際に作成したnginxの設定を紹介します。

まず、ECS側だけ読み込まれる設定では変数に環境変数で渡したブランチ名を埋め込みます。

 map '' $http_branch_by_template { default ${BRANCH}; }

その後ECSでもEC2でも使う設定でmapを使って差分を吸収してブランチ名を決定し、うまくS3から目的のファイルを配信しています。

# コンテナ環境でBRANCH環境変数を渡していたらそれを使い、EC2環境ではデフォルトのブランチを使う
# http_を付けることで存在しない場合は空文字にすることができる
map $http_branch_by_template $branch {
    default $http_branch_by_template;

    "" master;
}

server {
   location ~ ^/(html|css|images|js|fonts)/ {
       expires max;
        more_set_headers 'Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT';
        more_set_headers 'Access-Control-Allow-Origin: *';
 
        # アプリケーション側で指定しているREVISIONのルールと合わせる
        if ($arg_version ~ "^(?<revision>[0-9a-f]{30})$") {
            rewrite "^/(.*)$" /static/revision/$revision/$1 break;
            proxy_pass http://$s3_static_domain;
        }
 
        rewrite "^/(.*)$" /static/branch/$branch/$1 break;
        proxy_pass http://$s3_static_domain;
    }
}

versionパラメータの形式もこのタイミングで変えており、今までで使われてないversionパラメータの規則に変更することで以前の形式で参照されていた場合でもったできる限り最新のファイルが参照されるようになりました。

また以前はアプリケーション側でversionパラメータの指定の仕方が複数箇所でバラバラだったのでそれらをちゃんと統一するようにしたり、そもそもversionパラメータがついていないものもたくさんあったのでアプリケーション側もかなり泥臭く対応していきました。

とは言えこちらは前述のアクセスログ改善で高速に検索ができるようになったので、調べては対応してを繰り返し無事問題なく移行できました。

静的ファイルが配信できないリビジョンはデプロイできないように

もしS3にデプロイしたいリビジョンのファイルが存在しないものを指定してデプロイしてしまうと静的ファイルが配信されない障害になってしまうのでそれを防ぐ仕組みも入れています。

コンテナ環境では単純に起動スクリプトに環境変数で指定したリビジョンを元にS3にファイルが存在するかチェックしダメだったら終了する仕組みを入れました。

これにより、もし存在しないリビジョンでECSタスクを起動しようとするとコンテナが終了することでALBのヘルスチェックに通らなくなりデプロイが成功しないため、問題なく元々のタスクを利用し続けることができます。

元々起動していたECSタスクに関しても問題なく、こちらは既に対応するリビジョンのS3のファイルが存在していることは確認できているのでもしタスクが異常終了した時も問題なく起動することができます。

リリースフローを改善しリリース頻度が倍増

ボトルネックになっているところを確認し、改善する

コンテナ移行後に目指す目標として、リリースのリードタイムを削減するというのがありました。

リードタイムを削減する方法として有効なのはリリース頻度を上げることになりますが、リリース頻度を上げるには毎回のリリースにかかる手間を減らさないと開発にかけられる時間が減ってしまいます。

それを回避するために単純には毎回のリリースにかかる時間を半分にできればそのまま開発にかけられる時間を減らさずにリリース頻度を2倍にできます。

そのためこれらを改善する前にFindy Team+やshibayu36/merged-pr-statなどを使い、リリース頻度やボトルネックになっている箇所などのメトリックを確認するようにしました。

こちらはECS移行自体のブロッカーになっているわけではないですが、移行に伴いどのみちリリース手順が大きく変わることは避けられないので、どうせならより良い形になるように改善をしていくことにしました。

リリースを開始してから終了するまでで一番時間がかかっていた部分は動作確認の部分だったので事前に確認できるような手順にしたり、ブログ記事がちゃんと投稿できるかどうかの確認をCloudWatch Syntheticsを使ったE2Eテストの仕組みで自動化したりしました。

リリース後のエラー確認に関しても手動で行っていた部分をメトリック化して監視を設定するなどして、とにかく自動化を軸にリリースのハードルと手間を削減していきました。

その結果無理なくリリース頻度も倍増させることができました。

全サービスでリリース方法を統一

はてなブログのチームでははてなブログ本体以外にもはてなフォトライフを始めとしたいくつかのサービスを運用しています。

中でもオーナーシップがブログに後から移ったというサービスも少なくないため、サービスによってデプロイ方法がバラバラで認知負荷が非常に高いと言う問題がありました。

最初に説明した通り、はてなブログ以外の全てのサービスはECSで動くようになったのでこのタイミングでデプロイ方法を統一していくことにしました。

リリース用のリポジトリを作りecspressoのファイルをそこで管理することでCI/CDの設定も共通化されメンテナンスしやすくなりました。

こちらに関しての詳細は別の記事でまた紹介できればと思っています。

動作確認と移行作業で行なった工夫

最後の方はさまざまなコンポーネントを集中的に移行していく時期になりました。

移行に関しては問題が発生する可能性が高いため、誰でもわかりやすい・誰でも対応しやすい方法にすることを意識して手順を作成しました。

はてなフォトライフのコンテナ移行で利用していたALBの加重ルーティングを用いた段階移行やMackerelのダッシュボードはもちろんのこと、規模がさらに大きいサービスゆえの工夫も行いました。

フィーチャーフラグを利用して本番環境で動作確認を行う

はてなブログには任意のドメインで利用できるフィーチャーフラグの仕組みが既に存在していたため、これを利用してALBでトラフィックを切り替え本番環境で検証を行えるようにしました。

ただし、こういったインフラ移行でフィーチャーフラグを使うと基本的には画面上の変化がないため今EC2とECSのどちらにトラフィックを流しているのかわかりづらいという問題がありました。

また検証がフラグを有効にしっぱなしにしてしまい普段通りに利用して意図しない障害を起こしてしまうみたいなこともあり得るのでその対策も必要でした。

そこで今回利用するフラグを有効にした場合にfaviconを変える実装を追加し、本番で再現可能かつ事故の起きにくい形にしました。

デザイナーの方にも協力してもらい、デフォルトから区別のしやすいfaviconを用意してもらいそれを利用しました。

ローカル環境でのfaviconの例

このフラグの仕組みは加重ルーティングを行なっている状態でも有効にしておき、もし問題があったときの再現も簡単にできるようにしました。

移行前にチームメンバーに一通りの動作確認をお願いしましたが、その際にもこのfaviconを変える仕組みを利用してもらいスムーズに動作確認を進めることができました。

細かい単位で移行するための設定

はてなブログではさまざまなドメインで動かしている関係上、ALBのリスナルールで特定ドメインだけ移行するということが手段として可能でした。

とはいえユーザにサブドメインを提供している部分に関しては無限にドメインが存在するためALBのホスト名マッチでは単純には動きません。

これを解決するために下の例のようにプロキシで場合分けをしたい単位でリクエストヘッダを追加してALBでそのヘッダを見て分岐するようにしました。

http {
  server {
    server_name *.hatenablog.com;
    set $domain_partition "blogs";
  }
  more_set_input_headers "X-Blog-Domain-Partition $domain_partition";
}

これを使い、og:image自動生成のドメインだけ移行したり、独自ドメインだけ移行したりなどの細かい制御が可能になりました。

また、加重ルーティングの割合に関しても問題があった際に誰でも元の割合に戻せるようにコマンド1つで変えられるようにするなどの工夫も行っています。

また予期しないエラーはSentryで即時通知されるようになっているので、そちらでも様子を確認できるようにしてました。

ログの流量をメトリック化して比較する

Sentryである程度のエラーは発見できるようになっているものの、完全とはいえない EC2とECSでエラーの数が変わっているかどうかを簡単に確認できるようにする仕組みを入れました。

とはいえEC2とECSではログの出し方が違うので簡単に比較できるように一工夫しました。

このようにして全然違うプラグインでも同じグラフで確認できるようになりました。

式グラフでも同様の機能は実現できますが、サービスメトリック化することでアノテーションも表示できるようになったり、細かい監視設定ができるようになるメリットがあります。

エラーログの数とアノテーションを確認

このグラフを使って変化を確認し、どういったログが差分としているかを確認するというフローができるようになりました。

プロキシの段階的移行

プロキシの切り替えではALBではなくNLBを使っていたためヘッダによる切り替えも加重ルーティングも使えないため別の方法を採用しました。

まず、どのくらいのキャパシティが必要かを安全に確認するようにしました。

  • 新しくNLBを作成してそこではECSのプロキシをぶら下げておく
  • Athenaで各ドメインで全体に対してどの程度の割合でどのくらいアクセスが来ているかを調べる
  • 一部のドメインを新しいNLBに向けて負荷の傾向から全体でどのくらいのキャパシティが必要かを確認する

一部のドメインをECSに向ける

これで確認したキャパシティを設定し、一気にトラフィックを切り替えても良かったのですがここでも影響を最小限にするために段階的な移行をしました。

  • EC2で使っていたターゲットグループにECSのIPを1台ずつ追加して割合を変更
  • EC2で使っていたターゲットグループにEC2のIPを1台ずつ削除して割合を変更
  • ECS用のターゲットグループを使うように切り替える

これによりALBほどは細かくはないですがある程度割合を制御しながら移行できました。

大量のバッチを効率よく移行する

バッチの移行先と移行の順番を決定

バッジの実行ホストは1台だけ動いているバッチサーバ上でcronを動かすという形になっていました。

そういった前提での構成になっており冪等性が保証されていないバッチも中には存在しており、ホストの入れ替えが非常に難しく、基本的にはメンテナンスを告知しての入れ替えをしないといけない状態でした。

当然ながら1台構成であるためAZ障害やホストの障害にも弱いと言う状態でした。

はてなブログでは50個近いのバッチが存在しており、毎分実行するようなバッチもあれば頻度は低いけど数時間かかるようなバッチも存在していました。

そのため全てのバッチを同様に移行することは難しいため、各バッチに対して以下の観点で調査・分類をしました。

  • 実行時間
  • 冪等性
  • 実行されなかった場合の影響
  • スケジュールから即実行されて欲しいか

最後の観点がある理由ですが、もしFargateでスケジュールタスクを実行する場合、ECRからイメージをpullする時間やタスクを立ち上げる時間でどうしても数分の遅延が発生します。

日付が変わったタイミングで即実行されてほしいものなどタイミングに意味がバッチがあるのでそういったものを考慮できるようにするために追加してあります。

バッチが正しく実行されているかに関しては以前からhorensoを用いて実行時間や実行が成功したかのメトリックを投稿していたのでそれをそのまま使いました。

blog.shibayu36.org

これを使って実行時間の様子は把握できたのであとは冪等性や重要度などを順番に確認して移行対象を決めていきました。

実行時間が記録されたメトリック

冪等性もあり実行されなくても良いバッチを一番最初に移行対象に選び、それを使って動作確認を進めるようにしました。

バッチをLambda経由の実行に移設

以前のはてなフォトライフの移設でLambdaを使ってHTTPリクエスト経由で定期的にエンドポイントを叩く仕組みを作成していました。

これを真似すると言うのを考えましたが、50個もリクエストを受けるエンドポイントを新設すると言うのはそれだけで多くの時間がかかるほか、スクリプト想定のコードをAPIに対応すると言うのも一手間かかるためもっと効率よく移行できる仕組みを考えました。

その結果生まれたものとして、1つのエンドポイントを作成し、パラメータで実行するスクリプトのファイルパスと引数を指定するという形に変えました。

curl -X POST 'http://localhost/cron/execute_script' --data '{
  "command": "perl",
  "file": "script/cron/example.pl",
  "args": ["--dryrun"]
}'

これによりエンドポイントの実装は1つ作成・管理すればよく、新しくバッチを増やしたい場合もEventBridge側でルールを増やすだけなので非常にコピペが簡単でありミスも少ない形になりました。

もちろんこのエンドポイントは素朴に実装してしまうと任意のコマンドが実行できてしまうので、外部から実行できないように認証をつけたり、ディレクトリトラバーサルが行えないような仕組みも入れています。

この作戦はとてもスムーズにいき、エンドポイントの追加から一週間弱で全てのバッチを移設できました。

重いバッチはECSのスケジュールタスクに

また重いバッチはメモリやCPUを多く使う他、長時間実行する場合はLambdaやエンドポイントのタイムアウトに引っかかる可能性もあるため全てをHTTP経由で実行するのは難しい状態でした。

そういったバッチに関してはECSのスケジュールタスクを使い定期的にスクリプトを実行するタスクを立ち上げるようにしました。

スケジュール管理にはecscheduleを使っています。

またecscheduleで使うタスク定義もリポジトリで管理できるようにecspressoを利用してタスク定義もコード管理するようにしています。

スケジュールの重複排除はアプリケーション側で

AWSでスケジュール実行する場合は呼び出しがat least onceであることに気をつけないといけません。

上で説明したようにバッチを2種類の形で実行する上、ECS側ではそのままスクリプトを実行する形なので呼び出し側でロックを取るというのは諦め、単純にアプリケーション側でRedis::Setlockを用いたロック機構を入れるようにしました。

プロジェクトをスムーズに進める

移行の時に技術的に工夫した部分はまだありますが、一旦この辺りにして長期にわたるインフラ構成の変更をスムーズに進めるための工夫もいくつかしたので紹介します。

タスク分割と管理

以上の設計と実装のほとんどをメインで担当していましたが、担当するメンバーが増えたり、アプリケーションエンジニアに手伝ってもらいたい作業が見えてきたので、残り作業の依存関係と見積ポイント及び誰が担当するのが早いかを可視化して分担して作業にあたるようにしました。

残りタスクを色と矢印で可視化

特に量も多く構成も変更する必要があるバッチ移行などではかなり役に立ちました。

こういった大きなインフラ移行は想定外のタスクが途中で増えることが経験上多かったため、可視化や見積りなど難しい印象でしたが、この依存関係を作る前に不確実性の高い部分を終わらせていたこともありうまく機能させることができました。

費用削減をして構成の選択肢を減らさない

単純にコンテナ移行するとALBなどコンポーネントが増えたりログもマネージドになって料金が増えがちです。

はてなブログは複雑な構成であり、移行後の構成として複数の選択肢が考えられることも多いです。 その中で工数が掛かるかわりに費用が抑えられるものもあれば、一瞬で作成できるかわりに費用がかかるものもあります。

そういったものの選択肢を狭めないために、あらかじめ別のところで費用削減しました。

特に効果のあった変更は以下の2つです。

  • アプリケーション層でgzip圧縮してAZ間通信量を減らす
  • Fargate Spotの採用

ECS移行して改善されたもの

こうしてECSに移行しましたが、移行してすぐ良い変化がいくつか起きたのでこれをまとめプロダクトオーナーにも共有しました。共有したまとめの一部をこちらにも書いておきます。

  • オートスケールにより費用面・障害耐性が向上した
    • 手作業による変更が減りその際のミスによる障害も防げるように
  • クラウドの仕組みに乗っかることで費用を安く使える仕組みを利用できるように
  • その他世の中で使われている仕組みになって課題に対する解決策を探しやすくなった
  • リリースフローを改善し、開発に集中しやすくなった
  • 脆弱性やEOLの対応をチーム内ですぐ完結できるようになった
  • TLS 1.3やHTTP/2の導入といった改善もすぐ行えるようになった
  • システムの課題にチームで考えられるようになった
    • オブザーバビリティが必要だという話がチームでできるようになった

まとめ

いくつか安全に移行する手段をとれたことで無停止で全てのコンポーネントをECSに移行できました。

これまでいくつかECSの移行をしましたが、成功するためのポイントがわかってきたので下にまとめておきます。

  • 移行時に変わる部分は少ない方が良い
    • コンテナに移行した後でもいいものは全部後回しにする
  • 移行前後で比較できるように同様のメトリックを取れるようにしておく
    • 雑でもいいので比較できることが一番大事
  • 段階移行をできるようにしておくこと
    • 段階移行をしない場合でもすぐ戻せる手段として使える

現在

その後はブログチームを離れ、ブックマークチームで同様にECSに移行するプロジェクトを進めていましたが、ブログ時代の経験が生きこちらもスムーズに完了できました。

そして現在はSRE標準化委員会という横断的なグループで、ECSのリリースフローを社内で改善・統一していくプロジェクトを中心となって進めています。

こちらに関してもまた別の機会で紹介する予定です。