NewsPicksの課金基盤を作り直した話

皆様こんにちは、ソーシャル経済メディア「NewsPicks」(Media Infrastructureチーム)エンジニアの北見です。

tech.uzabase.com

先日は↑ の記事で

ECS Service そのものを CDK 管理してしまい、CDK をアップデートすることができなくなってしまったので、作りなおすことにしましたよ

という話をさせて頂きました。

今回は、その作り直しの話になります。

移行前のNewsPicksの課金基盤

図のように ECS 上で動作しており、リクエストの受け口として ALB 、プロキシサーバーとして nginx 、プロキシ先として APIServiceが存在する構成となっていました。

移行方針と計画

オーソドックスな手法ではありますが、

Service と同じ Cluster に新たな Service をデプロイする StepFunction を構築した後、nginxのプロキシ先となるリクエスト配分を徐々に新基盤に増やし、最終的に100%にする

という方法を採用しました。

もちろん、ALB のリスナールールによって振り分ける案もありました。

しかし、nginx のコンテナ側のアクセスログを保存するという運用していたので、ログ管理の構成も変えてしまうのは対応が難しそうだと判断し、nginx 側でプロキシ先の配分を調整する方針にしました。

1.新たなデプロイフローを StepFunction で構築し、現在稼働している基盤とは別に ECS Service を作成・更新するようにする

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

参考:nginxのupstream

今回は 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 をパース&加工すれば、新基盤への weight0 の時、upstream.conf

upstream billing {
  server old.api weight=100;
}
...

と、現行の基盤へ100%リクエストを配分する設定になりますし、weight50 なら

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}
]

という環境変数で起動した nginxupstream.conf

upstream billing {
  server new.api weight=100;
}
...

となり、新基盤へのリクエスト比率は100%になりました。

5.CDK 管理している旧基盤を削除する

ここまで来たら、あとは CDKupdate を阻害していた 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の統一

など、継続してシステム改善を続けていこうと思います。

ここまで読んで頂き、ありがとうございました。

Page top