1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TerraformでAWS CodePipeline + ECS Fargate CI/CD環境を構築する【Flask + MySQL + Blue/Green デプロイ】

1
Last updated at Posted at 2026-04-12

はじめに

本記事では Terraform を用いて AWS CodePipeline と Amazon ECS Fargate を組み合わせ、Flask + MySQL アプリケーションの CI/CD 環境を構築します
ECS Fargate によりサーバー管理を不要とし、CodePipeline による完全マネージドな CI/CD、Amazon ECR によるコンテナ管理を実現します
さらにオートスケーリングと高可用性を考慮した実践的な構成を解説します

参考記事(AWS CLI版) では AWS CLI を使い、各リソースを一つずつ手動で作成していましたが、本記事ではそれらすべてを Terraform でコード化し、再現性と管理性を大幅に向上させます

この記事でわかること

  • Terraform による AWS インフラのコード管理(IaC)
  • VPC / サブネット / ALB / ECS Fargate / RDS の一括構築
  • CodePipeline + CodeBuild + CodeDeploy による Blue/Green デプロイの自動化
  • Secrets Manager を利用した認証情報の安全な管理

前提条件

  • AWS アカウントを持っていること
  • Terraform がインストール済みであること(v1.5 以上推奨)
  • AWS CLI がインストール・設定済みであること
  • Docker Desktop がインストール済みであること
  • GitHub アカウントを持っていること

前提記事

参考記事

なお、本記事で使用するソースコードは、以下のGitHubリポジトリからダウンロードできます
flask-ecs-cicd(GitHubリポジトリ)

本記事で作るシステム

Flask(Python)製の Web アプリと MySQL DB を組み合わせたシステムに対して、以下の CI/CD パイプラインを構築します

フェーズ 内容
Source GitHub への push をトリガーにパイプライン起動
Build pytest でテスト → Docker ビルド → ECR へプッシュ
Deploy Blue/Green デプロイで ECS Fargate へゼロダウンタイム更新

アーキテクチャ概要

Flask-cicd最終2.png

通常、ECSはプライベートサブネットに配置するのが一般的ですが、本構成では、NAT Gatewayを利用していないため、ECRからのイメージ取得などの外部通信を行えるように、ECSをパブリックサブネットに配置しています

CI/CD の処理フロー

① 開発者が GitHub リポジトリにコードを push
② GitHub の変更をトリガーに CodePipeline が起動
③ CodeBuild がテストを実行し、Docker イメージをビルド
④ Docker イメージを Amazon ECR に push
⑤ CodeDeploy が Blue/Green デプロイを実行
⑥ ALB がトラフィックを Blue から Green へ切り替え
⑦ ECS Fargate が新しいコンテナを起動
⑧ Flask アプリケーションが RDS MySQL に接続
⑨ ユーザーがブラウザからアプリケーションへアクセス

Blue/Green デプロイの仕組み

Blue/Green デプロイでは、2つの環境を用意して安全に切り替えます

  1. 既存環境(Blue)が稼働中
  2. 新バージョンを Green 環境にデプロイ
  3. ヘルスチェック成功後、ALB がトラフィックを Green に切替
  4. 問題があれば即座に Blue にロールバック可能

この仕組みにより、ゼロダウンタイムかつ安全なリリースが実現できます

使用 AWS サービス一覧

サービス 用途
VPC ネットワーク分離
ALB ロードバランサー(Blue/Green 切替)
ECS Fargate コンテナのサーバーレス実行
ECR Docker イメージレジストリ
RDS MySQL マネージド DB
CodePipeline CI/CD パイプライン管理
CodeBuild ビルド・テスト自動化
CodeDeploy Blue/Green デプロイ
Secrets Manager DB 認証情報の安全な管理
IAM 各サービスの権限管理

プロジェクトのディレクトリ構成

flask-ecs-cicd/
├── terraform/
│   ├── main.tf                  # AWSプロバイダー設定・データソース定義
│   ├── versions.tf              # Terraform・プロバイダーのバージョン管理
│   ├── variables.tf             # 入力変数の定義(再利用性向上)
│   ├── terraform.tfvars         # 変数の値(環境ごとの設定 ※Git管理外推奨)
│   ├── vpc.tf                   # VPC・サブネット・IGW・ルートテーブル
│   ├── security_groups.tf       # セキュリティグループ定義
│   ├── rds.tf                   # RDS(MySQL)構築
│   ├── secrets.tf               # Secrets Manager(DBパスワード等の管理)
│   ├── ecr.tf                   # ECRリポジトリ作成(Dockerイメージ保存)
│   ├── alb.tf                   # ALB・ターゲットグループ・リスナー設定
│   ├── iam.tf                   # IAMロール・ポリシー定義
│   ├── ecs.tf                   # ECSクラスタ・タスク定義・サービス
│   ├── codebuild.tf             # CodeBuild(ビルド・テスト処理)
│   ├── codedeploy.tf            # CodeDeploy(Blue/Greenデプロイ)
│   ├── codepipeline.tf          # CodePipeline(CI/CD全体制御)
│   ├── cloudwatch.tf            # CloudWatch Logs・監視設定
│   ├── outputs.tf               # Terraformの出力値(ALB DNSなど)
│   └── bastion.tf               # 踏み台EC2(RDS初期化・SSM接続用)
│
├── app/
│   ├── app.py                  # Flaskアプリ本体
│   ├── requirements.txt        # Python依存パッケージ一覧
│   ├── test_app.py             # アプリのテストコード(pytest想定)
│   ├── Dockerfile              # アプリのコンテナイメージ定義
│   ├── templates/
│   │   └── index.html          # FlaskのHTMLテンプレート
│   └── init.sql                # DB初期化SQL(テーブル作成など)
│
├── buildspec.yml               # CodeBuildのビルド定義(テスト・Docker build/push)
├── appspec.yml                 # CodeDeployのデプロイ定義(Blue/Green設定)
└── taskdef.json                # ECSタスク定義(コンテナ設定・CPU/メモリ・環境変数)

手順1: プロジェクト構成の作成

※以降の手順で実施するコマンドは PowerShell を前提とします

1-1. プロジェクト初期ディレクトリ、ファイルの作成

# ルートディレクトリ作成
mkdir flask-ecs-cicd
cd flask-ecs-cicd

# ディレクトリ作成
mkdir terraform
mkdir app
mkdir app\templates

# =========================
# Terraform ファイル作成
# =========================
"main.tf","versions.tf","variables.tf","terraform.tfvars","vpc.tf","security_groups.tf",
"rds.tf","secrets.tf","ecr.tf","alb.tf","iam.tf","ecs.tf",
"codebuild.tf","codedeploy.tf","codepipeline.tf","cloudwatch.tf",
"outputs.tf","bastion.tf" | % {New-Item -ItemType File -Path "terraform\$_"}

