【AWS DOP学習】CodePipelineを使用してLambdaを作成するチュートリアルを、CloudTrailを覗きながらやってみた

# はじめに

DOP学習にあたり、CodePipelineのイメージを掴むためにチュートリアルを行います。

# 対象チュートリアル

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/applications-tutorial.html

# チュートリアルの構成図

![codepipeline_tutorial_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/6926f0a6-3e4f-9653-d2b9-da8c4c925e53.png)
※チュートリアル中、Lambdaの箇所にDynamoDBも追加されます。

# やってみた

## Lambdaからアプリケーションを作成

まず、Lambdaのアプリケーションを`一から作成`します。

![スクリーンショット 2021-08-12 19.09.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/f32f8418-7b8c-4082-c2c6-1266397e07f1.png)

Lambdaのアプリケーションを作成すると、Serverless Application Repositoryからアプリケーションをパイプライン付きで作成できます。

Serverless Application Repositoryとは、その名のとおりサーバレスアプリケーションのマネージド型リポジトリです。
AWSやサードパーティーだけでなく、個人もリポジトリにサーバレスアプリケーションを追加できます。

https://aws.amazon.com/jp/serverless/serverlessrepo/faqs/?nc=sn&loc=4

続いて、チュートリアルの指示どおり、各項目を埋めて作成します。

![スクリーンショット 2021-08-12 19.10.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/2789a2d9-22b4-bbd3-56df-636a7b81e707.png)

![スクリーンショット 2021-08-12 19.10.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/68108106-960e-2247-7e51-93ee214b669a.png)

アプリケーション作成後の画面がこちらです。
AWSリソースが作成され、リソース、インフラストラクチャと分類されています。

![スクリーンショット 2021-08-12 19.19.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/18ff89aa-4340-e4ae-6b3b-894783733843.png)

リソースはLambda。
インフラストラクチャはパイプラインに必要なAWSリソースです。
これらは、２つのCloudFormationにより作成されています。
こちらがそのCloudFormationです。


![スクリーンショット 2021-08-12 19.21.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/55f958e8-a16c-d5d2-a081-cfe584ef12bc.png)

my-appによりリソース、serverlessrepo-my-app-toolchainによりインフラストラクチャが作成されています。

Lambdaでアプリケーションを作成すると、まずserverlessrepo-my-app-toolchainが作成されます。
このCloudFormation Stackは、青で囲んだ以下のリソースを作成します。

![codepipeline_tutorial_1-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/83ca3519-2245-1dfd-98e6-8022e7a90db6.png)


そして、CodePipelineは作成すると自動実行されるので、`Deploy Stage`で設定したCloudFormation=my-appがデプロイされ、リソースであるLambdaが作成される、という流れになります。

## Lambdaからアプリケーションを作成する際のAPI履歴を追う

チュートリアルから脇道に逸れますが、気になるのでアプリケーション作成からのAPIの動きを、少しだけCloudTrailなどで追ってみました。

以下の画像は、CloudTrailのイベント履歴で、Lambdaでアプリケーションの作成をした直後の状態です。
アプリケーションの作成ボタンをクリックした後に走るAPIが分かります。

![スクリーンショット 2021-08-13 20.10.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/670bac4f-8e11-3f59-a864-c2df6e5cb8e1.png)

注目していただきたいのは、下の2つです。
それより上のAPIは、`ExecuteChangeSet`によりリソースが作成されているだけです。

Lambdaでアプリケーションを作成すると、`CreateChangeSet`、`CreateCloudFormationChangeSet`が走っていますが、時系列的には`CreateCloudFormationChangeSet` -> `CreateChangeSet`が正しいと思っています。

ドキュメントは提示できませんが、chromeの開発者ツールでリクエストペイロードを確認する限り、この順番の可能性が高いと考えます。

以下では、CloudTrailのAPIログ、chromeの開発者ツールの画像を用いて、Lambdaのアプリケーション作成後のAPIの動きを、時系列に沿って説明します。

こちらの画像は、アプリケーション作成ボタンクリック後のchrome開発者ツールのNetworkタブの状態です。

![スクリーンショット 2021-08-13 20.35.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/84017503-37df-a792-0a5e-40dd6d497057.png)

青で選択している一つ上をご覧ください。
`getApplication`APIを実行し、今回作成するサーバレスアプリケーションの情報を、サーバーレスアプリケーションリポジトリから取得しています。

