GitHubへのpushをトリガにCodePipelineを走らせてテスト・デプロイする

以前の記事([【知識0からのCloudFormation】LambdaをデプロイするCodePipelineを組む](https://qiita.com/Kept1994/items/b791e7a92b8e9a93653f))でCodeCommitにPushしたらデプロイするパイプラインの作成までやった。
今回は以下の変更を目標とする。

* デプロイする前にテストコードを挟む。
* ソースコードをGitHubで管理する。

### 前提

* Admin権限のIAMユーザを作成している。(adminである必要はないがとりあえず面倒なので)
* AWSコンソールに上記ユーザでサインインしている。
* git、aws-cliインストール済。

## CodeCommitにリポジトリを用意

### 秘密鍵と公開鍵の作成

```sh
$ ssh-keygen -t rsa -C "hogehoge@gmail.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/hoge/.ssh/id_rsa): ./codecommit_rsa
Enter passphrase (empty for no passphrase):                   # 何も指定せずEnter。GitHubとミラーリングさせる時にエラーするため。
Enter same passphrase again: 
Your identification has been saved in codecommit_rsa.
Your public key has been saved in codecommit_rsa.pub.
The key fingerprint is:
SHA256:EG3hs70mTNPFKE0AMJP0s9rij9QIeh1fQRhaPLNVErX hogehoge@gmail.com
The key's randomart image is:
+---[RSA 2048]----+
|         .* + .B#|
|     = *.%.  o.oB|
```

### IAMユーザと紐付け

IAM > ユーザ > 自分のユーザ名 > 認証情報(タブ) > AWS CodeCommitのSSHキー に公開鍵をUploadする。以下で中身をまるっとコピペする。

```
$ cat ~/.ssh/codecommit_rsa.pub
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/719897bd-250e-b0f8-7e2b-87a19c5c5482.png)

アップロードするとSSHキーIDが発行される。

### configファイルの設定

`User`はIAMにキーをUploadしたときに発行されるSSHキーID。

```:.ssh/cinfig
Host git-codecommit.*.amazonaws.com
  User APKA****************
  IdentityFile ~/.ssh/codecommit_rsa
```

### リポジトリ作成

CodeCommitコンソールから`リポジトリを作成`をクリックし、リポジトリ名を入力。
作成後gitクローン。

```
$ git clone ssh://git-codecommit.リージョン名.amazonaws.com/v1/repos/リポジトリ名
Cloning into 'リポジトリ名'...
warning: You appear to have cloned an empty repository.
```

### リファレンス

https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/getting-started-cc.html

## リポジトリのファイル構成

CodeBuildの実体はubuntuのDockerコンテナなのでその中でpythonのunittestを実行するコマンドを叩いてあげることでテストできる。
なお、コマンドを実行する階層はCodeCommitのリポジトリのルートの階層になる。
コマンドがどこで実行されるか初めわかっておらずはまった。

```sh
$ ./testRepository
└── pipeline_settings
    ├── buildspec.yml
    ├── hogehoge.py                                 # デプロイするコード
    ├── template.yml
    └── tests
        └── test_hogehoge.py                        # UTコード
```

<details><summary>各ファイル詳細</summary><div>

```yml:buildspec.yml
version: 0.2

phases:
  build:
    commands:
      - echo Build started on `date`
      - echo Compiling the Python code...
      - pwd                                         # /codebuild/output/src078561561/src
      - ls                                          # pipeline_settings
      - cd pipeline_settings
      - python3 -m unittest tests.test_hogehoge     # tests下のtest_hogehogeを実行。
      - |
        aws cloudformation package \
          --template-file template.yml \
          --s3-bucket $S3_BUCKET \
          --output-template-file $PACKAGED_TEMPLATE_FILE_PATH
  post_build:
    commands:
      - echo Build completed on `date`
artifacts:
  files:
    - $PACKAGED_TEMPLATE_FILE_PATH
    - pipeline_settings/*
    - hogehoge.py
  discard-paths: yes
```
```python:hogehoge.py
import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello')
    }
```

```yml:template.yml
AWSTemplateFormatVersion: "2010-09-09"

#パラメータにs3Bucketとs3keyを受け取る口を用意。
Parameters:
  funcName:
    Type: String
    Default: deployedLambda                        # デプロイするLambda関数名
  s3Bucket:
    Type: String
  s3Key:
    Type: String

Resources:
  # IAMロール作成
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties: 
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - 
            Effect: "Allow"
            Principal:      # 誰に権限を与えるのか
              Service: 
                - "lambda.amazonaws.com"
            Action:         # 何の権限を与えるのか
              - "sts:AssumeRole"    # Lambdaに対して一時的な権限を発行する。
      ManagedPolicyArns:
      - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      RoleName: !Ref funcName   # 実行時に指定

  # コードの参照先を変更。
  Function:
    Type: "AWS::Lambda::Function"
    Properties: 
      Code: 
        S3Bucket: !Ref s3Bucket
        S3Key: !Ref s3Key
      FunctionName: !Ref funcName
      Handler: "hogehoge.lambda_handler"
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: "python3.8"
```

</div></details>

### リファレンス

https://dev.classmethod.jp/articles/codebuildpipeline-unittest/

## パイプラインを作成

CodeCommitにPushしたらCloudFormationを用いてパイプラインを用意する。
以下でスタック`pipeline-stack`を用意する。

```sh
$ aws cloudformation deploy --stack-name pipeline-stack --template-file pipeline.yml --capabilities CAPABILITY_NAMED_IAMD
```

<details><summary>pipeline.yml詳細</summary><div>

```yml:pipeline.yml
AWSTemplateFormatVersion: "2010-09-09"

Resources:
  ArtifactBucket:
    Type: AWS::S3::Bucket

  BuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: 'testbuildproj'                        # buildプロジェクト名を指定
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/ubuntu-base:14.04
        EnvironmentVariables:                      # buildspec.yml内で使用する環境変数。
          - Name: PACKAGED_TEMPLATE_FILE_PATH
            Value: 'packaged.yml'
          - Name: S3_BUCKET
            Value: !Ref ArtifactBucket
      Source:
        Type: CODEPIPELINE
        BuildSpec: pipeline_settings/buildspec.yml # ルート直下にbuildspecがあるなら指定不要。

  # デプロイ時のロール。面倒なので全てAdmin権限を与えておく。
  PipelineDeployRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal: 
              Service: 
                - 'cloudformation.amazonaws.com'
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns:
      - 'arn:aws:iam::aws:policy/AdministratorAccess'
      RoleName: 'cfnrole'                         # ロール名

  # パイプラインのロール。面倒なので全てAdmin権限を与えておく。
  PipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal: 
              Service: 
                - 'codepipeline.amazonaws.com'
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns:
      - 'arn:aws:iam::aws:policy/AdministratorAccess'
      RoleName: 'pipelinerole'                    # ロール名

  # ビルド時のロール。面倒なので全てAdmin権限を与えておく。
  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/AdministratorAccess'
      RoleName: 'buildrole'                      # ロール名

  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: 'testpipeline'                         # パイプライン名
      RoleArn: !GetAtt PipelineRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: DownloadSource
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration:
                RepositoryName: 'testRepository'   # リポジトリ名
                BranchName: 'master'
              OutputArtifacts:
                - Name: SourceOutput               # オブジェクト名
        - Name: Build
          Actions:
            - InputArtifacts:
                - Name: SourceOutput               # オブジェクト名
              Name: Package
              ActionTypeId:
                Category: Build
                Provider: CodeBuild
                Owner: AWS
                Version: 1
              OutputArtifacts:
                - Name: BuildOutput                # オブジェクト名
              Configuration:
                ProjectName: !Ref BuildProject
        - Name: Deploy
          Actions:
            - Name: CreateChangeSet
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: '1'
              InputArtifacts:
                - Name: BuildOutput                # オブジェクト名
              Configuration:
                ActionMode: CHANGE_SET_REPLACE
                RoleArn: !GetAtt PipelineDeployRole.Arn
                StackName: 'system-stack'
                ChangeSetName: 'system-stack-changeset'
                Capabilities: CAPABILITY_NAMED_IAM
                TemplatePath: 'BuildOutput::packaged.yml'
                ParameterOverrides: |
                  {
                    "s3Bucket": { "Fn::GetArtifactAtt" : [ "BuildOutput", "BucketName" ] },
                    "s3Key": { "Fn::GetArtifactAtt" : [ "BuildOutput", "ObjectKey" ] }
                  }
              RunOrder: '1'
            - Name: ExecuteChangeSet
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: '1'
              InputArtifacts:
                - Name: BuildOutput                # オブジェクト名
              Configuration:
                ActionMode: CHANGE_SET_EXECUTE
                ChangeSetName: 'system-stack-changeset'
                StackName: 'system-stack'
              RunOrder: '2'
```

</div></details>

スタックの作成に成功すると勝手にパイプラインが走りLambdaが生成される。
以降、リポジトリにpushするとLambdaが更新される。

## GitHubのミラーリング

参考)
https://furiblog.com/github-codecommit-mirroring

参考ページに書いてある通り設定したらよい。
なお、環境変数の変数名は任意の名称で可

GitHubのリポジトリのSetting > Secretから以下の値を指定する。

`CODECOMMIT_SSH_PRIVATE_KEY`に秘密鍵をまるっとコピー

```:codecommit_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b6A...
              (略)
                              ...WCD
-----END OPENSSH PRIVATE KEY-----
```

`CODECOMMIT_SSH_PRIVATE_KEY_ID`に`SSHキーID`(前述)をまるっとコピー。
前述の通り、SSHキーにパスフレーズを設定しているとエラーした。

Actionでは以下を使わせていただいている。
https://github.com/pixta-dev/repository-mirroring-action

```yml:.github/workflows/main.yml
name: {任意の名前}

on: [ push, delete ]

jobs:
  {任意の名前}:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      - uses: pixta-dev/repository-mirroring-action@v1
        with:
          target_repo_url:
            ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/xxxxx
          ssh_private_key:
            ${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY }}
          ssh_username:
            ${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY_ID }}
```

### リファレンス

* https://furiblog.com/github-codecommit-mirroring
* https://github.com/pixta-dev/repository-mirroring-action