BLOG
エンジニアブログ

【第5回】ALB+ECR+ECS Fargateを作って覚えるAWS ECSの基礎

shibuya
shibuya
【第5回】ALB+ECR+ECS Fargateを作って覚えるAWS ECSの基礎

はじめに

想定している読者は次のような方です。

  • VPCやEC2等の設定、構築をした事がある方。
  • コンテナは触ったことがあるが、ECS はこれからという方。
  • ECS Fargateを使用して、ECSの基礎知識を確認しておきたい方。
  • 昔ECSを使ったが久しぶりのため、改めて作業方法の再確認をしたい方、復習をして知識を維持したい方。

前回について

ALB + ECS Fargate + ECR の基本構成から、CodePipeline、ECS ネイティブ Blue/Green、GitHub Actions までを段階的に作りました。ただし連載では学習のしやすさを優先し、タスクをパブリックサブネットに置いてパブリック IP を付ける構成にしていました。

応用編の今回は、本番でそのまま使える形に近づけます。

  • タスクはプライベートサブネットに置き、パブリックIPも NAT ゲートウェイも使わない
  • ECRなどのAWS サービスにはVPC エンドポイント経由でアクセスする
  • RDS for PostgreSQLに接続し、アプリはパスワードを一切持たない(IAMデータベース認証)
  • GitHub Actionsでビルドし、コミットIDを変数としてCodePipelineに渡してデプロイする
  • ECRのタグはイミュータブル(上書き不可)にする
  • ECSネイティブBlue/Green で、テストリスナーで確認 → 人が承認 → ベイク時間中も人が監視

この記事だけで完結するよう、VPCからゼロで作ります。ECSやBlue/Greenの基本的な説明についてはこれまでの各回やAWSマニュアルをご参照ください。

第何回 テーマ ゴール
第1回 コンテナと ECS の基礎、手動構築 ブラウザに「Hello from Amazon ECS」 が表示される
第2回 手動リリースと CodePipeline による自動リリース ECR への push でローリングアップデート
第3回 ECS ネイティブ Blue/Green デプロイ テストリスナーで確認してから本番切り替え
第4回 GitHub Actions で ECR への push を自動化

“git push”だけでリリースが完走する

第5回 プライベートサブネットを使用した構成

プライベートサブネットとエンドポイントを利用した環境構築の検証をする

本記事について

  • 本記事ではALBやFargateなどの有料サービスを使用するため、費用が発生します。ALBとFargateは起動している時間に応じた課金のため、数時間で完了すれば数USドル程度ですみますが、削除し忘れて1か月放置すると40USドル程度になります。検証後は必ずリソースの削除をお願いします(手順は記事末尾に記載しています)。
  • AWSマネジメントコンソールの画面構成は、随時更新されます。本記事は2026年9月時点の画面をもとにしています。項目名や配置が異なる場合は、適宜読み替えてください。
  • 掲載している手順は、実際にAWS上で検証した内容です。ただし、学習しやすさを優先してセキュリティ設定を簡略化している箇所があります(タスクをパブリックサブネットに配置する、セキュリティグループで広い範囲からのアクセスを許可する、など)。そのため、検証用のAWSアカウントでの実施を推奨します。
  • 本記事では、GitHubとAWSを連携させる設定(OIDCのIDプロバイダー、GitHubアプリの連携など)を行います。これらは他のプロジェクトと共有される設定のため、検証後に削除する際はご注意ください。同じアカウントやGitHub組織で他にも利用しているものがあると、そちらの連携まで止まってしまう可能性があります。削除対象かどうか分からない場合は、無理に消さずに残しておくほうが安全です(残しておいても費用は発生しません)。
  • 共用アカウントや業務で使用しているアカウントで実施する場合は、組織のルールを確認したうえで設定内容を見直してから、本記事を参考にしたり、手順の実施をしてください。

今回の構成

 

前提と準備

環境

項目 設定値
リージョン ap-northeast-1
起動タイプ AWS Fargate
作業方法 AWS マネジメントコンソール + GitHub の Web 画面
所要時間 約3〜4時間

検証用のAWS アカウントと、GitHubアカウント(リポジトリを1つ作成します)を用意してください。

本記事の構成料金目安

以下は2026年9月時点の東京リージョンでのおおよその目安です。正確な金額は必ず各サービスの公式料金ページで確認してください。

リソース 概算
Interface エンドポイント 4 種 × 2 AZ 約 0.11 USD / 時間(1 本 1 AZ あたり約 0.014 USD / 時間)
Application Load Balancer 約 0.025 USD / 時間 + LCU 従量
RDS for PostgreSQL db.t4g.micro(シングル AZ) 約 0.03 USD / 時間 + ストレージ
Fargate 0.25 vCPU / 0.5 GB × 2 約 0.025 USD / 時間
Secrets Manager 1シークレットあたり月 0.40 USD(日割り)
S3 Gateway エンドポイント 無料

合計すると1時間あたり0.2USD前後、1日放置すると5USD前後になります。本構成を試す場合、途中で数日空けるのはおすすめしません。1日で最後まで進めて、その日のうちに後片付け(後半にあるクリーンアップの章)まで実施していただきますようお願いいたします。

1.VPCを作る

1-1. VPC ウィザードで作成する

VPC コンソール →「VPCを作成」。以下の内容で設定します。

項目 設定値
作成するリソース VPCなど
名前タグの自動生成 ecs-private
IPv4 CIDR ブロック 10.0.0.0/16
アベイラビリティーゾーン (AZ) の数 2
パブリックサブネットの数 2
プライベートサブネットの数 2
NAT ゲートウェイ なし
VPC エンドポイント S3ゲートウェイ
DNS ホスト名を有効化 チェックをつける
DNS 解決を有効化 チェックをつける

2.セキュリティグループを作る

今回の構成概要です。

 

EC2 コンソール →「セキュリティグループ」→「セキュリティグループを作成」で、次の4つを上から順に作ります(後ろの SGが前のSGを参照するため)。VPCはすべて事前に作成したVPC(今回の名称は、ecs-private-vpc)を選びます。アウトバウンドルールはデフォルト(すべて許可)のままで構いません。

2-1.ALB用 

セキュリティグループ名:ecs-private-sg-alb

タイプ ポート ソース 説明
HTTP 80 0.0.0.0/0 Listener for production environment
カスタムTCP 8080

マイIP
(自分もしくはオフィスのIPを設定します)

Listener for test environment

2-2.ECSタスク用 

セキュリティグループ名:ecs-private-sg-app

タイプ ポート ソース 説明
カスタムTCP 8000 ecs-private-sg-alb from ALB

8000 と 8080 を取り違えないように:8000 はアプリ(gunicorn)が待ち受けるポートで、ALB → タスクの通信に使います。8080 は ALB のテストリスナーのポートで、インターネット → ALB の通信に使います。ここを 8080 にしてしまうと、ALB のヘルスチェックが失敗します。

2-3.RDS用 

セキュリティグループ名:ecs-private-sg-db

タイプ ポート ソース 説明
PostgreSQL 5432 ecs-private-sg-app from ECS task

2-4.VPCエンドポイント用 

セキュリティグループ名:ecs-private-sg-vpce

タイプ ポート ソース 説明
HTTPS 443 ecs-private-sg-app from ECS task

3.VPCエンドポイントを作る

3-1.作成エンドポイント

エンドポイント 種類 使用用途
com.amazonaws.ap-northeast-1.ecr.api Interface ECR の認証、イメージ情報の取得
com.amazonaws.ap-northeast-1.ecr.dkr Interface イメージの pull(Docker Registry API)
com.amazonaws.ap-northeast-1.s3 Gateway

イメージのレイヤー本体の取得(ECRの実体は S3)

com.amazonaws.ap-northeast-1.logs Interface CloudWatch Logsへのログ送信
com.amazonaws.ap-northeast-1.secretsmanager Interface タスク起動時のシークレット取得

ECR のイメージは、メタデータは ECR、レイヤーの実体は S3 に置かれています。ecr.apiとecr.dkrだけ作ってS3を忘れると、認証は通るのにレイヤーのダウンロードでタイムアウトしCannotPullContainerErrorになります。

今回は、RDSのIAM 認証用のエンドポイントは不要です。アプリが RDS に接続するときに使う認証トークンは、SDKが手元で署名計算をして作るもので、AWSのAPIは呼びません。また、タスクが IAM ロールの認証情報を受け取る仕組み(タスクメタデータエンドポイント)も VPC の外には出ません。

3-2.Interfaceエンドポイントを作る

VPC コンソール →「エンドポイント」→「エンドポイントを作成」

以下の内容で設定します。

項目 設定値
名前タグ ecs-private-vpce-ecr-api
タイプ AWS のサービス
サービス com.amazonaws.ap-northeast-1.ecr.api
VPC ecs-private-vpc
追加設定 →  プライベートDNS名を有効化

チェックがついていること(デフォルト設定)

サブネット

2つのAZで、それぞれプライベートサブネット(ecs-private-subnet-private1-…/ecs-private-subnet-private2-…)を選択

