はじめに
現代のクラウドインフラ構築において、Infrastructure as Code(IaC)とコンテナ技術は欠かせない要素となっています。特にAWSとDockerを組み合わせた環境を効率的に管理するためには、Terraformによるコード化が強力なソリューションとなります。この記事では、Terraformを使用してAWS上のDocker環境を構築・管理する方法について、具体的な手順とベストプラクティスを解説します。
Terraformとは
Terraformは、HashiCorp社が開発したオープンソースのIaCツールで、クラウドリソースを宣言的な設定ファイルで定義し、バージョン管理できます。主な特徴は以下の通りです:
- プロバイダーシステム: 様々なクラウドプロバイダー(AWS、GCP、Azureなど)に対応
- HCL(HashiCorp Configuration Language): 読みやすく書きやすい独自の設定言語
- ステート管理: インフラの現在の状態を追跡し、差分適用が可能
- モジュール化: 再利用可能なコンポーネントとしてインフラを設計可能
前提条件
この記事を進めるにあたり、以下が必要です:
- AWS アカウント
- Terraformのインストール(バージョン1.0以上推奨)
- AWS CLIのセットアップと認証情報の設定
- Dockerの基本的な知識
プロジェクト構成
まずは、効率的な管理のためのプロジェクト構成を考えましょう。以下のような構成がおすすめです:
terraform-aws-docker-project/
├── main.tf # メインの設定ファイル
├── variables.tf # 変数定義
├── outputs.tf # 出力定義
├── provider.tf # プロバイダー設定
├── ecr.tf # ECR関連設定
├── ecs.tf # ECS関連設定
├── vpc.tf # ネットワーク設定
├── security.tf # セキュリティグループ等
└── terraform.tfvars # 変数値(gitignore推奨)
1. プロバイダーとバックエンドの設定
まずはprovider.tf
ファイルでAWSプロバイダーとステート管理の設定を行います:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "aws-docker/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-lock"
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = var.aws_region
}
2. 変数の定義
variables.tf
で必要な変数を定義します:
variable "aws_region" {
description = "The AWS region to deploy resources"
type = string
default = "ap-northeast-1"
}
variable "project_name" {
description = "Name of the project"
type = string
default = "docker-app"
}
variable "environment" {
description = "Environment (dev, staging, prod)"
type = string
default = "dev"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "container_image" {
description = "Docker image to deploy"
type = string
default = "nginx:latest"
}
variable "container_port" {
description = "Port exposed by the docker image"
type = number
default = 80
}
3. VPCとネットワーク設定
vpc.tf
でAWSのネットワークリソースを定義します:
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.project_name}-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-subnet-${count.index}"
Environment = var.environment
}
}
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.project_name}-private-subnet-${count.index}"
Environment = var.environment
}
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
Environment = var.environment
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "${var.project_name}-public-rt"
Environment = var.environment
}
}
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# 利用可能なAZを取得
data "aws_availability_zones" "available" {
state = "available"
}
4. Amazon ECRリポジトリの作成
ecr.tf
でDockerイメージを保存するECRリポジトリを作成します:
resource "aws_ecr_repository" "app" {
name = "${var.project_name}-repo"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
tags = {
Name = "${var.project_name}-ecr"
Environment = var.environment
}
}
# ECRライフサイクルポリシー(古いイメージを自動削除)
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [{
rulePriority = 1,
description = "Keep last 10 images",
selection = {
tagStatus = "any",
countType = "imageCountMoreThan",
countNumber = 10
},
action = {
type = "expire"
}
}]
})
}
# ECRプッシュ用の出力
output "ecr_repository_url" {
value = aws_ecr_repository.app.repository_url
}
5. ECSクラスターとサービスの設定
ecs.tf
でAmazon ECSサービスを設定します:
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Name = "${var.project_name}-ecs-cluster"
Environment = var.environment
}
}
resource "aws_ecs_task_definition" "app" {
family = "${var.project_name}-task"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.ecs_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_role.arn
container_definitions = jsonencode([
{
name = "${var.project_name}-container"
image = var.container_image
essential = true
portMappings = [
{
containerPort = var.container_port
hostPort = var.container_port
protocol = "tcp"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.ecs_logs.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
}
])
tags = {
Name = "${var.project_name}-task-definition"
Environment = var.environment
}
}
resource "aws_cloudwatch_log_group" "ecs_logs" {
name = "/ecs/${var.project_name}"
retention_in_days = 30
tags = {
Name = "${var.project_name}-log-group"
Environment = var.environment
}
}
resource "aws_ecs_service" "app" {
name = "${var.project_name}-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 2
deployment_minimum_healthy_percent = 50
deployment_maximum_percent = 200
launch_type = "FARGATE"
scheduling_strategy = "REPLICA"
network_configuration {
security_groups = [aws_security_group.ecs_tasks.id]
subnets = aws_subnet.private.*.id
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "${var.project_name}-container"
container_port = var.container_port
}
lifecycle {
ignore_changes = [task_definition, desired_count]
}
depends_on = [aws_lb_listener.http]
tags = {
Name = "${var.project_name}-ecs-service"
Environment = var.environment
}
}
6. セキュリティグループとIAMロールの設定
security.tf
でセキュリティ設定を行います:
# ALB用セキュリティグループ
resource "aws_security_group" "alb" {
name = "${var.project_name}-alb-sg"
description = "Allow HTTP/HTTPS inbound traffic"
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"
Environment = var.environment
}
}
# ECSタスク用セキュリティグループ
resource "aws_security_group" "ecs_tasks" {
name = "${var.project_name}-ecs-tasks-sg"
description = "Allow inbound traffic from ALB only"
vpc_id = aws_vpc.main.id
ingress {
description = "Ingress from ALB"
from_port = var.container_port
to_port = var.container_port
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-tasks-sg"
Environment = var.environment
}
}
# ECS実行ロール
resource "aws_iam_role" "ecs_execution_role" {
name = "${var.project_name}-ecs-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = {
Name = "${var.project_name}-ecs-execution-role"
Environment = var.environment
}
}
resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy" {
role = aws_iam_role.ecs_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# ECSタスクロール
resource "aws_iam_role" "ecs_task_role" {
name = "${var.project_name}-ecs-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = {
Name = "${var.project_name}-ecs-task-role"
Environment = var.environment
}
}
7. 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.*.id
enable_deletion_protection = false
tags = {
Name = "${var.project_name}-alb"
Environment = var.environment
}
}
resource "aws_lb_target_group" "app" {
name = "${var.project_name}-tg"
port = var.container_port
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip"
health_check {
enabled = true
interval = 30
path = "/"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 5
protocol = "HTTP"
matcher = "200-299"
}
tags = {
Name = "${var.project_name}-tg"
Environment = var.environment
}
}
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.app.arn
}
tags = {
Name = "${var.project_name}-http-listener"
Environment = var.environment
}
}
8. 出力の定義
outputs.tf
で必要な情報を出力します:
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.main.id
}
output "alb_dns_name" {
description = "The DNS name of the load balancer"
value = aws_lb.main.dns_name
}
output "ecs_cluster_name" {
description = "The name of the ECS cluster"
value = aws_ecs_cluster.main.name
}
output "ecs_service_name" {
description = "The name of the ECS service"
value = aws_ecs_service.app.name
}
output "ecr_repository_url" {
description = "The URL of the ECR repository"
value = aws_ecr_repository.app.repository_url
}
9. Terraformの実行手順
設定ファイルの準備ができたら、以下の手順でTerraformを実行します:
- 初期化:
terraform init
- 設定の検証:
terraform validate
- 実行計画の確認:
terraform plan
- インフラのデプロイ:
terraform apply
- リソースの削除(必要な場合):
terraform destroy
10. CIパイプラインとの統合
GitHub ActionsやAWS CodePipelineなどのCIサービスとTerraformを統合することで、インフラの変更を自動化できます。
GitHub Actionsの例:
.github/workflows/terraform.yml
name: "Terraform"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
terraform:
name: "Terraform"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.2.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Terraform Init
run: terraform init
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Plan
run: terraform plan -no-color
if: github.event_name == 'pull_request'
- name: Terraform Apply
run: terraform apply -auto-approve
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
11. Dockerイメージの更新手順
アプリケーションの更新時には、新しいDockerイメージをビルドしてECRにプッシュし、ECSサービスを更新します:
# Dockerイメージのビルド
docker build -t $ECR_REPOSITORY_URL:latest .
# ECRへのログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $ECR_REPOSITORY_URL
# イメージのプッシュ
docker push $ECR_REPOSITORY_URL:latest
# ECSサービスの更新
aws ecs update-service --cluster $ECS_CLUSTER_NAME --service $ECS_SERVICE_NAME --force-new-deployment
ベストプラクティス
1. 環境の分離
異なる環境(開発、ステージング、本番)ごとにTerraformの設定を分けることをお勧めします:
terraform/
├── modules/ # 共通モジュール
├── dev/ # 開発環境
├── staging/ # ステージング環境
└── prod/ # 本番環境
2. 変数のスコープ管理
- 機密情報は環境変数または
terraform.tfvars
ファイルで管理 terraform.tfvars
はGitリポジトリにコミットしない- AWS Systems Managerパラメータストアを活用
3. モジュール化
再利用可能なコンポーネントはモジュール化することで、管理が容易になります:
module "vpc" {
source = "./modules/vpc"
vpc_cidr = var.vpc_cidr
project_name = var.project_name
environment = var.environment
}
4. ステート管理
- S3とDynamoDBを使用したリモートステート管理
- 環境ごとに異なるステートファイル
- ステートのロック機能を有効化
トラブルシューティング
1. コマンド実行時のエラー
terraform init
を実行したか確認- AWSの認証情報(IAM権限)を確認
- バージョン互換性の問題をチェック
2. デプロイ後のリソースアクセス問題
- セキュリティグループのルールを確認
- VPCエンドポイントの設定を確認
- IAMロールとポリシーの権限を確認
まとめ
TerraformとAWS、Dockerを組み合わせることで、インフラストラクチャをコードとして管理し、再現性の高い環境を構築できます。この記事で紹介した設定例とベストプラクティスを活用し、効率的なクラウドインフラ管理を実現しましょう。
コードはあくまでも例であり、実際の要件に合わせてカスタマイズすることをお勧めします。また、AWSのサービスを使用する際にはコストが発生する可能性があるため、不要なリソースは適切に削除するよう注意してください。
コメント