CloudFormation で構築した CodePipeline を使って AWS SAM プロジェクトを自動デプロイする
いまさらだが、CloudFormation で構築した CodePipeline を使って AWS SAM プロジェクトを自動デプロイする流れを追った。
どんな登場人物がいて、それぞれが果たす役割を理解したかったためである。
なお、現在は自分で CloudFormation のスタックを作って CodePipeline を構築しなくてもよい。
SAM Pipeline を使うと AWS SAM CLI のコマンドを数回実行するだけで CodePipeline を作ることができる。
全体の構成
次の図は、CodePipeline の構成と Pipeline におけるビルドとデプロイの流れを示している。
動作を確認した環境
- AWS CLI v2.9.11
- AWS SAM CLI v1.67.0
STEP1:SAM プロジェクトの作成
SAM プロジェクトの作成
AWS SAM CLI を使って、SAM プロジェクトを作成する。sam init
を実行すると、Lambda 関数のテンプレートと SAM の設定ファイルが作成される。これらのファイル群を GitHub リポジトリで管理する。
CodePipeline を構築した後は、GitHub への push を検知すると、パイプライン上でビルドやデプロイが自動で行なわれるようになる。
$ sam init --runtime nodejs18.x --name hello-world
...略...
Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)
-----------------------
Generating application:
-----------------------
Name: hello-world
Runtime: nodejs18.x
Architectures: x86_64
Dependency Manager: npm
Application Template: hello-world
Output Directory: .
Next steps can be found in the README file at ./hello-world/README.md
...略...
コマンド実行後に作成されたファイルは次のとおり。
hello-world
├── README.md
├── events
│ └── event.json
├── hello-world # Lambda 関数の本体
│ ├── app.mjs
│ ├── package.json
│ └── tests
│ └── unit
│ └── test-handler.mjs
└── template.yaml # AWS SAM テンプレート
今回は、この初期テンプレートの内容をそのまま利用する。
GET リクエストを送ると hello world
を返す REST API である。
buildspec.yml の作成
プロジェクトのディレクトリ下に、次の内容の buildspec.yml
を作成する。
パイプラインにおける Build Stage では、このファイルに記載された内容に従って処理が行なわれる。
今回は、aws cloudformation package
が実行されるため、AWS SAM テンプレート(template.yaml
)を利用して、次のファイルが S3 にアップロードされる。
- Lambda 関数のソースコード
template.yaml
を元に生成した SAM デプロイテンプレート(deploy_template.yml
)
環境変数の $BUCKET_NAME
は、この後の手順で構築する CodePipeline に設定する。
version: 0.2
phases:
build:
commands:
- aws cloudformation package --template-file template.yml \
--s3-bucket $BUCKET_NAME --output-template-file deploy_template.yml
artifacts:
files:
- deploy_template.yml
STEP2:CloudFormation テンプレートファイルの作成
次の 4 つの CloudFormation テンプレートファイルを作成する。
github-connection.yml
GitHub と連携する CodeStarConnections を作成するスタックartifactstore.yml
Source Stage や Build Stage の成果物を置く S3 を作成するスタックrole.yml
CodePipeline のための IAM ロールを作成するスタックpipeline.yml
CodePipeline を作成するスタック
github-connection.yml
github-connection.yml
では、GitHub と連携する CodeStarConnections を作成する。
参考:AWS::CodeStarConnections::ConnectionProviderType
で「GitHub」を設定すると、GitHub と連携できるようになる。
なお、GitHub アカウント名やリポジトリ名は、pipeline.yml
で設定する。
github-connection.yml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
SystemName:
Type: String
Default: example # 任意の値に変更する
Resources:
SourceConnection:
Type: "AWS::CodeStarConnections::Connection"
Properties:
ConnectionName: !Sub "${SystemName}-connection"
ProviderType: GitHub
Outputs:
SourceConnection:
Value: !Ref SourceConnection
Export:
Name: !Sub "${SystemName}-connection-arn"
artifactstore.yml
artifactstore.yml
では、Source Stage や Build Stage の成果物を置く S3 バケットを作成する。
参考:AWS::S3::Bucket
artifactstore.yml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
SystemName:
Type: String
Default: example # 任意の値に変更する
Resources:
ArtifactStore:
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Sub "${AWS::Region}-${AWS::AccountId}-${SystemName}"
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: True
IgnorePublicAcls: True
BlockPublicPolicy: True
RestrictPublicBuckets: True
LifecycleConfiguration:
Rules:
- Id: !Sub "${SystemName}-artifact-lifecycle"
Status: Enabled
ExpirationInDays: 1
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 1
NoncurrentVersionExpiration:
NewerNoncurrentVersions: 1
NoncurrentDays: 1
Outputs:
ArtifactStore:
Value: !Ref ArtifactStore
Export:
Name: !Sub "${SystemName}-articactstore"
ArtifactStoreARN:
Value: !GetAtt ArtifactStore.Arn
Export:
Name: !Sub "${SystemName}-articactstore-arn"
role.yml
role.yml
では、CodePipeline のための IAM ロールを作成する。
参考:AWS::IAM::Role
- CodePipeLineRole を構築する IAM ロール
- CodeBuild を実行する IAM ロール
- CodeDeploy を実行する IAM ロール
いくつかの Role で FullAccess なポリシーを適用しているため、本当は必要最低限なポリシーに絞ったほうが良い。
role.yml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
SystemName:
Type: String
Default: example # 任意の値に変更する
Resources:
CodePipeLineRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: !Sub "${SystemName}-codepipeline-role"
Path: "/service-role/"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- codepipeline.amazonaws.com
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AWSCloudFormationFullAccess"
Policies:
- PolicyName: !Sub "${SystemName}-codepipeline-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "codestar-connections:UseConnection"
Resource:
- {"Fn::ImportValue": !Sub "${SystemName}-connection-arn"}
- !Join
- "/"
- - {"Fn::ImportValue": !Sub "${SystemName}-connection-arn"}
- "*"
- Effect: Allow
Action:
- "s3:*"
Resource:
- {"Fn::ImportValue": !Sub "${SystemName}-articactstore-arn"}
- !Join
- "/"
- - {"Fn::ImportValue": !Sub "${SystemName}-articactstore-arn"}
- "*"
- Effect: Allow
Action:
- "codebuild:StartBuild"
- "codebuild:BatchGetBuilds"
Resource:
- !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${SystemName}-build"
- Effect: Allow
Action:
- "iam:PassRole"
Resource: "*"
CodeBuildRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: !Sub "${SystemName}-codebuild-role"
Path: "/service-role/"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- codebuild.amazonaws.com
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AmazonS3FullAccess"
Policies:
- PolicyName: !Sub "${SystemName}-codebuild-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource:
- !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*"
- !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*:*"
- Effect: Allow
Action:
- "cloudformation:ValidateTemplate"
Resource: "*"
CodeDeployRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: !Sub "${SystemName}-deploy-role"
Path: "/service-role/"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- cloudformation.amazonaws.com
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AWSLambda_FullAccess"
Policies:
- PolicyName: !Sub "${SystemName}-codedeploy-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- iam:CreateRole
- iam:DeleteRole
- iam:GetRole
- iam:PassRole
- iam:DeleteRolePolicy
- iam:AttachRolePolicy
- iam:DetachRolePolicy
- iam:PutRolePolicy
- iam:GetRolePolicy
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
- Effect: Allow
Action:
- "cloudformation:CreateChangeSet"
- "cloudformation:ExecuteChangeSet"
Resource:
- !Sub arn:aws:cloudformation:${AWS::Region}:aws:transform/Serverless-2016-10-31
- Effect: Allow
Action:
- "s3:GetObject"
Resource:
- {"Fn::ImportValue": !Sub "${SystemName}-articactstore-arn"}
- !Join
- "/"
- - {"Fn::ImportValue": !Sub "${SystemName}-articactstore-arn"}
- "*"
- Effect: Allow
Action:
- "apigateway:GET"
- "apigateway:PUT"
- "apigateway:POST"
- "apigateway:PATCH"
- "apigateway:DELETE"
Resource:
- !Sub "arn:aws:apigateway:${AWS::Region}::/restapis"
- !Sub "arn:aws:apigateway:${AWS::Region}::/restapis/*"
pipeline.yml
pipeline.yml
では、CodePipeline を作成する。 具体的には、次の 2 つを行なっている。
- ビルドプロジェクトの作成(
CodeBuild
)
参考:AWS::CodeBuild::Project - パイプラインの作成(
CodePipeline
)
参考:AWS::CodePipeline::Pipeline
ビルドプロジェクトの作成では、Source Stage の成果物を置いた S3 バケットの指定や、ビルドを実行するコンテナの定義や環境変数を定義する。
パイプラインの作成では、次の 3 つのステージで何を行うかを設定する。
- Source Stage
CodeStarConnections で接続する GitHub の情報を設定する - Build Stage
同じファイル内で定義したビルドプロジェクトを呼び出す - Deploy Stage
SAM デプロイテンプレートを使って構築した CloudFormation で、Lambda 関数をデプロイする
pipeline.yml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
SystemName:
Type: String
Default: example # 任意の値に変更する
RepositoryOwner:
Type: String
Default: chick-p # SAM プロジェクトを置いた GitHub アカウント名
RepositoryName:
Type: String
Default: practice-sam # SAM プロジェクトを置いた GitHub リポジトリ名
BranchName:
Type: String
Default: main # デプロイするブランチ
Resources:
CodeBuild:
Type: "AWS::CodeBuild::Project"
Properties:
Name: !Sub "${SystemName}-build"
BadgeEnabled: false
Source:
Type: "S3"
InsecureSsl: false
Location: !Join
- "/"
- - {"Fn::ImportValue": !Sub "${SystemName}-articactstore"}
- "index.zip"
Artifacts:
Type: "S3"
Location: {"Fn::ImportValue": !Sub "${SystemName}-articactstore"}
NamespaceType: "NONE"
OverrideArtifactName: false
Packaging: "ZIP"
Path: ""
Cache:
Type: "NO_CACHE"
Environment:
Type: "LINUX_CONTAINER"
ComputeType: "BUILD_GENERAL1_SMALL"
Image: "aws/codebuild/standard:3.0"
PrivilegedMode: false
EnvironmentVariables:
- Name: "BUCKET_NAME"
Type: "PLAINTEXT"
Value: {"Fn::ImportValue": !Sub "${SystemName}-articactstore"}
ServiceRole: !Sub "arn:aws:iam::${AWS::AccountId}:role/service-role/${SystemName}-codebuild-role"
TimeoutInMinutes: 5
QueuedTimeoutInMinutes: 60
LogsConfig:
CloudWatchLogs:
Status: "DISABLED"
S3Logs:
Status: "DISABLED"
CodePipeline:
Type: "AWS::CodePipeline::Pipeline"
Properties:
Name: !Sub ${SystemName}-pipeline
RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/service-role/${SystemName}-codepipeline-role"
ArtifactStore:
Type: "S3"
Location: {"Fn::ImportValue": !Sub "${SystemName}-articactstore"}
Stages:
- Name: "Source"
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: AWS
Provider: "CodeStarSourceConnection"
Version: 1
Configuration:
ConnectionArn: {'Fn::ImportValue': !Sub '${SystemName}-connection-arn'}
FullRepositoryId: !Sub "${RepositoryOwner}/${RepositoryName}"
BranchName: !Sub "${BranchName}"
OutputArtifactFormat: "CODE_ZIP"
OutputArtifacts:
- Name: "SourceArtifact"
Region: !Ref AWS::Region
Namespace: "SourceVariables"
RunOrder: 1
- Name: "Build"
Actions:
- Name: BuildAction
ActionTypeId:
Category: "Build"
Owner: "AWS"
Provider: "CodeBuild"
Version: 1
Configuration:
ProjectName: !Ref CodeBuild
InputArtifacts:
- Name: "SourceArtifact"
OutputArtifacts:
- Name: "BuildArtifact"
Region: !Ref AWS::Region
Namespace: "BuildVariables"
RunOrder: 2
- Name: "Deploy"
Actions:
- Name: DeployAction
ActionTypeId:
Category: "Deploy"
Owner: "AWS"
Provider: "CloudFormation"
Version: 1
Configuration:
ActionMode: "CREATE_UPDATE"
Capabilities: "CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND"
RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/service-role/${SystemName}-deploy-role"
StackName: !Sub "${SystemName}-deploy"
TemplatePath: "BuildArtifact::deploy_template.yml"
InputArtifacts:
- Name: BuildArtifact
Region: !Ref AWS::Region
Namespace: "DeployVariables"
RunOrder: 1
STEP3:CloudFormation のスタックの作成
STEP2 で作成した CloudFormation テンプレートを使って、CloudFormation のスタックを 4 つ作成する。
これらのスタックをネストさせて束ねる方法もあるが、テンプレートファイルを S3 に置く必要があるため、今回は別々にスタックを作成する。
github-connection.yml のスタック
# スタックを作成する
$ aws cloudformation create-stack \
--stack-name example-github-connection --template-body file://github-connection.yml
{
"StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/sam-example/12345678-1234-1234-1234-123434567890"
}
# CodestarConnections が作成されたことを確認する
$ aws codestar-connections list-connections | jq '.Connections[].ConnectionName'
"example-connection"
artifactstore.yml のスタック
# スタックを作成する
$ aws cloudformation create-stack \
--stack-name example-artifactstore --template-body file://artifactstore.yml
{
"StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/sam-example/12345678-1234-1234-1234-123434567890"
}
# バケットが作成されたことを確認する
$ aws s3 ls
2023-01-01 16:13:18 ap-northeast-1-123456789012-example
role.yml のスタック
# スタックを作成する
$ aws cloudformation create-stack \
--stack-name example-role --template-body file://role.yml --capabilities CAPABILITY_NAMED_IAM
{
"StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/sam-example/12345678-1234-1234-1234-123434567890"
}
# IAM ロールが作成されたことを確認する
$ aws iam list-roles --path-prefix /service-role/ | jq '.Roles[].RoleName'
"example-codebuild-role"
"example-codepipeline-role"
"example-deploy-role"
pipeline.yml のスタック
# スタックを作成する
$ aws cloudformation create-stack \
--stack-name example-pipeline --template-body file://pipeline.yml
{
"StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/sam-example/12345678-1234-1234-1234-123434567890"
}
# Pipeline が作成されたことを確認する
$ aws codepipeline list-pipelines | jq '.pipelines[].name'
"example-pipeline"
STEP4:CodeStar Connection の有効化
AWS Console でパイプラインを確認しても、この時点ではパイプラインが失敗している。
これは、GitHub との接続が有効になっていないためである。
GitHub リポジトリに AWS Connecter for GitHub をインストールし、GitHub の連携を有効化する。
この操作は、AWS CLI では操作できないため、AWS Console を使う。
- デベロッパー用ツール | 接続 にアクセスする
- 作成した CodeStar Connection を選択し、[保留中の接続を更新]をクリックする
- [新しいアプリをインストールする]をクリックする
- 接続する GitHub アカウントを選択する
- 「Repository access」セクションで、接続するリポジトリを選択し、[Save]をクリックする
- [接続]をクリックする
- 作成した CodeStar Connection(example-connection)のステータスが「利用可能」になったことを確認する
STEP5:失敗したパイプラインの再実行
AWS CLI で再実行するための Execution ID を、次のコマンドで取得する。
# Execution ID を取得する
$ aws codepipeline get-pipeline-state \
--name example-pipeline | jq '.stageStates[].latestExecution'
{
"pipelineExecutionId": "12345678-1234-1234-1234-123456789012",
"status": "Failed"
}
null
null
aws codepipeline retry-stage-execution
で再実行する。
# パイプラインを再実行する
$ aws codepipeline retry-stage-execution \
--pipeline-name example-pipeline --stage-name Source \
--pipeline-execution-id 12345678-1234-1234-1234-123456789012 \
--retry-mode FAILED_ACTIONS
{
"pipelineExecutionId": "12345678-1234-1234-1234-123456789012"
}
# すべてのパイプラインステージが成功したことを確認する
$ aws codepipeline get-pipeline-state \
--name example-pipeline | jq '.stageStates[].latestExecution'
{
"pipelineExecutionId": "12345678-1234-1234-1234-123456789012",
"status": "Succeeded"
}
{
"pipelineExecutionId": "12345678-1234-1234-1234-123456789012",
"status": "Succeeded"
}
{
"pipelineExecutionId": "12345678-1234-1234-1234-123456789012",
"status": "Succeeded"
}
STEP6:動作確認
次の 2 点を確認する。
- SAM プロジェクトのリポジトリにコミットを追加すると Pipeline が実行されること
- Lambda 関数が作成されたこと
コミットを追加すると Pipeline が実行されること
SAM プロジェクトの Lambda 関数を適当に変更し、コミットを push する。
'body': JSON.stringify({
- message: 'hello world',
+ message: 'hello world!!',
})
ステージのステータスが変わり、パイプラインが再実行されていることを確認できる。
$ aws codepipeline get-pipeline-state \
--name example-pipeline | jq '.stageStates[].latestExecution'
{
"pipelineExecutionId": "cd2cc850-cd34-45b9-807a-5aa9ecba0d25",
"status": "Succeeded"
}
{
"pipelineExecutionId": "cd2cc850-cd34-45b9-807a-5aa9ecba0d25",
"status": "InProgress"
}
{
"pipelineExecutionId": "e16aed25-2325-493f-b82f-24186ad2cdcf",
"status": "Succeeded"
}
Lambda 関数が作成されたこと
Lambda 関数が作成されたことを確認する。
API のエンドポイント URL には、API Gateway の ID が使われる。
次のコマンドで API Gateway の ID を確認できる。
$ aws apigateway get-rest-apis | jq '.items[] | select (.name == "example-deploy") | .id'
"randomvalueid"
API のエンドポイント URL に GET リクエストを送信する。
「hello world!!」が返却されることを確認できればよい。
$ curl https://randomvalueid.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
{"message":"hello world!!"}%