セキュリティグループ ecs-private-sg-vpce(defaultは使用しないため、チェックは外します。)
ポリシー フルアクセス

同様に、次の3つも名前タグとサービス名以外は同じ設定で作成します。

名前タグ サービス
ecs-private-vpce-ecr-dkr com.amazonaws.ap-northeast-1.ecr.dkr
ecs-private-vpce-logs com.amazonaws.ap-northeast-1.logs
ecs-private-vpce-secretsmanager com.amazonaws.ap-northeast-1.secretsmanager

合計4つ作成し、ステータスが使用可能になることを確認してください。

4.RDS for PostgreSQLを作る

4-1. DB サブネットグループを作る

RDS コンソール →「サブネットグループ」→「DB サブネットグループを作成」

項目 設定値
名前 ecs-private-db-subnet
説明 private subnets for ecs-private
VPC ecs-private-vpc
アベイラビリティゾーン ap-northeast-1a, ap-northeast-1c
サブネット

2 つのプライベートサブネット(CIDRが10.0.128.0/20と10.0.144.0/20)

4-2.DBインスタンスを作る

RDS コンソール → データベースの作成 → メニューからフル設定をクリック

項目 設定値
エンジンのタイプ PostgreSQL
データベース作成方法を選択 フル設定
テンプレート 開発/テスト

項目 設定値
可用性と耐久性 シングル AZ DB インスタンスのデプロイ

項目 設定値
マルチAZ DBクラスターをサポートするバージョンのみを表示 OFF
エンジンバージョン PostgreSQL 18.3-R2 (作業時の最新バージョンを選択してください)
RDS延長サポートの有効化 チェックなし
DB インスタンス識別子 ecs-private-db
マスターユーザー名 postgres(デフォルト)
認証情報管理 AWS Secrets Manager で管理
暗号化キー aws/secretsmanager(デフォルト)
データベース認証 パスワードと IAM データベース認証

項目 設定値
インスタンスの設定 バースト可能クラス → db.t4g.micro
ストレージタイプ 汎用 SSD (gp3)
ストレージ割り当て 20 GiB
ストレージの自動スケーリング ストレージの自動スケーリングを有効にするチェックを外す

項目 設定値
コンピューティングリソース EC2 コンピューティングリソースに接続しない
VPC ecs-private-vpc
DB サブネットグループ ecs-private-db-subnet
パブリックアクセス なし
VPC セキュリティグループ 既存の選択→ecs-private-sg-db(default は外す)
データベース認証 パスワードと IAM データベース認証
モニタリング Database Insights – 標準 (今回はその他モニタリング設定等はしません)
追加設定 → 最初のデータベース名 appdb
追加設定 → バックアップ保持期間 1日
追加設定 → 削除保護 削除保護の有効化チェックを外す

4-3.設定の確認

設定内容 設定方針
パブリックアクセスなし + ecs-private-sg-db ECSタスク以外からは、ネットワーク的に到達できない。
Secrets Managerで管理 マスターパスワードを人間が決めず、誰の手元にも残さない。自動ローテーションによるパスワード更新。
IAMデータベース認証 アプリ用ユーザーはパスワードを持たず、IAM ロールで認証する。

4-4.作成後設定値を控える

作成が完了したら、ecs-private-dbを開いて次の値をメモします。

値 設定箇所 例
エンドポイント

接続とセキュリティタブ

ecs-private-db.xxxxxxxx.ap-northeast-1.rds.amazonaws.com
リソース ID 設定タブ db-ABCDEFGHIJKLMNOPQRSTUVWXYZ
マスター認証情報の ARN 設定タブ→マスター認証情報 arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:rds!db-xxxx-AbCdEf

リソース IDは、インスタンス識別子(ecs-private-db)とは違います。後ほど、IAMポリシーの設定で使用します。

5.ECRとアプリケーション、GitHub Actions

5-1.ECRリポジトリ

ECR コンソール →「リポジトリを作成」

項目 設定値
リポジトリ名 ecs-private-app
イメージタグの変更可能性 Immutable(イミュータブル)
暗号化 AES-256(デフォルト)

5-2.アプリケーション

今回使用するアプリは、Python(Flask)で書いた小さな Web アプリです。アクセスのたびに DB に 1 行記録し、次の情報を表示します。

  • アプリのバージョン(コミット ID)と、応答したタスク
  • PostgreSQL のバージョン
  • 接続しているユーザーと、接続が TLS かどうか
  • これまでのアクセス回数

GitHub で新しいリポジトリを名称ecs-private-handsonで作ります。設定は、PrivateでREAD MEを追加しておきます。

「Add file」→「Create new file」または、VisualStudioCodeで次の4ファイルを作ります。(gitの操作は記載しませんので、マニュアル等をご参照ください。)

  • Python
    • app.py
    • init_db.py
  • txt
    • requirements.txt
  • Dockerfile
    • Dockerfile

 

app.py


import os
import socket

import boto3
import psycopg
from flask import Flask

# ▼ リリースのたびにここを書き換えます(11 章)
APP_MESSAGE = "v1: Hello from private subnet!"
THEME_COLOR = "#e8f0fe"

DB_HOST = os.environ["DB_HOST"]
DB_PORT = int(os.environ.get("DB_PORT", "5432"))
DB_NAME = os.environ.get("DB_NAME", "appdb")
DB_USER = os.environ.get("DB_USER", "app_user")
REGION = os.environ.get("AWS_REGION", "ap-northeast-1")
APP_VERSION = os.environ.get("APP_VERSION", "local")
CA_BUNDLE = "/app/global-bundle.pem"

app = Flask(__name__)
rds = boto3.client("rds", region_name=REGION)


def connect():
    # IAM 認証トークンを生成する(手元での署名計算のみ。AWS の API は呼ばない)
    token = rds.generate_db_auth_token(
        DBHostname=DB_HOST, Port=DB_PORT, DBUsername=DB_USER, Region=REGION
    )
    # トークンをパスワードとして使い、TLS + サーバー証明書検証ありで接続する
    return psycopg.connect(
        host=DB_HOST,
        port=DB_PORT,
        dbname=DB_NAME,
        user=DB_USER,
        password=token,
        sslmode="verify-full",
        sslrootcert=CA_BUNDLE,
        connect_timeout=5,
    )


@app.route("/health")
def health():
    # ALB のヘルスチェック用。DB には触らない(理由は 12-3 のコラム)
    return "ok", 200


@app.route("/")
def index():
    with connect() as conn, conn.cursor() as cur:
        cur.execute(
            "INSERT INTO access_log (app_version) VALUES (%s)", (APP_VERSION,)
        )
        cur.execute("SELECT count(*) FROM access_log")
        count = cur.fetchone()[0]
        cur.execute("SELECT version()")
        pg_version = cur.fetchone()[0].split(" on ")[0]
        cur.execute(
            "SELECT current_user, ssl, version FROM pg_stat_ssl "
            "WHERE pid = pg_backend_pid()"
        )
        user, ssl, tls_version = cur.fetchone()

    return f"""<!doctype html>
<html><head><meta charset="utf-8"><title>ecs-private-handson</title></head>
<body style="font-family:sans-serif;background:{THEME_COLOR};padding:2rem">
  <h1>{APP_MESSAGE}</h1>
  <table border="1" cellpadding="8" style="border-collapse:collapse;background:#fff">
    <tr><th align="left">アプリのバージョン</th><td>{APP_VERSION}</td></tr>
    <tr><th align="left">応答したタスク</th><td>{socket.gethostname()}</td></tr>
    <tr><th align="left">DB</th><td>{pg_version}</td></tr>
    <tr><th align="left">接続ユーザー</th><td>{user}</td></tr>
    <tr><th align="left">TLS</th><td>{ssl} ({tls_version})</td></tr>
    <tr><th align="left">アクセス回数</th><td>{count}</td></tr>
  </table>
</body></html>"""


 

init_db.py


"""マスターユーザーで接続し、アプリ用ユーザーとテーブルを用意する。
何度実行しても同じ結果になる(冪等)ように書いている。"""
import os

import psycopg

conn = psycopg.connect(
    host=os.environ["DB_HOST"],
    port=int(os.environ.get("DB_PORT", "5432")),
    dbname=os.environ.get("DB_NAME", "appdb"),
    user=os.environ["DB_MASTER_USER"],
    password=os.environ["DB_MASTER_PASSWORD"],
    sslmode="verify-full",
    sslrootcert="/app/global-bundle.pem",
    connect_timeout=5,
    autocommit=True,
)

with conn, conn.cursor() as cur:
    # アプリ用ユーザー(パスワードなし)を作り、IAM 認証を許可する
    cur.execute("""
        DO $$
        BEGIN
          IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN
            CREATE ROLE app_user LOGIN;
          END IF;
        END $$;
    """)
    cur.execute("GRANT rds_iam TO app_user")

    # アプリが使うテーブル
    cur.execute("""
        CREATE TABLE IF NOT EXISTS access_log (
          id          bigserial PRIMARY KEY,
          accessed_at timestamptz NOT NULL DEFAULT now(),
          app_version text NOT NULL
        )
    """)

    # アプリ用ユーザーには必要最小限の権限だけを渡す
    cur.execute("GRANT SELECT, INSERT ON access_log TO app_user")
    cur.execute("GRANT USAGE ON SEQUENCE access_log_id_seq TO app_user")

