Terraformは、HashiCorp社が開発するオープンソースのInfrastructure as Code(IaC)ツールであり、インフラの構築・変更・バージョン管理を安全かつ効率的に行うことができます。その中でも、Amazon Web Services(AWS)を操作するための「Terraform AWS Provider」は、世界中で最も広く利用されているプロバイダーの一つです。

本ガイドでは、AWS Providerの基本概念から、実践的なHCL(HashiCorp Configuration Language)コードの記述方法、State(状態)の管理、コンポーネントのモジュール化、そして本番運用における注意点やFAQまでを網羅的に解説します。これからTerraformでAWSインフラの構築を始める方はもちろん、より堅牢なインフラ構成を目指す実務者の方にも役立つ実践的な内容となっています。


1. Terraform AWS Providerの基本概念

1.1 TerraformとProviderの役割

Terraformは、コア(Terraform Core)とプラグイン(Provider)の2つのコンポーネントから構成されています。 Terraform Coreは、設定ファイル(HCL)のパース、リソースの依存関係グラフの作成、および「現在の状態」と「望ましい状態」の差異分析を行います。一方、Providerは、各種クラウドサービスやSaaSのAPIを呼び出すための具体的なロジックが実装されたプラグインです。

AWS Provider(hashicorp/aws)は、Terraform Coreからの指示を受け取り、AWS APIを呼び出して、VPC、EC2、S3、RDSなどのリソースを実際に作成、更新、削除する役割を担います。

1.2 AWS、GCP、Azureプロバイダーとの関係

Terraformはマルチクラウドに対応していますが、これは「1つのコードでAWSとGCPに同じリソースを同時にデプロイできる」という意味ではありません。各クラウドサービス(AWS、GCP、Azure)は、それぞれ固有のAPIアーキテクチャ、リソース設計、命名規則を持っています。

そのため、Terraformでは対象とするクラウドごとに異なるプロバイダーを使用します。

  • AWS (hashicorp/aws): aws_vpcaws_instance などのリソースを提供。
  • GCP (hashicorp/google): google_compute_networkgoogle_compute_instance などのリソースを提供。
  • Azure (hashicorp/azurerm): azurerm_virtual_networkazurerm_linux_virtual_machine などのリソースを提供。

プロバイダー設計の比較

項目 AWS Provider (aws) GCP Provider (google) Azure Provider (azurerm)
主な認証方法 IAM User/Role, AWS SSO, Environment Variables Service Account Key (JSON), ADC (Application Default Credentials) Service Principal, Managed Identity, Azure CLI
デフォルトスコープ リージョン単位 (region = "ap-northeast-1") プロジェクト単位 (project = "my-project") リソースグループ単位 (resource_group_name)
リソース構造の特徴 グローバルリソース(IAMなど)とリージョナルリソースの区別が明確。 プロジェクトを最上位概念とし、その配下にリソースを配置。 ほぼ全てのリソースが「リソースグループ」に属する。

マルチクラウド構成をとる場合、同一のTerraformプロジェクト内に複数のプロバイダー定義を記述し、AWSのデータソースから取得した値をGCPやAzureのリソースに渡すといった連携が可能です。


2. 実践的なHCLコード例(基本リソースの構築)

ここでは、AWS Providerを初期化し、基本的なネットワークインフラ(VPC、パブリックサブネット)、セキュリティグループ、およびEC2インスタンスをデプロイするための具体的なHCLコードを示します。

2.1 構成ディレクトリ構造

.
├── providers.tf
├── variables.tf
├── outputs.tf
└── main.tf

2.2 プロバイダー定義 (providers.tf)

Terraformブロック内で要求するプロバイダーのバージョンを固定し、AWS Providerの設定を行います。ここでは、マルチリージョン構成への対応を考慮し、エイリアス(Alias)を用いた複数リージョンの定義例も紹介します。

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0" # 5.x 系の最新バージョンを使用
    }
  }
}

# デフォルトのプロバイダー設定(東京リージョン)
provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      Project     = "OKPy-Demo"
      ManagedBy   = "Terraform"
    }
  }
}