```json:Request_Payload
{
    "service": "serverlessapplicationrepository.AWSServerlessApplicationRepository",
    "api": "getApplication",
    "payload": {
        "applicationId": "arn:aws:serverlessrepo:us-east-1:713482062377:applications/CICD-toolchain-for-serverless-applications"
    },
    "operation": "proxyAwsCall"
}
```

上記リクエストに対するレスポンスは抜粋して掲載します。

```json:Response
{
    "applicationId": "arn:aws:serverlessrepo:us-east-1:713482062377:applications/CICD-toolchain-for-serverless-applications",
    "author": "AWS Lambda console",
    "creationTime": "2019-10-02T23:27:14.536Z",
    "description": "Automated deployment toolchain used by new serverless applications created through the AWS Lambda console",
    "name": "CICD-toolchain-for-serverless-applications",
    "version": {
        "applicationId": "arn:aws:serverlessrepo:us-east-1:713482062377:applications/CICD-toolchain-for-serverless-applications",
        "creationTime": "2021-06-14T18:49:13.628Z",
        "parameterDefinitions": [
            {
                "name": "AppId",
                "type": "String"
            },
            {
                "name": "AppResourceArns",
                "type": "CommaDelimitedList"
            },
            {
                "name": "ConnectionArn",
                "type": "String"
            },
            {
                "name": "GitHubRepositoryOwner",
                "type": "String"
            },
            {
                "name": "RepositoryName",
                "type": "String"
            },
            {
                "name": "SourceCodeBucketKey",
                "type": "String"
            },
            {
                "name": "SourceCodeBucketName",
                "type": "String"
            },
            {
                "name": "UseCodeCommit",
                "type": "String"
            }
        ],
        "requiredCapabilities": [
            "CAPABILITY_NAMED_IAM",
            "CAPABILITY_RESOURCE_POLICY"
        ],
        "sourceCodeUrl": "https://github.com/aws-samples/aws-lambda-sample-applications/tree/master/CICD-toolchain-for-serverless-applications",
        "templateUrl": "https://awsserverlessrepo-applications-eji6x7x4bodi.s3.amazonaws.com/713482062377/CICD-toolchain-for-serverless-applications/741f579c-fb69-44a8-8e3b-aae190231505/1.0.5/transformed-template.yaml?"
    }
}
```

その後、`getApplication`で取得した情報を使用して、`createCloudFormationChangeSet`APIを実行してCloudFormationのStackと変更セットを作成しています。

おそらく、このAPIを実行すると、裏で`CreateChangeSet`が動くのでしょう。

```json:Request_Payload
{
    "service": "serverlessapplicationrepository.AWSServerlessApplicationRepository",
    "api": "createCloudFormationChangeSet",
    "payload": {
        "applicationId": "arn:aws:serverlessrepo:us-east-1:713482062377:applications/CICD-toolchain-for-serverless-applications",
        "stackName": "my-app-toolchain",
        "capabilities": [
            "CAPABILITY_RESOURCE_POLICY",
            "CAPABILITY_NAMED_IAM"
        ],
        "parameterOverrides": [
            {
                "name": "UseCodeCommit",
                "value": "true"
            },
            {
                "name": "SourceCodeBucketName",
                "value": "prodnrtstack-subsystemsn-apptemplatesbucket03a15d-1cj5ym80q3osj"
            },
            {
                "name": "SourceCodeBucketKey",
                "value": "sample-apps/nodejs/14.x/javascript/sam/fromScratch.zip"
            },
            {
                "name": "RepositoryName",
                "value": "app-name"
            },
            {
                "name": "AppId",
                "value": "app-name"
            },
            {
                "name": "AppResourceArns",
                "value": "arn:aws:lambda:ap-northeast-1:１１１１１１１１１１１１:function:my-app-helloFromLambda*"
            }
        ],
        "semanticVersion": "1.0.5"
    },
    "operation": "proxyAwsCall"
}
```

```json:Response
{
    "sdkResponseMetadata": null,
    "sdkHttpMetadata": null,
    "applicationId": "arn:aws:serverlessrepo:us-east-1:713482062377:applications/CICD-toolchain-for-serverless-applications",
    "changeSetId": "arn:aws:cloudformation:ap-northeast-1:111111111111:changeSet/a790d7ac8-5b84-4dfd-b87e-5a2da7892a26/aedfef54-0a60-437c-9b7b-3e8f9c1c87ed",
    "semanticVersion": "1.0.5",
    "stackId": "arn:aws:cloudformation:ap-northeast-1:111111111111:stack/serverlessrepo-my-app-toolchain/0c788e40-fc2c-11eb-873e-0a94ddda7ff3"
}
```

