CloudFrontのContinuous Deploymentを使ったフロントエンド(SPA)のk8s移行

はじめに

こちらの記事は、アソビュー! Advent Calendar 2024の6日目(表面)です。

PlatformSREチームの頭島です。

今回はCloudFrontのContinuous Deploymentを使って、フロントエンド(SPA)をS3からk8sに移行したお話です。

前提

アプリケーション構成

インフラはAWS上に構築しており、ほとんどのアプリケーションがAWS上のサービスにデプロイされています。

バックエンドサービス

ほとんどのアプリケーションがEKSにデプロイされています。通知サービスなど一部のアプリケーションがLambdaで稼働しています。

フロントエンド(SPA)

シングルページアプリケーション(SPA)のための静的コンテンツ(HTML, JS, CSSなど)はAmazon S3にホスティングされており、CDNとして採用しているCloudFrontから配信しています。

モチベーション

フロントエンドもCanaryReleaseしたい・・・

バックエンドのアプリケーションは、リリース戦略としてCanaryReleaseを採用しています。Argo Rolloutsを使って実現しています。

しかし、フロントエンドのアプリケーションはCICD一気通貫でリリースしていました。 具体的にはCIでS3に新しいリソースを配置してCloudFrontのキャッシュをInvalidationが完了すると即時新しいリソースが配信されてしまう状況でした。これにより不具合が混入していた場合のロールバックにも時間がかかり、リリースの心理的安全性が担保できていませんでした。

フロントエンドもバックエンド同様にCanaryReleaseを実施したいモチベーションが高まっていました。

手段検討

現実的な手段として、CloudFrontのContinuous Deployment機能を検討しました。

Continuous Deployment

さっくり説明すると、既存のCloudFrontのDistribution(Primary Distribution)に対してStaging Distributionを作成して、加重ベースまたはヘッダーベースでPrimary DistributionとStaging Distributionにトラフィックをコントロールできるようになります。最終的にはStaging Distributionを昇格させることで、安全にCloudFrontのDistributionの変更をリリースできる機能になっています。

しかし、Continuous Deploymentの採用は以下理由により見送りました。

  • リリース手順の認知負荷拡大
    • 同じCanaryReleaseの実行手順について、バックエンドアプリケーションとフロントエンドの場合の手順が異なることによる認知負荷拡大
  • CloudFrontの変更が発生
    • フロントエンドのリリースフローにCloudFrontに変更が追加されてしまい、従来よりも変更するリソースの増加
    • IaCでCloudFrontを管理している場合には、フロントエンド以外にTerraformの変更の工数増加

フロントエンド(SPA)をS3からKubernetesに移行する

様々な検討した結果、以下理由によりフロントエンド(SPA)をS3からKubernetesに移行することにしました

  • CanaryReleaseの手順をバックエンドアプリケーションと統一できる
    • デプロイ手法を統一できることは認知負荷において大きなメリットになります。弊社はフルサイクルエンジニアという指針のもと、バックエンドエンジニアがフロントエンド開発を実施することがあります。逆もしかりです。そのような場合であってもこれまでのナレッジを活かすことができます。
  • トラフィック管理をKubernetes上で完結できる
    • 当時サービスメッシュ導入を検討していたこともあり、トラフィック管理はKubernetes上で完結させたいモチベーションがありました

移行手順

移行前後のアーキテクチャ

今回の移行における一番リスクが高い作業は、CloudFrontのオリジンの向き先をS3からKubernetes向けのLBに変更することでした。ここで先程検討していた、CloudFrontのContinuous Deploymentを利用できそうだったため、以下の手順で移行することにしました。

1. k8sリソース新規作成

フロントエンド(SPA)用のワークロードを新規作成します。 構成はnginxからリソースを配信するシンプルな構成にしています。

2. CloudFrontのContinuous DeploymentでStaging Distribution作成

こちらはマネージメントコンソールから稼働中のDistributionに対してStaging Distributionを作成しました。Staging Distributionはk8sにトラフィックする設定にしています。

事前に全体公開前に社内のみで確認する必要があったためヘッダーベースのトラフィックに設定しています。

3. Production環境で動作確認

