GitHub Actions から ECS Fargate をデプロイしてみた

GitHub Actions から ECS Fargate をデプロイしてみた

2026.02.08

製造ビジネステクノロジー部の小林です。

最近は Lambda や EC2 を中心とした開発を行っていましたが「たまにはコンテナに触りたい!」という衝動に駆られました。

そこで今回は、GitHub Actions と ECS Fargate を組み合わせた CI/CD パイプライン構築に挑戦してみました。

前提条件

  • インフラ構築は AWS CDK を使用。
  • GitHub Actions から AWS 環境への認証は OIDC 認証を使用。
  • ECS、Docker イメージでは ARM64 を使用。
  • Docker は Rancher Desktop を使用。
  • 本記事では、VPC、サブネット、セキュリティグループなどのネットワーク構成は既に構築されていることを前提としています。

使用したサービス

技術 用途
AWS CDK (TypeScript) インフラのコード化
ECS Fargate コンテナ実行環境
ECR コンテナイメージの保管
ALB ロードバランサー
DynamoDB データベース
GitHub Actions CI/CD
OIDC AWS 認証

構成図

スクリーンショット 2026-02-09 0.37.18

CDK によるインフラ構築

ECR の作成

まず、Docker イメージの保存先となる ECR リポジトリを CDK で作成します。

デプロイ順序に関する注意点
ECS と ECR を同時に作成しようとすると、デプロイ時にエラーが発生する可能性があります。これは、ECS サービスが起動する際に ECR 上の Docker イメージを参照する必要があるためです。

この依存関係によるエラーを避けるため、以下の 4 ステップに分けて構築を進めます。

  1. ECR の作成: まずは箱(リポジトリ)だけを CDK で作成
  2. イメージの PUSH: 作成した ECR へ Docker イメージを手動でプッシュ
  3. ECS の作成: イメージが存在する状態で、CDK で ECS インフラをデプロイ
  4. CI/CD 運用: 以降の更新は GitHub Actions から ECS へ自動デプロイ
import * as cdk from "aws-cdk-lib";
import * as ecr from "aws-cdk-lib/aws-ecr";
import { Construct } from "constructs";

/**
 * ECR Construct
 * コンテナイメージを保存するECRリポジトリを作成する
 */
export class EcrConstruct extends Construct {
  public readonly backendRepository: ecr.Repository;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    const backendRepository = new ecr.Repository(this, "BackendRepository", {
      repositoryName: "ecs-backend",
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      emptyOnDelete: true,
      imageScanOnPush: true, // imageScanOnPush: プッシュ時に脆弱性スキャン実行
      /**
       * 古いイメージを自動削除(最新10件を保持)
       **/
      lifecycleRules: [
        {
          maxImageCount: 10,
          description: "Keep only 10 images",
        },
      ],
    });
    this.backendRepository = backendRepository;
  }
}

ECR のデプロイ

作成した ECR をデプロイします。

npx cdk deploy

この時点では、ECR リポジトリは空の状態です。

Dockerfile の作成

ECR へ登録するための、アプリケーション実行環境を定義した Dockerfile を作成します。

# ビルドステージ
# TypeScriptをコンパイルしてJavaScriptに変換
FROM node:22-alpine AS builder

WORKDIR /app

# 依存関係のインストール
# pnpm-lock.yaml があればロックファイルを使用
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN pnpm install --frozen-lockfile || pnpm install

# ソースコードをコピーしてビルド
COPY . .
RUN pnpm build

FROM node:22-alpine AS runner

WORKDIR /app

# 環境変数の設定
ENV NODE_ENV=production
ENV PORT=4000

# セキュリティ: 非 root ユーザーを作成
# root ユーザーでの実行を避けることでセキュリティリスクを軽減
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 expressjs

# 本番用依存関係のみをインストール
# devDependencies を除外してイメージサイズを削減
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN pnpm install --prod --frozen-lockfile || pnpm install --prod

# ビルドステージからコンパイル済みファイルをコピー
COPY --from=builder /app/dist ./dist

# 非 root ユーザーに切り替え
USER expressjs

# コンテナが待ち受けるポート
EXPOSE 4000

# アプリケーションの起動
CMD ["node", "dist/index.js"]

ポイント:

  1. マルチステージビルド - 最終イメージにビルド成果物のみを含む
  2. Alpine Linux - 軽量なベースイメージ
  3. 本番用依存関係のみ - --prod で devDependencies を除外

Docker イメージのビルド&ECR へプッシュ

ローカル環境で Docker イメージをビルドし、ECR にプッシュします。

