CloudFront+S3とCodePipelineをCFnでデプロイする – その2

2020.12.03

はじめに

この記事は、前回のCloudFront+S3とCodePipelineをCFnでデプロイする - その1の後半記事です。

構成図

CLoudFront、S3、ACM、Route53関連はデプロイが完了しているのでCodepipelineのテンプレートを作成して親スタックに追加していきます。

CodeStarSoruceConnectionテンプレートの作成

ソースプロバイダがGitHubの場合、GitHubバージョン2アクションを利用することが推奨されています。

GitHubバージョン 1 のソースアクションをGitHubバージョン 2 のソースアクションに更新する

CodeStarSourceConnectionリソースが必要です。リソース作成後のGithub側の操作は手動の実施する必要がある為、親スタックと別にスタックを作成します。テンプレートは以下の通りです。

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SystemName:
    Type: String
    Default: example
  ProviderType:
    Type: String
    Default: GitHub
    AllowedValues:
      - Bitbucket
      - GitHub
      - GitHubEnterpriseServer
    
Resources:
  SourceConnection:
    Type: 'AWS::CodeStarConnections::Connection'
    Properties:
      ConnectionName: !Sub ${SystemName}-Con
      ProviderType: !Ref ProviderType

Outputs:
  SourceConnection:
    Value: !Ref SourceConnection
    Export: 
      Name: !Sub ${SystemName}-ConnectionARN

上記テンプレートでスタックを作成するとスタックはCREATE_COMPLETEになります。Connectionリソースが作成されてステータスが保留中となっているので「保留中の接続を更新」クリックします。

別ウィンドウが表示されるので「新しいアプリをインストールする」をクリックします。

GitHubにサインインします。サインインすると↑の画面に戻るので再度「新しいアプリをインストールする」をクリックします。

AWS Connector for GitHubアプリをインストールするGitHubアカウントをクリックします。

AWS Connector for GitHubアプリがアクセスできるリポジトリを選択できます。今回はAll repositoriesを選択してInstallします。リポジトリを指定する場合はインストール後にGitHubアカウントで追加やAll repositoriesに変更できます。

インストールが終わると以下の画面で接続をクリックするとConnectionリソースのステータスが利用可能となります。

CodePipelineをデプロイする事前準備が完了です。それではCodePipelineテンプレートを作成していきます。アーティファクトストア用のS3バケットテンプレート、CodePipeline用IAMテンプレート、CodePipeline(CodeBuild含む)テンプレートを作成します。

S3バケットテンプレートの作成(子スタック)

アーティファクトストア用のS3バケットのテンプレートです。バケットを作成するだけなので無理して分割しなくても良いですが、バケットポリシーやアクセス管理、ライフサイクルを更新しやすくするために分割しています。

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SystemName:
    Type: String

Resources:
  S3BucketArtifactStore:
    Type: 'AWS::S3::Bucket'
    DeletionPolicy: 'Retain'
    Properties:
      BucketName: !Sub 'codepipeline-${AWS::Region}-${AWS::AccountId}'

Outputs:
  S3BucketArtifactStore:
    Value: !Ref S3BucketArtifactStore
    Export:
      Name: !Sub '${SystemName}-S3BucketArtifactStore'
  S3BucketArtifactStoreARN:
    Value: !GetAtt S3BucketArtifactStore.Arn
    Export: 
      Name: !Sub '${SystemName}-S3BucketArtifactStoreARN'

CodePipeline用IAMテンプレートの作成(子スタック)

CodePipeline、CodeBuildが利用するサービスロールを作成します。ロールおよびポリシーの内容は、GUIで自動作成される内容とほぼ同等です。

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SystemName:
    Type: String

