バックエンド(AWS SAM / Lambda)を例に、CloudFormation で CI/CD を構築する手順をまとめます。
0. 前提
- AWS SAM(Lambda)を使う想定
- ローカル検証には Docker + SAM CLI があると便利
- CI/CD の流れ: GitHub(Code Connections)→ CodePipeline(Source)→ CodeBuild(Build/Test/Package)→ CloudFormation(Deploy)
1. SAM アプリ(最小構成)を用意
sam init で生成してもよいし、手動で作ってもOKです。
ディレクトリ構成(最小)
※ Qiita ではリンク表記が混ざりやすいので、ファイル名はプレーンテキストで統一
<project-root>/
├─ src/
│ ├─ __init__.py # 空でOK
│ └─ app.py # (or index.ts) 空でOK
├─ tests/
│ └─ test_app.py # 空でOK
├─ buildspec.yaml # CI/CD用(後述)
├─ pipeline.yaml # CI/CD用(後述)
├─ README.md # 空でOK
├─ requirements.txt # 空でOK
└─ template.yaml # SAMテンプレ
template.yaml(最小:Lambda 前提)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: demo minimal SAM app
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.11
Architectures:
- arm64
Handler: app.lambda_handler
CodeUri: src/
MemorySize: 128
Timeout: 3
Policies:
- AWSLambdaBasicExecutionRole
2. ローカルでビルド・テンプレ検証
ビルド(Dockerでビルド)
sam build --use-container
validate(リージョン指定)
環境によってはリージョン未指定だとエラーになることがあるため、明示します。
sam validate --region <aws-region>
# 例: sam validate --region ap-northeast-1
3. アプリとテストを最小で書く
src/app.py
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": "Hello, World!"
}
tests/test_app.py
from src.app import lambda_handler
def test_lambda_handler():
res = lambda_handler({}, None)
assert res["statusCode"] == 200
4. CI/CD 用のファイルを追加
プロジェクトルートに以下を追加:
-
buildspec.yaml: CodeBuild のビルド/テスト/パッケージ手順 -
pipeline.yaml: CodePipeline + CodeBuild + CloudFormation Deploy を作る CloudFormation テンプレ
5. buildspec.yaml(SAM の build/test/package)
CodeBuild で以下を実行します:
- SAM CLI インストール
sam buildpytest-
sam package(packaged.yamlを生成し、S3 にアップロード)
version: 0.2
phases:
install:
runtime-versions:
python: 3.11
commands:
- python --version
- pip install --upgrade pip
- pip install aws-sam-cli
- pip install pytest
build:
commands:
- sam --version
- sam build
- PYTHONPATH=. pytest -q
# CloudFormation が読める “packaged” テンプレを作る
- sam package --s3-bucket "$PACKAGE_BUCKET" --output-template-file packaged.yaml
artifacts:
files:
- packaged.yaml
ポイント
-
PACKAGE_BUCKETは CodeBuild 側の環境変数として渡します(後述のpipeline.yamlで設定)。 -
packaged.yamlを CodePipeline のアーティファクトとして Deploy ステージに渡します。
6. pipeline.yaml
CloudFormation で CodePipeline 一式を作成
構成:
- ArtifactBucket(S3 / versioning / encryption)
- CodeConnections::Connection(GitHub接続)
- CodeBuild(buildspec.yaml 実行)
- CloudFormationExecutionRole(Deploy用)
- PipelineRole(CodePipeline 用)
- Pipeline(V2 + Push Trigger)
AWSTemplateFormatVersion: "2010-09-09"
Description: "demo: CodePipeline (GitHub via Code Connections) -> CodeBuild -> CloudFormation deploy (SAM)"
Parameters:
PipelineName:
Type: String
Default: <pipeline-name>
GitHubFullRepositoryId:
Type: String
Default: "<github-username>/<repository>" # owner/repo
BranchName:
Type: String
Default: "main"
AppStackName:
Type: String
Default: "<sam-app-stack-name>" # デプロイ先スタック名(SAMアプリ)
RegionName:
Type: String
Default: "<aws-region>" # 例: ap-northeast-1
Resources:
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration: { Status: Enabled }
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault: { SSEAlgorithm: AES256 }
Connection:
Type: AWS::CodeConnections::Connection
Properties:
ConnectionName: !Sub "${PipelineName}-github"
ProviderType: GitHub
CodeBuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { Service: codebuild.amazonaws.com }
Action: sts:AssumeRole
Policies:
- PolicyName: CodeBuildLogsAndS3
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetObjectVersion
- s3:GetBucketLocation
- s3:ListBucket
Resource:
- !GetAtt ArtifactBucket.Arn
- !Sub "${ArtifactBucket.Arn}/*"
BuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub "${PipelineName}-build"
ServiceRole: !GetAtt CodeBuildRole.Arn
Artifacts: { Type: CODEPIPELINE }
Source:
Type: CODEPIPELINE
BuildSpec: buildspec.yaml
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:7.0
PrivilegedMode: false
EnvironmentVariables:
- Name: PACKAGE_BUCKET
Value: !Ref ArtifactBucket
TimeoutInMinutes: 15
CloudFormationExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { Service: cloudformation.amazonaws.com }
Action: sts:AssumeRole
Policies:
- PolicyName: DeploySamAppBroad
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- cloudformation:*
- lambda:*
- iam:*
- logs:*
- s3:*
Resource: "*"
PipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { Service: codepipeline.amazonaws.com }
Action: sts:AssumeRole
Policies:
- PolicyName: PipelinePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetObjectVersion
- s3:GetBucketLocation
- s3:ListBucket
Resource:
- !GetAtt ArtifactBucket.Arn
- !Sub "${ArtifactBucket.Arn}/*"
- Effect: Allow
Action:
- codebuild:StartBuild
- codebuild:BatchGetBuilds
Resource: !GetAtt BuildProject.Arn
- Effect: Allow
Action:
- codestar-connections:UseConnection
- codeconnections:UseConnection
Resource:
- !Sub "arn:aws:codestar-connections:${AWS::Region}:${AWS::AccountId}:connection/*"
- !Sub "arn:aws:codeconnections:${AWS::Region}:${AWS::AccountId}:connection/*"
- Effect: Allow
Action:
- cloudformation:CreateStack
- cloudformation:UpdateStack
- cloudformation:DeleteStack
- cloudformation:DescribeStacks
- cloudformation:DescribeStackEvents
- cloudformation:GetTemplateSummary
- cloudformation:ValidateTemplate
- cloudformation:CreateChangeSet
- cloudformation:DescribeChangeSet
- cloudformation:ExecuteChangeSet
- cloudformation:DeleteChangeSet
Resource:
- !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AppStackName}/*"
- !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:changeSet/*"
- Effect: Allow
Action:
- iam:PassRole
Resource: !GetAtt CloudFormationExecutionRole.Arn
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Ref PipelineName
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
PipelineType: V2
Triggers:
- ProviderType: CodeStarSourceConnection
GitConfiguration:
SourceActionName: GitHub_Source
Push:
- Branches:
Includes:
- !Ref BranchName
Stages:
- Name: Source
Actions:
- Name: GitHub_Source
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeStarSourceConnection
Version: "1"
Configuration:
ConnectionArn: !Ref Connection
FullRepositoryId: !Ref GitHubFullRepositoryId
BranchName: !Ref BranchName
DetectChanges: false
OutputArtifacts:
- Name: SourceOutput
- Name: Build
Actions:
- Name: CodeBuild
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: "1"
Configuration:
ProjectName: !Ref BuildProject
InputArtifacts:
- Name: SourceOutput
OutputArtifacts:
- Name: BuildOutput
- Name: Deploy
Actions:
- Name: CloudFormation_Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: "1"
Configuration:
ActionMode: CREATE_UPDATE
StackName: !Ref AppStackName
Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
RoleArn: !GetAtt CloudFormationExecutionRole.Arn
TemplatePath: "BuildOutput::packaged.yaml"
InputArtifacts:
- Name: BuildOutput
Outputs:
PipelineName:
Value: !Ref Pipeline
ArtifactBucketName:
Value: !Ref ArtifactBucket
ConnectionArn:
Value: !Ref Connection
7. GitHub に push
(A) まだ Git 初期化していない場合
git init
git branch -M main
git add .
git commit -m "init sam cicd demo"
(B) remote を追加して push
git remote add origin <https://github.com/><github-username>/<repository>.git
git push -u origin main
8. pipeline.yaml を CloudFormation でデプロイ
aws cloudformation deploy \\
--stack-name <pipeline-stack-name> \\
--template-file pipeline.yaml \\
--capabilities CAPABILITY_NAMED_IAM \\
--region <aws-region>
# 例: --region ap-northeast-1
9. Connection(GitHub 連携)を承認してパイプラインを動かす
作成された Connection を Pending → Available にする(GitHub 側認可)。
手順:
- CloudFormation スタックと同じリージョンで AWS Console を開く
- CodePipeline を検索 → 設定 → 接続(Connections)
- ステータスが「保留中」の接続を選択 → 「保留中の接続を更新」
- 新規ウィンドウで GitHub 連携が求められる
- すでに GitHub 側で “AWS Connector for GitHub” が入っている場合
- GitHub: Settings → GitHub Apps → Installed GitHub Apps → AWS Connector for GitHub → Configure
- 対象リポジトリ(または全リポジトリ)を選択
- まだ無い場合
- 「新しいアプリをインストールする」→ 自分の GitHub アカウントでインストール
- 「接続」を押す
- 「利用可能」になったら、CodePipeline → 対象パイプライン → 「変更をリリースする」
CloudFormation スタック削除手順
1) 現状確認
STACK=<pipeline-stack-name>
REGION=<aws-region>
aws cloudformation describe-stacks \\
--stack-name "$STACK" --region "$REGION" \\
--query "Stacks[0].StackStatus"
2) スタック削除(まずは通常)
aws cloudformation delete-stack \\
--stack-name "$STACK" \\
--region "$REGION"
3) 削除完了待ち(失敗することが多い)
aws cloudformation wait stack-delete-complete \\
--stack-name "$STACK" \\
--region "$REGION"
ここで S3 などが残っていると DELETE_FAILED になりがち。
4) 残っているリソースを確認
aws cloudformation list-stack-resources \\
--stack-name "$STACK" --region "$REGION" \\
--query "StackResourceSummaries[].[LogicalResourceId,ResourceType,PhysicalResourceId,ResourceStatus]" \\
--output table
S3 バケットが残っている場合、PhysicalResourceId がバケット名。
BUCKET=<artifact-bucket-name>
5) バージョニング付きバケットを完全に空にする(Versions + DeleteMarkers)
# Versions削除(存在する分だけ)
aws s3api list-object-versions --bucket "$BUCKET" --region "$REGION" \\
--query '{Objects: (Versions || `[]`)[].{Key:Key,VersionId:VersionId}}' \\
--output json > /tmp/s3_versions.json
VCOUNT=$(jq '.Objects | length' /tmp/s3_versions.json)
if [ "$VCOUNT" -gt 0 ]; then
aws s3api delete-objects --bucket "$BUCKET" --region "$REGION" \\
--delete file:///tmp/s3_versions.json
fi
# DeleteMarkers削除(存在する分だけ)
aws s3api list-object-versions --bucket "$BUCKET" --region "$REGION" \\
--query '{Objects: (DeleteMarkers || `[]`)[].{Key:Key,VersionId:VersionId}}' \\
--output json > /tmp/s3_markers.json
MCOUNT=$(jq '.Objects | length' /tmp/s3_markers.json)
if [ "$MCOUNT" -gt 0 ]; then
aws s3api delete-objects --bucket "$BUCKET" --region "$REGION" \\
--delete file:///tmp/s3_markers.json
fi
# 空になったか確認
aws s3api list-object-versions --bucket "$BUCKET" --region "$REGION" \\
--query '{v: length(Versions || `[]`), m: length(DeleteMarkers || `[]`)}'
v:0, m:0 になったらバケット削除:
aws s3api delete-bucket --bucket "$BUCKET" --region "$REGION"
6) スタック削除をリトライ
aws cloudformation delete-stack --stack-name "$STACK" --region "$REGION"
aws cloudformation wait stack-delete-complete --stack-name "$STACK" --region "$REGION"
何も表示されなければ OK。