CodePipelineでCloudFormation用CI/CD環境を構築する

CodePipelineを使ってCloudFormation用のCI/CD環境を構築する

CodePipelineでCloudFormation用CI/CD環境を構築する

以下のAWS公式ページで、CodePipelineを使用したCloudFormation用のCI/CD環境を構築する方法が取り上げられています。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-basic-walkthrough.html

本ページでは、上記のページを参考にして構築を行い、動作を確認します。

構築する環境

Diagram of using CodePipeline to build CI/CD environment for CloudFormation.

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に関する基本的な事項については、以下のページをご確認ください。

https://awstut.com/2022/08/14/use-codepipeline-to-trigger-codecommit-pushes-to-push-images-to-ecr

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 オブジェクト)

変更セットに関する詳細については、以下のページをご確認ください。

https://awstut.com/2023/01/29/check-the-cloudformation-change-set-to-see-the-scope-of-impact-when-updating-the-stack

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スタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

https://awstut.com/2021/12/02/cloudformation-nested-stacks

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • SNSトピック:fa-117
  • CodeCommit:fa-117
  • CodePipeline:fa-117

メールアドレスの認証

SNSトピックのサブスクライバーとしてメールアドレスを指定した場合、そのメールアドレスを認証する必要があります。
指定したメールアドレスに、以下のような認証メールが送られてきます。

Detail of SNS 1.

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

Detail of SNS 2.

上記のページが表示されて、認証が完了したことがわかります。

リソース確認

AWS Management Consoleから各リソースを確認します。
まずSNSトピックを確認します。

Detail of SNS 3.

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

CodePipelineを確認します。

Detail of CodePipeline 1.

パイプラインの実行に失敗しています。
これは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)

正常にプッシュできました。
これでパイプラインが開始されるはずです。

しばらく待つと、以下のメールが届きました。

Detail of SNS 4.

テストスタックの内容を承認に関する内容です。

パイプラインを確認します。

Detail of CodePipeline 2.

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

作成されたテストスタックを確認します。

Detail of CloudFormation 1.

確かにテストスタックが作成されています。

スタック内のLambda関数を確認します。

Detail of Lambda 1.
Detail of Lambda 2.

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

正常にテストスタックが作成されたことが確認できましたので、パイプラインの承認アクションを行います。

Detail of CodePipeline 3.

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

改めてテストスタックを確認します。

Detail of CloudFormation 2.

パイプラインが再開されたことによって、自動的にテストスタックが削除されました。

再びしばらく待機すると、以下のメールが届きました。

Detail of SNS 5.

メールの内容は本番スタックの変更セットの承認に関するものです。

パイプラインを確認します。

Detail of CodePipeline 4.

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

作成された変更セットを確認します。

Detail of CloudFormation 3.
Detail of CloudFormation 4.

変更セットの内容を見ると、Lambda関数と関数用のIAMロールが新規作成されることが読み取れます。

正常に変更セットが作成されたことが確認できましたので、パイプラインの承認アクションを行います。

Detail of CodePipeline 5.

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

Detail of CodePipeline 6.

パイプラインが最後まで到達し、正常に終了しました。

変更セットから作成された本番スタックを確認します。

Detail of CloudFormation 5.

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

最後にLambda関数を確認します。

Detail of Lambda 3.
Detail of Lambda 4.

確かにLambda関数が作成されています。
関数名が「fa-117-sample-lambda-prod-Function」、メモリサイズが「512 MB」とありますので、本番スタック用のテンプレート設定ファイルが読み込まれた上で、スタックが作成されたことがわかります。

以上の通り、CodePipelineを使用したCloudFormation用のCI/CD環境が構築できました。

まとめ

CodePipelineを使用したCloudFormation用のCI/CD環境を構築する方法をご紹介しました。