Resources:
    IAMCodePipelineRole:
        Type: 'AWS::IAM::Role'
        Properties:
            Path: '/service-role/'
            RoleName: !Sub '${SystemName}-pipeline-ServiceRole'
            AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"codepipeline.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
            MaxSessionDuration: 3600
            ManagedPolicyArns: 
              - !Ref IAMCodePipelinePolicy

    IAMCodePipelinePolicy:
        Type: 'AWS::IAM::ManagedPolicy'
        Properties:
            ManagedPolicyName: !Sub '${SystemName}-pipeline-policy'
            Path: '/service-role/'
            PolicyDocument: |
                {
                    "Statement": [
                        {
                            "Action": [
                                "iam:PassRole"
                            ],
                            "Resource": "*",
                            "Effect": "Allow",
                            "Condition": {
                                "StringEqualsIfExists": {
                                    "iam:PassedToService": [
                                        "cloudformation.amazonaws.com",
                                        "elasticbeanstalk.amazonaws.com",
                                        "ec2.amazonaws.com",
                                        "ecs-tasks.amazonaws.com"
                                    ]
                                }
                            }
                        },
                        {
                            "Action": [
                                "codecommit:CancelUploadArchive",
                                "codecommit:GetBranch",
                                "codecommit:GetCommit",
                                "codecommit:GetRepository",
                                "codecommit:GetUploadArchiveStatus",
                                "codecommit:UploadArchive"
                            ],
                            "Resource": "*",
                            "Effect": "Allow"
                        },
                        {
                            "Action": [
                                "codedeploy:CreateDeployment",
                                "codedeploy:GetApplication",
                                "codedeploy:GetApplicationRevision",
                                "codedeploy:GetDeployment",
                                "codedeploy:GetDeploymentConfig",
                                "codedeploy:RegisterApplicationRevision"
                            ],
                            "Resource": "*",
                            "Effect": "Allow"
                        },
                        {
                            "Action": [
                                "codestar-connections:UseConnection"
                            ],
                            "Resource": "*",
                            "Effect": "Allow"
                        },
                        {
                            "Action": [
                                "elasticbeanstalk:*",
                                "ec2:*",
                                "elasticloadbalancing:*",
                                "autoscaling:*",
                                "cloudwatch:*",
                                "s3:*",
                                "sns:*",
                                "cloudformation:*",
                                "rds:*",
                                "sqs:*",
                                "ecs:*"
                            ],
                            "Resource": "*",
                            "Effect": "Allow"
                        },
                        {
                            "Action": [
                                "lambda:InvokeFunction",
                                "lambda:ListFunctions"
                            ],
                            "Resource": "*",
                            "Effect": "Allow"
                        },
                        {
                            "Action": [
                                "opsworks:CreateDeployment",
                                "opsworks:DescribeApps",
                                "opsworks:DescribeCommands",
                                "opsworks:DescribeDeployments",
                                "opsworks:DescribeInstances",
                                "opsworks:DescribeStacks",
                                "opsworks:UpdateApp",
                                "opsworks:UpdateStack"
                            ],
                            "Resource": "*",
                            "Effect": "Allow"
                        },
                        {
                            "Action": [
                                "cloudformation:CreateStack",
                                "cloudformation:DeleteStack",
                                "cloudformation:DescribeStacks",
                                "cloudformation:UpdateStack",
                                "cloudformation:CreateChangeSet",
                                "cloudformation:DeleteChangeSet",
                                "cloudformation:DescribeChangeSet",
                                "cloudformation:ExecuteChangeSet",
                                "cloudformation:SetStackPolicy",
                                "cloudformation:ValidateTemplate"
                            ],
                            "Resource": "*",
                            "Effect": "Allow"
                        },
                        {
                            "Action": [
                                "codebuild:BatchGetBuilds",
                                "codebuild:StartBuild",
                                "codebuild:BatchGetBuildBatches",
                                "codebuild:StartBuildBatch"
                            ],
                            "Resource": "*",
                            "Effect": "Allow"
                        },
                        {
                            "Effect": "Allow",
                            "Action": [
                                "devicefarm:ListProjects",
                                "devicefarm:ListDevicePools",
                                "devicefarm:GetRun",
                                "devicefarm:GetUpload",
                                "devicefarm:CreateUpload",
                                "devicefarm:ScheduleRun"
                            ],
                            "Resource": "*"
                        },
                        {
                            "Effect": "Allow",
                            "Action": [
                                "servicecatalog:ListProvisioningArtifacts",
                                "servicecatalog:CreateProvisioningArtifact",
                                "servicecatalog:DescribeProvisioningArtifact",
                                "servicecatalog:DeleteProvisioningArtifact",
                                "servicecatalog:UpdateProduct"
                            ],
                            "Resource": "*"
                        },
                        {
                            "Effect": "Allow",
                            "Action": [
                                "cloudformation:ValidateTemplate"
                            ],
                            "Resource": "*"
                        },
                        {
                            "Effect": "Allow",
                            "Action": [
                                "ecr:DescribeImages"
                            ],
                            "Resource": "*"
                        },
                        {
                            "Effect": "Allow",
                            "Action": [
                                "states:DescribeExecution",
                                "states:DescribeStateMachine",
                                "states:StartExecution"
                            ],
                            "Resource": "*"
                        },
                        {
                            "Effect": "Allow",
                            "Action": [
                                "appconfig:StartDeployment",
                                "appconfig:StopDeployment",
                                "appconfig:GetDeployment"
                            ],
                            "Resource": "*"
                        }
                    ],
                    "Version": "2012-10-17"
                }

    IAMCodeBuildRole:
        Type: 'AWS::IAM::Role'
        Properties:
            Path: '/service-role/'
            RoleName: !Sub '${SystemName}-build-ServiceRole'
            AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"codebuild.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
            MaxSessionDuration: 3600
            ManagedPolicyArns: 
              - !Ref IAMCodeBuildPolicy

    IAMCodeBuildPolicy:
        Type: 'AWS::IAM::ManagedPolicy'
        Properties:
            ManagedPolicyName: !Sub '${SystemName}-build-Policy'
            Path: '/service-role/'
            PolicyDocument: !Sub |
                {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Resource": [
                                "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*",
                                "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*:*"
                            ],
                            "Action": [
                                "logs:CreateLogGroup",
                                "logs:CreateLogStream",
                                "logs:PutLogEvents"
                            ]
                        },
                        {
                            "Effect": "Allow",
                            "Resource": [
                                "arn:aws:s3:::codepipeline-${AWS::Region}-*"
                            ],
                            "Action": [
                                "s3:PutObject",
                                "s3:GetObject",
                                "s3:GetObjectVersion",
                                "s3:GetBucketAcl",
                                "s3:GetBucketLocation"
                            ]
                        },
                        {
                            "Effect": "Allow",
                            "Action": [
                                "codebuild:CreateReportGroup",
                                "codebuild:CreateReport",
                                "codebuild:UpdateReport",
                                "codebuild:BatchPutTestCases",
                                "codebuild:BatchPutCodeCoverages"
                            ],
                            "Resource": [
                                "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${SystemName}-build-*"
                            ]
                        }
                    ]
                }

