🌟

CloudFrontの継続的デプロイにメンテナンス画面を組み込む

に公開

CloudFrontの継続的デプロイ(ブルーグリーンデプロイ)にメンテナンス画面への切り替えを組み込んで構築した方法の紹介になります。
一般的なソリューションは以下で紹介されていますが、いくつかの制約や要求に基づいて少し異なる方法で構築をしました。

https://aws.amazon.com/jp/blogs/news/networking-and-content-delivery-achieving-zero-downtime-deployments-with-amazon-cloudfront-using-blue-green-continuous-deployments/

やりたかったこと

  • ブルーグリーンデプロイでダウンタイムゼロのデプロイメントを実現する一方、どうしてもダウンタイムが発生してしまうリリースに備えてメンテナンス画面への切り替えができるようにしたかった。
  • メンテナンスフローの統制のため、パイプラインとしてはStepFuctionを利用せずGitHubActionsで構築・管理ができるようにしたかった。

一方で、対象となるCloudFrontのディストリビューションには100を越えるサブドメインが紐づいており、Route53側でメンテナンス画面へ瞬時にレコードを付け替えることが難しいというような制約がありました。

メンテナンス画面組み込みの仕組み

前提

CloudFrontは動的なサイトのホスティングをしており、S3をオリジンとしています。

方針・ワークフローの状態遷移

CloudFrontの継続的デプロイの基本的な仕組みは、プライマリディストリビューションとは別にステージングディストリビューションを用意し、トラフィックを分離した後にPromote(昇格)をすることによってプライマリディストリビューションへの適用を行います。
トラフィックが分離されている時は、プライマリ・ステージングそれぞれで/blue/greenというようなイメージで別々のオリジンパスを参照します。

これに対し、/maintenanceというオリジンパスを参照する状態を作成し、デプロイワークフローの中で必要に応じてメンテナンス画面を表示するような方針としました。
/maintenanceには固定ページとしてメンテナンス表示のためのhtmlを配置しています。

メンテナンス画面にしない場合のオリジンパス状態遷移

メンテナンス画面切り替えを必要としない場合、以下のような流れでデプロイメントが行われます。
メンテナンス画面なしのwf

💡ディストリビューションにおけるリクエストの振り分け方法にはHeader-basedな方法とWeight-basedな方法の2種類が選択できます。
Header-basedで振り分けをする際は、ModHeaderという拡張機能を利用することによって、chromeでの動作確認をすることが可能です。

https://chromewebstore.google.com/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj?hl=ja&pli=1

メンテナンス画面にする場合のオリジンパス状態遷移

一方、メンテナンス画面切り替えを必要とする場合、以下のような流れとなります。
メンテナンス画面ありのwf

実装

ステージングディストリビューションの準備

cdkで実装する場合は以下のようなイメージです。

    const primaryDistribution = new cloudfront.CfnDistribution(this, 'PrimaryDistribution', {
      distributionConfig: new CfDistributionConfiguration(
        props,
        false,
      ).configuration,
    });

    if (props.frontDistribution.needStagingDistribution) {
      const stagingDistribution = new cloudfront.CfnDistribution(this, 'StagingDistribution', {
        distributionConfig: new CfDistributionConfiguration(
          props,
          true,
        ).configuration,
      });
      const cdPolicy = new cloudfront.CfnContinuousDeploymentPolicy(
        this,
        "CfnContinuousDeploymentPolicy",
        {
          continuousDeploymentPolicyConfig: {
            enabled: true,
            stagingDistributionDnsNames: [ stagingDistribution.attrDomainName],
  
            trafficConfig: {
              type: "SingleHeader",
              singleHeaderConfig: {
                header: "aws-cf-cd-xxxx",
                value: "true",
              },
            },
          },
        }
      );
      primaryDistribution.addPropertyOverride('DistributionConfig.ContinuousDeploymentPolicyId', cdPolicy.ref);
    }

プライマリディストリビューションとステージングディストリビューションを作成した上で、デプロイポリシーをアタッチしています。
DistributionConfigは共通化していますが、プライマリかステージングかによってstagingプロパティを設定してあげるだけでOKです。
https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-cloudfront-distribution-distributionconfig.html#aws-properties-cloudfront-distribution-distributionconfig-properties

各GithubActions Workflowの作成

各ワークフローの流れとポイントを記載します。