# =========================
# アプリケーションファイル作成
# =========================
"app.py","requirements.txt","test_app.py","Dockerfile","init.sql" | % {
  New-Item -ItemType File -Path "app\$_"
}

# templates 配下
New-Item -ItemType File -Path "app\templates\index.html"

# =========================
# ルートファイル作成
# =========================
"buildspec.yml","appspec.yml","taskdef.json" | % {New-Item -ItemType File -Name $_}

1-2. アプリケーションファイルの作成

app/Dockerfile

FROM python:3.11-slim

WORKDIR /app

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

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

COPY . .

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "120", "app:app"]

app/test_app.py

import pytest
from app import app

@pytest.fixture
def client():
    app.config['TESTING'] = True
    with app.test_client() as client:
        yield client

def test_health_endpoint(client):
    """ヘルスチェックエンドポイントのテスト"""
    response = client.get('/health')
    assert response.status_code in [200, 503]

以下は参考記事(AWS CLI版) と同一のため省略

app/requirements.txt
app/app.py
app/init.sql
app/templates/index.html

手順2: CI/CD 設定ファイルの作成

2-1. buildspec.yml

CodeBuild が実行する処理内容を定義するファイルです
本構成では、ECR へのログイン、Docker イメージのビルドと push、テスト実行に加え、taskdef.json に含まれるプレースホルダを環境変数で置き換える処理を行います
また、imageDetail.json を生成することで、CodePipeline がコンテナイメージをデプロイ時に自動反映できるようにしています

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
      
      - export REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME
      - export COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - export IMAGE_TAG=${COMMIT_HASH:-latest}

      - echo REPOSITORY_URI=$REPOSITORY_URI
      - echo IMAGE_TAG=$IMAGE_TAG

      - echo Installing dependencies...
      - pip install -r app/requirements.txt

      - echo Replacing placeholders in taskdef.json...
      - sed -i "s|<AWS_ACCOUNT_ID>|$AWS_ACCOUNT_ID|g" taskdef.json
      - sed -i "s|<RDS_ENDPOINT>|$RDS_ENDPOINT|g" taskdef.json
      - sed -i "s|<DB_USER_SECRET_ARN>|$DB_USER_SECRET_ARN|g" taskdef.json
      - sed -i "s|<DB_PASSWORD_SECRET_ARN>|$DB_PASSWORD_SECRET_ARN|g" taskdef.json
      - sed -i "s|<REGION>|$AWS_DEFAULT_REGION|g" taskdef.json

      # 確認ログ(デバッグ用)
      - echo ===== taskdef.json after replace =====
      - cat taskdef.json

  build:
    commands:
      - echo Running tests...
      - cd app && python -m pytest test_app.py -v && cd ..

      - echo Building Docker image...
      - docker build -t $REPOSITORY_URI:latest ./app
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG

  post_build:
    commands:
      - echo Pushing Docker image...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG

      - echo Writing image definitions file...
      - printf '{"ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json

artifacts:
  files:
    - imageDetail.json
    - appspec.yml
    - taskdef.json

2-2. appspec.yml

CodeDeploy が ECS にどのようにデプロイするかを定義するファイルです
タスク定義の差し替えと、ALB のターゲットグループを切り替えるための設定を行います
これにより、Blue/Green デプロイ時にトラフィックの安全な切り替えが実現されます

version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "flask-container"
          ContainerPort: 5000

2-3. taskdef.json

ECS タスク定義のテンプレートファイルです
コンテナイメージ、ポート、環境変数、Secrets、ログ設定、ヘルスチェックなどを定義し、ECS 上でアプリケーションをどのように動かすかを決定します
CodeDeploy(ECS Blue/Green)では、デプロイ時にタスク定義を動的に更新する必要があるため、Terraform のタスク定義とは別にテンプレートファイル(taskdef.json)を用意します

ファイル内のプレースホルダは、CodeBuild によって実際の値に置き換えられ、最終的なタスク定義としてデプロイに使用されます

{
  "family": "flask-ecs-cicd-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/flask-ecs-cicd-ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/flask-ecs-cicd-ecsTaskRole",
  "containerDefinitions": [
    {
      "name": "flask-container",
      "image": "<IMAGE1_NAME>",
      "portMappings": [
        {
          "containerPort": 5000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "environment": [
        {
          "name": "DB_HOST",
          "value": "<RDS_ENDPOINT>"
        },
        {
          "name": "DB_NAME",
          "value": "flaskdb"
        }
      ],
      "secrets": [
        {
          "name": "DB_USER",
          "valueFrom": "<DB_USER_SECRET_ARN>"
        },
        {
          "name": "DB_PASSWORD",
          "valueFrom": "<DB_PASSWORD_SECRET_ARN>"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/flask-ecs-cicd-task",
          "awslogs-region": "<REGION>",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "healthCheck": {
        "command": [
          "CMD-SHELL",
          "curl -f http://localhost:5000/health || exit 1"
        ],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}

手順3: Terraform 基本設定ファイルの作成

Terraform 本体と AWS プロバイダーのバージョンを指定し、使用するリージョンやプロジェクト名、DB 情報、GitHub 連携情報などの変数をまとめて管理します
これにより、環境差分を減らしつつ、後続の Terraform ファイルから共通設定を再利用できるようにします

3-1. versions.tf(バージョン管理)

Terraform 本体とプロバイダーのバージョン要件を versions.tf に集約します

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

3-2. main.tf(プロバイダー設定)

provider "aws" {
  region = var.region
}

data "aws_caller_identity" "current" {}

3-3. variables.tf(変数定義)

variable "region" {
  description = "AWS リージョン"
  type        = string
  default     = "ap-northeast-1"
}

variable "project_name" {
  description = "プロジェクト名"
  type        = string
  default     = "flask-ecs-cicd"
}

variable "db_name" {
  description = "データベース名"
  type        = string
  default     = "flaskdb"
}

variable "db_username" {
  description = "データベースユーザー名"
  type        = string
  default     = "flaskuser"
  sensitive   = true
}

variable "db_password" {
  description = "データベースパスワード"
  type        = string
  sensitive   = true
}

variable "github_owner" {
  description = "GitHub ユーザー名"
  type        = string
}

variable "github_repo" {
  description = "GitHub リポジトリ名"
  type        = string
  default     = "flask-ecs-cicd"
}

variable "github_branch" {
  description = "GitHub ブランチ名"
  type        = string
  default     = "main"
}

variable "codestar_connection_arn" {
  description = "CodeStar Connections の ARN"
  type        = string
}

3-4. terraform.tfvars(変数値の設定)

region                  = "ap-northeast-1"
project_name            = "flask-ecs-cicd"
db_username             = "flaskuser"
db_password             = "your-secure-password"
github_owner            = "<GITHUB_USERNAME>"
github_repo             = "flask-ecs-cicd"
codestar_connection_arn = "<CONNECTION_ARN>"

以下の値はプレースホルダになっているため、自身の環境に合わせて必ず書き換えてください

項目 内容
<GITHUB_USERNAME> 自分の GitHub ユーザー名
<CONNECTION_ARN> CodeStar Connections の ARN

⚠️ codestar_connection_arn は、CodePipeline が GitHub にアクセスするための接続情報です
AWS マネジメントコンソールで CodeStar Connections を作成すると取得できます(手順19)
terraform.tfvars には機密情報が含まれるため、今回は .gitignore に追加します (手順18-2)
本番環境では環境変数(TF_VAR_*)や CI/CD のシークレット管理を利用することを推奨します

手順4: VPC とネットワークの構築(vpc.tf)

アプリケーションを配置するためのネットワーク基盤を作成します
VPC、パブリックサブネット、プライベートサブネット、インターネットゲートウェイ、ルートテーブルを定義し、外部公開が必要なリソースと内部専用リソースを分離できる構成を作成します
これにより、ALB はインターネットからアクセス可能にしつつ、RDS は外部から直接アクセスできない安全な配置にできます

# ==================================================
# VPC
# ==================================================
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

# ==================================================
# パブリックサブネット(ALB・ECS 用)
# ==================================================
resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "${var.region}a"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-1"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "${var.region}c"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-2"
  }
}

# ==================================================
# プライベートサブネット(RDS 用)
# ==================================================
resource "aws_subnet" "private_1" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.10.0/24"
  availability_zone = "${var.region}a"

  tags = {
    Name = "${var.project_name}-private-1"
  }
}

resource "aws_subnet" "private_2" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.11.0/24"
  availability_zone = "${var.region}c"

  tags = {
    Name = "${var.project_name}-private-2"
  }
}

# ==================================================
# インターネットゲートウェイ
# ==================================================
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-igw"
  }
}

# ==================================================
# ルートテーブル(パブリック)
# ==================================================
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "${var.project_name}-public-rt"
  }
}