print("init_db: done")

 

requirements.txt


flask==3.1.*
gunicorn==23.*
psycopg[binary]==3.2.*
boto3==1.*

 

Dockerfile


FROM public.ecr.aws/docker/library/python:3.12-slim

WORKDIR /app

# RDS のサーバー証明書を検証するための CA 証明書(全リージョン共通バンドル)
ADD https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem /app/global-bundle.pem
RUN chmod 644 /app/global-bundle.pem

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py init_db.py ./

ARG GIT_SHA=local
ENV APP_VERSION=${GIT_SHA}

# root 以外のユーザーで実行する
RUN useradd --system --uid 10001 app
USER app

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "app:app"]

  • CA 証明書をイメージに入れる理由

    • sslmode=”verify-full”は、「接続先が本物のRDSであること」を証明書で確かめる設定です。これがないと、TLSで暗号化はされていても、なりすましたサーバーに接続してしまう可能性を排除できません。プライベートサブネットのタスクは実行時にインターネットから証明書を取ってこられないので、ビルド時に(インターネットに出られるGitHub Actions上で)取り込んでおきます。

  • ベースイメージをpublic.ecr.awsから取る理由
    • Docker Hubには未認証のpull回数制限があります。ECR Public GalleryにあるDocker公式イメージのミラーを使い、さらにワークフローでECR Publicにログインしてからpullすることで、制限にかかりにくくしています。

5-3. GitHub Actions用のOIDCとIAMロール

IDプロバイダーを作る(既に存在する場合は、スキップしてください)

IAM コンソール → ID プロバイダ → プロバイダを追加

項目 設定値
プロバイダのタイプ OpenID Connect
プロバイダのURL https://token.actions.githubusercontent.com
対象者 sts.amazonaws.com

ロールの作成

IAM コンソール →「ロール」→「ロールを作成」→「カスタム信頼ポリシー」で、次を貼り付けます。ACCOUNT_IDとGITHUB_OWNERは自身の値に置き換えてください。

  • ロール名ecs-private-github-actions

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:GITHUB_OWNER/ecs-private-handson:ref:refs/heads/main"
        }
      }
    }
  ]
}

許可ポリシーは付けずに次へ進み、ロール名ecs-private-github-actionsで作成します。

作成後、ロールを開いて「許可を追加」→「インラインポリシーを作成」→ JSON で次を貼り付けecs-private-github-actions-policyという名前で保存します。

  • ecs-private-github-actions-policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EcrLogin",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Sid": "EcrPush",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage",
        "ecr:DescribeImages"
      ],
      "Resource": "arn:aws:ecr:ap-northeast-1:ACCOUNT_ID:repository/ecs-private-app"
    },
    {
      "Sid": "EcrPublicLogin",
      "Effect": "Allow",
      "Action": [
        "ecr-public:GetAuthorizationToken",
        "sts:GetServiceBearerToken"
      ],
      "Resource": "*"
    },
    {
      "Sid": "StartPipeline",
      "Effect": "Allow",
      "Action": "codepipeline:StartPipelineExecution",
      "Resource": "arn:aws:codepipeline:ap-northeast-1:ACCOUNT_ID:ecs-private-pipeline"
    }
  ]
}

パイプラインはまだ作っていませんが、ARNは名前から決まるので先に書いておけます。

5-4.ワークフローを作る

GitHubのリポジトリ →「Settings」→「Secrets and variables」→「Actions」→「Variables」タブで、変数を1つ追加します。

Name Value
AWS_ROLE_ARN arn:aws:iam::ACCOUNT_ID:role/ecs-private-github-actions

PIPELINE_NAMEは、パイプラインを作った後に追加します。

Repository variablesに登録したところの画面

GitHubのリポジトリに.github/workflows/build-and-deploy.ymlを作ります。

  • .github/workflows/build-and-deploy.yml
    • ワークフローで使用しているactions(actions/checkout@v4、aws-actions/amazon-ecr-login@v2)は、最新のバージョンを使用するようにお願いいたします。

name: build-and-deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  id-token: write   # OIDC トークンの取得に必要
  contents: read

env:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: ecs-private-app

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Login to Amazon ECR Public
        uses: aws-actions/amazon-ecr-login@v2
        with:
          registry-type: public
        env:
          AWS_REGION: us-east-1
          AWS_DEFAULT_REGION: us-east-1

      - name: Build and push image
        id: push
        env:
          REGISTRY: ${{ steps.ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          IMAGE=${REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}

          docker build --build-arg GIT_SHA=${IMAGE_TAG::7} -t ${IMAGE} .
          docker push ${IMAGE}

          # push したイメージのダイジェストを取得
          DIGEST=$(aws ecr describe-images \
            --repository-name ${ECR_REPOSITORY} \
            --image-ids imageTag=${IMAGE_TAG} \
            --query 'imageDetails[0].imageDigest' --output text)

          echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
          echo "### Pushed image" >> "$GITHUB_STEP_SUMMARY"
          echo "- tag: \`${IMAGE_TAG}\`" >> "$GITHUB_STEP_SUMMARY"
          echo "- digest: \`${DIGEST}\`" >> "$GITHUB_STEP_SUMMARY"

      - name: Start CodePipeline
        if: vars.PIPELINE_NAME != ''
        run: |
          aws codepipeline start-pipeline-execution \
            --name ${{ vars.PIPELINE_NAME }} \
            --variables name=IMAGE_TAG,value=${{ github.sha }} \
            --source-revisions actionName=Source,revisionType=IMAGE_DIGEST,revisionValue=${{ steps.push.outputs.digest }}


コミットすると(mainへのpushで)ワークフローが起動します。「Actions」タブで緑になり、ECR コンソールのecs-private-appに40桁のコミットIDのタグが付いたイメージが 1 つ入っていれば成功です。

このタグ(フルのコミット ID)をメモしておきます。ECSのタスク定義で使用します。

CodePipelineで、最後のステップが「スキップ」になっているのは正常です。 パイプラインを作るまでは起動のしようがないため、変数の有無で実行を切り替えています。

6.ECS用のIAMロールを作る

6-1. 「タスク実行ロール」と「タスクロール」

ECSのタスクには、性格の違う2種類のロールがあります。

タスク実行ロール タスクロール
使用する ECS(Fargate)自身 コンテナ内のアプリ
使用タイミング タスクの起動時(コンテナが動く前) コンテナが動いている間
権限

ECRからのpull、ログ送信、シークレットの取得

アプリが呼ぶAWS サービス(RDSへのIAM認証など)

今回は次の 4 つのロールを作ります

ロール 種類 誰が使うか 割り当てたい権限
ecs-private-execution-role タスク実行ロール アプリのタスク ECR pull、ログ
ecs-private-init-execution-role タスク実行ロール 初期化タスク

ECR pull、ログ、マスターパスワードの取得

ecs-private-app-task-role タスクロール アプリのコンテナ rds-db:connect(app_userのみ)
ecsInfrastructureRoleForLoadBalancers ECSのインフラストラクチャロール ECS(Blud/Green) ALB のルール書き換え

アプリのタスクはマスターパスワードを取得できないようにすることで、秘密情報が漏れないようにしています。マスターパスワードを読めるのは、初期化タスクの実行ロールだけです。

6-2.ecs-private-execution-role

IAM コンソール →「ロール」→「ロールを作成」

項目 設定値
信頼されたエンティティタイプ AWSのサービス
サービスまたはユースケース

Elastic Container Service → Elastic Container Service Task

許可ポリシー AmazonECSTaskExecutionRolePolicy
ロール名 ecs-private-execution-role

ユースケースの選び直しに注意:「Elastic Container Service」を選ぶと、ユースケースは一番上の「Elastic Container Service」が最初から選択された状態になります。これは ECS サービス自身(ecs.amazonaws.com)用のもので、タスク用ではありません。下にある「Elastic Container Service Task」(ecs-tasks.amazonaws.com)を選び直してください。作成後、ロールの「信頼関係」タブでecs-tasks.amazonaws.comになっていれば正しく作成できています。

作成後、ロールを開いて「許可を追加」→「インラインポリシーを作成」→ JSON で次を追加します。ポリシー名は、create-log-groupで作成します。
ACCOUNT_IDは自身のAWSアカウントIDを入力します。

  • インラインポリシー:create-log-group

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "logs:CreateLogGroup",
      "Resource": "arn:aws:logs:ap-northeast-1:ACCOUNT_ID:log-group:/ecs/ecs-private-*"
    }
  ]
}

 

6-3.ecs-private-init-execution-role

IAM コンソール →「ロール」→「ロールを作成」

