CFNカスタムリソースとCodeBuildを使って、テストイメージをECRに自動的にプッシュする

CFNカスタムリソースとCodeBuildを使って、テストイメージをECRに自動的にプッシュする

CloudFormationスタック作成時に、ECRリポジトリに自動的にテストイメージをプッシュすることを考えます。

今回はCloudFormationカスタムリソースとCodeBuildを使用して、上記を実現する方法をご紹介します。

構築する環境

Diagram of automatically push test image to ECR using CFN custom resources and CodeBuild.

CloudFormationで4つのリソースを作成します。

Secrets ManagerはDockerHubのアカウント情報を保存するために使用します。
JSON形式でアカウント名とパスワードを登録します。

CodeBuildでテスト用のDockerイメージをビルドします。
イメージはDockerHubからプルしたものをベースにします。
ビルドしたイメージをECRリポジトリにプッシュします。

Lambda関数を作成します。
この関数の働きは、CodeBuildを開始させることです。
関数のランタイム環境はPython3.8です。

この関数がCloudFormationスタック作成時に自動的に実行されるように、CloudFormationカスタムリソースに関連づけます。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/129

テンプレートファイルのポイント解説

ECR

Resources:
  ECRRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref Prefix
Code language: YAML (yaml)

ECRリポジトリを作成します。
特別な設定は不要です。

Secrets Manager

Resources:
  Secret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Ref Prefix
      SecretString: !Sub '{"username":"${Username}","password":"${Password}"}'
Code language: YAML (yaml)

Secrets ManagerにDockerHubのアカウント情報を保存します。

Secrets Managerに関する詳細は、こちらもご確認ください。

あわせて読みたい
Secrets Managerを使用して、ランダムなパスワードを生成する 【Secrets Managerを使用して、ランダムなパスワードを生成する】 Secrets Managerを使用すると、ランダムなパスワードを生成することができます。 https://docs.aws.am...

CodeBuild

Resources:
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: NO_ARTIFACTS
      Cache:
        Type: NO_CACHE
      Environment:
        ComputeType: !Ref ProjectEnvironmentComputeType
        EnvironmentVariables:
          - Name: DOCKERHUB_PASSWORD
            Type: SECRETS_MANAGER
            Value: !Sub "${Secret}:password"
          - Name: DOCKERHUB_USERNAME
            Type: SECRETS_MANAGER
            Value: !Sub "${Secret}:username"
        Image: !Ref ProjectEnvironmentImage
        ImagePullCredentialsType: CODEBUILD
        Type: !Ref ProjectEnvironmentType
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: DISABLED
        S3Logs:
          Status: DISABLED
      Name: !Ref Prefix
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source:
        Type: NO_SOURCE
        BuildSpec: !Sub |
          version: 0.2

          phases:
            pre_build:
              commands:
                - echo Logging in to Amazon ECR...
                - aws --version
                - aws ecr get-login-password --region ${AWS::Region} | docker login --username AWS --password-stdin ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com
                - REPOSITORY_URI=${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryName}
                - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
                - IMAGE_TAG=${!COMMIT_HASH:=latest}
                - echo Logging in to Docker Hub...
                - echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin
                - |
                  cat << EOF > Dockerfile
                  FROM nginx:latest
                  EXPOSE 80
                  EOF
            build:
              commands:
                - echo Build started on `date`
                - echo Building the Docker image...
                - docker build -t $REPOSITORY_URI:latest .
                - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Pushing the Docker images...
                - docker push $REPOSITORY_URI:latest
                - docker push $REPOSITORY_URI:$IMAGE_TAG
      Visibility: PRIVATE
Code language: YAML (yaml)

CodeBuildを使用して、Dockerイメージをビルド後、ECRリポジトリにイメージをプッシュする方法については、以下のページをご確認ください。

あわせて読みたい
CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする 【CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする】 CodePipelineを使用することによって、CI/CD構成を構築することができます。...

上記のページでは、CodePipeline内において、CodeCommitリポジトリ上のDockerfileをソースとして読み込んでビルドしました。

本ページでは、CodeBuild単体でビルドする方法を中心に取り上げます。

今回はアーティファクトは存在しません。
ですからArtifactsプロパティ内のTypeプロパティに「NO_ARTIFACTS」を指定します。

またソースも存在しないため、Sourceプロパティ内のTypeプロパティに「NO_SOURCE」を指定します。

