はじめに
Amazon CloudFront の継続的デプロイを軸にして、いろいろ書いてきました。最初は紹介記事でした。
次に、継続的デプロイを AWS CodePipeline に載せて自動化しました。シェルスクリプトをたくさん書いて AWS CodeBuild で動かしたので辛かったです。
その後、シェルスクリプトをステートマシン化しました。あとはこのふたつを元々の CodePipeline に統合することで、やりたいことがすべて達成できそうです。
- Amazon CloudFront の継続的デプロイを AWS Step Functions でやってみた 1
- Amazon CloudFront の継続的デプロイを AWS Step Functions でやってみた 2
本記事では以下の環境を CDK で構築します。S3 バケットへのデプロイは s3 sync
が気軽すぎて引き続き CodeBuild にしてしまいました。
ステージ | アクション | 説明 |
---|---|---|
Source | CodeCommit | リポジトリのコード変更を検知して AWS CodePipeline を起動する |
Build | CodeBuild | サンプル React アプリを AWS CodeBuild で Lint, Test, Build する |
Deploy | CodeBuild | s3 sync コマンドを実行してデプロイする |
Configure | StepFunctions | Amazon CloudFront ステージングを作成または有効化し、Continuous deployment を構成する |
Approve | ManualApproval | 手動でリリースを承認する |
Promote | StepFunctions | Amazon CloudFront ディストリビューションを昇格する |
概要
この構成のパイプラインが、
こうなります。
リポジトリはこちらです。
補足
今回 CDK によるリソースの準備では、ステージングと Continuous Deployment Policy のセットアップを行っていません。L2 コンストラクト + エスケープハッチで実現できないか試しはしたのですが、以下の通りなかなかうまくいきませんでした。
- L2 の
Distribution
クラスではステージングディストリビューションを作るためのstaging
フラグを直接扱えない - ステージングでは CNAME の設定ができないが、CNAME なし + 証明書ありの設定だと以下エラーになった
Error: Must specify at least one domain name to use a certificate with a distribution
ディストリビューションで証明書を使用するには、少なくとも1つのドメイン名を指定する必要があります。
- Continuous Deployment Policy をプライマリディストリビューションにアタッチしようとしたところ、以下のエラーが発生
"Invalid request provided: AWS::CloudFront::Distribution: Continuous deployment policy is not supported during distribution creation.
ディストリビューション作成時の Continuous deployment policy はサポートされていません。
たいていのエラーはエスケープハッチを駆使することで回避できたのですが、最後のエラーだけは初回デプロイ後に変更を加える必要があります。最終的にはこれがネックになりました。
こういった経緯もあり、CloudFront まわりは以前構築したリソース構成から大きく変更しない方針にしました。つまり、パイプライン側でステージングディストリビューションと Continuous Deployment Policy を制御する方式です。前々回のステートマシン設計はこの背景に基づいています。
詳細
Step Functions 関連の CDK の設定を見ていきます。以下 2 つのステートマシンを作ります。
ステージ | やること |
---|---|
Configure | ステージングを作成または有効化し、Continuous deployment を構成する |
Promote | 昇格する |
ロールの作成
Configure
まずはステートマシンにアタッチするロールを作成します。
// Create step functions role for cloudfront continuous deployment configuration const frontendConfigureSfnRole = new iam.Role(this, "FrontendConfigureSfnRole", { roleName: `${serviceName}-frontend-configure-sfn-role`, assumedBy: new iam.ServicePrincipal("states.amazonaws.com"), inlinePolicies: { ["FrontendConfigureSfnRoleAdditionalPolicy"]: new iam.PolicyDocument({ statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["ssm:GetParameter", "ssm:PutParameter"], resources: [`arn:aws:ssm:${this.region}:${this.account}:*`], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["s3:GetBucketAcl", "s3:PutBucketAcl"], resources: [cloudfrontLogBucket.bucketArn, cloudfrontLogBucket.bucketArn + "/*"], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "cloudfront:GetDistribution", "cloudfront:GetDistributionConfig", "cloudfront:CreateDistribution", "cloudfront:UpdateDistribution", "cloudfront:CopyDistribution", ], resources: [`arn:aws:cloudfront::${this.account}:distribution/*`], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "cloudfront:GetContinuousDeploymentPolicy", "cloudfront:CreateContinuousDeploymentPolicy", "cloudfront:UpdateContinuousDeploymentPolicy", ], resources: [`arn:aws:cloudfront::${this.account}:continuous-deployment-policy/*`], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets", ], resources: ["*"], }), ], }), }, });
このロールには以下の権限を設定しています。必要に応じてリソースを絞り込めばよいでしょう。
- SSM パラメータストアを読み書きするための権限
- CloudFront がログバケットにアクセスするための権限
- ステージングを準備するための権限
- Continuous Deployment Policy を操作するための権限
- ステートマシンをマネコンで作成した際に設定される X-Ray のデフォルト権限
Promote
Promote に関しても同様です。
// Create step functions role for frontend promote const frontendPromoteSfnRole = new iam.Role(this, "FrontendPromoteSfnRole", { roleName: `${serviceName}-frontend-promote-sfn-role`, assumedBy: new iam.ServicePrincipal("states.amazonaws.com"), inlinePolicies: { ["FrontendPromoteSfnRoleAdditionalPolicy"]: new iam.PolicyDocument({ statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["ssm:GetParameter", "ssm:PutParameter"], resources: [`arn:aws:ssm:${this.region}:${this.account}:*`], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["s3:GetBucketAcl", "s3:PutBucketAcl"], resources: [cloudfrontLogBucket.bucketArn, cloudfrontLogBucket.bucketArn + "/*"], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "cloudfront:GetDistribution", "cloudfront:GetDistributionConfig", "cloudfront:UpdateDistribution", "cloudfront:GetInvalidation", "cloudfront:CreateInvalidation", ], resources: [`arn:aws:cloudfront::${this.account}:distribution/*`], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets", ], resources: ["*"], }), ], }), }, });
- SSM パラメータストアを読み書きするための権限
- CloudFront がログバケットにアクセスするための権限
- ディストリビューションを昇格するための権限
- ディストリビューションのキャッシュを削除するための権限
- ステートマシンをマネコンで作成した際に設定される X-Ray のデフォルト権限
ステートマシンの作成
前々回、前回でステートマシンを作り切っていたので、CDK で作成する際には ASL をファイルで渡すだけで済みました。ロールを渡すのを忘れないようにしましょう。
Configure
// Create step functions state machine for frontend configure const frontendConfigureSfn = new stepfunctions.StateMachine(this, "FrontendConfigureSfn", { stateMachineName: `${serviceName}-frontend-configure-sfn`, definitionBody: stepfunctions.DefinitionBody.fromFile("src/sfn/configure.json", {}), // <= ファイルで渡すだけ role: frontendConfigureSfnRole, });
Promote
// Create step functions state machine for frontend promote const frontendPromoteSfn = new stepfunctions.StateMachine(this, "FrontendPromoteSfn", { stateMachineName: `${serviceName}-frontend-promote-sfn`, definitionBody: stepfunctions.DefinitionBody.fromFile("src/sfn/promote.json", {}), // <= ファイルで渡すだけ role: frontendPromoteSfnRole, });
パイプラインアクションの作成
各アクションが必要とする権限のロールは、CDK が以下のような形で作ってくれます。
Configure
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "states:DescribeStateMachine", "states:StartExecution" ], "Resource": "arn:aws:states:<region>:<account>:stateMachine:cfcd-test-frontend-configure-sfn", "Effect": "Allow" }, { "Action": "states:DescribeExecution", "Resource": "arn:aws:states:<region>:<account>:execution:cfcd-test-frontend-configure-sfn:*", "Effect": "Allow" } ] }
Promote
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "states:DescribeStateMachine", "states:StartExecution" ], "Resource": "arn:aws:states:<region>:<account>:stateMachine:cfcd-test-frontend-promote-sfn", "Effect": "Allow" }, { "Action": "states:DescribeExecution", "Resource": "arn:aws:states:<region>:<account>:execution:cfcd-test-frontend-promote-sfn:*", "Effect": "Allow" } ] }
今回は命名を制御したいので、空のロールを作ることで上記ポリシーがインラインで設定されるようにします。
Configure
// Create step functions configure role for frontend const frontendConfigureActionRole = new iam.Role(this, "FrontendConfigureActionRole", { roleName: `${serviceName}-frontend-configure-action-role`, assumedBy: new iam.ArnPrincipal(`arn:aws:iam::${this.account}:root`), });
Promote
// Create step fucntions promote role for frontend const frontendPromoteActionRole = new iam.Role(this, "FrontendPromoteActionRole", { roleName: `${serviceName}-frontend-promote-action-role`, assumedBy: new iam.ArnPrincipal(`arn:aws:iam::${this.account}:root`), });
パイプラインアクションでは StepFunctionInvokeAction
を使います。ここでは StateMachineInput
が特に重要です。SSM パラメータストアのキーを渡したりしています。
Configure
// Create frontend pipeline action for configure stage const frontendConfigureAction = new codepipeline_actions.StepFunctionInvokeAction({ actionName: configureStageName, StateMachineInput: codepipeline_actions.StateMachineInput.literal({ ParameterKeyFrontendVersion: `/${serviceName}/version/frontend`, ParameterKeyStagingDistributionId: `/${serviceName}/cloudfront/cfcd-staging`, PrimaryDistributionId: primaryDistributionId, }), stateMachine: frontendConfigureSfn, role: frontendConfigureActionRole, runOrder: 1, });
Promote
// Create frontend pipeline action for promote stage const frontendPromoteAction = new codepipeline_actions.StepFunctionInvokeAction({ actionName: promoteStageName, StateMachineInput: codepipeline_actions.StateMachineInput.literal({ ParameterKeyStagingDistributionId: `/${serviceName}/cloudfront/cfcd-staging`, PrimaryDistributionId: primaryDistributionId, }), stateMachine: frontendPromoteSfn, role: frontendPromoteActionRole, runOrder: 1, });
パイプラインの作成
あとはパイプラインに各ステージをマッピングすれば OK です。
// Create frontend pipeline const frontendPipeline = new codepipeline.Pipeline(this, "FrontendPipeline", { pipelineName: `${serviceName}-frontend-pipeline`, pipelineType: codepipeline.PipelineType.V2, role: frontendPipelineRole, artifactBucket: frontendArtifactBucket, }); frontendPipeline.addStage({ stageName: sourceStageName, actions: [frontendSourceAction], }); frontendPipeline.addStage({ stageName: buildStageName, actions: [frontendBuildAction], }); frontendPipeline.addStage({ stageName: deployStageName, actions: [frontendDeployAction], }); frontendPipeline.addStage({ stageName: configureStageName, actions: [frontendConfigureAction], }); frontendPipeline.addStage({ stageName: approveStageName, actions: [frontendApproveAction], }); frontendPipeline.addStage({ stageName: promoteStageName, actions: [frontendPromoteAction], });
動かしてみる
では動作確認します。スタックはすでにデプロイされている前提です。以下のような画面です。
React のサンプルアプリに変更を加えます。
import React from "react"; import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; import json from "react-syntax-highlighter/dist/esm/languages/hljs/json"; - import { githubGist } from "react-syntax-highlighter/dist/esm/styles/hljs"; + import { solarizedDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; import "./App.css"; SyntaxHighlighter.registerLanguage("json", json); function App() { return ( <div className="container"> - <SyntaxHighlighter language="json" style={githubGist}> + <SyntaxHighlighter language="json" style={solarizedDark}> {JSON.stringify({ version: process.env.REACT_APP_VERSION_FRONTEND }, null, 2)} </SyntaxHighlighter> </div> ); } export default App;
アプリケーションのバージョン情報は SSM パラメータストアで管理しているので、値を変更します。この情報はオリジンパスとして使用され、かつ S3 へのデプロイ時に新しいバージョンのプレフィックスとして設定されます。
$ aws ssm put-parameter --name "/cfcd-test/version/frontend" --value "v2" --type String --overwrite { "Version": 2, "Tier": "Standard" }
変更を Push します。ステージングに反映され、手動承認ステージで止まるはずです。
git add src/s3/hosting/src/App.js git commit -m "test: run pipeline" git push origin main
AWS Step Functions による Configure アクションをパスし、手動承認で止まりました。
初回起動時
2 回目以降起動時
続いて Requestly という Chrome 拡張を使い、ヘッダーを出し分けて動作確認します。
ヘッダーなし (Requestly: Off)
ヘッダーあり (Requestly: On)
ヘッダーの有無でリクエストの向き先が変わっているのがわかります。この状態で開発者はテストを実施し、OK であれば承認して昇格を走らせます。
成功しました!
ヘッダーなし (Requestly: Off) でも v2
になっています。
おわりに
もともと ASL を整備していたのもあると思いますが、AWS CodePipeline と AWS Step Functions の組み合わせの体験がよく、もっといろいろなユースケースをためしてみたくなりました。とはいえ、以下の課題も少し残っています。
- アプリバージョンがパラメータストア管理であり、更新を失念してもパイプラインはそのまま走ってしまう
- S3 へのデプロイを引き続き AWS CodeBuild で妥協している
これらについても最適化できないか、機会があれば検証してみたいと思います。