項目 設定値
信頼されたエンティティタイプ AWSのサービス
サービスまたはユースケース

Elastic Container Service → Elastic Container Service Task

許可ポリシー AmazonECSTaskExecutionRolePolicy
ロール名 ecs-private-init-execution-role

作成後、ロールを開いて「許可を追加」→「インラインポリシーを作成」→ JSON で次を追加します。6-2と同じポリシーcreate-log-groupのインラインポリシーを付けます。追加で、read-db-master-secretという名前で追加します。Resourceには 4-4 でメモしたマスター認証情報の ARNを、末尾の 6 文字(-AbCdEfの部分)まで含めてそのまま入れます。
ACCOUNT_IDは自身のAWSアカウントIDを入力します。

  • インラインポリシー:create-log-group

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "logs:CreateLogGroup",
      "Resource": "arn:aws:logs:ap-northeast-1:ACCOUNT_ID:log-group:/ecs/ecs-private-*"
    }
  ]
}

 

  • インラインポリシー:read-db-master-secret

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:ap-northeast-1:ACCOUNT_ID:secret:rds!db-xxxx-AbCdEf"
    }
  ]
}

シークレットは AWS マネージドキー(aws/secretsmanager)で暗号化されているため、KMS の権限を別途付ける必要はありません。

作成後、ロールの「許可」タブに次の 3 つが並んでいることを確認してください

ポリシー 種類値
AmazonECSTaskExecutionRolePolicy AWS管理
create-log-group インライン
read-db-master-secret インライン

6-4. ecs-private-app-task-role

IAM コンソール →「ロール」→「ロールを作成」

項目 設定値
信頼されたエンティティタイプ AWSのサービス
サービスまたはユースケース

Elastic Container Service → Elastic Container Service Task

許可ポリシー 選択しない
ロール名 ecs-private-app-task-role

作成後、インラインポリシーrds-iam-connectを追加します。ACCOUNT_IDは自身のID、RESOURCE_IDは 4-4 でメモしたリソースID(db-で始まる値)になります。

  • インラインポリシー:rds-iam-connect

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "rds-db:connect",
      "Resource": "arn:aws:rds-db:ap-northeast-1:ACCOUNT_ID:dbuser:RESOURCE_ID/app_user"
    }
  ]
}

ARN の形に注意:rds-db:connectのARNは、サービス名がrdsではなくrds-db、インスタンスの指定がインスタンス識別子ではなくリソースID、末尾がDBユーザー名です。ここを間違えると、接続時エラーになります。

6-5.ecsInfrastructureRoleForLoadBalancers

ECS ネイティブ Blue/Green で、ECS が ALB のリスナールールの重みを書き換えるためのロールです。

IAM コンソール →「ロールを作成」→「カスタム信頼ポリシー」で次を貼り付けます。


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ecs.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

許可ポリシーにAmazonECSInfrastructureRolePolicyForLoadBalancersを選びます。

ロール名ecsInfrastructureRoleForLoadBalancersで作成します。

7.ECSを作る

7-1.ECSクラスターを作る

ECS コンソール →「クラスター」→「クラスターの作成」

項目 設定値
クラスター名 ecs-private-cluster
インフラストラクチャ AWS Fargate(サーバーレス)のみ
モニタリング デフォルト(Container Insightsはオフ)

7-2.初期化用のタスク定義を作る

ECS コンソール →「タスク定義」→「新しいタスク定義の作成」
以下の設定で作成します。

  • タスク定義
項目 設定値
タスク定義ファミリー ecs-private-init
起動タイプ AWS Fargate
OS/アーキテクチャ Linux/X86_64
CPU/メモリ 0.25 vCPU / 0.5 GB
タスクロール なし(初期化タスクは AWS の API を呼ばない)
タスク実行ロール ecs-private-init-execution-role

  • コンテナ – 1
    ACCOUNT_IDは自身のAWSアカウントIDを入力します。
項目 設定値
名前 init
イメージURI <ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-private-app:<5-4 でメモしたコミット ID>
必須コンテナ はい
ポートマッピング

削除する(Web サーバーとして動かさないため)

環境変数

環境変数 – オプションを展開し、環境変数を追加ボタンを押下して変数の追加をします。(値のタイプを間違えないようにお願いいたします。)

キー 値のタイプ 設定値
DB_HOST 値 4-4 でメモしたエンドポイント
DB_NAME 値 appdb
DB_MASTER_USER ValueFrom <マスター認証情報のARN>:username::
DB_MASTER_PASSWORD ValueFrom <マスター認証情報のARN>:password::

DB_HOSTに入れるのは、ecs-private-db.xxxxxxxx.ap-northeast-1.rds.amazonaws.comの形式の値

DB_MASTER_USERとDB_MASTER_PASSWORDに入れる、マスター認証情報のARNはarn:aws:secretsmanager:ap-northeast-1:123456789012:secret:rds!db-xxxx-AbCdEfの形式の値です。
どちらも、事前の作業4-4でメモしているはずです。

ValueFromの書き方:Secrets ManagerのシークレットはJSONで{“username”: “…”, “password”: “…”}の形をしています。ARN の後ろに「:キー名::」を付けると、JSONの中の特定のキーだけを環境変数に入れられます。末尾の :: は「バージョンは最新」の意味です。値はタスク起動時に実行ロールの権限で取得され、タスク定義にはARNしか残りません。

ログ収集設定

基本的にはデフォルト設定で問題ありません。念のため、設定されていることを確認します。
ログ収集を展開します。ログ収集の使用にチェックがついていること、送信先をAmazon CloudWatchが設定されていることを確認します。

各設定が、以下のように設定されていることを確認します。

キー 値のタイプ 設定値
awslogs-group 値 /ecs/ecs-private-init
awslogs-create-group 値 true
awslogs-region 値 ap-northeast-1
awslogs-stream-prefix 値 ecs

 

Docker設定

Docker設定 – オプションを展開します。以下の設定入れます。

項目 設定値
コマンド python,init_db.py

7-3.初期化タスクを実行する

ECSコンソール → ecs-private-cluster → タスクタブ → 新しいタスクの実行

項目 設定値
タスク定義ファミリー ecs-private-init(最新リビジョン)
必要なタスク 1
タスクグループ ブランク(設定しない)
コンピューティングオプション 起動タイプ
起動タイプ Fargate
プラットフォームバージョン LATEST
ネットワーキング→VPC ecs-private-vpc
サブネット プライベートサブネットを2つ選択(パブリックは選択しない)
セキュリティグループ ecs-private-sg-app (defaultは設定から外しておく)
パブリックIP オフ

「作成」を押し、タスクのステータスが「停止」になるまで待ちます(1〜2 分)。

タスクタブを開いて、次の 3 点を確認します。

確認する項目 確認したい結果
ログのタブ init_db: done
コンテナの終了コード 0
停止理由 Essential container in task exited
(文字が青色であること。)

タスクライフサイクルの「実行中」に赤い × が付いていても、終了コードが0(ゼロ)なら問題ありません。初期化タスクは処理が終われば止まるのが正常としています。

8.ALBとターゲットグループ

8-1.ターゲットグループの作成

EC2 コンソール →「ターゲットグループ」→「ターゲットグループの作成」

ターゲットグループを ecs-private-tg-blueとecs-private-tg-greenの2つ作成します。

  • ecs-private-tg-blue
項目 設定値
ターゲットタイプ IPアドレス
ターゲットグループ名 ecs-private-tg-blue
プロトコル : ポート HTTP:8000
VPC ecs-private-vpce
ヘルスチェックパス /health
  • ecs-private-tg-green (設定内容はblueと同じです。)
項目 設定値
ターゲットタイプ IPアドレス
ターゲットグループ名 ecs-private-tg-green
プロトコル : ポート HTTP:8000
VPC ecs-private-vpce
ヘルスチェックパス /health

ヘルスチェックの詳細設定等は、デフォルトのままで問題ありません。

「次へ」→ ターゲットは何も登録せずに「ターゲットグループの作成」

8-2.ALBを作る

EC2 コンソール →「ロードバランサー」→「ロードバランサーの作成」→ロードバランサータイプ「Application Load Balancer」

項目 設定値
名前 ecs-private-alb
スキーム インターネット向け
VPC ecs-private-vpc
マッピング 2つのAZで、それぞれパブリックサブネットを選択
セキュリティグループ

ecs-private-sg-alb(default設定は外します。)

リスナーとルーティング

項目 設定値
プロトコル HTTP
ポート 80
アクションのルーティング ターゲットグループへ転送
ターゲットグループ ecs-private-tg-blue   重み:100
ecs-private-tg-green 重み: 0

リスナーの追加ボタンを押下し、つづけてポート8080番を作成します。

項目 設定値
プロトコル HTTP
ポート 8080
アクションのルーティング ターゲットグループへ転送
ターゲットグループ ecs-private-tg-blue   重み:100
ecs-private-tg-green 重み: 0

ロードバランサーの作成ボタンを押下します。作成したら、ALBの詳細タブにあるDNS名をメモして控えておきます。