# オプション:マルチリージョン用(バージニア北部リージョン)
provider "aws" {
  alias  = "us_east"
  region = "us-east-1"

  default_tags {
    tags = {
      Environment = var.environment
      Project     = "OKPy-Demo"
      ManagedBy   = "Terraform"
    }
  }
}

2.3 変数定義 (variables.tf)

再利用性を高めるため、設定値は直接ハードコードせず変数(Variables)として定義します。

variable "aws_region" {
  type        = string
  description = "AWSをデプロイするメインリージョン"
  default     = "ap-northeast-1"
}

variable "environment" {
  type        = string
  description = "デプロイ環境の識別子 (dev, staging, prod)"
  default     = "dev"
}

variable "vpc_cidr" {
  type        = string
  description = "VPCのCIDRブロック"
  default     = "10.0.0.0/16"
}

variable "subnet_cidr" {
  type        = string
  description = "パブリックサブネットのCIDRブロック"
  default     = "10.0.1.0/24"
}

variable "instance_type" {
  type        = string
  description = "EC2インスタンスのタイプ"
  default     = "t3.micro"
}

2.4 主要インフラリソース定義 (main.tf)

VPC、インターネットゲートウェイ、ルートテーブル、サブネット、セキュリティグループ、およびEC2インスタンスを定義します。

# 最新のAmazon Linux 2023 AMIのIDを動的に取得
data "aws_ami" "amazon_linux_2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-kernel-6.1-x86_64"]
  }
}

# VPCの作成
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

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

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

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

# パブリックサブネット
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.subnet_cidr
  map_public_ip_on_launch = true
  availability_zone       = "${var.aws_region}a"

  tags = {
    Name = "${var.environment}-public-subnet-1a"
  }
}

# ルートテーブル
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

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

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

# サブネットへのルートテーブルの関連付け
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# セキュリティグループ(HTTPとSSHの許可)
resource "aws_security_group" "web_sg" {
  name        = "${var.environment}-web-sg"
  description = "Allow inbound HTTP and SSH traffic"
  vpc_id      = aws_vpc.main.id

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

  # SSH (本番運用では特定のIPに制限すべき)
  ingress {
    from_port   = 22
    to_port     = 22
    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.environment}-web-sg"
  }
}

# EC2インスタンスの作成
resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux_2023.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  user_data = <<-EOF
              #!/bin/bash
              dnf update -y
              dnf install -y httpd
              systemctl start httpd
              systemctl enable httpd
              echo "<h1>Hello from OKPy Managed EC2</h1>" > /var/www/html/index.html
              EOF

  tags = {
    Name = "${var.environment}-web-server"
  }
}

2.5 出力定義 (outputs.tf)

デプロイ完了後に確認したい主要なパラメータを出力(Outputs)として設定します。

output "vpc_id" {
  value       = aws_vpc.main.id
  description = "作成されたVPCのID"
}

output "ec2_public_ip" {
  value       = aws_instance.web.public_ip
  description = "EC2インスタンスのグローバルIPアドレス"
}

output "web_server_url" {
  value       = "http://${aws_instance.web.public_ip}"
  description = "デプロイしたWebサーバーのURL"
}

3. Terraform State(状態管理)の重要性とAWSでのベストプラクティス

