CodePipelineでCloudFormation用CI/CD環境を構築する
以下のAWS公式ページで、CodePipelineを使用したCloudFormation用のCI/CD環境を構築する方法が取り上げられています。
本ページでは、上記のページを参考にして構築を行い、動作を確認します。
構築する環境

CodePipelineでパイプラインを構成します。
パイプラインは以下の流れで動作します。
まずCodeCommitです。
CodeCommitはCodePipelineのソースステージを担当します。
Gitリポジトリとして使用します。
次にCloudFormationです。
このCloudFormationの働きは、テスト用スタックを構築することです。
スタック作成後、承認アクションとして、SNSでメール通知します。
メール通知を受けたユーザは、パイプラインのページにアクセスし、承認ボタンを押すとパイプラインが再開されます。
最後に再びCloudFormationです。
このCloudFormationの働きは、本番環境用スタックおよび変更セットを作成することです。
変更セット作成後、承認アクションとして、再びSNSでメール通知します。
承認ボタンが押されますと、パイプラインが再開され、変更セットから本番スタックが作成・更新されます。
CodePipelineが開始されるきっかけですが、CodeCommitへのプッシュを条件とします。
具体的には、EventBridgeに上記を満たすルールを用意します。
テストスタック・本番スタックで作成するリソースはLambda関数とします。
合わせて関数用のIAMロールも作成します。
CloudFormationテンプレートファイル
上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。
https://github.com/awstut-an-r/awstut-fa/tree/main/117
テンプレートファイルのポイント解説
本ページは、CodePipelineを使って、CloudFormation用のCI/CD環境を構築することを目的としています。
CodePipelineに関する基本的な事項については、以下のページをご確認ください。

CodePipeline
Resources:
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref BucketName
        Type: S3
      Name: !Ref Prefix
      RoleArn: !GetAtt CodePipelineRole.Arn
      Stages:
        - Actions:
            - ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeCommit
                Version: 1
              Configuration:
                BranchName: !Ref BranchName
                OutputArtifactFormat: CODE_ZIP
                PollForSourceChanges: false
                RepositoryName: !GetAtt CodeCommitRepository.Name
              Name: SourceAction
              OutputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: CodeCommitSource
        - Actions:
            - ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: 1
              Configuration:
                ActionMode: REPLACE_ON_FAILURE
                Capabilities: CAPABILITY_IAM
                RoleArn: !GetAtt CloudFormationRole.Arn
                StackName: !Ref TestStackName
                TemplateConfiguration: !Sub "${PipelineSourceArtifact}::${TestStackConfig}"
                TemplatePath: !Sub "${PipelineSourceArtifact}::${TemplateFileName}"
              InputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Name: CreateTestStack
              RunOrder: 1
            - ActionTypeId:
                Category: Approval
                Owner: AWS
                Provider: Manual
                Version: 1
              Configuration:
                CustomData: !Sub "Do you want to create a change set against the production stack and delete the ${TestStackName} stack?"
                NotificationArn: !Ref TopicArn
              Name: ApproveTestStack
              RunOrder: 2
            - ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: 1
              Configuration:
                ActionMode: DELETE_ONLY
                RoleArn: !GetAtt CloudFormationRole.Arn
                StackName: !Ref TestStackName
              Name: DeleteTestStack
              RunOrder: 3
          Name: DeployTestStack
        - Actions:
            - ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: 1
              Configuration:
                ActionMode: CHANGE_SET_REPLACE
                Capabilities: CAPABILITY_IAM
                ChangeSetName: !Ref ChangeSetName
                RoleArn: !GetAtt CloudFormationRole.Arn
                StackName: !Ref ProdStackName
                TemplateConfiguration: !Sub "${PipelineSourceArtifact}::${ProdStackConfig}"
                TemplatePath: !Sub "${PipelineSourceArtifact}::${TemplateFileName}"
              InputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Name: CreateProdStack
              RunOrder: 1
            - ActionTypeId:
                Category: Approval
                Owner: AWS
                Provider: Manual
                Version: 1
              Configuration:
                CustomData: !Sub "A new change set was created for the ${ProdStackName} stack. Do you want to implement the changes?"
                NotificationArn: !Ref TopicArn
              Name: ApproveChangeSet
              RunOrder: 2
            - ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: 1
              Configuration:
                ActionMode: CHANGE_SET_EXECUTE
                ChangeSetName: !Ref ChangeSetName
                RoleArn: !GetAtt CloudFormationRole.Arn
                StackName: !Ref ProdStackName
              Name: ExecuteChangeSet
              RunOrder: 3
          Name: DeployProdStack
