0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWSでCI/CDを構築する(CloudFormation + CodePipeline + CodeBuild + SAM)

0
Posted at

バックエンド(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 build
  • pytest
  • sam packagepackaged.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 側認可)。

手順:

  1. CloudFormation スタックと同じリージョンで AWS Console を開く
  2. CodePipeline を検索 → 設定 → 接続(Connections)
  3. ステータスが「保留中」の接続を選択 → 「保留中の接続を更新」
  4. 新規ウィンドウで GitHub 連携が求められる
  5. すでに GitHub 側で “AWS Connector for GitHub” が入っている場合
    • GitHub: Settings → GitHub Apps → Installed GitHub Apps → AWS Connector for GitHub → Configure
    • 対象リポジトリ(または全リポジトリ)を選択
  6. まだ無い場合
    • 「新しいアプリをインストールする」→ 自分の GitHub アカウントでインストール
  7. 「接続」を押す
  8. 「利用可能」になったら、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。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?