2で設定したヘッダーを付けてリクエストします。リソース取得元がS3からnginxに変わっていること、CloudFrontのキャッシュ設定が想定通りに動いていることを確認します。

Continuous Deploymentでは、Primary DistributionとStaging Distributionのキャッシュビヘイビアが完全に分離しているため、安全にテストすることができました。

4. Staging Distributionを昇格して、全トラフィックをk8sに向ける

こちらはマネージメントコンソールからStaging Distributionを昇格させます。事前にヘッダーベースでテストできたため安全に移行することができました。

移行後のフロントエンド(SPA)リリースフロー

ざっくり下記になります。

  1. フロントエンド(SPA)の変更PRをmainブランチにマージする
  2. CanaryPod向けのリクエストヘッダーを付けて事前検証して問題ないことを確認する
  3. Argo Rollouts のUI上でPromoteすることでCanaryPodをStablePodに昇格させる

バックエンドアプリケーションと同じフローでフロントエンド(SPA)をCanaryReleaseできるようになりました。

困ったこと

CloudFrontのInvalidationのトリガー

  • 移行前 : CIのワークフローでS3にリソース配置直後のタイミング
  • 移行後 : Argo RolloutsのRolloutが完了したタイミング

上記の通り、移行前はCICD一気通貫だったことに対して、移行後はCICDのタイミングが分離するためデプロイ直後にInvalidationを実行する必要がありました。つまり、Argo RolloutsのRolloutが完了したタイミングです。

結論、Argo Rollouts Notificationを利用してon-rollout-completedイベントをSubscribeして、Webhookを実行することで実現することができました。 Notifications機能ではデフォルトで用意されているイベント、またユーザ独自で定義したイベントをRolloutにSubScribeさせることで任意の処理を実行することができます。今回は以下で実現しました。

NamespaceベースのNotification定義を有効化する

デフォルトではArgo RolloutのcontrollerがデプロイされているNamespaceのみ、Notificationを定義できるようになっています。今回Notificationの定義にはInvalidation先のDistributionIDなどアプリケーション毎に異なる要素が存在します。それらをアプリケーション毎に定義可能にするためにArgo Rolloutの起動パラメタにパッチを当ててNamespaceベースのNotification定義を有効化しました。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: argo-rollouts
spec:
  template:
    spec:
      containers:
      - name: argo-rollouts
        args:
        - --self-service-notification-enabled

Webhookの定義

社内基盤内にCloudFrontのInvalidation機能を汎用的に提供しているWeb APIエンドポイントを呼び出す設定にしています。またInvalidationを確実に成功させる必要があるためRetry設定を入れています。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argo-rollouts-notification-configmap
data:
  service.webhook.webhook-cloudfront-invalidation: |
    url: https://cloudfront-invalidation-endpoint
    headers:
    - name: Content-Type
      value: application/json
  template.cloudfront-invalidation: |
    webhook:
      webhook-cloudfront-invalidation:
        method: POST
        path: /execute
        body: |
          {
             "hoge" : "fuga"
          }
  trigger.on-rollout-completed: |
    - send: [cloudfront-invalidation]

RolloutリソースにSubscribeを設定

RolloutリソースのLabelを定義することで実現できます。

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: spa-rollout
  annotations:
    notifications.argoproj.io/subscribe.on-rollout-completed.webhook-cloudfront-invalidation: ""

Continuous Deployment とIaCの相性がよくない

Continuous DeploymentにおけるStaging Distributionの昇格はTerraformでも実現できますが、ロールバックのリードタイムや確実に設定を反映させるという観点でマネージメントコンソールから実行した方が良さそうでした。

そうなった場合、CloudFrontをIaCで管理している場合は一時的にTerraform管理外の動的な変更を許容する必要があります。今回は最終的なCloudFrontのSpecに合わせる形でTerraformを修正しました。このあたりはもっとスマートにできる方法があるかもしれません。

最後に

今回ご紹介したフロントエンド(SPA)のCanaryReleaseはリリースしたものの運用開始前で、これから運用開始予定になっています!

アソビューでは、一緒に働くメンバーを大募集しています!カジュアル面談も実施しておりますので、少しでもご興味をお持ちいただけましたら、ぜひお気軽にご応募ください! www.asoview.com