9.ECSサービスを作る

9-1.アプリ用のタスク定義を作る

ECS コンソール →「タスク定義」→「新しいタスク定義の作成」

項目 設定値
タスク定義ファミリー ecs-private-app
起動タイプ AWS Fargate
オペレーティングシステム/アーキテクチャ Linux/X86_64
タスクサイズ CPU 0.25 vCPU
タスクサイズ メモリ 0.5 GB
タスクロール ecs-private-app-task-role
タスク実行ロール ecs-private-execution-role

コンテナ – 1 の設定

  • ACCOUNT_IDは自身のAWSアカウントIDを入力します。
  • コミットIDは確認した5-4 でメモしたコミット IDを入力します。
項目 設定値
名前 app
イメージURI <ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-private-app:<コミットID>
ポートマッピング

コンテナポート:8000、プロトコル:TCP、ポート名:app-8000、アプリケーションプロトコル:HTTP

環境変数

キー 値のタイプ 値
DB_HOST 値 4-4 でメモしたエンドポイント
例:ecs-private-db.xxxxxxxx.ap-northeast-1.rds.amazonaws.com
DB_NAME 値 appdb
DB_USER 値 app_user
AWS_REGION 値 ap-northeast-1

パスワードに関する環境変数設定はいれません。

ログ収集の設定などは、そのままデフォルト設定で問題ありません。

確認したら作成ボタンを押下します。

9-2.サービスを作る

ECS コンソール → クラスター → ecs-private-cluster →「サービス」タブ →「作成」

サービスの詳細

項目 設定値
タスク定義ファミリー ecs-private-app
タスク定義のリビジョン 最新リビジョン
サービス名 ecs-private-service

環境

項目 設定値
コンピューティングオプション 起動タイプ
起動タイプ Fargate
プラットフォームバージョン LATEST

デプロイ設定

項目 設定値
スケジューリング戦略 レプリカ
必要なタスク 2
アベイラビリティゾーンのリバランスを有効にする チェックをつける
ヘルスチェックの猶予期間 特に設定しません

デプロイオプション

項目 設定値
デプロイコントローラのタイプ ECS
デプロイ戦略 ブルー/グリーン
デプロイベイク時間 5
トラフィック移行 全てのトラフィックを更新済みのAmazon ECSコンテナに一度に移行

デプロイライフサイクルフック

一時停止フック

項目 設定値
ライフサイクルステージ テストトラフィック移行後
タイムアウト 30
タイムアウトアクション ロールバック

デプロイ不具合の検出

項目 設定値
Amazon ECS デプロイサーキットブレーカーを使用する チェックをつける
失敗時のロールバック チェックをつける
希望するカウントの上限・加減付き割合 選択する
しきい値 50

ネットワーキング

項目 設定値
VPC ecs-private-vpc
サブネット

プライベートサブネット 2 つ
(パブリックサブネットは選択しません)

セキュリティグループ ecs-private-sg-app
(defaultは外す)
パブリックIP オフ

ロードバランシング

項目 設定値
ロードバランサーの種類 Application Load Balancer
コンテナ app 8000:8000
ロードバランサー 既存のロードバランサーを使用 → ecs-private-alb
リスナー

既存のリスナーを使用 → HTTP:80

本番リスナールール 優先度:default
テストリスナールール 空欄にします
ターゲットグループ 2 つの既存のターゲットグループを使用
ブルーターゲットグループ ecs-private-tg-blue
グリーンターゲットグループ ecs-private-tg-green
ロール ecsInfrastructureRoleForLoadBalancers

※ テストリスナールールは作成後の「サービスを更新」で追加します。

9-3.サービスを更新してテストリスナーを追加

タスクが2つRUNNINGになり、サービスの「デプロイ」タブでデプロイが完了していることを確認したら、テストリスナーを追加します。

ECS → クラスター → ecs-private-cluster → サービス → ecs-private-app-serviceを選択 → サービスを更新ボタンを押下 → ecs-private-app-service を更新画面が表示されるので、ロードバランシングセクションまでスクロールします。

項目 設定値
テストリスナー HTTP:8080
テストリスナールール 優先度:default

他は変更せずに「更新」を押します。

更新そのものでデプロイが始まります。 中身は同じイメージのまま、Blue/Greenの流れ(Green起動 → テストトラフィック移行 → 一時停止)が1周します。デプロイが「テストトラフィック移行後」で一時停止したら、次の2つを開いて確認します。

URL 環境 表示
http://<ALB の DNS 名>/ 本番 v1(Blue が応答)
http://<ALB の DNS 名>:8080/ テスト v1(Green が応答)

 

 


どちらも同じv1ですが、「応答したタスク」の値が本番とテストで異なることを確認してください。確認できたら、デプロイ詳細の「アクションを実行する」→「続行」を押します。ベイク時間(5分)が過ぎれば、このデプロイは完了です。


動作確認

ブラウザで、http://<ALB の DNS 名>/を開いて(httpも含めて、明示的にhttpと入力してください。ブラウザによってはhttpsに補完される場合があります。)確認します。

項目 想定される表示結果
アプリのバージョン コミットIDの先頭7桁
応答したタスク ip-10-0-1xx-xx.ap-northeast-1.compute.internal
DB PostgreSQL 18.x
接続ユーザー app_user
TLS True (TLSv1.3)
アクセス回数 1

テストが正常に表示されて、本番が一時停止フックで500番台が表示されていると思います。確認したら、アクションを実行するで処理を続行し、テストも本番も両方のサイトが正しく表示されることを確認します。

 

リロードするたびにアクセス回数が増え、「応答したタスク」が 2 つの値で切り替わります。RDSへの接続ユーザーがapp_userで、TLSがTrueになっていることが、IAM認証とTLS接続が機能しているとなります。

もし’/’を開いて500 エラーになる場合は、DBへの接続で失敗しています。タスクのログを確認して原因を確認していく必要があります。


ここで本番(80)だけ 503 になった場合:503 Service Temporarily Unavailableは、「本番リスナーの転送先(Blue)に正常なターゲットが1つもない」ことを意味します。9-2の時点でセキュリティグループなどの設定を誤っていて、最初のデプロイが一度も正常にならなかった場合に起きます。

「ロールバックに失敗」「No rollback candidate was found」と表示された場合:Greenがヘルスチェックに通らずサーキットブレーカーが作動したものの、戻り先となる「正常に動いた実績のあるリビジョン」が存在しないときの表示です。上と同じく、初回構築時に設定を誤っていた場合に起きます。原因(多くはセキュリティグループにあります。)を直してから、「サービスを更新」で「新しいデプロイの強制」にチェックを入れて更新すれば、デプロイをやり直せます。


10.CodePipelineを作る

10-1.パイプライン全体像

 

今回作成するパイプラインは、パイプラインが自分で変更を検知して動くのではなく、GitHub Actionsから「このイメージを出して」と指定されて動くことです。パイプラインは変数IMAGE_TAGを受け取り、それをもとにデプロイするイメージを決めます。

10-2.パイプラインを作る

ステップ 1:作成オプションを選択する

CodePipeline コンソール →「パイプラインを作成する」→「カスタムパイプラインを構築する」

ステップ 2:パイプラインの設定

項目 設定値
パイプライン名 ecs-private-pipeline
実行モード キュー
サービスロール 新しいサービスロール
ロール名 ロール名は表示されたロール名を使用します。
AWS CodePipelineがサービスロールを作成できるようになるため、この新しいパイプラインでの使用が可能になります。にチェックをつける
アーティファクトストア デフォルトのロケーション
暗号化キー デフォルトのAWSマネージド型キー
変数 名前 :IMAGE_TAG、デフォルト値:空欄、説明:空欄

ステップ3:ソースステージ

項目 設定値
ソースプロバイダー Amazon ECR
リポジトリ名 ecs-private-app
画像(イメージ)タグ 空欄

それ以外はデフォルト設定にします。

ステップ 4:ビルドステージ

プロバイダーを構築するでコマンドを選択します。以下のコマンドを、コマンド欄に記載します。(testコマンド始まるのと、catコマンドで始まる2行になります。)

test -n “#{variables.IMAGE_TAG}” || (echo “IMAGE_TAG is empty” && exit 1)
printf ‘[{“name”:”app”,”imageUri”:”%s”}]’ “<ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-private-app:#{variables.IMAGE_TAG}” > imagedefinitions.json

cat imagedefinitions.json

<ACCOUNT_ID>は自分のアカウント ID に置き換えてください。1 行目は「変数が渡されていなければ止める」ための安全装置です。

ステップ5: テストステージ

ビルドステージをスキップボタンを押下して、スキップします。

ステップ6:デプロイステージを追加する

導入段階をスキップボタンを押下して、スキップします。後ほど、デプロイステージを追加します。

ステップ7: レビュー

内容に問題がなければ、パイプラインを作成するボタンを押下します。

10-3.出力ファイルとデプロイステージを追加する

作成したパイプラインを開き、「編集する」を押します。

