Amazon CloudFrontの昇格をAWS Step Functionsからやってみた

2024.02.12

しばたです。

最近Amazon CloudFrontの継続的デプロイが気になっていろいろ試しています。

CloudFrontの継続的デプロイでは2つのディストリビューションを「プライマリ(本番)」「ステージング」という形に分け紐づけます。
ステージング環境に対するアクセスに問題が無ければ「昇格」を行いステージング環境の設定をプライマリにコピーして展開 *1することができます。

今回この昇格処理を自動化する必要があり、AWS Step Functionsを使って実装したので共有します。

昇格のためのAPI仕様

CloudFrontの昇格はUpdateDistributionWithStagingConfig APIで行い、このAPIは

  • プライマリディストリビューションID
  • プライマリディストリビューションのETag
  • ステージングディストリビューションのID
  • ステージングディストリビューションのETag

を必要とします。
CloudFrontのETagは更新の都度変わるため昇格の直前に取得してやる必要があります。

AWS CLIだとこんな感じの指定で昇格できます。
--if-match引数に両者のETagを順に設定するのがポイントです。

Bashでの実行例

# AWS CLI on Bashで昇格する例

# プライマリディストリビューションの情報取得
export PRIMARY_DIST_ID='EXXXXXXXXXX'
export PRIMARY_DIST_ETAG=$(aws cloudfront get-distribution-config --id $PRIMARY_DIST_ID --query 'ETag' --output text)
# ステージングディストリビューションの情報取得
export STAGING_DIST_ID='EYYYYYYYYYY'
export STAGING_DIST_ETAG=$(aws cloudfront get-distribution-config --id $STAGING_DIST_ID --query 'ETag' --output text)
# 昇格
aws cloudfront update-distribution-with-staging-config \
    --id $PRIMARY_DIST_ID \
    --staging-distribution-id $STAGING_DIST_ID \
    --if-match "$PRIMARY_DIST_ETAG, $STAGING_DIST_ETAG"

そしてUpdateDistributionWithStagingConfig APIを利用するには以下の権限が必要です。

  • "cloudfront:GetDistribution"
    • プライマリ、ステージング両方に対して必要
  • "cloudfront:UpdateDistribution"
    • プライマリのみに必要

権限から予想するに内部的には「ステージング環境の設定を読み取り、コピーした内容でプライマリを更新」している様です。

Step Functions実装

APIの仕様が分かればあとはそれをStep Functionsのステートマシンに落とし込んでやるだけです。

今回はこんな感じのシンプルなフローを作りました。

最初にGetDistribution APIを呼び出しプライマリ、ステージング環境の両者のETagを取得し、その結果をふまえてUpdateDistributionWithStagingConfig APIを呼び出しています。

ASLの定義はこんな感じになります。

ASL定義

{
  "Comment": "Promote CloudFront distribution.",
  "StartAt": "Get Primary distribution ETag",
  "States": {
    "Get Primary distribution ETag": {
      "Type": "Task",
      "Parameters": {
        "Id.$": "$.PrimaryId"
      },
      "Resource": "arn:aws:states:::aws-sdk:cloudfront:getDistribution",
      "ResultSelector": {
        "ETag.$": "$.ETag"
      },
      "Next": "Get Staging distribution ETag",
      "ResultPath": "$.PrimaryDistribution"
    },
    "Get Staging distribution ETag": {
      "Type": "Task",
      "Parameters": {
        "Id.$": "$.StagingId"
      },
      "Resource": "arn:aws:states:::aws-sdk:cloudfront:getDistribution",
      "ResultSelector": {
        "ETag.$": "$.ETag"
      },
      "Next": "Promote distribution",
      "ResultPath": "$.StagingDistribution"
    },
    "Promote distribution": {
      "Type": "Task",
      "Next": "Success",
      "Parameters": {
        "Id.$": "$.PrimaryId",
        "StagingDistributionId.$": "$.StagingId",
        "IfMatch.$": "States.Format('{},{}', $.PrimaryDistribution.ETag, $.StagingDistribution.ETag)"
      },
      "Resource": "arn:aws:states:::aws-sdk:cloudfront:updateDistributionWithStagingConfig"
    },
    "Success": {
      "Type": "Succeed"
    }
  },
  "TimeoutSeconds": 300
}

