はじめに
DOP学習にあたり、CodePipelineのイメージを掴むためにチュートリアルを行います。
対象チュートリアル
チュートリアルの構成図
※チュートリアル中、Lambdaの箇所にDynamoDBも追加されます。
やってみた
Lambdaからアプリケーションを作成
まず、Lambdaのアプリケーションを一から作成
します。
Lambdaのアプリケーションを作成すると、Serverless Application Repositoryからアプリケーションをパイプライン付きで作成できます。
Serverless Application Repositoryとは、その名のとおりサーバレスアプリケーションのマネージド型リポジトリです。
AWSやサードパーティーだけでなく、個人もリポジトリにサーバレスアプリケーションを追加できます。
続いて、チュートリアルの指示どおり、各項目を埋めて作成します。
アプリケーション作成後の画面がこちらです。
AWSリソースが作成され、リソース、インフラストラクチャと分類されています。
リソースはLambda。
インフラストラクチャはパイプラインに必要なAWSリソースです。
これらは、2つのCloudFormationにより作成されています。
こちらがそのCloudFormationです。
my-appによりリソース、serverlessrepo-my-app-toolchainによりインフラストラクチャが作成されています。
Lambdaでアプリケーションを作成すると、まずserverlessrepo-my-app-toolchainが作成されます。
このCloudFormation Stackは、青で囲んだ以下のリソースを作成します。
そして、CodePipelineは作成すると自動実行されるので、Deploy Stage
で設定したCloudFormation=my-appがデプロイされ、リソースであるLambdaが作成される、という流れになります。
Lambdaからアプリケーションを作成する際のAPI履歴を追う
チュートリアルから脇道に逸れますが、気になるのでアプリケーション作成からのAPIの動きを、少しだけCloudTrailなどで追ってみました。
以下の画像は、CloudTrailのイベント履歴で、Lambdaでアプリケーションの作成をした直後の状態です。
アプリケーションの作成ボタンをクリックした後に走るAPIが分かります。
注目していただきたいのは、下の2つです。
それより上のAPIは、ExecuteChangeSet
によりリソースが作成されているだけです。
Lambdaでアプリケーションを作成すると、CreateChangeSet
、CreateCloudFormationChangeSet
が走っていますが、時系列的にはCreateCloudFormationChangeSet
-> CreateChangeSet
が正しいと思っています。
ドキュメントは提示できませんが、chromeの開発者ツールでリクエストペイロードを確認する限り、この順番の可能性が高いと考えます。
以下では、CloudTrailのAPIログ、chromeの開発者ツールの画像を用いて、Lambdaのアプリケーション作成後のAPIの動きを、時系列に沿って説明します。
こちらの画像は、アプリケーション作成ボタンクリック後のchrome開発者ツールのNetworkタブの状態です。
青で選択している一つ上をご覧ください。
getApplication
APIを実行し、今回作成するサーバレスアプリケーションの情報を、サーバーレスアプリケーションリポジトリから取得しています。
{
"service": "serverlessapplicationrepository.AWSServerlessApplicationRepository",
"api": "getApplication",
"payload": {
"applicationId": "arn:aws:serverlessrepo:us-east-1:713482062377:applications/CICD-toolchain-for-serverless-applications"
},
"operation": "proxyAwsCall"
}
上記リクエストに対するレスポンスは抜粋して掲載します。
{
"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
が動くのでしょう。
{
"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:111111111111:function:my-app-helloFromLambda*"
}
],
"semanticVersion": "1.0.5"
},
"operation": "proxyAwsCall"
}
{
"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ログの抜粋です。
{
"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
実行後でなければ実行できない。と思っていました。
しかし、変更セットタイプが何なのか確実に判断する方法は分かりませんでした。
CLIでcreate-change-set
を--change-set-type
を付けて実行したところ、CloudTrailのAPIログに変更セットタイプが記録されなかったからです。
また、describe-change-set
を実行しても変更セットタイプは出力されませんでした。
変更セットタイプを知るには、変更セット作成時に、CloudFormation Stackが存在していたかどうかで判別するしかなさそうです(確認する必要性はないと思いますが)。
CreateChangeSet
で一点気になるのは、apigateway
によってリクエストされていることです。
CreateCloudFormationChangeSet
を実行すると、裏でapigateway
によってCreateChangeSet
が実行される仕組みになっているのでしょうか。
パラメータが空だったりと具体的動作が不明ですが、裏でよしなにやってくれているのでしょう。
ちなみに以下はCreateCloudFormationChangeSet
のAPIログです。
開発者ツールで確認したリクエストと同じ感じですね。
{
"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をテストし、正常に動作するか確認します。
正常に動作できていますね。
DynamoDBを追加
次に、このアプリケーションにDynamoDBを追加してみます。
ローカルからCodeCommitに接続するため、認証情報を取得します。
私はHTTPS認証情報を生成しました。
ローカルにリポジトリのクローンを作成しようとしましたが、失敗しました。
# 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ユーティリティで保存されている認証情報の有効期限が切れているからです。
以下を参考に修正後、クローンします。
DynamoDBをSAMに追加してプッシュします。
(gitの操作は省略します。)
DynamoDBのデプロイが確認できました。
Lambdaのアクセス権限の境界を変更
しかし、このままだとLambdaはDynamoDBへのアクセス許可がありません。
Lambdaの実行ロールはアクセス権限の境界が設定されており、その中でDynamoDBが許容されていないからです。
以下が設定されているアクセス権限の境界です。
許容しているのは、
- Lambda
my-app-helloFromLambda*
に対して行える全てのAPI許可 - CloudWatchLogsのロググループ作成やログの送信
- X-Rayに対する送信
- 特定のタグキーを持つリソースに対するタグ付け
です。
{
"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
に追加します。
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;
}
ハンドラーを変更しているため、テンプレートも変更します。
(関係箇所抜粋)
helloFromLambdaFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
Handler: src/handlers/index.handler
Runtime: nodejs10.x
gitでプッシュ後、デプロイまで確認したらLambdaのテスト、DynamoDBテーブル確認を繰り返し、正常に動作しているかチェックします。
確認の中で、DynamoDBのデータが更新されていれば、成功です。
以上でチュートリアルは終了です。
CodePipelineを調べてみた
チュートリアルは終了しましたが、本記事はそもそもCodePipelineの学習のため始めました。
そのため、次はCodePipelineの内容に注目します。
今回、CodePipelineは3つのステージを作成しています。
Source,Build,Deployです。
簡単に説明すると、SourceのCodeCommitの変更によりCodePipelineはトリガーされ、BuildによりSAMテンプレートが作成され、DeployによりCloudFormation Stackの変更セットが実行される流れです。
以下、具体的に説明します。
Sourceステージ
Sourceに設定しているCodeCommitリポジトリのブランチが変更されると、EventBridgeルールによりCodePipelineが実行されます(CodePipelineは作成時にも実行されます)。
CodePipelineは、Sourceの出力アーティファクトとして、CodeCommitリポジトリ内のファイルをまとめたzipファイルを、指定したS3の〇〇-Pipeline/〇〇-Sou/
にアップロードします。
Buildステージ
Buildでは、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ではその手順どおり実行します。
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も)が作成されます。
パイプラインの流れ
パイプラインの流れを構成図で表すと以下のとおりです。
さいごに
チュートリアル自体はすぐに終わりましたが、裏でどのようにAPIが動いているのか調べると、思った以上に時間がかかってしまいました。
その代わりにServerless Application Repositoryや、CodePipelineの仕組みがなんとなく分かってきました。
また、CloudTrailで記録したAPIログのリクエスト内容は、CLIなどで実行するときのパラメーター全てを記載している訳ではないということを、改めて実感できたのは良かったと思います。
自身の学習目的の記事ですが、誰かのお役に立てたら幸いです。