Code language: YAML (yaml)
基本的な構成は以下のページを参考にしています。
https://s3.amazonaws.com/cloudformation-examples/user-guide/continuous-deployment/basic-pipeline.yml
パイプラインを構成する各ステージを確認します。
ステージ1
1つ目はソースステージです。
上記のページで紹介されているコードでは、ソースにS3バケットが設定されています。
本ページでは、ソースにCodeCommitを指定します。
ステージ2
2つ目はテストスタックを構築するステージで、3つのアクションから構成されます。
1つ目のアクションは、CloudFormationでテストスタックを構築するアクションです。
設定方法については、AWS公式サイトで以下の通り説明されています。
ステージのアクションで CloudFormation をプロバイダーとして指定する場合は、Configuration プロパティ内で次のプロパティを定義します。AWS CLI、CodePipeline API、または AWS CloudFormation テンプレートの場合は、JSON オブジェクトを使用します。
設定プロパティ (JSON オブジェクト)
ポイントとなるパラメータを取り上げます。
スタックを更新・新規作成する場合、ActionModeプロパティに「REPLACE_ON_FAILURE」を指定します。
このオプションについては、以下の通りに説明されています。
REPLACE_ON_FAILURE は、指定されたスタックが存在しない場合、スタックを作成します。スタックが存在しており、失敗状態の場合 (ROLLBACK_COMPLETE、ROLLBACK_FAILED、CREATE_FAILED、DELETE_FAILED または UPDATE_ROLLBACK_FAILED として報告されている場合)、AWS CloudFormation はそのスタックを削除して新しいスタックを作成します。スタックが失敗状態ではない場合は、AWS CloudFormation はそれを更新します。失敗したスタックをリカバリーまたはトラブルシューティングせずに自動的に置き換えるには、このアクションを使用します。通常、このモードはテスト用に選択されます。
設定プロパティ (JSON オブジェクト)
TemplatePathプロパティで、スタックの素ととなるテンプレートファイルを指定します。
記法は以下の通りです。
Artifactname::TemplateFileName
設定プロパティ (JSON オブジェクト)
上記に従い、テストスタック用のテンプレートファイルを指定します。
TemplateConfigurationプロパティで、テストスタック作成用のテンプレート設定ファイルを指定することができます。
記法は以下の通りです。
Artifactname::TemplateConfigurationFileName
設定プロパティ (JSON オブジェクト)
上記に従い、テストスタック用のコンフィギュレーションファイルを指定します。
2つ目のアクションは承認アクションです。
SNSトピックで指定したメールアドレス宛に、承認メールを送付します。
メールの受信者はテストスタックの内容を確認し、問題なければ承認します。
3つ目のアクションはテストスタックを削除するアクションです。
ActionModeプロパティで、「DELETE_ONLY」を指定します。
こちらのオプションについては、以下の通りに説明されています。
DELETE_ONLY は、スタックを削除します。存在しないスタックを指定した場合は、アクションはスタックを削除せずに正常に終了します。
設定プロパティ (JSON オブジェクト)
ステージ3
3つ目は本番スタックを構築するステージで、3つのアクションから構成されます。
1つ目のアクションは、CloudFormationで本番スタック用の変更セットを構築するアクションです。
ポイントはActionModeプロパティでして、「CHANGE_SET_REPLACE」を指定します。
こちらのオプションについては、以下の通りに説明されています。
CHANGE_SET_REPLACE は、変更セットが存在しない場合、指定されたスタック名とテンプレートに基づいて変更セットを作成します。変更セットが存在する場合は、AWS CloudFormation はそれを削除して新しいものを作成します。
設定プロパティ (JSON オブジェクト)
変更セットに関する詳細については、以下のページをご確認ください。