そしてアタッチするロールの権限はこんな感じです。

ステートマシン実行に必要な権限

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Action": [
				"xray:PutTraceSegments",
				"xray:PutTelemetryRecords",
				"xray:GetSamplingTargets",
				"xray:GetSamplingRules"
			],
			"Effect": "Allow",
			"Resource": "arn:aws:states:<regon>:<account_id>:stateMachine:<function_name>"
		},
		{
			"Action": "cloudfront:GetDistribution",
			"Effect": "Allow",
			"Resource": [
				"arn:aws:cloudfront::<account_id>:distribution/<primary_distribution_id>",
				"arn:aws:cloudfront::<account_id>:distribution/<staging_distribution_id>"
			]
		},
		{
			"Action": "cloudfront:UpdateDistribution",
			"Effect": "Allow",
			"Resource": "arn:aws:cloudfront::<account_id>:distribution/<primary_distribution_id>"
		}
	]
}

想定する初期入力

このステートマシンで想定する初期入力は以下の通りで、PrimaryIdにプライマリディストリビューションのID、StagingIdにステージングディストリビューションのIDを指定します。

{
    "PrimaryId": "EXXXXXXXXXX",
    "StagingId": "EYYYYYYYYYY"
}

1. プライマリディストリビューションのETag取得

最初にプライマリディストリビューションのETagを取得します。
今回はResultPathを指定してPrimaryDistribution配下にETagの値を設定する様にしました。

このステップを終えた後の結果は以下となる想定です。

{
  "PrimaryId": "EXXXXXXXXXX",
  "StagingId": "EYYYYYYYYYY",
  "PrimaryDistribution": {
    "ETag": "ETAGXXXXXX"
  }
}

2. ステージングディストリビューションのETag取得

次にステージングディストリビューションのETagを取得します。
プライマリディストリビューション同様にResultPathを指定してやります。

このステップを終えた後の結果は以下となる想定です。

{
  "PrimaryId": "EXXXXXXXXXX",
  "StagingId": "EYYYYYYYYYY",
  "PrimaryDistribution": {
    "ETag": "ETAGXXXXXX"
  },
  "StagingDistribution": {
    "ETag": "ETAGYYYYYY"
  }
}

これで必要なETagが集まりました。

3. CloudFrontの昇格

最後にUpdateDistributionWithStagingConfig APIを呼び出せば完了ですが、ここでAPIパラメーターの指定方法で少しハマりました。

最初に結果から書くと以下の様な指定をしてやります。

入力パラメーター

{
  "Id.$": "$.PrimaryId",
  "StagingDistributionId.$": "$.StagingId",
  "IfMatch.$": "States.Format('{},{}', $.PrimaryDistribution.ETag, $.StagingDistribution.ETag)"
}

パラメーターはそれぞれ

  • IdにはプライマリディストリビューションのID
  • StagingDistributionIdにはステージングディストリビューションのID
  • IfMatchに両者のETag

なのですが、IfMatchには「カンマ区切りの文字列」を設定します。

最初は「入力がJSONだし配列で渡せばよいのかな?」と誤解してたのですが、結果として必要だったのはただの文字列でした。
カンマ区切りにしてやるためStates.Format組み込み関数を使うのがポイントです。

ちなみにですがPython SDKなど他言語SDKでの引数を見て誤解が解けました。

動作確認

最後に手元の環境で動作確認した結果だけ共有します。
下図の様に2つのディストリビューションで継続的デプロイを設定してる環境でステートマシンを実行します。

権限設定に問題無ければエラー無く処理が終了します。

APIが実行されれるとプライマリディストリビューションに対してデプロイが開始されます。
デプロイの完了待ちはしないので、完了待ちをしたい場合は自分で実装する必要があります。

終わりに

以上となります。

IfMatchの設定方法で少しハマったので記事にしてみました。
こういうシンプルな処理にはStep Functionsがばっちりハマるのでとても良いですね。

脚注

  1. 全ての設定をコピーするわけではないのですが、この点については本記事では語りません