BuildSpecプロパティ内のpre_buildフェーズがポイントです。
本フェーズ内でcatコマンドを使用して、Dockerfileを生成します。
つまりCodeCommit等の外部のリソースをソースとするのではなく、CodeBuild内の環境でDockerfileを生成して、こちらに基づいてビルドするということです。

Secrets Managerの値をCodeBuildで参照する場合、EnvironmentVariablesプロパティもポイントとなります。
詳細は以下のページをご確認いただきたいのですが、環境変数としてSecrets Managerの値を設定する場合は、Typeプロパティに「SECRETS_MANAGER」を、ValueプロパティにシークレットのARNと値のキーを組み合わせた文字列を指定します。

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-spec-ref.html#build-spec.env.secrets-manager

以下がCodeBuild用のIAMロールです。

Resources:
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
      Policies:
        - PolicyName: GetSecretValuePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource:
                  - !Ref Secret
Code language: YAML (yaml)

AWS管理ポリシーAmazonEC2ContainerRegistryPowerUserに加えて、インラインポリシーとしてSecrets Managerの値を参照する権限を与えます。

Lambda関数

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt Function.Arn

  Function:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          CODEBUILD_PROJECT: !Ref CodeBuildProject
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os

          codebuild_project = os.environ['CODEBUILD_PROJECT']

          CREATE = 'Create'
          response_data = {}

          client = boto3.client('codebuild')

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                response = client.start_build(
                  projectName=codebuild_project
                )
                print(response)

              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      FunctionName: !Sub "${Prefix}-function"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)

Lambda関数で実行するコードをインライン表記で定義します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationでLambdaを作成する3パータン(S3/インライン/コンテナ) 【CloudFormationでLambdaを作成する】 CloudFormationでLambdaを作成する場合、大別すると以下の3パターンあります。 S3バケットにコードをアップロードする インライ...

cfnresponseモジュールを使用して、関数をLambda-backedカスタムリソースとして実装します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationカスタムリソース入門 【CloudFormationカスタムリソースの挙動を確認する構成】 CloudFormationの機能の1つにカスタムリソースがあります。 カスタムリソースを使用すると、テンプレートにカ...

実行するコードの内容ですが、以下の通りです。

  • os.environにアクセスして、CloudFormationテンプレートで定義した環境変数を取得する。
  • Boto3でCodeBuild用クライアントオブジェクトを作成して、CodeBuildの実行を開始する。

関数用のIAMロールは以下の通りです。

Resources:
  FunctionRole:
    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
      Policies:
        - PolicyName: GetSecretValuePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - codebuild:StartBuild
                Resource:
                  - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CodeBuildProject}"
Code language: YAML (yaml)

AWS管理ポリシーであるAWSLambdaBasicExecutionRoleに加えて、インラインポリシーでCodeBuildを開始する権限を与えます。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

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

  • ECRリポジトリ:fa-129
  • Secrets Manager:fa-129
  • Lambda関数:fa-129-function
  • CodeBuild:fa-129

AWS Management Consoleから各リソースを確認します。

Secrets Managerを確認します。

Detail of Secrets Manager 1.

シークレットが正常に作成されています。

Lambda関数および実行状況を確認します。

Detail of Lambda 1.
Detail of Lambda 2.

正常に関数が作成された上で、実行されていることがわかります。
つまりCloudFormationカスタムリソースによって、CloudFormationスタック作成時に、自動的にLambda関数が実行されたことになります。

動作確認

準備が整いましたので、CodeBuildを確認します。

Detail of CodeBuild 1.

確かにCodeBuildが実行されています。
つまりCloudFormationカスタムリソースに紐付くLambda関数が実行されたことによって、ビルドが開始されたということです。

ちなみにCodeBuildの環境変数の設定も確認します。

Detail of CodeBuild 2.

確かにSecrets Managerの値が参照されるように設定されています。

最後にECRリポジトリを確認します。

Detail of ECR 1.

確かにイメージがプッシュされています。

このようにCodeBuildおよびCloudFormationカスタムリソースを使用することによって、自動的にテストイメージをECRリポジトリにイメージをプッシュすることができました。

まとめ

CloudFormationカスタムリソースとCodeBuildを使用して、CloudFormationスタック作成時に、ECRリポジトリに自動的にテストイメージをプッシュする方法を確認しました。