TemplatePath・TemplateConfigurationプロパティに、それぞれ本番スタック用のテンプレートファイル、本番スタック作成用のテンプレート設定ファイルを指定します。
2つ目のアクションは承認アクションです。
SNSトピックで指定したメールアドレス宛に、承認メールを送付します。
メールの受信者は本番スタック用の変更セットの内容を確認し、問題なければ承認します。
3つ目のアクションは、CloudFormationを使用して、本番スタックを作成するアクションです。
先ほど作成した変更セットから本番スタックを作成します。
ActionModeプロパティに「CHANGE_SET_EXECUTE」を指定します。
こちらのオプションについては、以下の通りに説明されています。
CHANGE_SET_EXECUTE は変更セットを実行します。
設定プロパティ (JSON オブジェクト)
IAMロール
以下の2つのIAMロールを作成します。
- CodePipeline実行用IAMロール
 - CloudFormation実行用IAMロール
 
CodePipeline実行用のIAMロールを確認します。
Resources:
  CodePipelineRole:
    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
                Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetRepository
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource:
                  - !GetAtt CodeCommitRepository.Arn
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                Resource:
                  - !Sub "arn:aws:s3:::${BucketName}"
                  - !Sub "arn:aws:s3:::${BucketName}/*"
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource:
                  - !Ref TopicArn
              - Effect: Allow
                Action:
                  - cloudformation:CreateStack
                  - cloudformation:DescribeStacks
                  - cloudformation:DeleteStack
                  - cloudformation:UpdateStack
                  - cloudformation:CreateChangeSet
                  - cloudformation:ExecuteChangeSet
                  - cloudformation:DeleteChangeSet
                  - cloudformation:DescribeChangeSet
                  - cloudformation:SetStackPolicy
                  - iam:PassRole
                Resource: "*"
Code language: YAML (yaml)
パイプラインを構成するリソース(CodeCommit、S3、SNS、CloudFormation)に関する権限を与えます。
次にCloudFormation実行用IAMロールを確認します。
Resources:
  CloudFormationRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - cloudformation.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: CloudFormationDeployPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - iam:*
                  - lambda:*
                Resource: "*"
Code language: YAML (yaml)
CloudFormationを通じて作成するリソース(Lambda関数、関数用IAMロール、CloudWatch Logs)に関する権限を与えます。
CloudFormation関係のファイル
CodePipeline内のCloudFormationを実行するためのファイルを確認します。
テンプレートファイル
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  Prefix:
    Type: String
    Default: fa-117-sample-lambda
  Environment:
    Type: String
    Default: test
  Handler:
    Type: String
    Default: index.lambda_handler
  MemorySize:
    Type: Number
    Default: 128
  Runtime:
    Type: String
    Default: python3.8
  Timeout:
    Type: Number
    Default: 5
Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          def lambda_handler(event, context):
            print('sample lambda')
      FunctionName: !Sub "${Prefix}-${Environment}-Function"
      Handler: !Ref Handler
      MemorySize: !Ref MemorySize
      Runtime: !Ref Runtime
      Role: !GetAtt LambdaRole.Arn
  LambdaRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Code language: YAML (yaml)
Lambda関数およびIAMロールを定義します。
関数名(FunctionName)とメモリサイズ(MemorySize)がポイントです。
それぞれパラメータを埋め込む形で指定しますが、これらの値は後述のテンプレート設定ファイルによって、テストスタック・本番スタックで異なる値を設定します。
テストスタック用テンプレート設定ファイル
{
  "Parameters" : {
    "Environment": "test",
    "MemorySize": "128"
  }
}
Code language: JSON / JSON with Comments (json)
2つのパラメータを設定します。
特にメモリサイズがポイントです。
128(MB)を指定します。
本番スタック用テンプレートファイル
{
  "Parameters" : {
    "Environment": "prod",
    "MemorySize": "512"
  }
}
Code language: JSON / JSON with Comments (json)
こちらも2つのパラメータを設定します。
メモリサイズは512(MB)を指定します。
環境構築
CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。
CloudFormationスタックを作成する
CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- SNSトピック:fa-117
 - CodeCommit:fa-117
 - CodePipeline:fa-117
 
メールアドレスの認証
SNSトピックのサブスクライバーとしてメールアドレスを指定した場合、そのメールアドレスを認証する必要があります。
指定したメールアドレスに、以下のような認証メールが送られてきます。

「Confirm subscription」を押下して、認証を進めます。

上記のページが表示されて、認証が完了したことがわかります。
リソース確認
AWS Management Consoleから各リソースを確認します。
まずSNSトピックを確認します。

