TerraformでAWS × Docker環境をコード管理する方法

はじめに

現代のクラウドインフラ構築において、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を実行します:

  1. 初期化: terraform init
  2. 設定の検証: terraform validate
  3. 実行計画の確認: terraform plan
  4. インフラのデプロイ: terraform apply
  5. リソースの削除(必要な場合): 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のサービスを使用する際にはコストが発生する可能性があるため、不要なリソースは適切に削除するよう注意してください。

コメント

タイトルとURLをコピーしました