ステップ1:Buildステージのアクションを編集する

「ステージを編集する」→ Commands アクションの編集アイコン

項目 設定値
出力アーティファクト BuildArtifact
出力ファイル imagedefinitions.json(入力後「ファイルパスに追加」を押して登録する)

補足:ステージを編集するボタンを押下すると、下記画像のようになるのでペンのアイコンをクリックすると編集できます。

出力アーティファクトは下の方にあります。

完了ボタンを押下します。編集:Buildの画面も完了ボタンを押下して、設定を保存します。

ステップ2:Deploy ステージを追加する

Build ステージの下の「+ ステージを追加する」をクリックします→ ステージを追加するが表示されるので、Deployと入力し、ステージを追加するボタンを押下します →「+ アクショングループを追加する」をクリックします

アクションを編集する画面が表示されるので、以下のように設定します。

項目 設定値
アクション名 Deploy
アクションプロバイダー Amazon ECS
リージョン

アジアパシフィック(東京)

入力アーティファクト BuildArtifact
クラスター名 ecs-private-cluster
サービス名 ecs-private-service
イメージ定義ファイル imagedefinitions.json
デプロイのタイムアウト 60分

デプロイのタイムアウトを長めにする理由:このアクションは、ECSのデプロイが完了するまで待ちます。今回は「一時停止フック(最大30 分)+ベイク時間(5分)」があるので、既定値では足りなくなる可能性があります。実際の業務では、業務要件に合わせてください。

入力したら「完了」ボタンを押下→ 編集:Deployの「完了」をクリック→画面上部の「保存」を押下→パイプラインの変更を保存する画面が表示されるので「保存」ボタンを押下します。

10-4. パイプラインのロールに ECS の権限を追加する

ウィザードでデプロイステージの作成をスキップしたため、自動作成されたサービスロールにはECSを操作する権限がありません。

IAM コンソール →「ロール」→ AWSCodePipelineServiceRole-ap-northeast-1-ecs-private-pipeline →「許可を追加」→「インラインポリシーを作成」→ JSON で次を貼り付け、ポリシー名をecs-deployという名前で保存します。

  • ポリシー名:ecs-deploy
    ACCOUNT_IDは自身のAWSアカウントIDを入力します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EcsDeploy",
      "Effect": "Allow",
      "Action": [
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:DescribeTasks",
        "ecs:ListTasks",
        "ecs:RegisterTaskDefinition",
        "ecs:TagResource",
        "ecs:UpdateService"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PassTaskRoles",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": [
        "arn:aws:iam::ACCOUNT_ID:role/ecs-private-execution-role",
        "arn:aws:iam::ACCOUNT_ID:role/ecs-private-app-task-role"
      ],
      "Condition": {
        "StringEquals": { "iam:PassedToService": "ecs-tasks.amazonaws.com" }
      }
    }
  ]
}

11.GitHub Actionsからリリースする

11-1. パイプラインの起動を有効にする

GitHubのリポジトリ →「Settings」→「Secrets and variables」→「Actions」→「Variables」→「New repository variables」で、変数を追加します。ワークフローの最後のステップ(Start CodePipeline)がスキップされなくなります。

Name Value
PIPELINE_NAME ecs-private-pipeline

 

 

11-2. v2 をリリースする

GitHubでapp.pyを編集し、先頭の2行を変更し(以下の箇所の内容を変更します)mainにマージします。(鉛筆アイコンで直接編集とコミットをしても今回は問題ありません。)


APP_MESSAGE = "v2: Released via GitHub Actions + CodePipeline"
THEME_COLOR = "#e6f4ea"

11-3. 連鎖を観察する

コミットから数分で、次の連鎖が起きます。

順番 発生箇所 内容
1 GitHub Actions イメージをビルドし、コミット ID のタグで ECR に push
2 GitHub Actions ダイジェストを取得し、IMAGE_TAG変数付きでパイプラインを起動
3 CodePipeline Source 指定されたダイジェストのイメージを取得
4 CodePipeline Build imagedefinitions.jsonを生成
5 CodePipeline Deploy 新しいタスク定義を登録し、サービスを更新
6 ECS

Green タスクを起動 → テストリスナーを Green に向ける → 一時停止

CodePipeline の実行履歴を開くと、「変数」の欄にIMAGE_TAGの値(コミット ID)が表示されます。どのコミットのデプロイなのかが、パイプライン側にも記録として残ります。

11-4. テストリスナーで Green を確認する

ECS コンソール → ecs-private-service →「デプロイ」タブで、デプロイがテストトラフィック移行後で一時停止していることを確認します。

この状態で、ブラウザで2つのURLを開き比べます。

URL 環境 表示
http://<ALB の DNS 名>/ 本番 v1(背景色:青)
http://<ALB の DNS 名>:8080/ テスト v2(背景色:緑)

 

本番とテストのどちらを開いても、同じカウンターが増えていきます。BlueとGreenは、同じ1 つのデータベースを共有しているからです。これは便利な一方で、Blue/Green で DB を扱うときの最大の注意点でもあります。後ほど解説します。

11-5. 承認して本番に出す

確認して問題なければ、デプロイ詳細の「アクションを実行する」→一時停止フックアクションを実行するで「続行」を選択し、「確認」ボタンを押下します。

本番トラフィック移行まで進んだら、本番(ポート番号:80)をブラウザで開き直してv2が表示されれば、本番の切り替えは完了です。

11-6. ベイク時間中に「人が判断する」

本番を切り替えた直後から、5分間のベイク時間が始まります。この間、Blue(v1)のタスクは停止されずに残っています。ベイク時間中での確認したい点は以下になります。

確認先 確認すること
ブラウザ(本番) 画面が正しく表示されるか
CloudWatch Logs /ecs/ecs-private-app エラーログが出ていないか
ALB の「モニタリング」タブ 500番台のエラーが増えていないか
ターゲットグループ Green のターゲットがすべてhealthyになっているか

問題を見つけた場合は、ECSコンソール → サービス →「デプロイ」タブ → 対象のデプロイを開き、「ロールバック」を押します。ALBの重みがBlueに戻り、残っていたv1のタスクがそのまま本番を引き継ぎます。新しくタスクを起動し直す必要がないので、数秒で元に戻ります。

何もしなければ、5 分後に Blue のタスクが停止され、デプロイは完了します。CodePipelineのDeployアクションも、このタイミングで「成功」になります。

11-7. ロールバックを練習する

実業務でいきなりロールバックする前に、ここで練習します。

  1. app.pyをAPP_MESSAGE = “v3: This release will be rolled back” 、 THEME_COLOR = “#fce8e6″に変更してコミットします。コードパイプラインが動いて、テスト側にリリースされることを確認します。
  2. ブラウザでテストリスナー側がv3であることを確認したら、デプロイ詳細の「アクションを実行する」→ 一時停止フックアクションを実行するで「続行」を選択し、「確認」ボタンを押下し本番側にリリースされることを確認します。
  3. ベイク時間に入るので、ベイク時間中に「ロールバック」ボタンを押下します。サービスデプロイをロールバックしますか?と聞かれるので、「ロールバック」ボタンを押下します。
  4. 本番側がロールバックされることを確認するため、ブラウザで本番側リスナーを開いてv2に戻ることを確認します。
  5. CodePipelineの実行は、Deployアクションが「失敗」として記録されることを確認します。「リリースしたが戻した」ことをパイプラインの履歴に残ることを確認しておきます。

2 段階の判断ポイント:この構成では、人が判断する場面が 2 回あります。

タイミング 見るもの 判断を誤ったときの影響
一時停止中(本番切り替え前) テストリスナー(自分だけ) なし(まだ本番に出ていない)
ベイク時間中(本番切り替え後) 本番のトラフィック、メトリクス 数分間、利用者に影響が出る

一時停止では「出してよいか」を、ベイク時間では「出してよかったか」を判断します。一時停止のタイムアウトを「ロールバック」にしているのは、「誰もOKを出さなかったものは出さない」ためでした。一方、ベイク時間は「誰もNOを出さなければ完了する」仕組みになります。本番では、ベイク時間の監視を人の目だけに頼らず、CloudWatchアラームによる自動ロールバック(デプロイ設定の「アラーム」)を組み合わせると、夜間や見落としにも強くなります。
参考リンク:Amazon ECS で手動ロールバック(デプロイメントの手動停止)がサポートされました

11-8. 修正版をリリースして、正しい状態に戻す

ロールバックで本番は v2 に戻りましたが、これで終わりにしてはいけません。今の状態を整理すると、次のようになっています。

対象サービス 状態
本番(ECS) v2 (ロールバックで戻った)
GitHubのmainブランチ v3 (問題のあるコード)
ECR v3のイメージも残っている

ロールバックは「本番を戻す」操作で、ソースコードまでは戻りません。 この状態のまま、別の人が関係のない修正をmainにpushすると、v3の問題を抱えたコードがそのまま本番に出てしまいます。ロールバックの後は、必ずソースコードを直してリリースし直すところまでをセットで行います。

直し方には 2 通りあります。