Outputs:
  IAMCodePipelineRoleARN:
    Value: !GetAtt IAMCodePipelineRole.Arn
    Export:
      Name: !Sub ${SystemName}-IAMCodePipelineRoleARN
  IAMCodeBuildRoleARN:
    Value: !GetAtt IAMCodeBuildRole.Arn
    Export:
      Name: !Sub ${SystemName}-IAMCodeBuildRoleARN

CodePipelineテンプレートの作成(子スタック)

CodePipelineテンプレートを作成します。Codebuildビルドログのロググループも併せて作成します。CodeDeployのプロバイダは、S3を指定するとアップロードから解凍まで実行されます。

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SystemName:
    Type: String
  RepositoryName:
    Type: String
  BranchName:
    Type: String  

Resources:
  CWLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub /aws/codebuild/${SystemName}-build-project
  CodePipeline:
    Type: 'AWS::CodePipeline::Pipeline'
    DependsOn: CWLogGroup
    Properties:
      RoleArn: {'Fn::ImportValue': !Sub '${SystemName}-IAMCodePipelineRoleARN'}
      ArtifactStore:
        Type: 'S3'
        Location: {'Fn::ImportValue': !Sub '${SystemName}-S3BucketArtifactStore'}
      Stages:
        - Name: 'Source'
          Actions:
            - Name: 'SourceAction'
              ActionTypeId:
                Category: 'Source'
                Owner: 'AWS'
                Version: 1
                Provider: 'CodeStarSourceConnection'
              Configuration:
                ConnectionArn: {'Fn::ImportValue': !Sub '${SystemName}-ConnectionARN'}
                FullRepositoryId: !Ref RepositoryName
                BranchName: !Ref BranchName
                OutputArtifactFormat: 'CODE_ZIP'
              RunOrder: 1
              OutputArtifacts:
                - Name: 'Source'
        - Name: 'Build'
          Actions:
            - Name: 'Build'
              ActionTypeId:
                Category: 'Build'
                Owner: 'AWS'
                Version: '1'
                Provider: 'CodeBuild'
              Configuration:
                ProjectName: !Sub '${SystemName}-build-Project'
              RunOrder: 1
              InputArtifacts:
                - Name: 'Source'
              OutputArtifacts:
                - Name: 'Build'
        - Name: 'Deploy'
          Actions:
            - Name: 'Deploy'
              ActionTypeId:
                Category: 'Deploy'
                Owner: 'AWS'
                Version: '1'
                Provider: 'S3'
              Configuration:
                BucketName: {'Fn::ImportValue': !Sub '${SystemName}-S3bucketForOrigin'}
                Extract: 'true'
              RunOrder: 1
              InputArtifacts:
                - Name: 'Build'
  CodeBuildProject:
      Type: 'AWS::CodeBuild::Project'
      DependsOn: CWLogGroup
      Properties:
          Name: !Sub '${SystemName}-build-Project'
          Source: 
              InsecureSsl: false
              Type: 'CODEPIPELINE'
          Artifacts: 
              EncryptionDisabled: false
              Name: !Sub '${SystemName}-build-Project'
              Packaging: 'NONE'
              Type: 'CODEPIPELINE'
          Cache: 
              Type: 'NO_CACHE'
          Environment: 
              ComputeType: 'BUILD_GENERAL1_SMALL'
              Image: 'aws/codebuild/amazonlinux2-x86_64-standard:3.0'
              ImagePullCredentialsType: 'CODEBUILD'
              PrivilegedMode: true
              Type: 'LINUX_CONTAINER'
          ServiceRole: {'Fn::ImportValue': !Sub '${SystemName}-IAMCodeBuildRoleARN'}
          TimeoutInMinutes: 60
          QueuedTimeoutInMinutes: 480
          EncryptionKey: !Sub 'arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3'
          BadgeEnabled: false
          LogsConfig: 
              CloudWatchLogs: 
                  Status: 'ENABLED'
              S3Logs: 
                  Status: 'DISABLED'
                  EncryptionDisabled: false