その後、開発者ツールで`executeChangeSet`のリクエストまで確認しましたが、CloudTrailで記録されていた`CreateChangeSet`をリクエストした形跡はありませんでした。

このことから、`CreateCloudFormationChangeSet`を実行すると、裏で`CreateChangeSet`が動いているのだと思います。

そもそも、`CreateCloudFormationChangeSet`は、Serverless Application RepositoryAPIです。

サーバーレスアプリケーションリポジトリからアプリケーションを作成する場合、CloudFormation（SAMテンプレート）により作成されます。
そのため、`CreateCloudFormationChangeSet`を実行すると裏で`CreateChangeSet`が動くのは、自然なことだと考えられます。

以下は`CreateChangeSet`のAPIログの抜粋です。

```json:eventName_CreateChangeSet

{
    "eventVersion": "1.08",
    "userIdentity": {
　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　略
        },
        "invokedBy": "apigateway.amazonaws.com"
    },
    "eventTime": "2021-08-12T10:11:04Z",
    "eventSource": "cloudformation.amazonaws.com",
    "eventName": "CreateChangeSet",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "apigateway.amazonaws.com",
    "userAgent": "apigateway.amazonaws.com",
    "requestParameters": {
        "notificationARNs": [],
        "capabilities": [
            "CAPABILITY_NAMED_IAM"
        ],
        "changeSetName": "a2051af8f-a86a-4459-bde7-2360d603e728",
        "stackName": "serverlessrepo-my-app-toolchain",
        "parameters": [
            {
                "parameterKey": "UseCodeCommit"
            },
            {
                "parameterKey": "SourceCodeBucketName"
            },
            {
                "parameterKey": "SourceCodeBucketKey"
            },
            {
                "parameterKey": "RepositoryName"
            },
            {
                "parameterKey": "AppId"
            },
            {
                "parameterKey": "AppResourceArns"
            }
        ],
        略
}

```

当初、リクエストパラメータに`ChangeSetType`がないため、この`CreateChangeSet`の`ChangeSetType`は、デフォルトの`UPDATE`に当たると思っていました。

`UPDATE`タイプの変更セットは、既存スタックの変更セット作成時に指定されるものです。
そのため、この`CreateChangeSet`を実行するためには、既存スタックの存在が前提となり、Stackを作成している`CreateCloudFormationChangeSet`実行後でなければ実行できない。と思っていました。

https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateChangeSet.html

しかし、変更セットタイプが何なのか確実に判断する方法は分かりませんでした。
CLIで`create-change-set`を`--change-set-type`を付けて実行したところ、CloudTrailのAPIログに変更セットタイプが記録されなかったからです。
また、`describe-change-set`を実行しても変更セットタイプは出力されませんでした。
変更セットタイプを知るには、変更セット作成時に、CloudFormation Stackが存在していたかどうかで判別するしかなさそうです（確認する必要性はないと思いますが）。

`CreateChangeSet`で一点気になるのは、`apigateway`によってリクエストされていることです。
`CreateCloudFormationChangeSet`を実行すると、裏で`apigateway`によって`CreateChangeSet`が実行される仕組みになっているのでしょうか。
パラメータが空だったりと具体的動作が不明ですが、裏でよしなにやってくれているのでしょう。

ちなみに以下は`CreateCloudFormationChangeSet`のAPIログです。
開発者ツールで確認したリクエストと同じ感じですね。

```json:eventName_CreateCloudFormationChangeSet
{
    "eventVersion": "1.08",
　　　　　　　略
    "eventTime": "2021-08-12T10:11:05Z",
    "eventSource": "serverlessrepo.amazonaws.com",
    "eventName": "CreateCloudFormationChangeSet",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "${MyIP}",
    "userAgent": "aws-internal/3 aws-sdk-java/1.11.1030 Linux/5.4.116-64.217.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.292-b10 java/1.8.0_292 vendor/Oracle_Corporation cfg/retry-mode/legacy, AWSLambdaConsole/1.1",
    "requestParameters": {
        "capabilities": [
            "CAPABILITY_RESOURCE_POLICY",
            "CAPABILITY_NAMED_IAM"
        ],
        "semanticVersion": "1.0.5",
        "stackName": "my-app-toolchain",
        "parameterOverrides": [
            {
                "name": "UseCodeCommit",
                "value": "***"
            },
            {
                "name": "SourceCodeBucketName",
                "value": "***"
            },
            {
                "name": "SourceCodeBucketKey",
                "value": "***"
            },
            {
                "name": "RepositoryName",
                "value": "***"
            },
            {
                "name": "AppId",
                "value": "***"
            },
            {
                "name": "AppResourceArns",
                "value": "***"
            }
        ],
 　　　　　　　　　　　　　　略
}
```