resource "aws_route_table_association" "public_1" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_2" {
  subnet_id      = aws_subnet.public_2.id
  route_table_id = aws_route_table.public.id
}

手順5: セキュリティグループの作成(security_groups.tf)

各リソース間の通信を制御するためのファイアウォール設定を行います
ALB、ECS、RDS それぞれにセキュリティグループを作成し、必要な通信だけを許可します
具体的には、ALB は HTTP を受け付け、ECS は ALB からのアクセスのみを許可し、RDS は ECS や踏み台 EC2 からの MySQL 通信のみを許可する構成にします

# ==================================================
# ALB 用セキュリティグループ
# ==================================================
resource "aws_security_group" "alb" {
  name        = "${var.project_name}-alb-sg"
  description = "Security group for ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-alb-sg"
  }
}

# ==================================================
# ECS タスク用セキュリティグループ
# ==================================================
resource "aws_security_group" "ecs" {
  name        = "${var.project_name}-ecs-sg"
  description = "Security group for ECS tasks"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "Allow traffic from ALB"
    from_port       = 5000
    to_port         = 5000
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-ecs-sg"
  }
}

# ==================================================
# RDS 用セキュリティグループ
# ==================================================
resource "aws_security_group" "rds" {
  name        = "${var.project_name}-rds-sg"
  description = "Security group for RDS"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "MySQL from ECS"
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.ecs.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-rds-sg"
  }
}

手順6: RDS MySQL の構築(rds.tf)

アプリケーションが利用する MySQL データベースを構築します
RDS 用のサブネットグループを作成し、プライベートサブネット内に RDS インスタンスを配置します
これにより、DB は安全なネットワーク内で管理され、アプリケーションからのみアクセスできる構成になります

# ==================================================
# DB サブネットグループ
# ==================================================
resource "aws_db_subnet_group" "main" {
  name       = "${var.project_name}-db-subnet-group"
  subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id]

  tags = {
    Name = "${var.project_name}-db-subnet-group"
  }
}

# ==================================================
# RDS MySQL インスタンス
# ==================================================
resource "aws_db_instance" "main" {
  identifier     = "${var.project_name}-db"
  engine         = "mysql"
  engine_version = "8.0"
  instance_class = "db.t3.micro"

  allocated_storage     = 20
  max_allocated_storage = 100
  storage_type          = "gp3"

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  skip_final_snapshot = true
  publicly_accessible = false

  tags = {
    Name = "${var.project_name}-db"
  }
}

手順7: Secrets Manager(secrets.tf)

DB 接続に必要なユーザー名とパスワードを安全に管理します
Terraform 変数で受け取った認証情報を Secrets Manager に保存し、ECS タスクから参照できるようにします
これにより、認証情報をコンテナ定義やアプリケーションコードに直接書かずに済みます

# ==================================================
# DB ユーザー名シークレット
# ==================================================
resource "aws_secretsmanager_secret" "db_user" {
  name                    = "flask/db/user"
  recovery_window_in_days = 0
}

resource "aws_secretsmanager_secret_version" "db_user" {
  secret_id     = aws_secretsmanager_secret.db_user.id
  secret_string = var.db_username
}

# ==================================================
# DB パスワードシークレット
# ==================================================
resource "aws_secretsmanager_secret" "db_password" {
  name                    = "flask/db/password"
  recovery_window_in_days = 0
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id     = aws_secretsmanager_secret.db_password.id
  secret_string = var.db_password
}

手順8: ECR リポジトリの作成(ecr.tf)

Docker イメージを保存するためのコンテナレジストリを作成します
Flask アプリケーションのイメージを ECR に保存し、ECS Fargate から取得して起動できるようにします
CI/CD パイプラインでは、CodeBuild がビルドしたイメージをこの ECR に push します

resource "aws_ecr_repository" "main" {
  name                 = var.project_name
  image_tag_mutability = "MUTABLE"
  force_delete         = true

  image_scanning_configuration {
    scan_on_push = true
  }

  tags = {
    Name = var.project_name
  }
}

手順9: ALB の構築(alb.tf)

外部からのアクセスを受け付け、ECS にトラフィックを振り分ける Application Load Balancer を構築します
Blue 用と Green 用の 2 つのターゲットグループを作成し、CodeDeploy による Blue/Green デプロイに対応できるようにします
また、Terraform が CodeDeploy によるターゲット切替を差分として誤検知しないよう、ignore_changes を設定しています

# ==================================================
# ALB 本体
# ==================================================
resource "aws_lb" "main" {
  name               = "${var.project_name}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [aws_subnet.public_1.id, aws_subnet.public_2.id]

  tags = {
    Name = "${var.project_name}-alb"
  }
}

# ==================================================
# ターゲットグループ(Blue)
# ==================================================
resource "aws_lb_target_group" "blue" {
  name        = "${var.project_name}-tg-blue"
  port        = 5000
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "ip"

  health_check {
    path                = "/health"
    protocol            = "HTTP"
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    matcher             = "200"
  }

  tags = {
    Name = "${var.project_name}-tg-blue"
  }
}