方法 やり方 向いている場面
修正して前に進む 問題を直したコードを新しいバージョンとして出す 原因が分かっていて、すぐ直せる
元に戻す git revertで v3 のコミットを打ち消すコミットを作る 原因の調査に時間がかかる。まず確実にv2の内容に戻したい

どちらの場合も、新しいコミットを作ってリリースする点は同じです。ECR のタグはイミュータブルなので、v2 のコミットをもう一度 GitHub Actions で流すことはできません(同じタグの push が失敗します)。常に「新しいコミット = 新しいタグ」で前に進むのが、この構成の考え方です。

今回は「修正して前に進む」方法で、v4 を出します。

ステップ1: 修正版をコミットする

app.pyの先頭2行を以下のように変更し、mainにマージします。(鉛筆アイコンで直接編集とコミットをしても今回は問題ありません。)


APP_MESSAGE = "v4: Fixed after rollback"
THEME_COLOR = "#fff4e5"

 

ステップ2:リリースする

ここまで実施してきた内容と、同じになります。

  1. GitHub Actions がビルドし、CodePipelineを起動する
  2. デプロイが「テストトラフィック移行後」で一時停止する
  3. テスト側(8080番ポート)で、ブラウザにてv4を確認します。本番側(80番ポート)はまだv2のままです。
  4. 「アクションを実行する」→ 一時停止フックアクションを実行するで「続行」を選択し、「確認」ボタンを押下し本番側にリリースされることを確認します。
  5. 今回はロールバックせずに、ベイク時間(5分)が過ぎるのを待ちます。

ステップ3: 最終状態を確認する

ベイク時間が過ぎたら、次の 3 か所を確認します。

確認 期待される状態
本番側のhttp://<ALB の DNS 名>/をブラウザで確認 v4が表示される
ECS サービスの「デプロイ」タブ 最新のデプロイが完了する
CodePipeline の実行履歴 v2 = 成功、v3 = 失敗(ロールバック)、v4 = 成功

画面の「アクセス回数」も見てみてください。v1 から v4 まで、何度デプロイしてもロールバックしても、カウンターは途切れずに増え続けています。アプリのタスクは何度も入れ替わりましたが、データは RDS に残り続けていることの確認にもなります。

これで本記事は完了です。 おつかれさまでした。料金がかかり続けるので、クリーンアップ章の手順で後片付けを行ってください。12章のコラムは、後片付けの後に読んでいただいて構いません。


12.コラム

12-1. DB を共有する Blue/Green の落とし穴 ―「expand / contract」

11-4 で見たとおり、BlueとGreenは同じ DB を同時に使います。ベイク時間中も、ロールバック後も同じです。そのため、スキーマ変更を伴うリリースでは注意が必要です。

たとえば、v2でaccess_log.app_version列をversionに名前変更したとします。

  • Green(v2)が起動する前に列名を変えると、本番で動いている Blue(v1)が壊れる
  • 列名を変えた後にロールバックすると、戻った v1 が動かない

これを避ける定番の考え方が expand/contract(拡張と縮小)です。

ステップ リリース内容 DB 動くバージョン
1.expand 新しい列versionを追加。アプリは両方の列に書き込む。 古い列・新しい列が共存 旧アプリも新アプリも動く
2.移行 既存データを新しい列にコピー。アプリは新しい列を読む。 共存 同上
3.contract 十分に安定したら、古い列を削除 新しい列のみ 新アプリのみ

ポイントは、どのリリースの時点でも「1つ前のバージョンのアプリ」が動くDBであり続けることです。こうしておけば、ベイク時間中のロールバックが常に安全に行えます。

今回の初期化タスクは、このマイグレーションを流す場所としてそのまま使えます。本番では「マイグレーション用のタスク定義をCodePipelineのDeployの前にRunTaskで実行する」ステージを足す構成もよく見られます。

12-2. DB 認証情報の管理方式を比較する

DB の認証情報の置き場所として、よく使われる方式を比較します。

観点 Parameter Store(SecureString) Secrets Manager

IAM DB 認証(今回使用した方法)

アプリが持つもの 固定パスワード パスワード(ローテーション可) なし(15 分有効のトークンをその場で生成)
ローテーション 自前で作る 自動 不要
漏えい時の影響 変更するまで使われ続ける 次のローテーションまで トークンは 15 分で失効
権限の管理 パラメータの読み取り権限 シークレットの読み取り権限 タスクロールのrds-db:connect
料金 標準パラメータは無料 1シークレットあたり月額課金 追加料金なし
注意点 パスワードを知っている人が増えやすい ECSのsecrets

による注入はタスク起動時のみ

TLS 接続が必須。新規接続の頻度が非常に高い用途には不向き。
  • Secrets Managerの「起動時のみ」問題は、本番でよく踏む落とし穴です。ECSはタスクの起動時にシークレットを環境変数へ入れますが、その後ローテーションでDBのパスワードが変わっても、動いているタスクの環境変数は古いままです。結果として、コネクションを張り直した瞬間に接続できなくなります。対策としては、アプリが接続のたびにSDK でシークレットを取り直す、タスクを定期的に入れ替える、などがあります。

  • IAM 認証ではこの問題自体が起きません。今回の構成でマスターパスワードを Secrets Manager で管理しているのは、人も日常のアプリも使わない、非常用の鍵という位置づけだからです。初期化タスクは実行のたびに最新の値を取得するので、ローテーションの影響も受けません。

12-3. ヘルスチェックで DB を見ない理由

今回の”/health”は、DBに接続せずにokを返すだけにしています。「DBに接続できるかまで確認したほうが安全では?」と思うかもしれません。

ヘルスチェックでDBを見ると、DBが一時的に不調になっただけで、ALBが全タスクをunhealthyと判断します。ECSはそのタスクを停止して新しいタスクを起動しますが、DBが不調なのは変わらないので、新しいタスクもすぐにunhealthyになります。結果として、アプリは「DBを使わないページすら返せない」状態になり、タスクの入れ替えが延々と続きます。

ヘルスチェックは「このタスクを入れ替えれば直るか」を判断するためのもの、と考えると分かりやすいです。DBの不調はタスクを入れ替えても直らないので、ヘルスチェックの対象外にし、代わりにアラームで検知します。

12-4. IAM 認証を本番で使うときの補足

  • トークンが必要なのは接続を張るときだけです。一度つながったコネクションは、トークンの有効期限(15分)が過ぎても切れません。コネクションプールを使う場合は、「新しいコネクションを張るたびにトークンを生成する」ように書けば問題ありません。

  • 今回のアプリはリクエストごとに接続しています(学習のための単純化です)。本番ではコネクションプールを使い、接続数の多い構成ではRDS Proxyの導入も検討してください。RDS ProxyもIAM認証に対応しています。

12-5.ECR Public へのログインだけ、なぜ us-east-1 なのか

ワークフローでは、東京リージョンで作業しているのに、「Login to Amazon ECR Public」のステップだけAWS_REGION: us-east-1を指定しています。「東京に揃えるべきでは?」と感じた方もいると思います。理由は、ECR Public の仕組みにあります。

ECR(プライベート) ECR Public
アドレス <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com public.ecr.aws
リージョン リポジトリごとに特定のリージョンに属する

特定のリージョンに属さない(グローバル)

ログイン(認証)のAPI 各リージョンにある

一部のリージョンにしかない(us-east-1とus-west-2)

ECR Public は、世界中どこからでも同じアドレス(public.ecr.aws)で使えるレジストリです。その一方で、ログインのための API(ecr-public:GetAuthorizationToken)は us-east-1とus-west-2にしか用意されていません。ap-northeast-1を指定してログインしようとすると、東京リージョンには ECR Public の API がないため失敗します。
一度ログインしてしまえば、取得した認証情報は public.ecr.aws 全体に対して有効です。後続の docker build でのベースイメージの取得は、リージョンを意識せずにそのまま動きます。

リージョンの切り替えがほかのステップに影響しないか: ワークフローの中で、「env:」をステップの中に書いているので、us-east-1 になるのはこのステップだけです。

ステップ リージョン 相手
Login to Amazon ECR ap-northeast-1 自分のアカウントの ECR
Login to Amazon ECR Public us-east-1 ECR Public
Build and push image ap-northeast-1 東京の ECR に push
Start CodePipeline ap-northeast-1 東京のパイプライン

参考リンク:Amazon ECR Public エンドポイントとクォータ

12-6:ECR Public のレート制限ついて

ベースイメージにpublic.ecr.awsを使用しているのは、Docker Hub の匿名pull制限を避けるためですが、ECR Publicにも制限はあります。

pull の種類 上限
未認証 1 回/秒
認証済み 10 回/秒
ECS・Fargate・EC2 からの pull 10 回/秒(未認証でも)

※参考URL Amazon ECR Public endpoints and quotas