脇道に逸れましたが、そろそろチュートリアルに戻ります。

## Lambdaのテスト

作成されたLambdaをテストし、正常に動作するか確認します。

![スクリーンショット 2021-08-12 19.42.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/85c2746b-b20d-b497-78f5-a6ceaa8e2a6f.png)

正常に動作できていますね。

## DynamoDBを追加

次に、このアプリケーションにDynamoDBを追加してみます。
ローカルからCodeCommitに接続するため、認証情報を取得します。
私はHTTPS認証情報を生成しました。

https://docs.aws.amazon.com/codecommit/latest/userguide/setting-up.html

![スクリーンショット 2021-08-12 19.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/995b3798-d748-c86c-5bbc-535d20e16206.png)

ローカルにリポジトリのクローンを作成しようとしましたが、失敗しました。

```
# git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/my-app-repo
Cloning into 'my-app-repo'...
fatal: unable to access 'https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/my-app-repo/': The requested URL returned error: 403
```

私が以前CodeCommitにアクセスした際に使用したKeychain Accessユーティリティで保存されている認証情報の有効期限が切れているからです。

以下を参考に修正後、クローンします。

https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/troubleshooting-ch.html#troubleshooting-macoshttps

https://dev.classmethod.jp/articles/codecommit-keychainpassword-delete/

DynamoDBをSAMに追加してプッシュします。
（gitの操作は省略します。）

![スクリーンショット 2021-08-12 20.21.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/f4703c8d-261e-1677-ce32-4357fe18ab85.png)

DynamoDBのデプロイが確認できました。

![スクリーンショット 2021-08-12 20.22.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/16679b95-aade-cce1-fb49-917974dfed7f.png)

## Lambdaのアクセス権限の境界を変更

しかし、このままだとLambdaはDynamoDBへのアクセス許可がありません。
Lambdaの実行ロールはアクセス権限の境界が設定されており、その中でDynamoDBが許容されていないからです。

以下が設定されているアクセス権限の境界です。
許容しているのは、

- Lambda`my-app-helloFromLambda*`に対して行える全てのAPI許可
- CloudWatchLogsのロググループ作成やログの送信
- X-Rayに対する送信
- 特定のタグキーを持つリソースに対するタグ付け

です。

```json:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "*"
            ],
            "Resource": [
                "arn:aws:lambda:ap-northeast-1:111111111111:function:my-app-helloFromLambda*"
            ],
            "Effect": "Allow",
            "Sid": "StackResources"
        },
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:DescribeLogGroups",
                "logs:PutLogEvents",
                "xray:Put*"
            ],
            "Resource": "*",
            "Effect": "Allow",
            "Sid": "StaticPermissions"
        },
        {
            "Condition": {
                "StringEquals": {
                    "aws:RequestTag/aws:cloudformation:stack-name": [
                        "my-app"
                    ]
                },
                "ForAllValues:StringEquals": {
                    "aws:TagKeys": "aws:cloudformation:stack-name"
                }
            },
            "Action": "*",
            "Resource": "*",
            "Effect": "Allow",
            "Sid": "StackResourcesTagging"
        }
    ]
}
```

アクセス権限の境界の変更は、チュートリアルどおり行います。
アクセス権限の境界用のIAMポリシーを作成し、それをIAMロールの古いアクセス権限の境界と入れ替えます。

## LambdaのコードをDynamoDBテーブルを更新するように変更し、動作を確認

次に、DynamoDBテーブルを更新するようにコードを更新します。
以下のチュートリアル提供のコードを`src/handlers/index.js`に追加します。

```js:
const dynamodb = require('aws-sdk/clients/dynamodb');
const docClient = new dynamodb.DocumentClient();

exports.handler = async (event, context) => {
    const message = 'Hello from Lambda!';
    const tableName = process.env.DDB_TABLE;
    const logStreamName = context.logStreamName;
    var params = {
        TableName : tableName,
        Key: { id : logStreamName },
        UpdateExpression: 'set invocations = if_not_exists(invocations, :start) + :inc',
        ExpressionAttributeValues: {
            ':start': 0,
            ':inc': 1
        },
        ReturnValues: 'ALL_NEW'
    };
    await docClient.update(params).promise();

    const response = {
        body: JSON.stringify(message)
    };
    console.log(`body: ${response.body}`);
    return response;
}
```