# ==================================================
# ターゲットグループ(Green)
# ==================================================
resource "aws_lb_target_group" "green" {
  name        = "${var.project_name}-tg-green"
  port        = 5000
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "ip"

  health_check {
    path                = "/health"
    protocol            = "HTTP"
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    matcher             = "200"
  }

  tags = {
    Name = "${var.project_name}-tg-green"
  }
}

# ==================================================
# HTTP リスナー(本番用: ポート80)
# ==================================================
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.blue.arn
  }

  lifecycle {
    ignore_changes = [default_action]
  }
}

⚠️ lifecycle { ignore_changes = [default_action] } を指定している理由:
CodeDeploy が Blue/Green デプロイ時にリスナーのターゲットグループを自動で切り替えるため、Terraform が差分として検出しないようにしています

手順10: IAM ロールの作成(iam.tf)

AWS サービス同士が安全に連携するための権限設定を行います
ECS タスク実行ロール、ECS タスクロール、CodeBuild、CodeDeploy、CodePipeline 用の IAM ロールとポリシーを定義します
これにより、各サービスは必要最小限の権限で ECR、S3、Secrets Manager、CodeDeploy などを利用できるようになります

# ==================================================
# ECS タスク実行ロール
# ==================================================
resource "aws_iam_role" "ecs_task_execution" {
  name = "${var.project_name}-ecsTaskExecutionRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "ecs-tasks.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

# ECS タスク実行ロールに AWS 管理ポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# ECS タスク実行ロール用 Secrets Manager アクセス権限
resource "aws_iam_role_policy" "ecs_task_execution_secrets" {
  name = "${var.project_name}-ECSTaskExecutionSecretsPolicy"
  role = aws_iam_role.ecs_task_execution.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue"
        ]
        Resource = [
          aws_secretsmanager_secret.db_user.arn,
          aws_secretsmanager_secret.db_password.arn
        ]
      }
    ]
  })
}

