CloudFront + S3 静的サイト(開発/本番環境)のCI/CDパイプラインをCloudFormationで構築する
はじめに
こんにちは、フクナガです。
この記事はNHN テコラス Advent Calendar 2023の25日目の記事です。
本記事では、CloudFront + S3でホストする静的サイト環境とCI/CDパイプラインの環境概要や設定とそれらを構築するためのCloudFormationテンプレートをご紹介します。
構築する環境
S3のアクセス許可について
CloudFrontからS3の接続はOAC(Origin Access Control)を利用し、S3への直接接続を禁止することで、セキュアな接続を実現します。OACについての詳細はこちらの記事をご参考にしていただければ幸いです。
【アップデート】Amazon CloudFront で Origin Access Control (OAC) が利用開始されました!
運用IAMユーザーについて
ご紹介するテンプレート内にはIAMユーザーも含まれております。こちらを利用すると、CodeCommitへのソースコードアップロードが可能になります。
また、対象IAMユーザーが直接mainブランチへプッシュやマージを行うことができないように権限を設定してあります。
開発/本番環境デプロイについて
今回の環境は開発/本番環境と用途に応じて環境を分ける構成としています。また、S3にデプロイするソースコードはCodeCommit上で管理する設定となっています。
1. ブランチ戦略
CodeCommitの「dev」ブランチの更新時に開発環境へデプロイ、「main」ブランチの更新時に本番環境へデプロイする設定を実装します。
開発作業を実施する際は、個別でブランチを作成し、「dev」ブランチへプルリクエストを作成、マージします。
「main」ブランチの更新は、必ず「dev」ブランチの内容を「main」ブランチへマージすることで実施することとします。
2. S3へのデプロイ
CodeDeployを利用し、S3へのコンテンツアップロードを実施します。
3. キャッシュクリア
CloudFrontでキャッシュを利用する場合も考慮し、CI/CDパイプライン実行時にCloudFrontのキャッシュクリアを実行する設定とします。
CodeDeployでS3にコンテンツをデプロイ後、CodeBuildで下記キャッシュクリアコマンドを実施します。
aws cloudfront create-invalidation --distribution-id $CloudFront_Distribution --paths "$CloudFront_Invalidation_Path" --region us-east-1
CloudFormationテンプレート
※こちらのテンプレートはバージニア北部リージョン(us-east-1)で実行してください
AWSTemplateFormatVersion: 2010-09-09 Description: Static contents distribution using S3 and CloudFront. Parameters: SiteNameProd: Type: String Description: domain name e.g. test-site.com SiteNameDev: Type: String Description: domain name e.g. dev.test-site.com SiteIdentifier: Type: String Description: Site Identifier. Don't Use [.] !! e.g. test-site-com CloudFrontInvalidationPath: Description: input the invalidation path of CloudFront cache clear Type: String Default: /* Resources: AssetsBucketProd: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: BucketName: !Sub ${SiteNameProd}-s3-bucket BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True WebsiteConfiguration: IndexDocument: index.html AssetsBucketProdPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref AssetsBucketProd PolicyDocument: Statement: - Action: s3:GetObject Effect: Allow Resource: !Sub arn:aws:s3:::${AssetsBucketProd}/* Principal: Service: "cloudfront.amazonaws.com" Condition: StringEquals: AWS:SourceArn: - !Join - "" - - !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/" - !Ref AssetsCloudFrontDistributionProd - Action: - "s3:GetObject" - "s3:DeleteObject" - "s3:GetObjectVersion" - "s3:DeleteObjectVersion" - "s3:PutObject" - "s3:ListBucket" - "s3:ListBucketVersions" Effect: Allow Resource: - !Sub arn:aws:s3:::${AssetsBucketProd}/* - !Sub arn:aws:s3:::${AssetsBucketProd} Principal: AWS: !GetAtt CodeBuildServiceRole.Arn AssetsBucketDev: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: BucketName: !Sub ${SiteNameDev}-s3-bucket BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True WebsiteConfiguration: IndexDocument: index.html AssetsBucketDevPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref AssetsBucketDev PolicyDocument: Statement: - Action: s3:GetObject Effect: Allow Resource: !Sub arn:aws:s3:::${AssetsBucketDev}/* Principal: Service: "cloudfront.amazonaws.com" Condition: StringEquals: AWS:SourceArn: - !Join - "" - - !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/" - !Ref AssetsCloudFrontDistributionDev - Action: - "s3:GetObject" - "s3:DeleteObject" - "s3:GetObjectVersion" - "s3:DeleteObjectVersion" - "s3:PutObject" - "s3:ListBucket" - "s3:ListBucketVersions" Effect: Allow Resource: - !Sub arn:aws:s3:::${AssetsBucketDev}/* - !Sub arn:aws:s3:::${AssetsBucketDev} Principal: AWS: !GetAtt CodeBuildServiceRole.Arn AssetsCloudFrontDistributionProd: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Origins: - Id: S3 DomainName: !GetAtt AssetsBucketProd.DomainName OriginAccessControlId: !GetAtt OACProd.Id S3OriginConfig: OriginAccessIdentity: "" Enabled: true DefaultRootObject: index.html Comment: !Ref SiteNameProd DefaultCacheBehavior: AllowedMethods: - HEAD - GET CachedMethods: - HEAD - GET DefaultTTL: 0 MaxTTL: 0 MinTTL: 0 TargetOriginId: S3 ForwardedValues: QueryString: false ViewerProtocolPolicy: redirect-to-https IPV6Enabled: false OACProd: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Description: !Ref SiteNameProd Name: !Ref SiteNameProd OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 AssetsCloudFrontDistributionDev: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Origins: - Id: S3 DomainName: !GetAtt AssetsBucketDev.DomainName OriginAccessControlId: !GetAtt OACDev.Id S3OriginConfig: OriginAccessIdentity: "" Enabled: true DefaultRootObject: index.html Comment: !Ref SiteNameDev DefaultCacheBehavior: AllowedMethods: - HEAD - GET CachedMethods: - HEAD - GET DefaultTTL: 0 MaxTTL: 0 MinTTL: 0 TargetOriginId: S3 ForwardedValues: QueryString: false ViewerProtocolPolicy: redirect-to-https IPV6Enabled: false OACDev: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Description: !Ref SiteNameDev Name: !Ref SiteNameDev OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 CodeCommit: Type: AWS::CodeCommit::Repository Properties: RepositoryName: !Sub ${SiteIdentifier}-source-repository RepositoryDescription: CodeCommit Repository CodeBuildCWLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub ${SiteIdentifier}-CodeBuild AmazonCloudWatchEventRule: Type: AWS::Events::Rule Properties: EventPattern: source: - aws.codecommit detail-type: - "CodeCommit Repository State Change" resources: - !Join [ "", [ "arn:aws:codecommit:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !GetAtt CodeCommit.Name, ], ] detail: event: - referenceCreated - referenceUpdated referenceType: - branch referenceName: - main Targets: - Arn: !Join - "" - - "arn:aws:codepipeline:" - !Ref AWS::Region - ":" - !Ref AWS::AccountId - ":" - !Ref Pipeline RoleArn: !GetAtt AmazonCloudWatchEventRole.Arn Id: codepipeline-AppPipeline AmazonCloudWatchEventRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - "sts:AssumeRole" Path: / Policies: - PolicyName: cwe-pipeline-execution PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: "codepipeline:StartPipelineExecution" Resource: !Join - "" - - "arn:aws:codepipeline:" - !Ref AWS::Region - ":" - !Ref AWS::AccountId - ":" - !Ref Pipeline CodeBuildProjectCloudFrontCacheClearProd: Type: AWS::CodeBuild::Project Properties: Name: !Sub ${SiteIdentifier}-prod-CacheClear Artifacts: Type: CODEPIPELINE Source: Type: CODEPIPELINE BuildSpec: | version: 0.2 phases: build: commands: - aws cloudfront create-invalidation --distribution-id $CloudFront_Distribution --paths "$CloudFront_Invalidation_Path" --region us-east-1 Environment: Type: LINUX_CONTAINER Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0 ComputeType: BUILD_GENERAL1_SMALL EnvironmentVariables: - Name: CloudFront_Distribution Type: PLAINTEXT Value: !GetAtt AssetsCloudFrontDistributionProd.Id - Name: CloudFront_Invalidation_Path Type: PLAINTEXT Value: !Ref CloudFrontInvalidationPath LogsConfig: CloudWatchLogs: GroupName: !Sub ${SiteIdentifier}-CodeBuild Status: ENABLED ServiceRole: !Ref CodeBuildServiceRole TimeoutInMinutes: 60 QueuedTimeoutInMinutes: 480 CodeBuildServiceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: - "sts:AssumeRole" Policies: - PolicyName: CodeBuildAccess PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Resource: "*" Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Effect: Allow Resource: "arn:aws:s3:::*" Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketAcl - s3:GetBucketLocation - Effect: Allow Resource: !Join - "" - - "arn:aws:codebuild:" - !Ref AWS::Region - ":" - !Ref AWS::AccountId - ":report-group/" - !Ref SiteIdentifier - "-prod-CacheClear" Action: - codebuild:CreateReportGroup - codebuild:CreateReport - codebuild:UpdateReport - codebuild:BatchPutTestCases - codebuild:BatchPutCodeCoverages - Effect: Allow Resource: "*" Action: - iam:* - Effect: Allow Resource: - "*" Action: - cloudfront:CreateInvalidation Pipeline: Type: AWS::CodePipeline::Pipeline Properties: Name: !Sub ${SiteIdentifier}-prod-codepipeline RoleArn: !GetAtt CodePipelineServiceRole.Arn Stages: - Name: Source Actions: - Name: Source ActionTypeId: Category: Source Owner: AWS Provider: CodeCommit Version: 1 Configuration: RepositoryName: !GetAtt CodeCommit.Name PollForSourceChanges: false BranchName: main RunOrder: 1 OutputArtifacts: - Name: BuildArtifact - Name: "Deploy" Actions: - Name: "Deploy" ActionTypeId: Category: "Deploy" Owner: "AWS" Version: "1" Provider: "S3" Configuration: BucketName: !Ref AssetsBucketProd Extract: "true" RunOrder: 1 InputArtifacts: - Name: "BuildArtifact" - Name: CacheClear Actions: - Name: CacheClear ActionTypeId: Category: Build Owner: AWS Version: 1 Provider: CodeBuild Configuration: ProjectName: !Ref CodeBuildProjectCloudFrontCacheClearProd RunOrder: 1 InputArtifacts: - Name: BuildArtifact ArtifactStore: Type: S3 Location: !Ref ArtifactBucket CodePipelineServiceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: - "sts:AssumeRole" Policies: - PolicyName: PipelinePolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Resource: - !Sub arn:aws:s3:::${ArtifactBucket}/* Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketVersioning - Effect: Allow Resource: "*" Action: - cloudformation:* - codecommit:* - codedeploy:* - codebuild:* - s3:* CodeBuildCWLogGroupDev: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub ${SiteIdentifier}-CodeBuild-dev AmazonCloudWatchEventRuleDev: Type: AWS::Events::Rule Properties: EventPattern: source: - aws.codecommit detail-type: - "CodeCommit Repository State Change" resources: - !Join [ "", [ "arn:aws:codecommit:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !GetAtt CodeCommit.Name, ], ] detail: event: - referenceCreated - referenceUpdated referenceType: - branch referenceName: - dev Targets: - Arn: !Join - "" - - "arn:aws:codepipeline:" - !Ref AWS::Region - ":" - !Ref AWS::AccountId - ":" - !Ref PipelineDev RoleArn: !GetAtt AmazonCloudWatchEventRoleDev.Arn Id: codepipeline-AppPipeline-Dev AmazonCloudWatchEventRoleDev: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - "sts:AssumeRole" Path: / Policies: - PolicyName: cwe-pipeline-execution-dev PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: "codepipeline:StartPipelineExecution" Resource: !Join - "" - - "arn:aws:codepipeline:" - !Ref AWS::Region - ":" - !Ref AWS::AccountId - ":" - !Ref PipelineDev CodeBuildProjectCloudFrontCacheClearDev: Type: AWS::CodeBuild::Project Properties: Name: !Sub ${SiteIdentifier}-dev-CacheClear Artifacts: Type: CODEPIPELINE Source: Type: CODEPIPELINE BuildSpec: | version: 0.2 phases: build: commands: - aws cloudfront create-invalidation --distribution-id $CloudFront_Distribution --paths "$CloudFront_Invalidation_Path" --region us-east-1 Environment: Type: LINUX_CONTAINER Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0 ComputeType: BUILD_GENERAL1_SMALL EnvironmentVariables: - Name: CloudFront_Distribution Type: PLAINTEXT Value: !GetAtt AssetsCloudFrontDistributionDev.Id - Name: CloudFront_Invalidation_Path Type: PLAINTEXT Value: !Ref CloudFrontInvalidationPath LogsConfig: CloudWatchLogs: GroupName: !Sub ${SiteIdentifier}-CodeBuild-dev Status: ENABLED ServiceRole: !Ref CodeBuildServiceRoleDev TimeoutInMinutes: 60 QueuedTimeoutInMinutes: 480 CodeBuildServiceRoleDev: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: - "sts:AssumeRole" Policies: - PolicyName: CodeBuildAccessDev PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Resource: "*" Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Effect: Allow Resource: "arn:aws:s3:::*" Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketAcl - s3:GetBucketLocation - Effect: Allow Resource: !Join - "" - - "arn:aws:codebuild:" - !Ref AWS::Region - ":" - !Ref AWS::AccountId - ":report-group/" - !Ref SiteIdentifier - "-dev-CacheClear" Action: - codebuild:CreateReportGroup - codebuild:CreateReport - codebuild:UpdateReport - codebuild:BatchPutTestCases - codebuild:BatchPutCodeCoverages - Effect: Allow Resource: "*" Action: - iam:* - Effect: Allow Resource: - "*" Action: - cloudfront:CreateInvalidation PipelineDev: Type: AWS::CodePipeline::Pipeline Properties: Name: !Sub ${SiteIdentifier}-dev-codepipeline RoleArn: !GetAtt CodePipelineServiceRoleDev.Arn Stages: - Name: Source Actions: - Name: Source ActionTypeId: Category: Source Owner: AWS Provider: CodeCommit Version: 1 Configuration: RepositoryName: !GetAtt CodeCommit.Name PollForSourceChanges: false BranchName: dev RunOrder: 1 OutputArtifacts: - Name: BuildArtifact - Name: "Deploy" Actions: - Name: "Deploy" ActionTypeId: Category: "Deploy" Owner: "AWS" Version: "1" Provider: "S3" Configuration: BucketName: !Ref AssetsBucketDev Extract: "true" RunOrder: 1 InputArtifacts: - Name: "BuildArtifact" - Name: CacheClear Actions: - Name: CacheClear ActionTypeId: Category: Build Owner: AWS Version: 1 Provider: CodeBuild Configuration: ProjectName: !Ref CodeBuildProjectCloudFrontCacheClearDev RunOrder: 1 InputArtifacts: - Name: BuildArtifact ArtifactStore: Type: S3 Location: !Ref ArtifactBucket CodePipelineServiceRoleDev: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: - "sts:AssumeRole" Policies: - PolicyName: PipelinePolicyDev PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Resource: - !Sub arn:aws:s3:::${ArtifactBucket}/* Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketVersioning - Effect: Allow Resource: "*" Action: - cloudformation:* - codecommit:* - codedeploy:* - codebuild:* - s3:* ArtifactBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${SiteIdentifier}-artifact-bucket BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True IAMUserForGit: Type: AWS::IAM::User Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/AWSCodeCommitFullAccess - arn:aws:iam::aws:policy/ReadOnlyAccess Policies: - PolicyDocument: Version: "2012-10-17" Statement: - Effect: Deny Action: - codecommit:GitPush - codecommit:DeleteBranch - codecommit:PutFile - codecommit:CreateCommit - codecommit:MergeBranchesByFastForward - codecommit:MergeBranchesBySquash - codecommit:MergeBranchesByThreeWay - codecommit:MergePullRequestByFastForward - codecommit:MergePullRequestBySquash Resource: "*" Condition: StringEqualsIfExists: codecommit:References: - refs/heads/main "Null": codecommit:References: - false PolicyName: DenyCommit UserName: !Sub ${SiteIdentifier}-Developer
テンプレートの利用方法
バージニア北部リージョン(us-east-1)でCloudFormationコンソールから上記テンプレートをアップロードし、下記パラメータを入力し、実行することで構築が完了します。
- SiteNameProd
構築対象の本番サイトの名前を入力します。本番用S3バケット、CloudFrontのCommentなどで利用されます。
主に識別子としての役割になるので、S3バケットの命名規則に準じていれば任意の値で問題ありません。 -
SiteNameDev
構築対象の開発サイトの名前を入力します。開発用S3バケット、CloudFrontのCommentなどで利用されます。
主に識別子としての役割になるので、S3バケットの命名規則に準じていれば任意の値で問題ありません。 -
SiteIdentifier
CI/CD関連リソースの命名に利用される値です。他リソースや名前との兼ね合いからアンダースコアではなくハイフンを使うことを推奨します。 -
CloudFrontInvalidationPath
デフォルトのままで実行します。
稼働確認
実運用とCI/CDパイプラインが動作する様子を簡単にデモします。
事前準備
事前にこういったサイトをCI/CDパイプラインを使って初回デプロイをしておきます。
【初回デプロイ手順】
1. 作成したCodeCommitでdev、mainブランチを作成
2. 任意のhtmlを「index.html」という名前でアップロード
3. 自動デプロイが開始、1~2分以内に反映される
【サイトへの接続】
対象のCloudFront「ディストリビューションドメイン名」をURLとして入力すると、構築したサイトへ接続できます。
実行準備が整ったので、実際に一部を変更し、CI/CDパイプラインを実行します。
稼働確認
1. ソースの変更
今回は、元々「Technology Event 2023」となっていた部分を「Techblog Test Event 2023」と変更します。
CodeCommitからローカルに取得したソースを変更します。
【変更前】
【変更後】
ソース変更後に、「20231215-title-change」という名前で新たなブランチを作成しました。
2. devブランチへ変更をマージ
新規作成した「20231215-title-change」から「dev」ブランチへプルリクエストを作成し、マージします。
3. パイプラインの実行が開始される
4. パイプラインが完了後、開発環境サイトへアクセスする
開発環境へは、変更が適応されていることが確認できました!
「main」ブランチは変更していないため、本番環境サイトは変更前の状態でした!
まとめ
本記事では、CloudFront + S3でホストする静的サイト環境とCI/CDパイプラインの環境概要や設定とそれらを構築するためのCloudFormationテンプレートをご紹介しました。誰がどういった変更を加えた、という情報を素早く把握できるうえ、セキュアなデプロイを実現できるため、開発/本番環境の構築やCI/CDの構築は非常に大切です。今回ご紹介したテンプレートを活用して、CI/CDパイプライン構築のハードルが少しでも下がればとてもうれしいです!!
テックブログ新着情報のほか、AWSやGoogle Cloudに関するお役立ち情報を配信中!
Follow @twitterインフラエンジニア歴5年のフクナガです。2024 Japan AWS Top Engineers / Google Cloud Partner Top Engineer 2025 に選出されました! 生成 AI 多めで発信していますが、CI/CDやIaCへの関心も高いです。休日はベースを弾いてます。
Recommends
こちらもおすすめ
-
AWS Glueの「AmazonS3イベント通知を使用した加速クロール」とは何か
2022.12.22
-
Amazon CloudFrontでReactを動かす
2024.3.26
-
CloudFormationで認証情報を扱うベストプラクティス
2020.8.7
Special Topics
注目記事はこちら
データ分析入門
これから始めるBigQuery基礎知識
2024.02.28
AWSの料金が 10 %割引になる!
『AWSの請求代行リセールサービス』
2024.07.16