GitHub Actions から ECS Fargate をデプロイしてみた
製造ビジネステクノロジー部の小林です。
最近は 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 認証 |
構成図

CDK によるインフラ構築
ECR の作成
まず、Docker イメージの保存先となる ECR リポジトリを CDK で作成します。
デプロイ順序に関する注意点
ECS と ECR を同時に作成しようとすると、デプロイ時にエラーが発生する可能性があります。これは、ECS サービスが起動する際に ECR 上の Docker イメージを参照する必要があるためです。
この依存関係によるエラーを避けるため、以下の 4 ステップに分けて構築を進めます。
- ECR の作成: まずは箱(リポジトリ)だけを CDK で作成
- イメージの PUSH: 作成した ECR へ Docker イメージを手動でプッシュ
- ECS の作成: イメージが存在する状態で、CDK で ECS インフラをデプロイ
- 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 /app/dist ./dist
# 非 root ユーザーに切り替え
USER expressjs
# コンテナが待ち受けるポート
EXPOSE 4000
# アプリケーションの起動
CMD ["node", "dist/index.js"]
ポイント:
- マルチステージビルド - 最終イメージにビルド成果物のみを含む
- Alpine Linux - 軽量なベースイメージ
- 本番用依存関係のみ -
--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 クラスター
アプリケーションの実行基盤となるクラスターが作成されていることを確認します。

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

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

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)向けのイメージをビルドする際に使用します。 |
実装のポイント
確実な成功判定 (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 と入力して作成します。

Environment Variables の設定
環境変数の登録
作成した dev 環境に対して、ワークフロー内で使用する以下の変数を登録します。
| 変数名 | 役割・設定内容 |
|---|---|
| ECR_REPOSITORY_BACKEND | イメージの保存先: Dockerイメージをプッシュする ECR リポジトリの名前を指定します。 |
| ECS_CLUSTER_NAME | デプロイ先の基盤: タスクを実行する ECS クラスターの名前を指定します。 |
| ECS_SERVICE_NAME | 更新対象のサービス: 実際にコンテナを入れ替える ECS サービスの名前を指定します。 |

ワークフローの実行
設定がすべて完了したら、いよいよ手動でデプロイを実行してみましょう。Run workflow ボタンをクリックし、ブランチ(main)を確認して実行します。

実行されたワークフローをクリックし、様子を見守ります。

デプロイが無事完了しました。

これで GitHub Actions からアプリケーションをデプロイできることを確認できました。
動作確認
デプロイが完了したら、実際にインフラの設定が更新されたかを確認しましょう。今回は「タスク定義のリビジョン更新」に注目します。
- ECS タスク定義のリビジョン更新
ECS では、設定変更(メモリサイズの変更やイメージの更新など)を行うたびに、「リビジョン」と呼ばれるバージョン番号が自動的にカウントアップされます。
現在のタスク定義のリビジョンは 12 です。(メモリは 512 MiB)

- 設定の変更と再デプロイ
動作確認のため、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」を作成し、古いタスクを新しいタスクへと入れ替えます。
-
デプロイの実行
設定変更を終えたら、GitHub Actions からデプロイを開始します。ログから ECS が更新されていることがわかりますね。

-
反映の確認
デプロイ完了後、AWS 管理コンソールで以下の 3 点が更新されていれば成功です!
- リビジョン番号: 12 から 13 へ更新されている
- メモリ設定: 変更後の値(1024 MiB)が反映されている
- デプロイステータス: 新しいリビジョンのタスクが「ACTIVE」になっている

意図した通り、GitHub Actions 経由でタスク定義の更新が行われていることが確認できました。
まとめ
今回は GitHub Actions と AWS CDK、そして ECS Fargate を組み合わせた CI/CD パイプラインを構築してみました。
「たまにはコンテナを」という軽い気持ちで始めた構築でしたが、実際に自動でデプロイが完了し、ECS 上でサービスが動き出す瞬間はやはり格別ですね。
コンテナ運用の第一歩として、この記事が皆さんの参考になれば幸いです!






