CodeCommitレポジトリ作成時に指定のブランチに特定のファイルを作るカスタムリソースを作成してみた

突然ですが、CodeCommitレポジトリ作成時に自動で`README.md`を作りたくなった経験ありますか?私はアリます。
2022.05.30

こんにちは!AWS事業本部コンサルティング部のたかくにです。

突然ですが、CodeCommitレポジトリ作成時に自動でREADME.mdを作りたくなった経験ありますか?私はアリます。

今回は、CloudFormationカスタムリソースでリポジトリ作成時にREADME.mdを作ってみようと思います。

はじめに

既に作成されているのでは?と思い探したところ、CloudFormationマクロを使用して同じようなことをしているコードが見つかりました。

私自身のスキルセットの問題ですが、CloudFormationマクロ以前にカスタムリソースもあまり作ってこなかったため、今回はカスタムリソースでの実装に挑戦してみました。

コード

完成したコードが以下になります。

readme.yml

AWSTemplateFormatVersion: '2010-09-09'
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: "General Configuration"
      Parameters:
        - RepositoryName
        - BranchName
        - FilePath
        - FileContent
Parameters:
  RepositoryName:
    Type: String
    Description: "Enter the name of the repository"
    Default: "test-repo"
  BranchName:
    Type: String
    Description: "Enter the name of the branch where you want to store README.md"
    Default: "main"
  FilePath:
    Type: String
    Description: "Enter the name of the file"
    Default: "README.md"
  FileContent:
    Type: String
    Description: "Enter the contents of the README.md"
    Default: "Hello, World"

Resources:
#################################
# CodeCommit Repository
#################################
  Repository:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Ref RepositoryName

#################################
# Custom Resource
#################################
  PutReadMeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        - "arn:aws:iam::aws:policy/AWSCodeCommitPowerUser"

  PutReadMeLog:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${RepositoryName}-${BranchName}-put-readme"

  PutReadMeFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${RepositoryName}-${BranchName}-put-readme"
      Code:
        ZipFile: |
          import json
          import boto3
          import cfnresponse

          def handler(event, context):
              try:
                  repository = event['ResourceProperties']['RepositoryName']
                  branch = event['ResourceProperties']['BranchName']
                  content = event['ResourceProperties']['FileContent'].encode()
                  path = event['ResourceProperties']['FilePath']

                  if event['RequestType'] == 'Create':
                      codecommit = boto3.client('codecommit')
                      response = codecommit.put_file(
                          repositoryName=repository,
                          branchName=branch,
                          fileContent=content,
                          filePath=path,
                          commitMessage='Initial Commit',
                          name='Your Lambda Helper'
                      )
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, response)
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
                  if event['RequestType'] == 'Update':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
              except Exception as e:
                  print(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt PutReadMeRole.Arn
      Runtime: "python3.9"
      Timeout: 60
      Tags:
        - Key: "Name"
          Value: !Sub "${RepositoryName}-${BranchName}-put-readme"
    DependsOn: PutReadMeLog

  PutReadMe:
    Type: Custom::CodeCommitPutReadMe
    Properties:
      ServiceToken: !GetAtt PutReadMeFunction.Arn
      RepositoryName: !GetAtt Repository.Name
      BranchName: !Ref BranchName
      FileContent: !Ref FileContent
      FilePath: !Ref FilePath

マネジメントコンソールから確認

では、実際に動作確認を行い作成されているか確認してみます。

今回は以下の値を入力して作成しました。

設定値 備考
スタックの名前 codecommit-readme
RepositoryName codecommit-readme
BranchName dev
FilePath README.md
FileContent Hello, World

CodeCommitレポジトリを確認すると、問題なく指定したブランチにREADME.mdが作成されていました。

気をつけたこと

ただ、「完成したコードはこちらで、動作確認も問題ないです」だと味気ないので、実装で詰まった点や考慮した点をご紹介します。

DependsOnを使って依存関係

CloudFormationスタック削除時に、ログも消えるように設定したかったため、PutReadMeLog(AWS::Logs::LogGroup)PutReadMeFunction(AWS::Lambda::Function)DependsOnを使用して依存関係を設定しました。

もしログを残したい場合は、DeletionPolicy: Retainを使用してください。

スタックの削除時は以下の順序で削除が行われます。

  1. PutReadMe(Custom::CodeCommitPutReadMe)
  2. PutReadMeFunction(AWS::Lambda::Function)
  3. PutReadMeLog(AWS::Logs::LogGroup)

cfn-responseモジュール

cfn-response モジュールは、カスタムリソースとLambda関数間で応答するためのモジュールとして使用されます。

注意する点は、以下ブログのとおり、応答メソッドをリクエストタイプごとに実装する必要があります。

この分岐処理が実装されていない場合、DeleteUpdate時に応答メソッドがないために、カスタムリソースがIN_PROGRESSのままで止まってしまう可能性があります。

おわりに

以上、「CodeCommitレポジトリ作成時に指定のブランチに特定のファイルを作るカスタムリソースを作成してみた」でした。

今回はカスタムリソースで実装してみましたが、今後はCloudFormationマクロや拡張機能にも挑戦してみようと思いました。

この記事がどなたかのご参考になれば幸いです。

以上、AWS事業本部コンサルティング部のたかくにでした!