# ==================================================
# ECS タスクロール
# ==================================================
resource "aws_iam_role" "ecs_task" {
  name = "${var.project_name}-ecsTaskRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "ecs-tasks.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

# ==================================================
# CodeBuild サービスロール
# ==================================================
resource "aws_iam_role" "codebuild" {
  name = "${var.project_name}-CodeBuildServiceRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "codebuild.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

# CodeBuild 用インラインポリシー
resource "aws_iam_role_policy" "codebuild" {
  name = "${var.project_name}-CodeBuildPolicy"
  role = aws_iam_role.codebuild.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:PutImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload"
        ]
        Resource = "*"
      },
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:${var.region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/*"
      },
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:GetObjectVersion",
          "s3:PutObject",
          "s3:GetBucketLocation",
          "s3:ListBucket"
        ]
        Resource = [
          aws_s3_bucket.codepipeline.arn,
          "${aws_s3_bucket.codepipeline.arn}/*"
        ]
      }
    ]
  })
}

# CodeBuild 用 Secrets Manager アクセス権限
resource "aws_iam_role_policy" "codebuild_secrets" {
  name = "${var.project_name}-CodeBuildSecretsPolicy"
  role = aws_iam_role.codebuild.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue"
        ]
        Resource = [
          aws_secretsmanager_secret.db_user.arn,
          aws_secretsmanager_secret.db_password.arn
        ]
      }
    ]
  })
}

# ==================================================
# CodeDeploy サービスロール
# ==================================================
resource "aws_iam_role" "codedeploy" {
  name = "${var.project_name}-CodeDeployServiceRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "codedeploy.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

# CodeDeploy サービスロールに AWS 管理ポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "codedeploy" {
  role       = aws_iam_role.codedeploy.name
  policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}

# ==================================================
# CodePipeline サービスロール
# ==================================================
resource "aws_iam_role" "codepipeline" {
  name = "${var.project_name}-CodePipelineServiceRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "codepipeline.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

# CodePipeline 用インラインポリシー
resource "aws_iam_role_policy" "codepipeline" {
  name = "${var.project_name}-CodePipelinePolicy"
  role = aws_iam_role.codepipeline.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetBucketVersioning",
          "s3:GetBucketLocation",
          "s3:ListBucket",
          "s3:GetObject",
          "s3:PutObject",
          "s3:GetObjectVersion"
        ]
        Resource = [
          aws_s3_bucket.codepipeline.arn,
          "${aws_s3_bucket.codepipeline.arn}/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "codebuild:StartBuild",
          "codebuild:BatchGetBuilds"
        ]
        Resource = "*"
      },
      {
        Effect = "Allow"
        Action = [
          "ecs:*",
          "codedeploy:*",
          "iam:PassRole"
        ]
        Resource = "*"
      },
      {
        Effect = "Allow"
        Action = [
          "codestar-connections:UseConnection",
          "codeconnections:UseConnection"
        ]
        Resource = var.codestar_connection_arn
      }
    ]
  })
}

手順11: CloudWatch Logs(cloudwatch.tf)

ECS コンテナのログを保存・確認するための CloudWatch Logs グループを作成します
アプリケーションの起動ログやエラーログを CloudWatch に集約することで、トラブルシューティングや動作確認がしやすくなります

# ==================================================
# ECS 用 CloudWatch Logs グループ
# ==================================================
resource "aws_cloudwatch_log_group" "ecs" {
  name              = "/ecs/${var.project_name}-task"
  retention_in_days = 30

  tags = {
    Name = "${var.project_name}-ecs-logs"
  }
}

手順12: 踏み台 EC2 の構築(bastion.tf)

プライベートサブネット内の RDS に接続するための踏み台 EC2 を作成します
SSM 経由で接続できるようにし、RDS の初期化や動作確認に利用します

※RDS はプライベートサブネットに配置されているため、外部から直接アクセスできません

# ==================================================
# Amazon Linux 2023 の最新 AMI を取得
# ==================================================
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# ==================================================
# 踏み台用セキュリティグループ
# ==================================================
resource "aws_security_group" "bastion" {
  name        = "${var.project_name}-bastion-sg"
  description = "Security group for bastion EC2"
  vpc_id      = aws_vpc.main.id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-bastion-sg"
  }
}

# RDS セキュリティグループに踏み台からのアクセスを許可
resource "aws_security_group_rule" "rds_from_bastion" {
  type                     = "ingress"
  from_port                = 3306
  to_port                  = 3306
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.bastion.id
  security_group_id        = aws_security_group.rds.id
}

# ==================================================
# 踏み台 EC2 用 IAM ロール(SSM アクセス)
# ==================================================
resource "aws_iam_role" "bastion" {
  name = "${var.project_name}-bastion-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "ec2.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "bastion_ssm" {
  role       = aws_iam_role.bastion.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "bastion" {
  name = "${var.project_name}-bastion-profile"
  role = aws_iam_role.bastion.name
}

# ==================================================
# 踏み台 EC2
# ==================================================
resource "aws_instance" "bastion" {
  ami                         = data.aws_ami.amazon_linux.id
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.public_1.id
  vpc_security_group_ids      = [aws_security_group.bastion.id]
  associate_public_ip_address = true
  iam_instance_profile        = aws_iam_instance_profile.bastion.name

  user_data = <<-EOF
    #!/bin/bash
    dnf install -y mariadb105
  EOF

  tags = {
    Name = "${var.project_name}-bastion"
  }
}

Amazon Linux 2023 には SSM Agent がプリインストールされているため、SSH なしで SSM 経由でコマンドを実行できます

手順13: ECS クラスタ・タスク定義・サービスの構築(ecs.tf)

Flask アプリケーションを Fargate 上で実行するための ECS 環境を構築します
ECS クラスタ、タスク定義、サービスを作成し、ALB と連携してアプリケーションを公開します
また、CodeDeploy による Blue/Green デプロイに対応するため、deployment_controller や ignore_changes を設定しています

# ==================================================
# ECS クラスタ
# ==================================================
resource "aws_ecs_cluster" "main" {
  name = "${var.project_name}-cluster"

  tags = {
    Name = "${var.project_name}-cluster"
  }
}

# ==================================================
# ECS タスク定義
# ==================================================
resource "aws_ecs_task_definition" "main" {
  family                   = "${var.project_name}-task"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"
  execution_role_arn       = aws_iam_role.ecs_task_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name      = "flask-container"
      image     = "${aws_ecr_repository.main.repository_url}:latest"
      essential = true

      portMappings = [
        {
          containerPort = 5000
          protocol      = "tcp"
        }
      ]

      environment = [
        { name = "DB_HOST", value = aws_db_instance.main.address },
        { name = "DB_NAME", value = var.db_name }
      ]

      secrets = [
        { name = "DB_USER", valueFrom = aws_secretsmanager_secret.db_user.arn },
        { name = "DB_PASSWORD", valueFrom = aws_secretsmanager_secret.db_password.arn }
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.ecs.name
          "awslogs-region"        = var.region
          "awslogs-stream-prefix" = "ecs"
        }
      }

      healthCheck = {
        command     = ["CMD-SHELL", "curl -f http://localhost:5000/health || exit 1"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 60
      }
    }
  ])

  lifecycle {
    ignore_changes = [container_definitions]
  }
}

# ==================================================
# ECS サービス(Blue/Green デプロイ対応)
# ==================================================
resource "aws_ecs_service" "main" {
  name            = "${var.project_name}-service"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.main.arn
  desired_count   = 1
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = [aws_subnet.public_1.id, aws_subnet.public_2.id]
    security_groups  = [aws_security_group.ecs.id]
    assign_public_ip = true
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.blue.arn
    container_name   = "flask-container"
    container_port   = 5000
  }

  deployment_controller {
    type = "CODE_DEPLOY"
  }

  lifecycle {
    ignore_changes = [task_definition, load_balancer]
  }

  depends_on = [aws_lb_listener.http]
}

⚠️ deployment_controller { type = "CODE_DEPLOY" } を指定することで、CodeDeploy による Blue/Green デプロイが有効になります
lifecycle { ignore_changes } は CodeDeploy がタスク定義やロードバランサーを自動更新するため、Terraform が差分検出しないようにする設定です

手順14: CodeBuild プロジェクトの作成(codebuild.tf)

ソースコードのテスト、Docker イメージのビルド、ECR への push を自動化するためのビルド環境を作成します
CodePipeline の Build ステージから呼び出され、buildspec.yml に従って処理が実行されます
CI/CD における自動テストとイメージ作成の役割を担います

# ==================================================
# CodeBuild プロジェクト
# ==================================================
resource "aws_codebuild_project" "main" {
  name          = "${var.project_name}-build"
  description   = "Build project for ${var.project_name}"
  service_role  = aws_iam_role.codebuild.arn
  build_timeout = 30

  artifacts {
    type = "CODEPIPELINE"
  }

  environment {
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/standard:7.0"
    type            = "LINUX_CONTAINER"
    privileged_mode = true # Docker buildに必須

    # ECR関連)
    environment_variable {
      name  = "AWS_ACCOUNT_ID"
      value = data.aws_caller_identity.current.account_id
    }

    environment_variable {
      name  = "AWS_DEFAULT_REGION"
      value = var.region
    }

    environment_variable {
      name  = "IMAGE_REPO_NAME"
      value = aws_ecr_repository.main.name
    }

    # RDS エンドポイント
    environment_variable {
      name  = "RDS_ENDPOINT"
      value = aws_db_instance.main.address
    }

    # DBユーザー(Secrets Manager ARN)
    environment_variable {
      name  = "DB_USER_SECRET_ARN"
      value = aws_secretsmanager_secret.db_user.arn
    }

    # DBパスワード(Secrets Manager ARN)
    environment_variable {
      name  = "DB_PASSWORD_SECRET_ARN"
      value = aws_secretsmanager_secret.db_password.arn
    }
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec.yml"
  }

  logs_config {
    cloudwatch_logs {
      group_name  = "/aws/codebuild/${var.project_name}-build"
      stream_name = "build-log"
    }
  }

  tags = {
    Name = "${var.project_name}-codebuild"
  }
}

手順15: CodeDeploy の構築(codedeploy.tf)

ECS の Blue/Green デプロイを管理するための CodeDeploy アプリケーションとデプロイグループを作成すします
ALB の Blue/Green ターゲットグループと連携し、新バージョンのコンテナへ安全に切り替えられるようにします
障害時にはロールバックも可能です

# ==================================================
# CodeDeploy アプリケーション
# ==================================================
resource "aws_codedeploy_app" "main" {
  compute_platform = "ECS"
  name             = var.project_name
}

# ==================================================
# CodeDeploy デプロイグループ(Blue/Green)
# ==================================================
resource "aws_codedeploy_deployment_group" "main" {
  app_name               = aws_codedeploy_app.main.name
  deployment_group_name  = "${var.project_name}-dg"
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
  service_role_arn       = aws_iam_role.codedeploy.arn

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }

    terminate_blue_instances_on_deployment_success {
      action                           = "TERMINATE"
      termination_wait_time_in_minutes = 5
    }
  }

  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }

  ecs_service {
    cluster_name = aws_ecs_cluster.main.name
    service_name = aws_ecs_service.main.name
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [aws_lb_listener.http.arn]
      }

      target_group {
        name = aws_lb_target_group.blue.name
      }

      target_group {
        name = aws_lb_target_group.green.name
      }
    }
  }
}

手順16: CodePipeline の構築(codepipeline.tf)

GitHub への push を起点に、Build・Deploy を自動で流す CI/CD パイプライン全体を作成します
Source、Build、Deploy の各ステージを定義し、GitHub・CodeBuild・CodeDeploy を連携させます
これにより、コード変更から本番反映までを自動化できます

# ==================================================
# S3 バケット(アーティファクト保存用)
# ==================================================
resource "aws_s3_bucket" "codepipeline" {
  bucket        = "${var.project_name}-pipeline-${data.aws_caller_identity.current.account_id}"
  force_destroy = true

  tags = {
    Name = "${var.project_name}-pipeline-artifacts"
  }
}

# ==================================================
# CodePipeline
# ==================================================
resource "aws_codepipeline" "main" {
  name     = "${var.project_name}-pipeline"
  role_arn = aws_iam_role.codepipeline.arn

  artifact_store {
    location = aws_s3_bucket.codepipeline.bucket
    type     = "S3"
  }

  # ---- Source Stage ----
  stage {
    name = "Source"

    action {
      name             = "SourceAction"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["SourceOutput"]

      configuration = {
        ConnectionArn        = var.codestar_connection_arn
        FullRepositoryId     = "${var.github_owner}/${var.github_repo}"
        BranchName           = var.github_branch
        OutputArtifactFormat = "CODE_ZIP"
      }
    }
  }

  # ---- Build Stage ----
  stage {
    name = "Build"

    action {
      name             = "BuildAction"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      input_artifacts  = ["SourceOutput"]
      output_artifacts = ["BuildOutput"]

      configuration = {
        ProjectName = aws_codebuild_project.main.name
      }
    }
  }

  # ---- Deploy Stage ----
  stage {
    name = "Deploy"

    action {
      name            = "DeployAction"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "CodeDeployToECS"
      version         = "1"
      input_artifacts = ["BuildOutput"]

      configuration = {
        ApplicationName                = aws_codedeploy_app.main.name
        DeploymentGroupName            = aws_codedeploy_deployment_group.main.deployment_group_name
        TaskDefinitionTemplateArtifact = "BuildOutput"
        TaskDefinitionTemplatePath     = "taskdef.json"
        AppSpecTemplateArtifact        = "BuildOutput"
        AppSpecTemplatePath            = "appspec.yml"
        Image1ArtifactName             = "BuildOutput"
        Image1ContainerName            = "IMAGE1_NAME"
      }
    }
  }

  tags = {
    Name = "${var.project_name}-pipeline"
  }
}

手順17: 出力値の定義(outputs.tf)

Terraform 実行後に必要となる情報を確認しやすくします
ALB の URL、ECR リポジトリ URL、RDS エンドポイント、ECS クラスタ名、踏み台 EC2 のインスタンス ID などを出力し、後続の操作や動作確認に利用できるようにします

output "alb_dns_name" {
  description = "ALB の DNS 名(ブラウザでアクセスする URL)"
  value       = "http://${aws_lb.main.dns_name}"
}

output "ecr_repository_url" {
  description = "ECR リポジトリ URL"
  value       = aws_ecr_repository.main.repository_url
}

output "rds_endpoint" {
  description = "RDS エンドポイント"
  value       = aws_db_instance.main.address
}

output "ecs_cluster_name" {
  description = "ECS クラスタ名"
  value       = aws_ecs_cluster.main.name
}

output "ecs_service_name" {
  description = "ECS サービス名"
  value       = aws_ecs_service.main.name
}

output "codepipeline_name" {
  description = "CodePipeline 名"
  value       = aws_codepipeline.main.name
}

output "bastion_instance_id" {
  description = "踏み台 EC2 のインスタンス ID"
  value       = aws_instance.bastion.id
}

手順18: GitHub リポジトリの作成とプッシュ

18-1. GitHub リポジトリの作成

ブラウザで GitHub - New Repository にアクセスし、以下の通り作成します

  • Repository name: flask-ecs-cicd
  • Description: Flask + MySQL app with AWS ECS Fargate and Terraform
  • Public/Private: 任意(Private 推奨)

18-2. ローカルリポジトリの初期化とプッシュ

# プロジェクトルートに移動
cd flask-ecs-cicd

# .gitignore の作成
@"
terraform/.terraform/
terraform/*.tfstate
terraform/*.tfstate.backup
terraform/terraform.tfvars
terraform/.terraform.lock.hcl
__pycache__/
*.pyc
"@ | Set-Content .gitignore

# Git 初期化とプッシュ
git init
git add .
git commit -m "Initial commit: Flask + MySQL with Terraform CI/CD"
git branch -M main
git remote add origin https://github.com/<GITHUB_USERNAME>/flask-ecs-cicd.git
git push -u origin main

⚠️ <GITHUB_USERNAME>をご自分のユーザー名に書き換えてください

手順19: GitHub 接続の作成(CodeStar Connections)

Terraform の CodePipeline が GitHub リポジトリにアクセスするために、CodeStar Connections の GitHub 接続を作成します
これは AWS コンソールでの手動操作が必要なステップです

  1. AWS マネジメントコンソールにログイン
  2. CodePipeline コンソールを開く
  3. 左側のメニューから「設定」→「接続」を選択
  4. 「接続を作成」ボタンをクリック
  5. プロバイダーで「GitHub」を選択
  6. 接続名を入力: flask-ecs-cicd-connection
  7. 「GitHub に接続」ボタンをクリック
  8. GitHub アカウントでログインし、アクセスを許可
  9. 「Install」→「接続」をクリック
  10. 接続 ARN をコピーし、terraform.tfvarscodestar_connection_arn に設定します

手順20: Terraform の実行

ここまでで Terraform ファイル、アプリケーションコード、CI/CD 設定ファイルの作成がすべて完了しました
GitHub リポジトリと CodeStar 接続も準備できたので、Terraform を実行して AWS リソースを一括作成します

20-1. 初期化

cd terraform
terraform init

出力例:

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.x.x...

Terraform has been successfully initialized!

20-2. 実行計画の確認

terraform plan

作成されるリソースの一覧が表示されます

主なリソース

リソース 内容
VPC 関連 8 VPC, サブネット×4, IGW, ルートテーブル, 関連付け×2
セキュリティグループ 3 ALB用, ECS用, RDS用
RDS 2 サブネットグループ, DB インスタンス
Secrets Manager 4 シークレット×2, バージョン×2
ECR 1 リポジトリ
ALB 4 ALB, ターゲットグループ×2, リスナー
IAM 10+ ロール×5, ポリシー×5+
ECS 3 クラスタ, タスク定義, サービス
CI/CD 4 CodeBuild, CodeDeploy App, デプロイグループ, CodePipeline
踏み台 5 EC2, セキュリティグループ, SG ルール, IAM ロール, インスタンスプロファイル
その他 2 S3 バケット, CloudWatch Logs

20-3. リソースの作成

terraform apply

yes を入力して実行します
RDS の作成に5〜10分程度かかります

出力例:

Apply complete! Resources: 40+ added, 0 changed, 0 destroyed.

Outputs:

alb_dns_name       = "http://flask-ecs-cicd-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com"
ecr_repository_url = "123456789000.dkr.ecr.ap-northeast-1.amazonaws.com/flask-ecs-cicd"
rds_endpoint       = "flask-ecs-cicd-db.xxxxx.ap-northeast-1.rds.amazonaws.com"
ecs_cluster_name   = "flask-ecs-cicd-cluster"
ecs_service_name   = "flask-ecs-cicd-service"
codepipeline_name  = "flask-ecs-cicd-pipeline"

手順21: 初回 Docker イメージのビルドと ECR へのプッシュ

terraform apply により ECR リポジトリが作成されたので、ECS が起動するための初回イメージをプッシュします

# プロジェクトルートに戻る
cd ..

# Terraform の出力から ECR リポジトリ URL を取得
$ECR_REPO_URI = terraform -chdir=terraform output -raw ecr_repository_url
$REGION = (aws configure get region).Trim()

# ECR にログイン
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ECR_REPO_URI.Split('/')[0]

# Docker イメージをビルド
docker build -t ${ECR_REPO_URI}:latest ./app

# ECR にプッシュ
docker push ${ECR_REPO_URI}:latest

# 成功判定
if ($LASTEXITCODE -eq 0) {
    Write-Host "Docker イメージの push に成功しました" -ForegroundColor Green
} else {
    Write-Host "Docker イメージの push に失敗しました" -ForegroundColor Red
}

⚠️ 本手順を実行する前に、Docker Desktop を起動しておいてください

手順22: RDS の初期化

app/init.sql にテーブル定義と初期データをまとめて記述し、踏み台 EC2 から RDS に対して実行します
SQL を PowerShell に直接埋め込まないため、可読性と保守性が向上します

22-1. init.sql を踏み台 EC2 に転送

# 接続情報の取得
$INSTANCE_ID = terraform -chdir=terraform output -raw bastion_instance_id
$RDS_HOST    = terraform -chdir=terraform output -raw rds_endpoint
$DB_USER     = "flaskuser"
$DB_PASS     = "your-secure-password"
$DB_NAME     = "flaskdb"

# init.sql の内容を読み込む
$SQL_CONTENT = Get-Content -Path .\app\init.sql -Raw

# EC2上に init.sql を配置するコマンドを作成
$commands = @(
@"
cat > /home/ec2-user/init.sql <<'EOSQL'
$SQL_CONTENT
EOSQL
"@
)

# SSM パラメータを JSON 化
$PARAMS = @{
  commands = $commands
} | ConvertTo-Json -Compress

# 一時ファイルを絶対パスで作成
$PARAM_FILE = Join-Path (Get-Location) "ssm-params.json"

# UTF-8(BOMなし)で保存
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($PARAM_FILE, $PARAMS, $Utf8NoBom)

# 確認
Write-Host "PARAM_FILE = $PARAM_FILE"

# SSM コマンド送信
$COMMAND_ID = aws ssm send-command `
  --instance-ids $INSTANCE_ID `
  --document-name "AWS-RunShellScript" `
  --parameters file://$PARAM_FILE `
  --query "Command.CommandId" `
  --output text

Write-Host "COMMAND_ID = $COMMAND_ID"

22-2. init.sql を実行して RDS を初期化

# MySQL コマンドの作成(踏み台EC2上で実行)
$commands = @(
"mysql -h $RDS_HOST -u $DB_USER -p'$DB_PASS' $DB_NAME < /home/ec2-user/init.sql"
)

# SSM パラメータを JSON に変換
$PARAMS = @{
  commands = $commands
} | ConvertTo-Json -Compress

# 一時JSONファイルのパスを作成
$PARAM_FILE = Join-Path (Get-Location) "ssm-run-mysql.json"

# UTF-8(BOMなし)で保存
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($PARAM_FILE, $PARAMS, $Utf8NoBom)

# パラメータファイルの存在確認
if (-not (Test-Path $PARAM_FILE)) {
    Write-Host "パラメータファイルの作成に失敗しました" -ForegroundColor Red
    exit 1
}

# SSM send-command 実行
$COMMAND_ID = aws ssm send-command `
  --instance-ids $INSTANCE_ID `
  --document-name "AWS-RunShellScript" `
  --parameters file://$PARAM_FILE `
  --query "Command.CommandId" `
  --output text

# 実行IDを表示
Write-Host "MySQL 実行 CommandId = $COMMAND_ID"

22-3. 実行結果の確認

# SSMコマンドの実行完了を待機
# send-command は非同期実行のため、少し待機してから結果を取得する
Start-Sleep -Seconds 10

# SSMコマンドの実行結果を取得
# 指定した CommandId と InstanceId の実行結果を取得し、ステータス・標準出力・エラー内容を表示する
aws ssm get-command-invocation `
  --command-id $COMMAND_ID `
  --instance-id $INSTANCE_ID `
  --query '{Status:Status,Output:StandardOutputContent,Error:StandardErrorContent}' `
  --output table

期待される出力:
image.png

StatusSuccess と表示されれば初期化完了です

テーブル内容確認

# テーブル作成結果の確認用 MySQL コマンドを作成
$commands = @(
"mysql -h $RDS_HOST -u $DB_USER -p'$DB_PASS' $DB_NAME -e `"SHOW TABLES; SELECT * FROM users;`""
)

# SSM に渡すパラメータを JSON 形式に変換
$PARAMS = @{
  commands = $commands
} | ConvertTo-Json -Compress

# JSON パラメータファイルの保存先を作成
$PARAM_FILE = Join-Path (Get-Location) "ssm-check-mysql.json"

# UTF-8(BOMなし)で保存
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($PARAM_FILE, $PARAMS, $Utf8NoBom)

# パラメータファイルの存在確認
if (-not (Test-Path $PARAM_FILE)) {
    Write-Host "確認用パラメータファイルの作成に失敗しました" -ForegroundColor Red
    exit 1
}

# SSM send-command を実行
$CHECK_ID = aws ssm send-command `
  --instance-ids $INSTANCE_ID `
  --document-name "AWS-RunShellScript" `
  --parameters file://$PARAM_FILE `
  --query "Command.CommandId" `
  --output text

# 発行された CommandId を表示
Write-Host "CHECK_ID = $CHECK_ID"

# CommandId が取れなければ終了
if ([string]::IsNullOrWhiteSpace($CHECK_ID)) {
    Write-Host "SSM コマンド送信に失敗しました" -ForegroundColor Red
    exit 1
}

# 実行完了まで少し待機
Start-Sleep -Seconds 5

# SSM コマンドの実行結果を取得
aws ssm get-command-invocation `
  --command-id $CHECK_ID `
  --instance-id $INSTANCE_ID `
  --query '{Status:Status,Output:StandardOutputContent,Error:StandardErrorContent}' `
  --output table

手順23: CI/CD パイプラインの動作確認

23-1. パイプラインの実行状況を確認

# CodePipeline の各ステージの状態を確認
aws codepipeline get-pipeline-state `
  --name flask-ecs-cicd-pipeline `
  --query 'stageStates[*].{Stage:stageName,Status:latestExecution.status}' `
  --output table

期待される出力:
image.png

⚠️ get-pipeline-state は前回実行の状態が表示される場合があります
正確な実行状況は CodePipeline コンソール、または list-action-executions で確認してください

各ステージの所要時間:

ステージ 内容 目安時間
Source GitHub からコード取得 5〜20秒
Build CodeBuild → テスト → Docker build → ECR push 2〜5分
Deploy CodeDeploy → ECS Blue/Green → ヘルスチェック 3〜6分

23-2. ECS タスクの状態確認

# ECS サービスで起動しているタスク一覧を取得
aws ecs list-tasks `
  --cluster flask-ecs-cicd-cluster `
  --service-name flask-ecs-cicd-service `
  --query 'taskArns' `
  --output table

23-3. アプリケーションの動作確認

# ALB の URL を取得(Terraform優先 → 失敗時はAWS CLI)
$ALB_DNS = terraform -chdir=terraform output -raw alb_dns_name 2>$null

if (-not $ALB_DNS) {
  $ALB_DNS = aws elbv2 describe-load-balancers `
    --names flask-ecs-cicd-alb `
    --query "LoadBalancers[0].DNSName" `
    --output text
}

# ヘルスチェック
try {
  $response = Invoke-RestMethod -Uri "$URL/health"
  Write-Host "Health Check OK" -ForegroundColor Green
  $response
}
catch {
  Write-Host "Health Check FAILED" -ForegroundColor Red
  Write-Host $_
}

# URLを組み立て
$URL = if ($ALB_DNS.StartsWith("http")) { $ALB_DNS } else { "http://$ALB_DNS" }
Write-Host $URL -ForegroundColor Green

期待される結果として、以下のようなレスポンスが返れば正常です

image.png

database: connected → RDS接続成功
status: healthy → アプリ正常
environment: ecs-fargate → 実行環境
version: アプリバージョン

ブラウザで表示された URL を開くと、Flask アプリケーションが動作していることを確認できます

image.png

23-4. コード変更のテスト(CI/CD の動作確認)

app.py のバージョンを変更して GitHub に push すると、CodePipeline が自動で起動し、Build → Deploy が再実行されます

app/app.py ファイルのhealth() 関数のversionを'2.0.0'から'3.0.0'に手動変更します
image.png

変更したらGitHub にプッシュします

# GitHub にプッシュ
git add .\app\app.py
git commit -m "Update version to 3.0.0"
git push origin main

プッシュ後、CodePipeline が自動的に起動し、Blue/Green デプロイが実行されます

CodePipeline の実行状況を確認

aws codepipeline get-pipeline-state `
  --name flask-ecs-cicd-pipeline `
  --query 'stageStates[*].{Stage:stageName,Status:latestExecution.status}' `
  --output table

期待される出力:
image.png

アプリケーションのヘルスチェックを確認

$ALB_URL = terraform -chdir=terraform output -raw alb_dns_name
Invoke-RestMethod -Uri "$ALB_URL/health"

期待される出力:

database environment status version
connected ecs-fargate healthy 3.0.0

※version が 3.0.0 に更新されていれば、CI/CD によるデプロイが正常に完了しています

全リソースのクリーンアップ

Terraform の最大のメリットの一つがクリーンアップの容易さです
Terraform では1コマンドで完了します

cd terraform
terraform destroy

yes を入力すると、作成したすべてのリソースが削除されます

⚠️ terraform destroy 実行前に、CodeDeploy のデプロイが進行中でないことを確認してください
進行中のデプロイがある場合は、先に停止してから実行します

# デプロイが進行中の場合は停止
aws deploy stop-deployment --deployment-id <DEPLOYMENT_ID>

トラブルシューティング

1. terraform apply で RDS 作成がタイムアウト

症状: RDS インスタンスの作成に時間がかかりタイムアウトします
解決方法:

# タイムアウト後に再実行すれば、作成途中のリソースから継続されます
terraform apply

2. ECS サービスのタスクが起動しない

症状: ECS タスクが STOPPED になります
解決方法:

# サービスで起動しているタスク ARN を取得
$TASK_ARN = aws ecs list-tasks `
  --cluster flask-ecs-cicd-cluster `
  --service-name flask-ecs-cicd-service `
  --query 'taskArns[0]' `
  --output text

# タスクの停止理由を確認
aws ecs describe-tasks `
  --cluster flask-ecs-cicd-cluster `
  --tasks $TASK_ARN `
  --query 'tasks[0].stoppedReason' `
  --output text

よくある原因:

  • ECR に初回イメージがプッシュされていない → 手順21を実行
  • RDS が初期化されていない → 手順22を実行
  • セキュリティグループの設定ミス → terraform plan で差分を確認

3. CodeDeploy の Blue/Green デプロイが失敗

症状: Deploy ステージで失敗します
解決方法:

# デプロイの詳細を確認
# 最新のデプロイ ID を取得
$DEPLOY_ID = aws deploy list-deployments `
  --application-name flask-ecs-cicd `
  --deployment-group-name flask-ecs-cicd-dg `
  --query 'deployments[0]' `
  --output text

# デプロイの詳細を確認
aws deploy get-deployment `
  --deployment-id $DEPLOY_ID

4. terraform destroy が失敗する

症状: リソースの削除順序で依存関係エラーが発生します
解決方法:

# ECS サービスの desired_count を 0 にしてから destroy
aws ecs update-service `
  --cluster flask-ecs-cicd-cluster `
  --service flask-ecs-cicd-service `
  --desired-count 0

# 再度 destroy を実行
terraform destroy

まとめ

本記事では、参考記事 で AWS CLI を使って手動構築していた CI/CD 環境を、Terraform でコード化しました
実務にも応用可能であり、小規模なサービスから本番運用までスケールできる構成になっています

Terraform を使うことで得られる主なメリット:

  • 宣言的な記述により、インフラの「あるべき姿」をコードで表現できます
  • terraform plan で変更内容を事前に確認できます
  • terraform destroy で全リソースを1コマンドで削除できます
  • Git でバージョン管理し、チームで共有できます
  • 冪等性が保証され、何度実行しても同じ結果になります

参考記事で AWS の各サービスの仕組みを理解した上で、本記事の Terraform 化に取り組むことで、実務レベルの IaC スキルを身につけることができます

1
3
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
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?