Rancher Desktop を使用している場合:

# Docker コンテキストを確認
docker context ls

# Rancher Desktop に切り替え
docker context use rancher-desktop

# 動作確認
docker ps

ECR へのログイン

# 変数を設定
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
AWS_REGION=ap-northeast-1
ECR_REPOSITORY=ecs-backend

# ECR にログイン
aws ecr get-login-password --region $AWS_REGION | \
  docker login --username AWS --password-stdin \
  $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

Docker イメージのビルド&プッシュ

# Docker イメージをビルド(ARM64 用)
docker build -t $ECR_REPOSITORY:latest backend/

# ECR 用にタグ付け
docker tag $ECR_REPOSITORY:latest \
  $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:latest

# ECR にプッシュ
docker push \
  $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:latest

イメージの確認

# ECR にイメージがプッシュされたことを確認
aws ecr list-images --repository-name $ECR_REPOSITORY --region $AWS_REGION

出力例:

{
  "imageIds": [
    {
      "imageDigest": "sha256:e325bcbd...",
      "imageTag": "latest"
    }
  ]
}

ECS の作成

ECR にイメージがプッシュされたので、ECS クラスター、サービス、タスク定義をデプロイします。

/**
 * ECS Construct
 * ECSクラスター、タスク定義、Fargateサービスを作成する
 */
import * as cdk from "aws-cdk-lib";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as logs from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";
import { NetworkConstruct } from "../network";

export interface EcsConstructProps {
  readonly network: NetworkConstruct; // NetworkConstructのインスタンスを受け取る
  readonly backendRepository: ecr.IRepository; // コンテナイメージを取得する ECR リポジトリ
  readonly backendTargetGroup: elbv2.IApplicationTargetGroup; // ECSタスクを登録するALBターゲットグループ
}

export class EcsConstruct extends Construct {
  public readonly cluster: ecs.Cluster;
  public readonly backendService: ecs.FargateService;
  public readonly backendTaskDefinition: ecs.FargateTaskDefinition;

