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!!"}%