ステージングディトリビューションへのdeploy workflow

    steps:
      # checkout, credentials設定, AWS CLIのインストール等

      # デプロイポリシーの有効化
      - name: Enable continuous deployment policy
        run: |
          aws cloudfront get-continuous-deployment-policy-config --id ${{ env.CD_POLICY_ID }} --output json > cd-policy-config.json

          ETAG=$(jq -r '.ETag' cd-policy-config.json)
          jq 'del(.ETag) |
           .ContinuousDeploymentPolicyConfig.Enabled = true |
           .ContinuousDeploymentPolicyConfig
           ' cd-policy-config.json > updated-policy-config.json

          aws cloudfront update-continuous-deployment-policy --id ${{ env.CD_POLICY_ID }} --continuous-deployment-policy-config file://updated-policy-config.json --if-match "$ETAG"

      - name: Build Application
        # (省略)アプリケーションのビルド
      
      - name: Sync S3 Bucket
        # (省略)ステージングディストリビューションが参照するs3パスへsyncする

      # オリジンパスを変更し、トラフィックを分離する
      - name: Traffic separation
        run: |
          aws cloudfront get-distribution-config --id ${{ env.STAGING_DISTRIBUTION_ID }} --output json > stg-dist-config.json
          ETAG=$(jq -r '.ETag' stg-dist-config.json)
          STG_CURRENT_ORIGIN_PATH=$(jq -r '.DistributionConfig.Origins.Items[0].OriginPath' stg-dist-config.json)

          aws cloudfront get-distribution-config --id ${{ env.PRIMARY_DISTRIBUTION_ID }} --output json > primary-dist-config.json
          PRI_CURRENT_ORIGIN_PATH=$(jq -r '.DistributionConfig.Origins.Items[0].OriginPath' primary-dist-config.json)
          
          if [ "$STG_CURRENT_ORIGIN_PATH" = "$PRI_CURRENT_ORIGIN_PATH" ]; then
            NEW_ORIGIN_PATH=$([ "$STG_CURRENT_ORIGIN_PATH" = "/blue" ] && echo "/green" || echo "/blue")
            echo "TEST_ORIGIN_PATH=$NEW_ORIGIN_PATH" >> $GITHUB_ENV
          
            jq --arg path "$NEW_ORIGIN_PATH" '
              del(.ETag) |
              .DistributionConfig.Origins.Items[0].OriginPath = $path |
              .DistributionConfig
            ' stg-dist-config.json > updated-stg-distribution-config.json
          
            aws cloudfront update-distribution --id ${{ env.STAGING_DISTRIBUTION_ID }} --distribution-config file://updated-stg-distribution-config.json --if-match "$ETAG"
          else
            echo "TEST_ORIGIN_PATH=$STG_CURRENT_ORIGIN_PATH" >> $GITHUB_ENV
          fi
    
      - name: Invalidation Cloudfront Cache
        # (省略)aws cloudfront create-invalidationでエッジロケーションのキャッシュクリア

distribution-configに関しては、現状のオリジンパスが/blue/greenかに応じてupdateするパスが変わるので現状のものを取得する形としています。

プライマリディトリビューションへのpromote workflow

    steps:
      # checkout, credentials設定, AWS CLIのインストール等

      # ステージングディストリビューションの昇格
      - name: Promote Stg Distribution
        run: |
          aws cloudfront get-distribution-config --id ${{ env.PRIMARY_DISTRIBUTION_ID }} --output json > primary-dist-config.json
          PRIMARY_ETAG=$(jq -r '.ETag' primary-dist-config.json)

          aws cloudfront get-distribution-config --id ${{ env.STAGING_DISTRIBUTION_ID }} --output json > staging-dist-config.json
          STAGING_ETAG=$(jq -r '.ETag' staging-dist-config.json)

          aws cloudfront update-distribution-with-staging-config --id ${{ env.PRIMARY_DISTRIBUTION_ID }} \
            --staging-distribution-id ${{ env.STAGING_DISTRIBUTION_ID }} \
            --if-match "${PRIMARY_ETAG},${STAGING_ETAG}"

      - name: Invalidation Cloudfront Cache
        # (省略)aws cloudfront create-invalidationでエッジロケーションのキャッシュクリア

ステージングディトリビューションへのdeploy workflow内でも同様の課題に触れていますが、update-distributionを連続して実行することはできません。
deploy workflowの実行後、CloudFrontディトリビューションのステータスを見て、デプロイが完了している必要があります。(ステータスはAWSのマネコンからも確認できます。)

メンテナンス画面への切り替えworkflow

サンプルコードは省略しますが、同様にプライマリディストリビューションのオリジンパスを/maintenanceに変更します。

まとめ

ブルーグリーンデプロイを実装していても、諸々の都合でメンテナンス画面にしたいときはあるため完全なダウンタイムゼロのデプロイを実現することは難しいなと思います。
必要に応じてブルーグリーンデプロイを利用したり、メンテナンス画面を利用しながら少しずつでもダウンタイムを短縮し、安心安全のメンテナンスができるようにして行けたら良いですね!

GitHubで編集を提案

Discussion