皆様こんにちは、ソーシャル経済メディア「NewsPicks」(Media Infrastructureチーム)エンジニアの北見です。
先日は↑ の記事で
ECS Service そのものを CDK 管理してしまい、CDK をアップデートすることができなくなってしまったので、作りなおすことにしましたよ
という話をさせて頂きました。
今回は、その作り直しの話になります。
移行前のNewsPicksの課金基盤

図のように ECS 上で動作しており、リクエストの受け口として ALB 、プロキシサーバーとして nginx 、プロキシ先として API のServiceが存在する構成となっていました。
移行方針と計画
オーソドックスな手法ではありますが、
現 Service と同じ Cluster に新たな Service をデプロイする StepFunction を構築した後、nginxのプロキシ先となるリクエスト配分を徐々に新基盤に増やし、最終的に100%にする
という方法を採用しました。
もちろん、ALB のリスナールールによって振り分ける案もありました。
しかし、nginx のコンテナ側のアクセスログを保存するという運用していたので、ログ管理の構成も変えてしまうのは対応が難しそうだと判断し、nginx 側でプロキシ先の配分を調整する方針にしました。
1.新たなデプロイフローを StepFunction で構築し、現在稼働している基盤とは別に ECS Service を作成・更新するようにする

2.nginx の設定ファイルを変更し、新基盤と現行の基盤の両方にリクエストを配分するようにする

今回は nginx のタスク起動時に以下の様な JSON 形式の環境変数を読み込むようにしておいて、nginx 起動時に .conf ファイルとして配置するようにします。
( JSON 形式にすれば環境変数として渡す値は1つだけにできて便利でした)
[ {"proxyHost": "old.api", "weight": 100}, {"proxyHost": "new.api", "weight": 0} ]
また、設定を埋め込める nginx の設定ファイルのテンプレートを用意しておきます。
upstream billing {
$UPSTREM_CONFIG
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass https://xxxx;
access_by_lua_file /etc/nginx/src/request-body.lua;
body_filter_by_lua_file /etc/nginx/src/response-body.lua;
}
}
log_format ltsv escape=json xxx;
そして nginx 起動スクリプト内で環境変数をパースし、upstream.conf ファイルとして展開します。
#!/bin/bash set -eu # SPEC:この設定値は新serviceへの移行が終わり次第、削除する # nginxのupstreamの設定を、以下のようなJSON形式の環境変数からパースして埋め込む # 例 # [ # {"proxyHost": "old.api", "weight": 100}, # {"proxyHost": "new.api", "weight": 0} # ] export UPSTREM_CONFIG=$(echo $UPSTREAM_CONFIG_JSON | jq -r '.[] | select(.weight > 0) | " server \(.proxyHost) weight=\(.weight);"') mkdir -p /var/run/nginx/conf.d envsubst '$$UPSTREM_CONFIG' < /etc/nginx/conf.d/upstream.conf.template > /var/run/nginx/conf.d/upstream.conf /usr/local/openresty/bin/openresty -g 'daemon off;'
upstream.conf.template にリクエスト配分を埋め込んで、upstream.conf ファイルとして出力しています。
jq コマンドで↑のように JSON をパース&加工すれば、新基盤への weight が 0 の時、upstream.conf は
upstream billing {
server old.api weight=100;
}
...
と、現行の基盤へ100%リクエストを配分する設定になりますし、weight が 50 なら
upstream billing {
server old.api weight=50;
server new.api weight=50;
}
...
と、現行の基盤と新基盤に50:50の比率でリクエストを配分する upstream.conf が出力されます。
3.新基盤へのリクエスト配分を徐々に増やしていく

nginx 起動コンテナの環境変数を変更し、少しずつ新基盤APIにリクエストを分けるようにしていきます。
[ {"proxyHost": "old.api", "weight": 50}, {"proxyHost": "new.api", "weight": 50} ]
upstream.conf は
upstream billing {
server old.api weight=50;
server new.api weight=50;
}
...
となります。
4.新基盤へのリクエスト配分を100%にする

[ {"proxyHost": "old.api", "weight": 0}, {"proxyHost": "new.api", "weight": 100} ]
という環境変数で起動した nginx の upstream.conf は
upstream billing {
server new.api weight=100;
}
...
となり、新基盤へのリクエスト比率は100%になりました。
5.CDK 管理している旧基盤を削除する

ここまで来たら、あとは CDK の update を阻害していた ECS Service を消しましょう。
↓は CDK 管理していた ECS Service を削除したコード(一部抜粋・変更)です。これを丸々削除し、cdk deploy しました。
const taskDefinition = new ecs.FargateTaskDefinition( this, "xxx-app-task-def", { family: `xxx-app-service-task`, taskRole: taskRole, executionRole: taskExecutionRole, cpu: xxx, memoryLimitMiB: xxx, } ): const appContainer = appTaskDefinition.addContainer( props.accountProps.prefix + "xxx-app-container-settings", { image: ecs.ContainerImage.fromEcrRepository(xxx), memoryLimitMiB: xxx, cpu: xxx, logging: appLogDriver, } ); appContainer.addPortMappings({ containerPort: xxxx, }); // ↓これのせいでcdk updateできなかった! const appEcsService = new ecs.FargateService("xxx-app-service", { serviceName: "xxx-app-service", cluster, taskDefinition: appTaskDefinition, desiredCount: xxx, securityGroups: [xxx], deploymentController: { type: ecs.DeploymentControllerType.CODE_DEPLOY, }, vpcSubnets: xxx, }); const appScaling = appEcsService.autoScaleTaskCount({ minCapacity: xxx, maxCapacity: xxx, }); appScaling.scaleOnCpuUtilization(props.accountProps.prefix + "billing-app-scaling", { targetUtilizationPercent: xxx, });
結果

無事 CDK のバージョンを(当時の)最新のものにすることができました。
このプロジェクトで私が得られたもの
1. システムリプレイスのお作法を学べた
まずプロキシサーバーを立てて、リクエストを配分するようにし、その配分比率を変えていく。
世の中で何度も実践されたであろうオーソドックスな手法でしたが、実際に自分の手を動かしてそれに対応したのは初めてでしたので、とても良い経験になりました。
2. リリース前に開発環境で確認し、改めてドメイン知識のおさらいができた
課金系はトラブルがあったらいけない部分ですので、事前のテストは厚めに行いました。
開発者である私自身が見落としていた課金導線をおさらいすることができ、自社サービスの知識の整理に役立ちました。
今後の改善点
とはいえまだ改善点も残っており、
- 開発環境のリモートデバッグ対応
- 社内で利用するデプロイ用ChatBotの統一
など、継続してシステム改善を続けていこうと思います。
ここまで読んで頂き、ありがとうございました。