CloudFront で Continuous Deployment (継続的デプロイメント) がサポートされました。これまで CloudFront では Blue/Green デプロイメントを実現するには、別の Distribution を作って DNS で切り替えたり、オリジンを付け替えたりと少々作り込む必要がありました。今回のアップデートによりマネージドサービスとしてその辺りが利用できそうなので試してみました。また、Lambda@Edge、CloudFront Functions まわりもどうなるか確認してみました。

事前準備

事前に以下のように構成されているとします。

アーキテクチャ図

今回は以下のように構成しています。

  • 独自ドメインを設定しています
  • オリジンは S3 とし、オリジンアクセスコントロール(OAC, Origin Access Control)を使用します
  • 現時点では Continuous Deployment では HTTP/3 がサポートされていないので無効化しておきます
  • オリジンパスは S3 バケットの直下にフォルダ(下記例:v1)を1つ作り、そこをコンテンツのルートにしています
      <bucket-root>
         + v1/
         |   + index.html
         |
         + v2/
    
    • 新しいバージョンのコンテンツは v2/、その次は v3/… と運用していくイメージです

Continuous Deployment の仕組み概要

Conitunuous Deployment はざっくり言うと、ステージング用のディストリビューションを作成し、同じドメイン名のまま、リクエストをステージング用ディストリビューションにも振り分けており、切り替え時はステージング用ディストリビューションを昇格(Promote)させることで実現されています(正確にはステージングでの設定を本番側(Primary)に上書きする動作となります)。

Continuous Deployment を構成したアーキテクチャ図

リクエスト振り分け方法

リクエストの振り分けには2つの方法が提供されています

  • ウェイトベース
    • カナリアデプロイのように指定された割合のリクエストがステージングに振り分けられます
    • Cookie を埋め込んでスティッキーセッションのように同じクライアントからのリクエストを同じディストリビューションに固定することもできるようです
    • 本番リクエストの一部を流して確認したい時なんかに有効かと思います
  • ヘッダーベース
    • リクエストに特定のヘッダーが設定されている場合にステージングに振り分けられます
    • 社内や身内の方など小規模な環境で確認したい時に有効かと思います

Continuous Deployment の設定

実際に設定してみたので記録として残しておきたいと思います。

1. ステージング用ディストリビューションの作成

マネジメントコンソールからディストリビューションを選択し、画面下の方にいくと Continuous deployment の設定セクションがあるので Create staging distribution を選択します。

Create Distribution

次の画面(“Create staging distribution”)ではそのまま「次へ」いきます。

次の画面(“Configure settings”)にてオリジンパスを変更します。ステージングが新しいバージョンに向くようにしてあげるわけです。その他設定は今回はそのままにします。

Configure settings in Create Distribution

次の画面(“Specify traffic details”)でリクエストの振り分け方法を設定します。

  • ウェイトベースの場合
    Specify traffic details (weight based)
    • Enable session stickiness で同じクライアントからのリクエストを同じディストリビューションに固定します。idle duration の間、アクセスがない場合セッションをクリアし新規のクライアントとしてみなす動作になります(他にリセットされる条件はドキュメントを参照してください)。
  • ヘッダーベースの場合
    Specify traffic details (header based)
    • ヘッダーの名前、値を指定します。このヘッダーが付いたリクエストであればステージングの方に振り分けられます。

今回は ヘッダーベース を使用していきます。

2. 動作確認

元々存在していたディストリビューションが Primary、新たに作成したステージング用ディストリビューションが Staging としてラベル付けされ、マネジメントコンソール上では Primary の中に Staging が存在しているイメージになりました。

Primary Distribution Staging Distribution

動作確認には簡単に curl で以下のように実行してみました。

# Primary Distribution へのアクセス
curl -X GET https://example.com/
v1

# Staging Distribution へのアクセス
curl -X GET -H 'x-is-staging: yes' https://example.com
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Meesage>Access Denied</Message><RequestId>...</RequestId><HostId>...</HostId></Error>

Staging の方が中々繋がらず、少々ハマってしまったのですが S3 をオリジンとし、オリジンアクセスコントロールを使用している場合は注意が必要です

ステージングもオリジンアクセスコントロールで S3 を参照する場合

今回のようにステージングのオリジンもオリジンアクセスコントロールで S3 を参照している場合、プライマリとは異なるディストリビューションからのアクセスとなるため、バケットポリシーの更新が必要でした。

具体的にはステージングディストリビューションの ARN もバケットポリシーに追加してあげます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<Your-Origin-Bucket>/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": [
                        "arn:aws:cloudfront::<Account-Id>:distribution/E2WXXXXX", // Primary Distribution
                        "arn:aws:cloudfront::<Account-Id>:distribution/E2TXXXXX"  // Staging Distribution
                    ]
                }
            }
        }
    ]
}

これで特定のヘッダーの有無でリクエストが振り分けられるようになりました。

# Primary Distribution へのアクセス
curl -X GET https://example.com/
v1

# Staging Distribution へのアクセス
curl -X GET -H 'x-is-staging: yes' https://example.com
v2

現在では古い方式で非推奨となったオリジンアクセスアイデンティティ(OAI、Origin Access Identity)の場合は、プライマリとステージングで同じ値だったのでバケットポリシーの変更は不要だと思います(確認してません🙇‍♂️)。

3. ステージングの昇格(Promote)