ハンドラーを変更しているため、テンプレートも変更します。
（関係箇所抜粋）

```yaml:template.yml
  helloFromLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./
      Handler: src/handlers/index.handler
      Runtime: nodejs10.x
```

gitでプッシュ後、デプロイまで確認したらLambdaのテスト、DynamoDBテーブル確認を繰り返し、正常に動作しているかチェックします。

![スクリーンショット 2021-08-12 21.08.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/036f56b0-448b-8a5f-6e87-1eb0c82da702.png)

確認の中で、DynamoDBのデータが更新されていれば、成功です。

![スクリーンショット 2021-08-12 21.18.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/cb3224ef-fd75-e68c-881b-c259e0e9401e.png)

以上でチュートリアルは終了です。

# CodePipelineを調べてみた

チュートリアルは終了しましたが、本記事はそもそもCodePipelineの学習のため始めました。
そのため、次はCodePipelineの内容に注目します。

今回、CodePipelineは3つのステージを作成しています。
Source,Build,Deployです。

![スクリーンショット 2021-08-14 9.36.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/66a718ff-59ce-c905-b155-a272905dab14.png)

![スクリーンショット 2021-08-14 9.36.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/6224151d-504b-bca1-d67d-745f2f6de528.png)

簡単に説明すると、SourceのCodeCommitの変更によりCodePipelineはトリガーされ、BuildによりSAMテンプレートが作成され、DeployによりCloudFormation Stackの変更セットが実行される流れです。

以下、具体的に説明します。

## Sourceステージ

Sourceに設定しているCodeCommitリポジトリのブランチが変更されると、EventBridgeルールによりCodePipelineが実行されます（CodePipelineは作成時にも実行されます）。

CodePipelineは、Sourceの出力アーティファクトとして、CodeCommitリポジトリ内のファイルをまとめたzipファイルを、指定したS3の`〇〇-Pipeline/〇〇-Sou/`にアップロードします。

## Buildステージ

Buildでは、Sourceにより出力されたアーティファクトを使用します。

```:Sourceのアーティファクト
.
├── LICENSE
├── README.md
├── __tests__
│   └── unit
│       └── handlers
│           └── hello-from-lambda.test.js
├── buildspec.yml
├── package.json
├── src
│   └── handlers
│       ├── hello-from-lambda.js
│       └── index.js
└── template.yml
```

`buildspec.yml`によりビルドの方法が指定されているので、Buildではその手順どおり実行します。

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

```yaml:buildspec.yml
version: 0.2
phases:
  install:
    commands:
      - npm install
  pre_build:
    commands:
      - npm run test
      - npm prune --production
  build:
    commands:
      - aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template-file template-export.yml
artifacts:
  files:
    - template-export.yml
```

今回のBuildは、

- package.json記載のパッケージインストール
- テスト
- 不要パッケージ削除
- cloudformation packageコマンド

を実行しています。

出力アーティファクトとして、`cloudformation package`コマンドにより作成された`template-export.yml`を指定しています。
このファイルが、変更セットのテンプレートとして使用されます。

また、Lambdaのコードはアーティファクトと同じS3バケットにアップロードされており、`template-export.yml`でLambdaのコードとして指定されています。

Buildの出力アーティファクトは、指定したS3の`〇〇-Pipeline/〇〇-Bui/`にZipとしてアップロードされます。

## Deployステージ

Deployでは、Buildで出力したアーティファクト（SAMテンプレート）を使用し、CloudFormation変更セットを作成・実行します。

その結果、テンプレート内で設定したリソース、今回の場合はLambda（追加していたらDynamoDBも）が作成されます。

## パイプラインの流れ

パイプラインの流れを構成図で表すと以下のとおりです。

![codepipeline_tutorial_1-CodePipeline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344711/9596d161-9d9d-6cff-9a90-672554ee2107.png)

# さいごに

チュートリアル自体はすぐに終わりましたが、裏でどのようにAPIが動いているのか調べると、思った以上に時間がかかってしまいました。

その代わりにServerless Application Repositoryや、CodePipelineの仕組みがなんとなく分かってきました。
また、CloudTrailで記録したAPIログのリクエスト内容は、CLIなどで実行するときのパラメーター全てを記載している訳ではないということを、改めて実感できたのは良かったと思います。

自身の学習目的の記事ですが、誰かのお役に立てたら幸いです。