SNSトピックのサブスクライバーとして、メールアドレスが指定されています。
CodePipelineを確認します。

パイプラインの実行に失敗しています。
これはCloudFormationスタック作成時に、CodeCommitが作成されたことがきっかけとして、パイプラインが実行されたためです。
現時点ではCodeCommitにコードをプッシュしていないため、パイプライン実行の過程でエラーが発生しました。
作成されているステージに注目します。
DeployTestStackとDeployTestStackという名前で、CloudFormationスタックを作成するステージが用意されています。
各ステージの内容を見ます。
前者はテストスタック作成後、承認を得ると、そのテストスタックを削除します。
後者は変更セット作成後、承認を得ると、本番スタックを作成・更新します。
動作確認
準備が整いましたので、CodeCommitにコードをプッシュします。
まずCodeCommitリポジトリをプルします。
$ git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-117
Cloning into 'fa-117'...
warning: You appear to have cloned an empty repository.
Code language: Bash (bash)
空のリポジトリがプルされました。
リポジトリに3つのファイル(sample-lambda.yaml、test-stack-configuration.json、prod-stack-configuration.json)を加えます。
$ ls -al
total 12
drwxrwxr-x 3 ec2-user ec2-user  118 Jan 18 12:33 .
drwxrwxr-x 4 ec2-user ec2-user  131 Jan 18 12:32 ..
drwxrwxr-x 7 ec2-user ec2-user  119 Jan 18 12:32 .git
-rw-rw-r-- 1 ec2-user ec2-user   94 Jan 18 11:50 prod-stack-configuration.json
-rw-rw-r-- 1 ec2-user ec2-user 1667 Jan 18 11:40 sample-lambda.yaml
-rw-rw-r-- 1 ec2-user ec2-user   93 Jan 18 11:48 test-stack-configuration.json
Code language: Bash (bash)
3ファイルをプッシュします。
$ git add .
$ git commit -m 'first commit'
...
 3 files changed, 92 insertions(+)
 create mode 100644 prod-stack-configuration.json
 create mode 100644 sample-lambda.yaml
 create mode 100644 test-stack-configuration.json
$ git push
...
 * [new branch]      master -> master
Code language: Bash (bash)
正常にプッシュできました。
これでパイプラインが開始されるはずです。
しばらく待つと、以下のメールが届きました。

テストスタックの内容を承認に関する内容です。
パイプラインを確認します。

テストスタック作成が成功し、認証アクションで一時停止しています。
作成されたテストスタックを確認します。

確かにテストスタックが作成されています。
スタック内のLambda関数を確認します。


確かにLambda関数が作成されています。
関数名が「fa-117-sample-lambda-test-Function」、メモリサイズが「128 MB」とありますので、テストスタック用のテンプレート設定ファイルが読み込まれた上で、スタックが作成されたことがわかります。
正常にテストスタックが作成されたことが確認できましたので、パイプラインの承認アクションを行います。

「Approve」を押下すると、一時停止していたパイプラインが再開されます。
改めてテストスタックを確認します。

パイプラインが再開されたことによって、自動的にテストスタックが削除されました。
再びしばらく待機すると、以下のメールが届きました。

メールの内容は本番スタックの変更セットの承認に関するものです。
パイプラインを確認します。

本番スタック用の変更セットの作成が成功し、認証アクションで一時停止しています。
作成された変更セットを確認します。


変更セットの内容を見ると、Lambda関数と関数用のIAMロールが新規作成されることが読み取れます。
正常に変更セットが作成されたことが確認できましたので、パイプラインの承認アクションを行います。

「Approve」を押下すると、一時停止していたパイプラインが再開されます。

パイプラインが最後まで到達し、正常に終了しました。
変更セットから作成された本番スタックを確認します。

確かに本番スタックが作成されています。
スタック内に2つのリソース(Lambda関数、IAMロール)が作成されていることもわかります。
最後にLambda関数を確認します。


確かにLambda関数が作成されています。
関数名が「fa-117-sample-lambda-prod-Function」、メモリサイズが「512 MB」とありますので、本番スタック用のテンプレート設定ファイルが読み込まれた上で、スタックが作成されたことがわかります。
以上の通り、CodePipelineを使用したCloudFormation用のCI/CD環境が構築できました。
まとめ
CodePipelineを使用したCloudFormation用のCI/CD環境を構築する方法をご紹介しました。