Terraformを使用する上で最も重要な概念の一つが State(状態ファイル: terraform.tfstate です。

3.1 Stateファイルとは

Stateファイルは、Terraformによって構築された実際のリソースのメタデータや設定をJSON形式で記録したものです。Terraformは、このStateファイルを「真実のソース(Source of Truth)」として使用し、HCLの修正差分を反映するための実行計画(Plan)を計算します。

3.2 ローカルStateの危険性

初期検証などでは、手元のローカルPCに terraform.tfstate が出力されますが、複数人のチーム開発や本番運用において、ローカルでStateを管理することは以下のリスクを伴います。

  1. コンフリクトの発生: 複数人が同時にデプロイを実行した場合、お互いの変更が上書きされ、Stateが破損する恐れがあります。
  2. 機密情報の漏洩: Stateファイルには、DBのパスワードや秘密鍵などの機密情報がプレーンテキスト(平文)で保存されます。Gitなどのバージョン管理システムに誤ってコミットしてしまうと、深刻なセキュリティインシデントにつながります。

3.3 AWSにおけるリモートState(S3 + DynamoDB)

AWS環境では、Stateファイルをセキュアに一元管理するため、Amazon S3(Stateファイルの保存先)と DynamoDB(ステートロック用のKey-Valueストア)を組み合わせた「S3 Backend」を使用するのがベストプラクティスです。

動作の仕組み

  • S3: バージョニングを有効にしたS3バケットにStateファイルを保存。誤って削除または破損した場合でも過去の履歴から復元可能。暗号化(SSE-S3またはKMS)を強制。
  • DynamoDB: LockID というプライマリキーを持つテーブルを作成。Terraform実行時(plan/apply)にこのテーブルにロックを取得し、実行完了後にロックを解放。これにより、並行して同じインフラを操作することを防止(排他制御)。

リモートBackendの設定コード例

通常、Backendの設定はリソースの作成が完了した後に記述します。Backendを構成するS3バケットとDynamoDBテーブルをあらかじめ手動または別途作成しておき、providers.tf(または backend.tf)に以下のように定義します。

terraform {
  backend "s3" {
    bucket         = "okpy-tfstate-bucket"      # 一意のS3バケット名
    key            = "state/dev/terraform.tfstate" # バケット内でのパス
    region         = "ap-northeast-1"
    encrypt        = true                         # 暗号化を強制
    dynamodb_table = "okpy-tflock-table"         # DynamoDBテーブル名
  }
}

4. モジュール化(Module)による再利用性の向上

コードベースが拡大するにつれて、重複するコードが増え、管理が困難になります。Terraformでは、リソースのまとまりを「モジュール(Module)」としてパッケージ化することで、コードの再利用性、保守性、および一貫性を向上させることができます。

4.1 モジュールの基本構成

例えば、VPCとその配下のサブネットを簡単に作成できる「vpcモジュール」を自作する場合、以下のようなフォルダ構成を作成します。

. (ルートディレクトリ)
├── main.tf
├── variables.tf
├── outputs.tf
└── modules/
    └── vpc/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

4.2 子モジュールの実装 (modules/vpc/)

modules/vpc/variables.tf

モジュールに入力するパラメータを定義します。

variable "vpc_cidr" {
  type        = string
  description = "VPC CIDR block"
}

variable "environment" {
  type        = string
  description = "Environment name"
}

variable "public_subnet_cidr" {
  type        = string
  description = "CIDR block for public subnet"
}

modules/vpc/main.tf

モジュール内部のリソース定義です。ハードコードを避け、変数(var.*)を使用します。

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

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

resource "aws_subnet" "public" {
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.public_subnet_cidr
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "${var.environment}-public-subnet"
  }
}

modules/vpc/outputs.tf

他のコード(ルートモジュール)から参照できるようにするため、出力を定義します。

output "vpc_id" {
  value       = aws_vpc.this.id
  description = "The ID of the VPC"
}

output "public_subnet_id" {
  value       = aws_subnet.public.id
  description = "The ID of the public subnet"
}

4.3 ルートモジュールからの呼び出し例 (./main.tf)

作成したモジュールは、以下のように引数を渡して呼び出します。これにより、同じモジュール定義を再利用して、本番環境用(prod)と開発環境用(dev)のVPCを簡単に増設できます。

# 開発環境用VPC
module "dev_vpc" {
  source = "./modules/vpc"

  vpc_cidr           = "10.10.0.0/16"
  public_subnet_cidr = "10.10.1.0/24"
  environment        = "dev"
}

# 本番環境用VPC
module "prod_vpc" {
  source = "./modules/vpc"

  vpc_cidr           = "10.20.0.0/16"
  public_subnet_cidr = "10.20.1.0/24"
  environment        = "prod"
}

# モジュールの出力を利用してリソースを作成する例
resource "aws_security_group" "dev_sg" {
  name   = "dev-sg"
  vpc_id = module.dev_vpc.vpc_id # モジュールから取得したVPC IDを使用
}

5. AWS Providerを利用する上での注意点とトラブルシューティング

