Amazon CloudFrontのStaging Distributionをboto3で使うのが大変だったのでスクリプトを共有します

Amazon CloudFrontのStaging Distributionはマネジメントコンソールでは簡単に設定できるけど、boto3からだと細かく気をつける部分がたくさんあります。新しい機能であんまりナレッジもなかったのでやったことを公開します
2023.06.08

こんにちは、臼田です。

みなさん、ちょっとした処理の自動化してますか?(挨拶

今回は少し前にアップデートされたAmazon CloudFrontでContinuous Deployment(継続的デプロイ)をするためにStaging Distributionを使ってテストをしてから設定を更新する方法をPythonとboto3で実施した話です。

何だこれは…大変だ!

皆さんはもうAWSマネジメントコンソールを利用してこの機能を利用しましたか?とっても素敵で便利な機能です。

これまではCloudFrontの設定を更新する際には、適用するとそのまま本番展開されてしまうため、簡単に試すことができませんでした。でも上記記事の通り、これからはStaging Distributionを利用して、新しい設定に10%のリクエストをルーティングしたり、あるいはヘッダを使ってテスターだけ確認するなどの手法が取れるようになりました。とっても便利。

AWSマネジメントコンソールで設定するときも非常に簡単で直感的です。Create staging distributionボタンからStagingのために変更する設定を入れて、どのようにトラフィックをステージングにルーティングするかを設定して、よければ本番展開できます。

ところがどっこい、これをCLIやSDKでやろうと思ったら結構ステップが多くて大変でした。手順は以下のドキュメントにあります。

CloudFront の継続的デプロイを使用して CDN 設定の変更を安全にテストする - Amazon CloudFront

箇条書きにするとこんな感じです。

  • 本番のディストリビューションをCopyDistributionでStagingとして複製する
  • Stagingのディストリビューションの設定を更新(UpdateDistribution)する
  • CreateContinuousDeploymentPolicyで継続的デプロイポリシーを作成する(StagingDistributionと紐づく)
  • 本番のディストリビューションに継続的デプロイポリシーをアタッチ(UpdateDistribution)する
  • (動作確認する)
  • 本番のディストリビューションにStagingの設定を適用(UpdateDistributionWithStagingConfig)する

内部的にも結構気にするところがあり、マネジメントコンソールからで十分なときは、そちらのほうが気楽でいいでしょう。

今回は私がこれをPythonとboto3で実行してみたので、参考に使ってください。

スクリプト

こんな感じです。なんとなしに手元で動かした、普段開発をやっていない私のコードですから、参考程度にしておいてください。

import boto3
import uuid


cf = boto3.client('cloudfront')
waiter = cf.get_waiter('distribution_deployed')

def create_staging_distribution(primary_distribution_id, if_match, distribution_config, traffic_config):
    # Create Staging Distribution
    stg_distri = cf.copy_distribution(PrimaryDistributionId=primary_distribution_id, Staging=True, IfMatch=if_match, CallerReference=str(uuid.uuid4()))

    # Update Staging Policy
    stg_distri_id = stg_distri['Distribution']['Id']
    stg_distri_config = cf.get_distribution_config(Id=stg_distri_id)
    distribution_config['CallerReference'] = stg_distri_config['DistributionConfig']['CallerReference']
    distribution_config['Staging'] = True
    cf.update_distribution(Id=stg_distri_id,IfMatch=stg_distri['ETag'],DistributionConfig=distribution_config)
    waiter.wait(Id=stg_distri_id)

    # Create Continuous Deployment Policy
    continuous_deployment_policy_config={
        'StagingDistributionDnsNames': {
            'Quantity': 1,
            'Items': [
                stg_distri['Distribution']['DomainName'],
            ]
        },
        'Enabled': True,
        'TrafficConfig': traffic_config
    }

    continuous_deployment_policy = cf.create_continuous_deployment_policy(ContinuousDeploymentPolicyConfig=continuous_deployment_policy_config)

    # Attach Continuous Deployment Policy
    primary_distri_config = cf.get_distribution_config(Id=primary_distribution_id)
    update_distri_config = primary_distri_config['DistributionConfig']
    update_distri_config['ContinuousDeploymentPolicyId'] = continuous_deployment_policy['ContinuousDeploymentPolicy']['Id']
    cf.update_distribution(Id=primary_distribution_id,IfMatch=primary_distri_config['ETag'],DistributionConfig=update_distri_config)
    waiter.wait(Id=primary_distribution_id)

    # Create return
    return stg_distri_id

# Create staging config
distri_id = 'XXXXXXXXXXXXXX'
distri_config = cf.get_distribution_config(Id=distri_id)
update_config = distri_config['DistributionConfig']
default_root_object = 'index.html'
update_config['DefaultRootObject'] = default_root_object

# Create continuous deployment policy traffic config
traffic_config = {
    'Type': 'SingleHeader',
    'SingleHeaderConfig': {
        'Header': 'aws-cf-cd-stage',
        'Value': 'staging'
    }
}

stg_distri_id = create_staging_distribution(distri_id, distri_config['ETag'], update_config, traffic_config)

stg_distri_etag = cf.get_distribution_config(Id=stg_distri_id)['ETag']
distri_etag = cf.get_distribution_config(Id=distri_id)['ETag']
cf.update_distribution_with_staging_config(Id=distri_id, StagingDistributionId=stg_distri_id, IfMatch=f'{distri_etag}, {stg_distri_etag}')

コードを書いていく中でいろんな仕様を理解し、ハマり、コツを掴んだのでそのあたりも含めて補足説明します。

スクリプトの概要

このスクリプトではcreate_staging_distribution()という関数を作成して、だいたい画面上でStaging環境をリクエストするときと同じ領域をひとまとめにしてみました。つまり、更新対象のディストリビューションを選択し、Staging用の設定を入れ、継続的デプロイポリシーを作成するところまでです。

関数外で、本番への反映update_distribution_with_staging_config()を行っています。ちなみに余談ですが、cf = boto3.client('cloudfront')するときにどのデフォルトリージョンで作業していても問題ありません。CloudFrontのクライアントであれば自動的にグローバルとなります。

CloudFrontの設定更新にはETagが必要

copy_distribution()update_distribution()を実行する際には、ディストリビューションのIDだけではなく、IfMatchパラメータにディストリビューションの設定のETagが必要となり、これはget_distribution()list_distributions()などでは取得できず、get_distribution_config()を呼び出して取得する必要があります。

別のディストリビューションの設定を使うのは面倒

update_distribution()で利用するDistributionConfigパラメータには、ディストリビューションで一意のCallerReferenceを指定する必要があり、単純な使い回しができません。必ずこれを差し替えるようにします。

処理を継続する前にwaiterでデプロイを待つ

update_distribution()などでデプロイが発生する場合にはwaiterを利用して完了を待ちます。じゃないとエラーが出ます。

update_distribution_with_staging_config()するときのIfMatchの扱い

update_distribution_with_staging_config()するときのIfMatchの扱いは両方のETagを<primary ETag>, <staging ETag>のように表現します。

まとめ

GUIで簡単でも、CLIやSDKでは簡単ではないことは色々あるわけですが、これもその1つでした。

なのでとりあえず書いたものを共有しました。皆さんも是非書いたコードを共有してください。