はじめに
この記事では、AWS CodePipeline を使った CI/CD パイプラインの構築方法を記載します。
具体例として、React / TypeScript / Vite で作成したアプリケーションを GitHub から自動デプロイする仕組みを構築します。
AWS CodePipeline とは
AWS CodePipeline は、アプリケーションのビルド、テスト、デプロイを自動化する CI/CD サービスです。
以下のような特徴があります。
- GitHub などのソースコード管理システムとの連携
- AWS CodeBuild によるビルド・テストの自動実行
- Amazon ECS や Lambda などへの自動デプロイ
- 視覚的なパイプライン管理
- 他の AWS サービスとのシームレスな統合
CI/CD とは
CI/CD は、継続的インテグレーション(Continuous Integration)と継続的デリバリー(Continuous Delivery)の略です。
- CI(継続的インテグレーション): コードの変更を頻繁にメインブランチに統合し、自動でビルド・テストを実行
- CD(継続的デリバリー): ビルドされたアプリケーションを自動で本番環境にデプロイ
これにより、手動でのデプロイ作業が不要になり、リリースサイクルを高速化できます。
今回構築するアーキテクチャ
以下の構成で、GitHub へのプッシュをトリガーに自動デプロイするパイプラインを構築します。
GitHub → CodePipeline → CodeBuild → ECR → ECS(Fargate)
- GitHub: ソースコードの管理
- CodePipeline: パイプライン全体の制御
- CodeBuild: Docker イメージのビルド
- ECR: Docker イメージの保存
- ECS(Fargate): コンテナの実行環境
開発環境
開発環境は以下の通りです。
- Windows 11
- React 19.2.0
- TypeScript 5.9.3
- Vite 7.2.4
- Node.js 24.11.1
- AWS CLI 2.x
事前準備
サンプルアプリケーションの準備
Vite で React / TypeScript プロジェクトを作成します。
npm create vite@latest code-pipeline-sample-app -- --template react-ts
cd code-pipeline-sample-app
npm install
Dockerfile の作成
プロジェクトルートに Dockerfile を作成します。
# ビルドステージ
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 実行ステージ
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf の作成
SPA のルーティングに対応するため、nginx.conf を作成します。
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
GitHub リポジトリへのプッシュ
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
git push -u origin main
AWS リソースの構築
ECR リポジトリの作成
Docker イメージを保存するための ECR リポジトリを作成します。
AWS マネジメントコンソールから、「Elastic Container Registry」を開き、「リポジトリを作成」をクリックします。
- リポジトリ名:
code-pipeline-sample-app - その他の設定: デフォルトのまま
ECS クラスターの作成
CloudFormation テンプレートを使って、ECS 関連のリソースを作成します。
ecs-cluster.yaml を作成します。
AWSTemplateFormatVersion: '2010-09-09'
Description: ECS Cluster with Fargate
Parameters:
ProjectName:
Type: String
Default: my-app
Description: Project name for resource naming
Resources:
# VPC
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub ${ProjectName}-vpc
# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${ProjectName}-igw
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# Public Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${ProjectName}-public-subnet-1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${ProjectName}-public-subnet-2
# Route Table
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${ProjectName}-public-rt
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
SubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
SubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
# Security Group
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for ALB
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub ${ProjectName}-alb-sg
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for ECS tasks
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub ${ProjectName}-ecs-sg
# Application Load Balancer
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub ${ProjectName}-alb
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub ${ProjectName}-alb
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub ${ProjectName}-tg
Port: 80
Protocol: HTTP
VpcId: !Ref VPC
TargetType: ip
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckIntervalSeconds: 30
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ALB
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
# ECS Cluster
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ${ProjectName}-cluster
# ECS Task Execution Role
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# ECS Task Definition
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub ${ProjectName}-task
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: '256'
Memory: '512'
ExecutionRoleArn: !Ref ECSTaskExecutionRole
ContainerDefinitions:
- Name: !Sub ${ProjectName}-container
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/my-app:latest
PortMappings:
- ContainerPort: 80
Protocol: tcp
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref CloudWatchLogsGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ecs
# CloudWatch Logs
CloudWatchLogsGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /ecs/${ProjectName}
RetentionInDays: 7
# ECS Service
ECSService:
Type: AWS::ECS::Service
DependsOn: ALBListener
Properties:
ServiceName: !Sub ${ProjectName}-service
Cluster: !Ref ECSCluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 1
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ECSSecurityGroup
LoadBalancers:
- ContainerName: !Sub ${ProjectName}-container
ContainerPort: 80
TargetGroupArn: !Ref TargetGroup
Outputs:
ALBEndpoint:
Description: Application Load Balancer endpoint
Value: !GetAtt ALB.DNSName
Export:
Name: !Sub ${ProjectName}-alb-endpoint
ECSClusterName:
Description: ECS Cluster name
Value: !Ref ECSCluster
Export:
Name: !Sub ${ProjectName}-cluster-name
ECSServiceName:
Description: ECS Service name
Value: !GetAtt ECSService.Name
Export:
Name: !Sub ${ProjectName}-service-name
AWS CLI でスタックを作成します。
aws cloudformation create-stack \
--stack-name my-app-ecs \
--template-body file://ecs-cluster.yaml \
--capabilities CAPABILITY_IAM
初回デプロイ時の注意点
ECS タスク定義で指定している Docker イメージ(my-app:latest)は、まだ ECR に存在しません。そのため、CloudFormation スタックの作成は成功しますが、ECS サービスがタスクを起動できず、エラー状態になります。
この状態は正常で、後の手順で CodePipeline が初回のビルドを実行し、イメージを ECR にプッシュすると、ECS サービスが正常に動作するようになります。
CodeBuild プロジェクトの作成
buildspec.yml をプロジェクトルートに作成します。
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG=${COMMIT_HASH:=latest}
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:latest
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo Writing image definitions file...
- printf '[{"name":"%s","imageUri":"%s"}]' $CONTAINER_NAME $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
CodeBuild 用の CloudFormation テンプレート codebuild.yaml を作成します。
AWSTemplateFormatVersion: '2010-09-09'
Description: CodeBuild Project
Parameters:
ProjectName:
Type: String
Default: my-app
GitHubRepo:
Type: String
Description: GitHub repository (owner/repo)
Resources:
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
Policies:
- PolicyName: CodeBuildPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: '*'
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${ProjectName}-artifacts-${AWS::AccountId}
VersioningConfiguration:
Status: Enabled
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub ${ProjectName}-build
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:7.0
PrivilegedMode: true
EnvironmentVariables:
- Name: AWS_DEFAULT_REGION
Value: !Ref AWS::Region
- Name: AWS_ACCOUNT_ID
Value: !Ref AWS::AccountId
- Name: IMAGE_REPO_NAME
Value: my-app
- Name: CONTAINER_NAME
Value: !Sub ${ProjectName}-container
Source:
Type: CODEPIPELINE
BuildSpec: buildspec.yml
Outputs:
CodeBuildProjectName:
Value: !Ref CodeBuildProject
Export:
Name: !Sub ${ProjectName}-codebuild-project
ArtifactBucketName:
Value: !Ref ArtifactBucket
Export:
Name: !Sub ${ProjectName}-artifact-bucket
スタックを作成します。
aws cloudformation create-stack \
--stack-name my-app-codebuild \
--template-body file://codebuild.yaml \
--parameters ParameterKey=GitHubRepo,ParameterValue=YOUR_USERNAME/YOUR_REPO \
--capabilities CAPABILITY_IAM
CodePipeline の作成
CodePipeline 用の CloudFormation テンプレート codepipeline.yaml を作成します。
AWSTemplateFormatVersion: '2010-09-09'
Description: CodePipeline for ECS Deployment
Parameters:
ProjectName:
Type: String
Default: my-app
GitHubOwner:
Type: String
Description: GitHub repository owner
GitHubRepo:
Type: String
Description: GitHub repository name
GitHubBranch:
Type: String
Default: main
Description: GitHub branch name
Resources:
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CodePipelinePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetObjectVersion
Resource:
- !Sub
- arn:aws:s3:::${BucketName}/*
- BucketName: !ImportValue my-app-artifact-bucket
- Effect: Allow
Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
Resource: !Sub
- arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${ProjectName}
- ProjectName: !ImportValue my-app-codebuild-project
- Effect: Allow
Action:
- ecs:DescribeServices
- ecs:DescribeTaskDefinition
- ecs:DescribeTasks
- ecs:ListTasks
- ecs:RegisterTaskDefinition
- ecs:UpdateService
- iam:PassRole
Resource: '*'
- Effect: Allow
Action:
- codestar-connections:UseConnection
Resource: !Ref GitHubConnection
GitHubConnection:
Type: AWS::CodeStarConnections::Connection
Properties:
ConnectionName: !Sub ${ProjectName}-github
ProviderType: GitHub
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Sub ${ProjectName}-pipeline
RoleArn: !GetAtt CodePipelineServiceRole.Arn
ArtifactStore:
Type: S3
Location: !ImportValue my-app-artifact-bucket
Stages:
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeStarSourceConnection
Version: '1'
Configuration:
ConnectionArn: !Ref GitHubConnection
FullRepositoryId: !Sub ${GitHubOwner}/${GitHubRepo}
BranchName: !Ref GitHubBranch
OutputArtifactFormat: CODE_ZIP
OutputArtifacts:
- Name: SourceOutput
- Name: Build
Actions:
- Name: BuildAction
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
Configuration:
ProjectName: !ImportValue my-app-codebuild-project
InputArtifacts:
- Name: SourceOutput
OutputArtifacts:
- Name: BuildOutput
- Name: Deploy
Actions:
- Name: DeployAction
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: ECS
Version: '1'
Configuration:
ClusterName: !ImportValue my-app-cluster-name
ServiceName: !ImportValue my-app-service-name
FileName: imagedefinitions.json
InputArtifacts:
- Name: BuildOutput
Outputs:
PipelineName:
Value: !Ref Pipeline
GitHubConnectionArn:
Value: !Ref GitHubConnection
Description: Complete the connection setup in the AWS Console
スタックを作成します。
aws cloudformation create-stack \
--stack-name my-app-pipeline \
--template-body file://codepipeline.yaml \
--parameters \
ParameterKey=GitHubOwner,ParameterValue=YOUR_USERNAME \
ParameterKey=GitHubRepo,ParameterValue=YOUR_REPO \
--capabilities CAPABILITY_IAM
GitHub 接続の承認
CodePipeline が GitHub にアクセスするには、接続の承認が必要です。
- AWS マネジメントコンソールで「設定」→「接続」を開く
- 作成された接続を選択し、「保留中の接続を更新」をクリック
- GitHub にログインして、アクセスを承認
動作確認
GitHub リポジトリに変更をプッシュすると、自動的にパイプラインが実行されます。
# ファイルを編集
echo "// Updated" >> src/App.tsx
# コミット & プッシュ
git add .
git commit -m "Update App"
git push origin main
パイプラインが成功すると、CloudFormation の Outputs に表示された ALB の DNS 名でアプリケーションにアクセスできます。
aws cloudformation describe-stacks \
--stack-name my-app-ecs \
--query 'Stacks[0].Outputs[?OutputKey==`ALBEndpoint`].OutputValue' \
--output text
パイプラインの各ステージ
Source ステージ
GitHub リポジトリから最新のコードを取得します。
- トリガー:
mainブランチへのプッシュ - 出力: ソースコードの ZIP ファイル
Build ステージ
CodeBuild でアプリケーションをビルドし、Docker イメージを作成します。
-
buildspec.ymlに従ってビルドを実行 - Docker イメージを ECR にプッシュ
-
imagedefinitions.jsonを生成(ECS がどのイメージを使うかを定義)
Deploy ステージ
ECS サービスを新しいイメージで更新します。
- ECS タスク定義を更新
- ローリングアップデートで新しいタスクを起動
- 古いタスクを停止
トラブルシューティング
ビルドが失敗する場合
CodeBuild のログを確認します。
aws codebuild batch-get-builds \
--ids <build-id> \
--query 'builds[0].logs.deepLink'
よくあるエラー:
-
npm ciの失敗 →package-lock.jsonがコミットされているか確認 - Docker ビルドの失敗 →
Dockerfileの構文を確認 - ECR へのプッシュ失敗 → IAM ロールの権限を確認
ECS タスクが起動しない場合
ECS サービスのイベントを確認します。
aws ecs describe-services \
--cluster my-app-cluster \
--services my-app-service \
--query 'services[0].events[0:5]'
よくあるエラー:
- イメージが見つからない → ECR にイメージがプッシュされているか確認
- タスク実行ロールの権限不足 → CloudWatch Logs への書き込み権限を確認
- ヘルスチェック失敗 → アプリケーションが正しくポート 80 で起動しているか確認
リソースのクリーンアップ
作成したリソースを削除する場合は、以下の順序で実行します。
# Pipeline スタックの削除
aws cloudformation delete-stack --stack-name my-app-pipeline
# ECS スタックの削除
aws cloudformation delete-stack --stack-name my-app-ecs
# CodeBuild スタックの削除
aws cloudformation delete-stack --stack-name my-app-codebuild
# ECR イメージの削除
aws ecr batch-delete-image \
--repository-name my-app \
--image-ids imageTag=latest
# ECR リポジトリの削除
aws ecr delete-repository --repository-name my-app
まとめ
AWS CodePipeline を使うことで、GitHub へのプッシュから本番環境へのデプロイまでを完全に自動化できます。
主なポイントは以下の通りです。
- CodePipeline による Source → Build → Deploy の自動化
- CodeBuild での Docker イメージのビルドと ECR へのプッシュ
- ECS Fargate でのコンテナ実行
- CloudFormation によるインフラのコード管理
この構成により、開発者はコードを GitHub にプッシュするだけで、自動的にアプリケーションが本番環境にデプロイされます。手動でのデプロイ作業が不要になり、リリースサイクルを大幅に短縮できます。