1 回のビルドでbaseイメージを1回pullするだけなら、未認証でも問題ありません。それでもワークフローでログインしているのは、CIの同時実行が増えたときに効いてくるためです。複数のブランチでビルドが同時に走ると、未認証の1 回/秒はすぐに頭打ちになります。また、ECSやFargateからのpullは、未認証でも認証済みと同じ上限になります。

 


クリーンアップ

今回の構成は時間課金のリソースが多いので、必ずその日のうちに削除してください。 依存関係があるので、上から順に実施します。

順番 リソース 操作
1 ECS サービス ecs-private-service →「サービスを削除」(強制削除にチェック)
2 ECS クラスター ecs-private-cluster →「クラスターの削除」
3 タスク定義 ecs-private-appとecs-private-init それぞれをクリック、全リビジョンを「登録解除」します。タスク定義画面にもどり、更新ボタンで削除されていることを確認します。
4 CodePipeline ecs-private-pipelineを削除
5 S3 CodePipeline のアーティファクト用バケット(codepipeline-ap-northeast-1-…)を空にして削除
注意:バケットの中をみて、他の人や別プロジェクトで使用している場合は、バケットを削除せずに自分が使用したフォルダのみ削除してください。フォルダ名や実施した日付及び時間で検討がつくはずです。
6 ECR ecs-private-appを削除
7 RDS

ecs-private-dbを削除

・最終スナップショットは作成しない → チェックを外す
・自動バックアップも保持しない → チェックを外す
・私は、インスタンスの削除後、システムスナップショットとポイントインタイムの復元を含む自動バックアップが利用不可となることを了承します。 → チェックをつける

マスター認証情報のシークレットも自動で削除されます

8 RDS

DB サブネットグループecs-private-db-subnetを削除(すぐに削除できない場合は、DBインスタンス削除が完了するまで待ってから再度実行します。)

9 EC2

・ALB ecs-private-alb を削除
・ターゲットグループ 2 つ(ecs-private-tg-blueとecs-private-tg-green)を削除

10 VPC

Interface エンドポイント 4 つを削除(削除完了まで数分待つ)

対象となるインターフェイスエンドポイント
・ecs-private-vpce-ecr-api
・ecs-private-vpce-ecr-dkr
・ecs-private-vpce-logs
・ecs-private-vpce-secretsmanager

11 VPC

DSとInterfaceエンドポイントの削除完了を待ってから実行します。

ecs-private-vpcを削除(サブネット、ルートテーブル、IGW、S3 Gateway エンドポイント、SG も一緒に削除される)

12 IAM ロール ecs-private-execution-role、ecs-private-init-execution-role、ecs-private-app-task-role、ecsInfrastructureRoleForLoadBalancers、ecs-private-github-actions、AWSCodePipelineServiceRole-ap-northeast-1-ecs-private-pipeline を削除
13 IAM 他の人や別プロジェクトで使用している場合は、絶対に削除しないでください。
ID プロバイダtoken.actions.githubusercontent.comを削除
14 CloudWatch Logs /ecs/ecs-private-app、/ecs/ecs-private-init、/aws/codepipeline/ecs-private-pipeline を削除
15 GitHub リポジトリの Variables を削除、またはリポジトリごと削除

VPC が削除できないとき:VPC の中にネットワークインターフェイスが残っていると削除できません。Interface エンドポイント、ALB、RDS、ECS タスクのいずれかが削除中のことが多いので、数分待ってから再実行してください。

 


まとめ

  1. プライベートサブネット = インターネットへの経路がないサブネット。ルートテーブルに 0.0.0.0/0 がないことで実現している
  2. Fargate の起動には ECR(api/dkr)、S3、Logs のエンドポイントが必要。S3はECRのレイヤーの実体があるため、忘れるとpullに失敗する
  3. 通信の許可はセキュリティグループの連鎖で表現する。ALB → アプリ → DB / エンドポイントの順に、IPではなく SG を参照する
  4. アプリはパスワードを持たない。IAMデータベース認証で、タスクロールのrds-db:connectだけで接続する
  5. タスク実行ロールとタスクロールを分ける。マスターパスワードを読めるのは初期化タスクの実行ロールだけ
  6. DBの初期化・マイグレーションはECSの単発タスクで行う。プライベートな DB に届く場所で、ログを残して実行できる
  7. ECRをイミュータブルにし、GitHub Actions がコミットIDを渡してパイプラインを起動する。どのコミットがデプロイされたかが、ECR・パイプライン・画面のすべてで一致する
  8. 人の判断は2回。一時停止で「出してよいか」、ベイク時間で「出してよかったか」を判断する
  9. BlueとGreen は同じDBを共有する。スキーマ変更は expand / contract で、常に1つ前のバージョンが動くようにする
  10. ロールバックは本番を戻すだけで、ソースコードは戻らない。ロールバックの後は、新しいコミットで修正版をリリースするところまでをセットで行う

 

以上で全て完了です。最後まで読んでいただき、ありがとうございました。

 


本記事でトラブル発生時の参考

症状 原因と対処
Actions のログにAssuming role with OIDCが何行も繰り返され、最後に失敗する

信頼ポリシーのsubが一致していない。CloudTrail のAssumeRoleWithWebIdentityイベントから実際の値をコピーする(5-3)。リポジトリ名・ユーザー名の綴りも確認。

初期化タスクがecr:GetAuthorizationTokenのAccessDeniedExceptionで起動前に停止 実行ロールにAmazonECSTaskExecutionRolePolicy

が付いていない(6-3)。タイムアウトではなく AccessDenied なら、エンドポイントまでは到達できている

初期化タスクのログにpassword authentication failed for user “postgres” DB_MASTER_PASSWORD の値のタイプが「値」のままで、ARN の文字列がパスワードとして渡っている(7-2)。新しいリビジョンで ValueFrom に修正する。
ターゲットグループのターゲットがRequest timed outでunhealthy sg-appのインバウンドのポートが 8000 になっているか(8080 と取り違えやすい)

サービスのセキュリティグループがdefaultのままになっていないか

サービス作成画面でテストリスナールールが選べない 作成画面では、選んだリスナー(HTTP:80)のルールしか候補に出ない。空欄で作成し、9-3 の「サービスを更新」で 8080 を追加する

デプロイが「ロールバックに失敗」
No rollback candidate was found

初回のデプロイから一度も正常になっていないため、戻り先がない。原因を直してから「新しいデプロイの強制」で更新する(9-3)
テスト(8080)は表示されるが、本番(80)が
503 Service Temporarily Unavailable
本番の転送先(Blue)に正常なターゲットがない。初回構築時のトラブルから復旧する途中で起きる。一時停止中なら「続行」で本番が Green に切り替わり解消する(9-3)
タスクが CannotPullContainerError (タイムアウト)で停止する

① ecr.api / ecr.dkr のエンドポイントがない、または「DNS 名を有効化」がオフ

② sg-vpce のインバウンドに sg-app からの 443 がない

③ S3 Gateway エンドポイントがプライベートサブネットのルートテーブルに関連付いていない

ResourceInitializationError: unable to retrieve secret

① Secrets Manager のエンドポイントがない

② 実行ロールにシークレットの読み取り権限がない(cs-private-init-execution-roleを選んだか確認)

③ ValueFrom の末尾の :username::/ :password:: の書き方

ResourceInitializationError: failed to validate logger args / ロググループ作成エラー

① Logs のエンドポイントがない

② 実行ロールにlogs:CreateLogGroupがない(6-2)

初期化タスクのログにconnection timeout

sg-dbのインバウンドにsg-appからの 5432 がない。初期化タスクのセキュリティグループが sg-app になっているか確認

“/”が500エラー。ログにcertificate verify failed

DB_HOSTがエンドポイント名になっているか(IP アドレスだと証明書の検証に失敗する)

“/”が500エラー。ログにdatabase “appdb” does not exist

RDS 作成時に「最初のデータベース名」を入れ忘れた。DB_NAMEをpostgresにして初期化タスクからやり直す。

サービス作成画面で green のターゲットグループが選べない

ALB のリスナーに、blue と green の両方を重み付きで設定していない(8-2)

Actions の Start CodePipeline が AccessDenied

ecs-private-github-actionsのポリシーのパイプライン ARN(名前・リージョン・アカウント ID)を再確認する。

パイプラインの Source が失敗する

手動の「変更をリリース」や作成直後の実行では、 latest を探しに行って失敗する。GitHub Actions から起動する。

パイプラインの Build が IMAGE_TAG is empty で失敗

変数なしで起動された。GitHub Actions から起動する。

パイプラインの Deploy が権限エラー

10-4 のインラインポリシー。特に「iam:PassRole」の対象ロール名を再確認する。

デプロイが一時停止のまま進まない

「アクションを実行する」→「続行」を押していない。30 分放置するとロールバックされる。

 

shibuya
著者:shibuya
クラウドセントリック株式会社 所属ITエンジニア(主にインフラ構築) 時々、演劇を観にいくことで気晴らししています。

エンジニア積極採用中!

クラウドセントリックでは、業務拡大に伴いエンジニアを積極採用中です。
一緒に働きたいと思った方は、採用フォームからぜひご応募ください。
MENU