0
0

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】CodePipeline を使った CI/CD パイプライン構築

0
Posted at

はじめに

この記事では、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)
  1. GitHub: ソースコードの管理
  2. CodePipeline: パイプライン全体の制御
  3. CodeBuild: Docker イメージのビルド
  4. ECR: Docker イメージの保存
  5. 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 にアクセスするには、接続の承認が必要です。

  1. AWS マネジメントコンソールで「設定」→「接続」を開く
  2. 作成された接続を選択し、「保留中の接続を更新」をクリック
  3. 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 にプッシュするだけで、自動的にアプリケーションが本番環境にデプロイされます。手動でのデプロイ作業が不要になり、リリースサイクルを大幅に短縮できます。

参考

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?