AWS Providerを運用していく中で遭遇しやすい罠や、ベストプラクティスに基づいた対処法を解説します。

5.1 認証情報のハードコード回避

最も犯しやすい重大なセキュリティミスは、AWSの access_keysecret_key をプロバイダーブロック内に直接ハードコードすることです。

絶対に避けるべきアンチパターン:

# 🚨 セキュリティ上の重大なリスク!
provider "aws" {
  region     = "ap-northeast-1"
  access_key = "AKIAXXXXXXXXXXXXXXXX"
  secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

推奨される対策:

  1. 環境変数: AWS CLIと同じ環境変数 (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_PROFILE 等) を使用します。Terraformは実行時に自動的にこれらを参照します。
  2. IAM Identity Center (AWS SSO): 開発用PCで aws sso login を実行し、生成されたプロファイル(例:defaultwork-sso)を環境変数 AWS_PROFILE にセットしてTerraformを実行します。
  3. GitHub Actions / CI/CD: OIDC(OpenID Connect)トークンによる一時的なIAM Roleの引き受け(AssumeRole)を利用し、永続的な認証鍵を発行しないようにします。

5.2 暗黙的依存関係と明示的依存関係 (depends_on)

Terraformはリソース間の参照(例:vpc_id = aws_vpc.main.id)を自動的に検知し、適切な順序で作成を行います(暗黙的な依存関係)。

しかし、APIの仕様や一部の非同期処理において、Terraformが依存関係を正しく認識できない場合があります。 例えば、EC2が起動する際に「IAMロールがAWS側で完全に伝播・反映されていること」が必要であるにもかかわらず、IAMの作成完了の瞬間にEC2が起動を開始し、権限不足でエラーになるようなケースです。

このような場合は、depends_on メタ引数を用いて明示的な依存関係を宣言します。

resource "aws_iam_role_policy_attachment" "s3_readonly" {
  role       = aws_iam_role.ec2_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

resource "aws_instance" "web" {
  ami           = "ami-xyz"
  instance_type = "t3.micro"

  # IAMポリシーが完全にアタッチされた後にEC2を作成
  depends_on = [
    aws_iam_role_policy_attachment.s3_readonly
  ]
}

5.3 ドリフト(Drift)の検知と対応

手動操作(AWSマネジメントコンソールでの変更など)によって、実際のAWSインフラがTerraformのStateと乖離することを「ドリフト」と呼びます。

ドリフトを検知するには、定期的に以下のコマンドを実行します。

terraform plan

planを実行すると、Terraformは現在のAWS上の最新状態をAPI経由で取得し、Stateおよびコードとの差分を表示します。もし手動変更が検出された場合の対応アプローチは2つあります。

  1. コード側を修正する: 手動で行った変更が正しい場合、その内容をHCLコードに反映させ、terraform apply を行うことでStateを追従させます。
  2. 手動変更を破棄する: 手動変更が誤りの場合、そのまま terraform apply を実行することで、Terraformがインフラをコードで定義された元の状態へ上書き修復(強制置換、またはロールバック)します。

5.4 破壊的変更(Recreate)への警戒

AWSリソースの一部の属性は、作成後に変更することができず、変更を適用するために「一度既存リソースを削除(Destroy)し、再度新規作成(Create)」する必要があります。

例えば、aws_instanceami を変更する場合や、サブネットの availability_zone を変更する場合などがこれに該当します。

terraform plan を実行した際、出力に -/+ destroy and then create replacementforces replacement という表記がある場合は、リソースが再作成されることを意味します。本番環境のデータベース(RDS)やステートフルなサーバーなどでこれを実行すると重大なデータ消失につながるため、変更を適用する前に必ずPlanの詳細を確認してください。

これを防止するため、破壊的変更を防ぐライフサイクルルールを設定することが推奨されます。

resource "aws_db_instance" "production" {
  # ... 各種パラメータ

  lifecycle {
    prevent_destroy = true # 誤った操作によるリソース削除を防止する
  }
}

6. FAQ(よくある質問と回答)

Q1: terraform apply 時に認証エラー(ExpiredTokenNo valid credential sources found)が発生します。どう対応すればよいですか?

A1: このエラーは、TerraformがAWS APIにアクセスするための一時認証情報(クレデンシャル)が期限切れであるか、正しく読み込めていない場合に発生します。以下の手順に沿ってデバッグを行ってください。

  1. AWS環境変数の確認: 手元のシェルで env | grep AWS を実行し、AWS_ACCESS_KEY_ID などの値が古くなっていないか確認します。
  2. AWS CLIでのアクセス確認: aws sts get-caller-identity コマンドを実行し、現在のアカウントおよびロール情報を正常に取得できるか検証します。この時点でエラーが出る場合、TerraformではなくAWS CLIの認証が切れています。
  3. SSOセッションの更新: AWS SSOを使用している場合は、aws sso login --profile <プロファイル名> を実行してログイン状態を更新した上で、export AWS_PROFILE=<プロファイル名> を設定して再実行してください。

Q2: すでに手動(コンソール)で作ってしまったAWSリソースを、既存の構成を崩さずにTerraformの管理下に置くにはどうすればいいですか?

A2: すでに存在するAWSリソースをTerraformにインポートする方法として、Terraform 1.5以降で導入された import ブロック を使用する方法が最も安全かつ推奨されます。

例えば、すでに存在するS3バケット my-existing-bucket をTerraformに取り込む手順は以下の通りです。

1. コード内に import ブロックを記述

import {
  to = aws_s3_bucket.imported_bucket
  id = "my-existing-bucket" # AWS上でのリソース識別名
}

2. 空のリソース定義を作成

resource "aws_s3_bucket" "imported_bucket" {
  # この時点では中身は空で構いません
}

3. 自動生成機能付きのインポートコマンドを実行

以下のコマンドを実行すると、Terraformが現在のS3バケットの設定状況を解析し、適切なHCL定義を自動的に作成、またはStateファイルへのインポートを実行します。

terraform plan -generate-config-out=generated_resources.tf

生成されたコードを確認・調整し、問題なければ terraform apply を実行してStateに登録します。これにより、既存のインフラを破壊することなく安全にTerraform管理下に移行できます。


Q3: AWS Providerのバージョンアップはどのように行うべきですか?(依存関係の固定と -upgrade

A3: AWS Providerは頻繁にアップデート(機能追加、バグ修正など)が行われます。意図しないバージョンアップによるコードの破損を防ぐため、プロジェクトごとに使用するバージョンを固定(または制約を記述)することが重要です。

1. バージョン制約の書き方

providers.tf 内で ~> 演算子を用いてバージョン範囲を制限します。

required_providers {
  aws = {
    source  = "hashicorp/aws"
    version = "~> 5.10.0" # 5.10.x の範囲内(マイナーバージョン変更まで)で追従
  }
}

2. バージョンの更新プロセス

マイナーアップデートやパッチアップデートを適用してプロバイダープラグインを更新する場合は、以下のコマンドを実行します。

terraform init -upgrade

このコマンドを実行すると、定義したバージョン制約の範囲内で、利用可能な最新のプロバイダーが再ダウンロードされ、ディレクトリ内の .terraform.lock.hcl ファイル(依存関係ロックファイル)が更新されます。 本番環境へ適用する前に、必ず検証環境(Staging/Dev)で terraform plan および apply を通してテストを実行し、変更に伴うエラーや非推奨(Deprecated)の警告が出ていないか検証してください。


まとめ

Terraform AWS Providerは、AWS上のほぼすべてのコンポーネントをコードで安全に宣言・管理できる、強力なインフラオーケストレーションツールです。

安定したインフラ運用を実現するためには、

  1. プロバイダーとTerraformバージョンの厳密な固定
  2. S3 + DynamoDB によるリモートStateとロック管理の徹底
  3. モジュール化によるクリーンなコード構造設計
  4. terraform plan 出力の徹底的な監査(特に再作成の有無)

これらを原則として開発パイプラインに組み込むことが重要となります。本ガイドのコードテンプレートや設計思想を起点として、よりスケーラブルで信頼性の高いAWSインフラのコード化を実践してください。