はじめに
本記事では 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 へゼロダウンタイム更新 |
アーキテクチャ概要
通常、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つの環境を用意して安全に切り替えます
- 既存環境(Blue)が稼働中
- 新バージョンを Green 環境にデプロイ
- ヘルスチェック成功後、ALB がトラフィックを Green に切替
- 問題があれば即座に 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 コンソールでの手動操作が必要なステップです
- AWS マネジメントコンソールにログイン
- CodePipeline コンソールを開く
- 左側のメニューから「設定」→「接続」を選択
- 「接続を作成」ボタンをクリック
- プロバイダーで「GitHub」を選択
- 接続名を入力:
flask-ecs-cicd-connection - 「GitHub に接続」ボタンをクリック
- GitHub アカウントでログインし、アクセスを許可
- 「Install」→「接続」をクリック
- 接続 ARN をコピーし、
terraform.tfvarsのcodestar_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
Status が Success と表示されれば初期化完了です
テーブル内容確認
# テーブル作成結果の確認用 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
⚠️
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
期待される結果として、以下のようなレスポンスが返れば正常です
database: connected → RDS接続成功
status: healthy → アプリ正常
environment: ecs-fargate → 実行環境
version: アプリバージョン
ブラウザで表示された URL を開くと、Flask アプリケーションが動作していることを確認できます
23-4. コード変更のテスト(CI/CD の動作確認)
app.py のバージョンを変更して GitHub に push すると、CodePipeline が自動で起動し、Build → Deploy が再実行されます
app/app.py ファイルのhealth() 関数のversionを'2.0.0'から'3.0.0'に手動変更します

変更したら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
アプリケーションのヘルスチェックを確認
$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 スキルを身につけることができます