  constructor(scope: Construct, id: string, props: EcsConstructProps) {
    super(scope, id);

    const { network } = props;

    /**
     * ECSクラスター
     * コンテナを実行する論理グループ
     */
    const cluster = new ecs.Cluster(this, "Cluster", {
      vpc: network.vpc,
      clusterName: "ecs-cluster",
      containerInsights: true, // CloudWatch Container Insightsを有効化
    });
    this.cluster = cluster;

    /**
     * タスク定義
     * 実行ロール・タスクロールはCDKが自動生成
     */
    const backendTaskDefinition = new ecs.FargateTaskDefinition(
      this,
      "BackendTaskDef",
      {
        memoryLimitMiB: 512,
        cpu: 256,
        runtimePlatform: {
          cpuArchitecture: ecs.CpuArchitecture.ARM64,
          operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
        },
      }
    );
    this.backendTaskDefinition = backendTaskDefinition;

    /**
     * ロググループ: コンテナログを CloudWatch へ出力
     */
    const backendLogGroup = new logs.LogGroup(this, "BackendLogGroup", {
      logGroupName: "/ecs/backend",
      retention: logs.RetentionDays.ONE_WEEK,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    /**
     * コンテナ定義
     * ECR からイメージを取得
     */
    backendTaskDefinition.addContainer("BackendContainer", {
      containerName: "backend",
      image: ecs.ContainerImage.fromEcrRepository(
        props.backendRepository,
        "latest"
      ), // ECR リポジトリから latest タグのイメージを取得
      portMappings: [{ containerPort: 4000 }], // コンテナが待ち受けるポート
      logging: ecs.LogDrivers.awsLogs({
        //  コンテナのログを CloudWatch Logs へ出力
        streamPrefix: "backend",
        logGroup: backendLogGroup,
      }),
    });

    /**
     * Fargateサービス
     * ECS タスクを実行するサービス
     */
    const backendService = new ecs.FargateService(this, "BackendService", {
      cluster, // タスクを実行するECSクラスター
      taskDefinition: backendTaskDefinition, // 実行するタスク定義
      desiredCount: 2, // 起動するタスク数(2つで可用性を確保)
      serviceName: "backend-service", // サービス名
      securityGroups: [network.ecsSecurityGroup], // NetworkConstructから取得
      vpcSubnets: { subnetType: cdk.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS },
      healthCheckGracePeriod: cdk.Duration.seconds(60), // ヘルスチェック開始までの猶予時間
    });
    this.backendService = backendService;

    /**
     * ALB ターゲットグループに ECS サービスを登録
     */
    backendService.attachToApplicationTargetGroup(props.backendTargetGroup);
  }
}

ECS リソースのデプロイ

準備が整ったので、CDK を使用して ECS のインフラ環境をデプロイします。

npx cdk deploy

デプロイ結果の確認

デプロイ完了後、AWS 管理コンソールから各リソースが正しく作成されているか確認しましょう。

ECS クラスター
アプリケーションの実行基盤となるクラスターが作成されていることを確認します。
スクリーンショット 2026-02-08 22.46.18

ECS サービス
指定したタスク数が「実行中」になっており、ロードバランサーやネットワーク設定が適切に紐付いているかを確認します。
スクリーンショット 2026-02-08 22.47.24

ECS タスク定義
使用する Docker イメージのタグや、CPU・メモリの割り当て、環境変数が正しく設定されているかを確認します。
スクリーンショット 2026-02-08 22.48.40

GitHub Actions からのデプロイ

インフラの構築が完了したら、次はアプリケーションのデプロイ環境を整備します。GitHub Actions を利用して、コードの変更を自動的に ECS へ反映する仕組みを構築します。

ワークフローの定義

リポジトリの .github/workflows/deploy.yml に以下の設定を記述します。

# ECS Fargate へのデプロイワークフロー
# CDK でインフラをデプロイし、Docker イメージをビルドして ECS サービスをローリングデプロイする
name: Deploy to ECS

on:
  workflow_dispatch: # 手動実行のみ

# OIDC 認証に必要な権限設定
permissions:
  id-token: write # OIDC トークンの発行を許可
  contents: read # リポジトリの読み取りを許可

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: dev # GitHub Environment(環境変数を管理)

    steps:
      # ソースコードをチェックアウト
      - name: Checkout
        uses: actions/checkout@v6

      # Node.js のセットアップ
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22.x"

      # pnpm のセットアップ
      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      # 依存関係のインストール
      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      # TypeScript をビルド
      - name: Build CDK
        run: pnpm build

      # OIDC 認証で AWS にログイン
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      # CDK でインフラをデプロイ
      - name: CDK Deploy
        run: npx cdk deploy --require-approval never

      # ECR へのログイン
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # Docker Buildx のセットアップ
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      # Docker イメージをビルドして ECR にプッシュ
      # ARM64 アーキテクチャでビルド(Graviton 対応)
      - name: Build and push Backend image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker buildx build \
            --platform linux/arm64 \
            -t $ECR_REGISTRY/${{ vars.ECR_REPOSITORY_BACKEND }}:$IMAGE_TAG \
            -t $ECR_REGISTRY/${{ vars.ECR_REPOSITORY_BACKEND }}:latest \
            --push \
            ./backend

      # ECS サービスを更新してローリングデプロイを開始
      # --force-new-deployment: 同じイメージでも新しいタスクを起動
      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster ${{ vars.ECS_CLUSTER_NAME }} \
            --service ${{ vars.ECS_SERVICE_NAME }} \
            --force-new-deployment

      # デプロイ完了まで待機(新しいタスクが正常に起動するまで)
      - name: Wait for service to stabilize
        run: |
          aws ecs wait services-stable \
            --cluster ${{ vars.ECS_CLUSTER_NAME }} \
            --services ${{ vars.ECS_SERVICE_NAME }}

使用している主要なアクション
今回のワークフローで核となる、AWS と Docker 関連のアクションの詳細はこちらから確認できます。

アクション名 / 公式リンク 役割とポイント
Amazon ECR Login AWS ECR へのログインを自動化します。ログイン後に取得できる registry の URL は後続のビルドステップで利用します。
Setup Docker Buildx 高機能なビルド環境「Buildx」をセットアップします。今回のように ARM64(Graviton)向けのイメージをビルドする際に使用します。

https://github.com/aws-actions/amazon-ecr-login
https://github.com/docker/setup-buildx-action

実装のポイント
確実な成功判定 (wait services-stable)
AWS への更新命令は一瞬で終わりますが、実際にはその後にコンテナの入れ替えが行われます。このステップを入れることで、「新しいコンテナが無事に起動し、サービスが安定したこと」を GitHub Actions 上で最後まで見届けることができます。

ワークフローの全体像

GitHub Actions が実行する一連のプロセスは以下の通りです。

┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Checkout   │───▶│ OIDC認証    │───▶│ CDKデプロイ  │───▶│ ECRログイン  │
└─────────────┘    └─────────────┘    └─────────────┘    └──────┬──────┘
                                                                 │
       ┌─────────────────────────────────────────────────────────┘
       ▼
┌─────────────┐     ┌─────────────┐   ┌─────────────┐
│ イメージ      │───▶│ ECSデプロイ   │───▶│ 安定待機    │
│ ビルド&プッシュ│    │           │    │             │
└─────────────┘     └─────────────┘   └─────────────┘

ポイント:

  • GitHub Environment Variables で設定値を管理
  • --force-new-deployment で最新のイメージを使って新しいタスクを起動
  • aws ecs wait services-stable でデプロイ完了を待機する

GitHub の設定

ワークフロー内で利用する GitHub Environments を設定します。

まず、開発環境(dev)用の管理グループを作成します。これにより、環境ごとの変数管理を設定できるようになります。

リポジトリの Settings → Environments を開きます。
New environment をクリックし、名前を dev と入力して作成します。
スクリーンショット 2026-02-08 22.53.51

Environment Variables の設定

環境変数の登録
作成した dev 環境に対して、ワークフロー内で使用する以下の変数を登録します。

変数名 役割・設定内容
ECR_REPOSITORY_BACKEND イメージの保存先: Dockerイメージをプッシュする ECR リポジトリの名前を指定します。
ECS_CLUSTER_NAME デプロイ先の基盤: タスクを実行する ECS クラスターの名前を指定します。
ECS_SERVICE_NAME 更新対象のサービス: 実際にコンテナを入れ替える ECS サービスの名前を指定します。

スクリーンショット 2026-02-09 0.43.04

ワークフローの実行

設定がすべて完了したら、いよいよ手動でデプロイを実行してみましょう。Run workflow ボタンをクリックし、ブランチ(main)を確認して実行します。
スクリーンショット 2026-02-08 23.04.54

実行されたワークフローをクリックし、様子を見守ります。
スクリーンショット 2026-02-08 23.43.06

デプロイが無事完了しました。
スクリーンショット 2026-02-08 23.50.06

これで GitHub Actions からアプリケーションをデプロイできることを確認できました。

動作確認

デプロイが完了したら、実際にインフラの設定が更新されたかを確認しましょう。今回は「タスク定義のリビジョン更新」に注目します。

  1. ECS タスク定義のリビジョン更新
    ECS では、設定変更(メモリサイズの変更やイメージの更新など)を行うたびに、「リビジョン」と呼ばれるバージョン番号が自動的にカウントアップされます。

現在のタスク定義のリビジョンは 12 です。(メモリは 512 MiB)
スクリーンショット 2026-02-09 0.10.42

  1. 設定の変更と再デプロイ
    動作確認のため、CDK のコードでタスク定義のメモリサイズを以下のように変更してみます。
/**
 * タスク定義
 * 実行ロール・タスクロールはCDKが自動生成
 */
const backendTaskDefinition = new ecs.FargateTaskDefinition(
  this,
  "BackendTaskDef",
  {
    memoryLimitMiB: 1024, // 512 から 1024 に変更してみる
    cpu: 256,
    runtimePlatform: {
      cpuArchitecture: ecs.CpuArchitecture.ARM64,
      operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
    },
  }
);
this.backendTaskDefinition = backendTaskDefinition;

この状態で GitHub Actions を実行すると、ECS サービスは新しい設定を反映するために「リビジョン 13」を作成し、古いタスクを新しいタスクへと入れ替えます。

  1. デプロイの実行
    設定変更を終えたら、GitHub Actions からデプロイを開始します。ログから ECS が更新されていることがわかりますね。
    スクリーンショット 2026-02-09 0.17.05

  2. 反映の確認
    デプロイ完了後、AWS 管理コンソールで以下の 3 点が更新されていれば成功です!

  • リビジョン番号: 12 から 13 へ更新されている
  • メモリ設定: 変更後の値(1024 MiB)が反映されている
  • デプロイステータス: 新しいリビジョンのタスクが「ACTIVE」になっている

スクリーンショット 2026-02-09 0.47.42
意図した通り、GitHub Actions 経由でタスク定義の更新が行われていることが確認できました。

まとめ

今回は GitHub Actions と AWS CDK、そして ECS Fargate を組み合わせた CI/CD パイプラインを構築してみました。

「たまにはコンテナを」という軽い気持ちで始めた構築でしたが、実際に自動でデプロイが完了し、ECS 上でサービスが動き出す瞬間はやはり格別ですね。

コンテナ運用の第一歩として、この記事が皆さんの参考になれば幸いです!

この記事をシェアする

FacebookHatena blogX

関連記事