皆様こんにちは、ソーシャル経済メディア「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の統一
など、継続してシステム改善を続けていこうと思います。
ここまで読んで頂き、ありがとうございました。