ステージングでの動作確認が問題なければ、リクエストが新しいバージョンを向くように切り替えていくわけですが、そのためにはステージングの昇格を行います。

Promote

Promote Confirm

昇格による影響について確認が必要になります(雑な和訳)。

  • 本番用ディストリビューションはダウンタイムなしで稼働し続けます
  • 現在の本番環境の設定は(ステージング環境の設定で)上書きされます
  • Continuous Deployment の設定は無効になります。再度有効にしたり、削除できます
  • リクエストはディストリビューション間で振り分けられなくなります

昇格すると、プライマリディストリビューションの設定がステージングで設定したものに上書きされました。プライマリディストリビューションの arn はそのままになります。

注意というほどでもないかもですが、例えば「ステージングディストリビューションは動作確認のためキャッシュを無効にする」ということも設定上できるわけですが(“ビヘイビア"からキャッシュポリシーを変更)、昇格時にその設定がそのままプライマリである本番環境に上書きされてしまい、本番環境もキャッシュが無効になってしまうので注意が必要です。

ちなみに、プライマリとステージングのキャッシュは共有されず、それぞれ個別に持っているようです(ドキュメント)。なので、ステージングへの最初のリクエストはオリジンまで到達することになります。キャッシュを削除(Invalidate)したい場合は、ステージングディストリビューションでキャッシュ削除ができます。

昇格しない場合

ステージングで検証し問題が見つかった場合は、Staging Distribution を無効化することでステージングへのリクエスト振り分けを中止することができます。

Lambda@Edge についても確認

せっかくなので Lambda@Edge まわりもどのようになるのか確認してみました。

まず、先ほど昇格したことにより無効になっている Contiuous Deployment を有効にします。

Staging Distribution の有効化

簡単な Lambda 関数を Lambda@Edge として、まずはプライマリディストリビューションに設定します。

Lambda@Edge の設定

Lambda のコード(TypeScript)は以下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {
  CloudFrontRequestEvent,
  CloudFrontResponseResult,
  CloudFrontRequestCallback,
  Context,
} from 'aws-lambda';

export const lambdaHandler = async (event: CloudFrontRequestEvent, context: Context, callback: CloudFrontRequestCallback) => {

    const request = event.Records[0].cf.request;

    if (request.uri.indexOf('/lambda') >= 0){
      const response: CloudFrontResponseResult = {
        status: '200',
        statusDescription: 'OK',
        headers: {
          'content-type': [{ key: 'Content-Type', value: 'text/plain' }],
        },
        body: `Lambda@Edge function version:${context.functionVersion}`
      };

      callback(null, response);
    }
    callback(null, request);
};

ハイライトした12行目の通り、/lambda へリクエストが来たら Lambda のバージョンを返すようにしてみます。

この状態で確認すると、プライマリディストリビューションへリクエストすると結果が得られますが、ヘッダーを設定したアクセス、すなわちステージングへのアクセスはコンテンツがないためアクセスが拒否されます(ステージングには Lambda@Edge が設定されていないため)。

# Primary Distribution へのアクセス
curl -X GET https://example.com/lambda
Lambda@Edge function version:1

# Staging Distribution へのアクセス
curl -X GET -H 'x-is-staging: yes' https://example.com/lambda
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>...</RequestId><HostId>...</HostId></Error>

次に、Lambda で新しいバージョンを発行し、ステージングディストリビューションのビヘイビアで新しいバージョンの Lambda を設定します。

Setting new Lambda version at Lambda@Edge in Staging distribution

# Primary Distribution へのアクセス
curl -X GET https://example.com/lambda
Lambda@Edge function version:1

# Staging Distribution へのアクセス
curl -X GET -H 'x-is-staging: yes' https://example.com/lambda
Lambda@Edge function version:2

私の確認した範囲では1分ほどでステージングの方に反映され、リクエストが振り分けられるようになりました。
プライマリ / ステージングでそれぞれ異なる Lambda@Edge を設定できるので Lambda を更新するようなケースでも Continuous Deployment は使えそうです。

CloudFront Functions の時は?

CloudFront Functions はバージョンの概念がないため、ステージングで事前検証、といったことはできません。下図のように同じ関数がプライマリ / ステージングディストリビューションに関連付けされるため、関数を更新後、発行すると両方ともに影響します。

Associated distribution of CloudFront Functions

CloudFront Functions の時は新しい関数を作成してステージングに関連付けるのが良さそうです。

まとめ

Continuous Deployment を試してみました。以前までは CloudFront で Blue/Green やカナリアデプロイを実現するには作り込みが必要でしたが、マネージドサービスとして利用しやすくなりました。

心に留めておきたいポイントとしては…

  • ステージングの設定内容がプライマリに上書きされる(「ステージングではキャッシュ無効」とかする時に注意)
  • S3 をオリジンとしており、Origin Access Control を利用する場合はステージング側の arn をバケットポリシーに追加する必要がある
  • Lambda@Edge についてもバージョンを利用することで、ステージングでの事前確認がしやすい
  • CloudFront Functions をステージングで事前確認したい場合は新しい関数を作った方が良さそう

今回は試さなかったですが、ウェイトベースで本番リクエストの一部をステージングにルーティングさせる場合は、リアルタイムログの利用などすぐに異常を検知できる仕組みが必要になりそうですね。

ご参考になれば幸いです🙇‍♂️

最後に・・・

この投稿は個人的なものであり、所属組織を代表するものではありません。ご了承ください。