NestedStackテンプレートの更新(親スタック)

作成済みの親スタックのテンプレートにCodePipeline関連のテンプレートとParameters(RepositoryNameBranchName)を追加します。また、Parameterが増えてきたのでParameterGroupで整理しました。PIPELINEリソースは、DependsOn属性で依存関係を設定します。

AWSTemplateFormatVersion: '2010-09-09'

Metadata: 
  AWS::CloudFormation::Interface: 
    ParameterGroups: 
      -
        Label:
          default: System Name Configuration
        Parameters:
          - SystemName
      -
        Label:
          default: Hosting Configuration
        Parameters:
          - BucketName
          - HostedZone
          - MinimumProtocolVersion
          - SslSupportMethod
          - CertificateARN
      -
        Label:
          default: Pipeline Configuration
        Parameters:
          - RepositoryName
          - BranchName

Parameters:
  SystemName:
    Type: String
    Default: example
  BucketName:
    Type: String
    Default: www.example.xxx
  HostedZone:
    Type: String
    Default: ZXXXXXXXXXXXXXXXXXXXX
  MinimumProtocolVersion:
    Type: String
    Default: TLSv1.2_2019
    AllowedValues:
      - SSLv3
      - TLSv1
      - TLSv1.1_2016
      - TLSv1.2_2018
      - TLSv1.2_2019
      - TLSv1_2016
  SslSupportMethod:
    Type: String
    Default: sni-only
    AllowedValues:
      - sni-only
      - static-ip
      - vip
  CertificateARN:
    Type: String
    Default: ''
  RepositoryName:
    Type: String
  BranchName:
    Type: String

Resources: 
  HOSTING: 
    Type: AWS::CloudFormation::Stack
    Properties: 
      TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/hosting.yml'
      Parameters: 
        SystemName: !Sub ${SystemName}
        BucketName: !Sub ${BucketName}
        HostedZone: !Sub ${HostedZone}
        MinimumProtocolVersion: !Sub ${MinimumProtocolVersion}
        SslSupportMethod: !Sub ${SslSupportMethod}
        CertificateARN: !Sub ${CertificateARN}
  DNS: 
    Type: AWS::CloudFormation::Stack
    DependsOn: HOSTING
    Properties: 
      TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/dnsrecord.yml'
      Parameters: 
        SystemName: !Sub ${SystemName}
        BucketName: !Sub ${BucketName}
        HostedZone: !Sub ${HostedZone}
  ARTIFACT: 
    Type: AWS::CloudFormation::Stack
    Properties: 
      TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/pipeline-artifact.yml'
      Parameters: 
        SystemName: !Sub ${SystemName}
  IAMPIPELINE: 
    Type: AWS::CloudFormation::Stack
    Properties: 
      TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/pipeline-iam.yml'
      Parameters: 
        SystemName: !Sub ${SystemName}
  PIPELINE: 
    Type: AWS::CloudFormation::Stack
    DependsOn: 
      - ARTIFACT
      - IAMPIPELINE
      - HOSTING
    Properties: 
      TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/pipeline.yml'
      Parameters: 
        SystemName: !Sub ${SystemName}
        RepositoryName: !Sub ${RepositoryName}
        BranchName: !Sub ${BranchName}

NestedStackの実行(更新)

子スタックのテンプレートをS3バケットに格納し、親スタックを更新します。

パラメータでGitHubのRipositoryName(GitHubアカウント名/リポジトリ名)、Branchを入力します。

親スタックを更新すると追加した子スタックが作成されてすべてのステータスがCREATE_COMPLETEになったら完了です。

CodePipelineを見るとPipelineが実行されてDeployが完了していればOKです。

動作確認

URLを確認しましょう。実施した環境ではAngulerのサンプルソースコードをGitHubに格納していたので以下のページが表示されました。

各テンプレートをS3バケットに格納してご自身の環境に応じてParametersを変えて頂ければそのまま実行できるかと思います。

最後に

初回のCFnテンプレートの作成は、GUIの操作よりリソースの作成に時間がかかるのがデメリットですが、同様の構成を横展開しやすくなるのとパラメータや構成がコードで確認できるようになるのがメリットだと思います。様々なタイプのテンプレートをどんどん作っていきたいと思います。