---
title: "TerraformでAWSを構築する手順｜aws 6系の認証とS3バックエンド・エラー対処"
url: "https://www.issoh.co.jp/tech/details/1900/"
published: 2024-02-02
updated: 2026-10-18
categories: ["IaC"]
publisher: "株式会社一創"
---

# TerraformでAWSを構築する手順｜aws 6系の認証とS3バックエンド・エラー対処

TerraformでAWSを構築する手順は、2024年までの入門記事から前提が3つ変わりました。awsプロバイダ6系でリソース単位の`region`引数が入ったこと、tfstateのロックがDynamoDBテーブルからS3のネイティブロックへ移ったこと、ローカルの認証が長期アクセスキーからIAM Identity Centerのプロファイルへ寄ったことです。本記事はTerraform 1.16.5とaws 6.68.0（いずれも2026年10月12日時点の最新）を前提に、版の固定、認証、tfstateの置き場、VPCからEC2までの最小構成、`terraform plan`で止まるエラーの対処、AWS純正ツールとの使い分けを実装順に並べます。掲載したHCLは同じ版の組み合わせで`terraform validate`を通しています。

## まとめ：AWS構築前に決める3点と最初の実行順

- **版の固定**：CLIは`required_version`、プロバイダは`version = "~> 6.68"`で固定し、`.terraform.lock.hcl`をリポジトリに入れる
- **認証**：ローカルはIAM Identity Centerのプロファイル、CIはOIDCなどの一時認証情報。長期アクセスキーはコードにもCIの変数にも置かない
- **tfstate**：S3バックエンド＋`use_lockfile = true`。新規構築でDynamoDBテーブルは作らない

実行順は`AWS_PROFILE`の設定と`aws sso login`→`terraform init`→`terraform plan`→`terraform apply`で、検証環境は使い終わったら`terraform destroy`で消します。Terraformを使うメリットは構成を差分でレビューでき、同じ定義から同じ環境を作り直せる点ですが、対象がAWSだけならCloudFormationでも同じ利点は得られます。Terraformを選ぶ理由が立つのは、AWSの外側まで1つの記法で管理したいときです。設計からAWS環境の構築・移行までをまとめて任せたい場合は、一創の[インフラ構築（AWS・Google Cloud・Azure）](https://www.issoh.co.jp/service/system/aws/)でTerraformによるコード化を含めて対応しています。

## Terraform 1.16系とaws 6系のバージョン固定

### versions.tfとS3バックエンドを書く入口ファイル

Terraform CLIの安定版は1.16.5（2026年10月2日UTC公開）です。1.16系は[1.16.0のリリースノート](https://github.com/hashicorp/terraform/releases/tag/v1.16.0)のとおり2026年8月26日に出ており、1.17.0は10月7日にrc1が出た段階なので、本番構成の既定は1.16系にします。awsプロバイダは[6.68.0](https://github.com/hashicorp/terraform-provider-aws/releases/tag/v6.68.0)（2026年10月7日UTC公開）が最新で、6.64.0から6.68.0までの5本が9月9日から10月7日の約4週間に出ています。入口のファイルは次の形です。

```
terraform {
  required_version = "~> 1.16.0"

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

  backend "s3" {
    bucket       = "example-tfstate-prod"
    key          = "network/terraform.tfstate"
    region       = "ap-northeast-1"
    use_lockfile = true
  }
}

provider "aws" {
  region = "ap-northeast-1"

  default_tags {
    tags = {
      Project   = "sample"
      ManagedBy = "terraform"
    }
  }
}
```

`required_version = "~> 1.16.0"`は1.16系のパッチ更新だけを許す指定です。S3ネイティブロックだけなら1.11以上で動きますが、後述の`lifecycle`の`destroy = false`とモジュール内の`import`ブロックは1.16.0からの機能なので、本記事の構成は1.16系に揃えます。プロバイダ側の`~> 6.68`は右端の要素だけ増加を許す指定で、6.69は入り7.0.0は入りません。パッチ更新だけに絞るなら`~> 6.68.0`と書きます。実際に使う版は`.terraform.lock.hcl`に記録され、`terraform init -upgrade`を実行するまで動きません。`default_tags`を置くと、タグに対応するリソースへ共通のタグが付き、リソース側では値の上書きだけを書けば済みます。制約演算子の選び分けは[terraform providerのバージョン制約とalias設計｜lockファイルの運用判断](/tech/details/16540/)、CLIの入れ方は[Terraformのインストール手順をOS別に整理｜tfenv・CIでの版固定まで](/tech/details/16545/)、版ごとの新機能は[Terraformの最新バージョンは1.16｜terraform versionでの確認とアップグレード手順・新機能](/tech/details/8928/)で扱っています。

### aws 6.0のregion引数を使う場面

6.0.0で、大半のリソースとデータソースにトップレベルの`region`引数が入りました。プロバイダを東京に設定したまま、特定のリソースだけ大阪に置けます。

```
resource "aws_vpc" "dr" {
  region     = "ap-northeast-3"
  cidr_block = "10.1.0.0/16"
}
```

`region`の値を変えるとリソースは置換されます。記述を消した場合はstateに残った値が使われるため置換は起きません。インポートIDは`<id>@ap-northeast-3`の形でリージョンを付けます。IAMのようなグローバルリソースは対象外です。この引数が効くのは「基本は単一リージョン、DR用など一部だけ別リージョン」の構成で、環境全体のリージョンを切り替える用途には向きません。従来のプロバイダalias構成は6系でも有効で、非推奨にもなっていません。5系から上げる場合は、[公式の6系移行ガイド](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-6-upgrade)のとおり、まず5系の最新版で`terraform plan`の警告を消してから制約を6系へ広げます。

## AWSへの認証をローカル実行とCI実行で分ける方式

### 認証情報が読まれる順序と別アカウントへの誤適用

awsプロバイダは、プロバイダ設定のパラメータ、環境変数、共有credentialsファイル、共有configファイル、コンテナの認証情報、インスタンスプロファイルの順に認証情報を探します。AWS CLIおよびAWS SDK for Go v2と同じ順序です。プロファイルを何も指定しないと、シェルに残った`AWS_ACCESS_KEY_ID`がdefaultプロファイルより先に使われ、意図しないアカウントへ適用される原因になります。一方、1.16.5とaws 6.68.0で試すと、`profile`引数か`AWS_PROFILE`でプロファイルを指定した場合は、環境変数にアクセスキーを置いてもプロファイル側が読まれ、存在しない名前を指定すると環境変数のキーで代替されずにプロファイルのエラーで止まりました。ローカルではプロファイルを必ず明示し、アクセスキーをプロバイダブロックへ直書きする書き方は採りません。

### IAM Identity Centerのプロファイルでローカルから実行する手順

手元のPCから実行するときは、長期のアクセスキーを発行せず、IAM Identity Centerのプロファイルで一時認証情報を使います。[AWS CLIのIAM Identity Center設定手順](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso-tutorial.html)が推奨する形式は、`sso-session`セクションを定義してプロファイルに関連付けるものです。`aws configure sso`を実行すると、次のような設定が`~/.aws/config`に書き込まれます。

```
[profile example-dev]
sso_session    = example
sso_account_id = 123456789012
sso_role_name  = TerraformDeveloper
region         = ap-northeast-1

[sso-session example]
sso_start_url = https://example.awsapps.com/start
sso_region    = ap-northeast-1
sso_registration_scopes = sso:account:access
```

```
export AWS_PROFILE=example-dev
aws sso login
terraform init
terraform plan
```

プロファイルは環境変数`AWS_PROFILE`で渡します。`provider "aws"`の`profile`引数はS3バックエンドに引き継がれないため、引数で指定するとバックエンド側にも別途`profile`を書く必要があります。`AWS_PROFILE`ならプロバイダとバックエンドの両方が同じプロファイルを読みます。セッションが切れたら`aws sso login`をやり直します。CLIはこのログインのためだけに必要で、Terraform本体はCLIを呼び出しません。

### assume\_roleで実行専用ロールへ委譲する構成

複数アカウント構成では、開発者自身の権限ではapplyせず、Terraform実行用のロールへ委譲します。

```
provider "aws" {
  region = "ap-northeast-1"

  assume_role {
    role_arn     = "arn:aws:iam::123456789012:role/TerraformExecution"
    session_name = "terraform-cli"
  }
}
```

`session_name`はCloudTrailで実行者を追う単位になるため、命名規則を決めておきます。第三者のアカウントへ委譲するなら`external_id`を併用します。踏み台ロールを経由して本命のロールへ渡すロールチェーンは、`assume_role`ブロックを実行順に2つ並べればプロバイダ側だけで完結します。

### 実行環境ごとの認証方式

| 実行環境                    | 使う認証                | プロバイダが読む経路                                     |
| ----------------------- | ------------------- | ---------------------------------------------- |
| 開発者のPC                  | IAM Identity Center | AWS\_PROFILEで選ぶ共有configのプロファイル                 |
| GitHub ActionsなどAWS外のCI | OIDC＋IAMロール         | 環境変数の一時認証情報                                    |
| EC2                     | インスタンスプロファイル        | インスタンスメタデータ                                    |
| ECS                     | タスクロール              | AWS\_CONTAINER\_CREDENTIALS\_\*                |
| CodeBuild               | サービスロール             | AWS\_CONTAINER\_CREDENTIALS\_\*                |
| EKS                     | IRSA                | AWS\_ROLE\_ARN・AWS\_WEB\_IDENTITY\_TOKEN\_FILE |

どの環境でも長期キーを配らずに済みます。GitHub Actionsのワークフローとロールの信頼ポリシーは[GitHub ActionsとTerraformでAWSのCI/CDを構築する手順（OIDC・S3ネイティブロック対応）](/tech/details/2715/)、fmt・validate・planをCIで回す設計は[TerraformのCI導入ガイド｜fmt・validate・planを自動化するパイプライン設計とツールの選び方](/tech/details/6528/)で扱っています。

## tfstateをS3に置くbackendの設定とロック方式

### use\_lockfileへの移行とDynamoDBテーブルを外す順序

S3ネイティブロックはTerraform 1.10.0で入り、このときはDynamoDBと併用すると双方からロックを取る挙動でした。1.11.0で`use_lockfile`が正式提供になり、DynamoDB関連の引数は非推奨になっています。[S3バックエンドのドキュメント](https://developer.hashicorp.com/terraform/language/backend/s3)では`dynamodb_table`が「Optional, Deprecated」で、`use_lockfile`の既定値は`false`です。書き忘れるとロック無しで動きます。

既存構成から移すときは、まず`use_lockfile = true`を足して`terraform init -reconfigure`で再初期化し、両方が有効な状態で運用します。backendブロックを書き換えたまま`plan`を打つと、「Backend initialization required」「Backend configuration block has changed」で止まります。同じstateを触る全員とCIが1.11以降になったことを確かめてから`dynamodb_table`を外して再び`terraform init -reconfigure`を実行し、最後にテーブルを消します。テーブルを先に消すと、旧版のCLIが残っている環境でロックが取れずに失敗します。

### backend用のIAM権限とバケット側の設定

必須引数は`bucket`、`key`、`region`の3つです。ネイティブロックを有効にすると、state本体に加えて拡張子`.tflock`のオブジェクトに`s3:GetObject`、`s3:PutObject`、`s3:DeleteObject`が要ります。state本体への`s3:DeleteObject`は不要です。DynamoDB時代の権限セットのままだとロック取得だけが失敗するので、移行時はIAMポリシーの更新を同じ変更に含めます。defaultワークスペースだけを使う場合の最小ポリシーは次のとおりです。

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::example-tfstate-prod",
      "Condition": {
        "StringEquals": { "s3:prefix": "network/terraform.tfstate" }
      }
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::example-tfstate-prod/network/terraform.tfstate"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::example-tfstate-prod/network/terraform.tfstate.tflock"
    }
  ]
}
```

バケットはバージョニングを有効にして、tfstateの世代を戻せるようにします。tfstateには属性値が平文で入るため、読める主体はTerraformの実行ロールに絞ります。stateの構造や別stateへの移動は[Terraform stateとは？tfstateの構造とS3バックエンド・移動削除の安全手順](/tech/details/16525/)で扱っています。

### 本番と非本番のstateと実行権限の分離

本番と非本番は、できればバケットとAWSアカウントごと分けます。誤ったworkspaceのまま`terraform apply`を打つ事故は、本番と非本番で実行ロールが分かれていれば権限エラーで止まります。同じバケットのプレフィックスで分けてもIAMでキー単位に絞れますが、1つの実行ロールで両方を触れる状態にしている限り止まりません。S3バックエンドでは、default以外のstateは`env:/<workspace名>/<key>`（`workspace_key_prefix`の既定値）に置かれるため、上のdefault専用ポリシーのままworkspaceを使うと権限不足になります。分割方式の比較は[terraform workspaceの使い方と環境分離の判断基準｜分割方式の比較](/tech/details/16529/)にあります。

### tfstateバケットを守るprevent\_destroyと1.16のdestroy = false

tfstate用のバケットをTerraformで作る構成では、`terraform destroy`の巻き添えで消える事故を防ぐ設定が要ります。従来の`prevent_destroy = true`に加え、1.16.0で`lifecycle`に`destroy = false`が入りました（[追加したプルリクエスト](https://github.com/hashicorp/terraform/pull/38784)は「破棄の代わりにstateから外す」と説明しています）。

```
resource "aws_s3_bucket" "tfstate" {
  bucket = "example-tfstate-prod"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id

  versioning_configuration {
    status = "Enabled"
  }
}
```

2つの違いを1.16.5で確かめると、`terraform destroy`に対する反応が分かれます。`prevent_destroy`は`Error: Instance cannot be destroyed`でplanごと止まり、`destroy = false`は「will no longer be managed by Terraform, but will not be destroyed」と表示して実物を残したまま管理から外します。環境をまるごと畳むときにバケットだけ残したいなら`destroy = false`、誤操作を止めたいなら`prevent_destroy`です。

共通の弱点もあります。リソースブロックごとコードから消すと、どちらを付けていても次のplanは通常の破棄（because … is not in configuration）になりました。保護はコードの中にしか無いので、ブロック削除のレビューは代替できません。tfstate用バケットはアプリとは別のstateで管理し、日常の`apply`から切り離します。lifecycle全体の使い分けは[terraform lifecycleの使い分け｜ignore\_changes・depends\_on・movedの判断](/tech/details/16538/)で整理しています。

## VPCからEC2までの最小構成をapplyする手順

### インターネットゲートウェイとSSM接続まで含めた最小構成

VPC、サブネット、EC2の3つだけでは、インスタンスは外へ出られず接続もできません。インターネットゲートウェイ、ルートテーブル、セキュリティグループ、Session Manager用のインスタンスプロファイルまで足したのが次の構成です。SSHのポートは開けず、キーペアも作りません。AMI IDはリージョンごとに異なり更新もされるため、SSMパブリックパラメータから引きます。

```
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
}

resource "aws_subnet" "public_a" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}

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
  }
}

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

resource "aws_security_group" "app" {
  name   = "sample-app"
  vpc_id = aws_vpc.main.id
}

resource "aws_vpc_security_group_egress_rule" "all" {
  security_group_id = aws_security_group.app.id
  ip_protocol       = "-1"
  cidr_ipv4         = "0.0.0.0/0"
}

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

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

resource "aws_iam_instance_profile" "ssm" {
  name = "sample-ec2-ssm"
  role = aws_iam_role.ssm.name
}

resource "aws_instance" "app" {
  ami                    = data.aws_ssm_parameter.al2023.value
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public_a.id
  vpc_security_group_ids = [aws_security_group.app.id]
  iam_instance_profile   = aws_iam_instance_profile.ssm.name

  metadata_options {
    http_tokens = "required"
  }
}

output "instance_id" {
  value = aws_instance.app.id
}
```

セキュリティグループは外向きのルールだけを持ち、受信は1本も許可していません。それでも接続できるのは、AL2023のAMIにSSM Agentが入っていること（[SSM Agentのプリインストール一覧](https://docs.aws.amazon.com/systems-manager/latest/userguide/ami-preinstalled-agent.html)に記載）と、インスタンス側から外へ出る経路があるためです。apply後は`aws ssm start-session --target <instance_id>`でシェルに入れます（手元にSession Manager pluginが必要です）。`http_tokens = "required"`はIMDSv2の強制です。`aws_ssm_parameter`の`value`はplan出力で常にsensitive扱いになり、AMI IDは伏字で表示されます。

### init・plan・apply・destroyの順序と検証環境の費用

実行は`terraform init`、`terraform plan`、`terraform apply`の順です。バックエンド用のバケットがまだ無い段階では、`terraform init -backend=false`でプロバイダの取得と`terraform validate`だけ先に済ませられます。バケットを用意したらフラグ無しで初期化し直します。planの記号の読み方や、applyを止めるべき差分の見分け方は[terraform planの差分の読み方｜init・apply・destroyの安全手順](/tech/details/16531/)にまとめています。

この構成で時間単位の課金が続くのは、EC2インスタンスとパブリックIPv4アドレスです。AWSの価格データでは東京リージョンのt3.micro（Linux）がオンデマンド1時間0.0136USD、使用中のパブリックIPv4アドレスが1時間0.005USDで、つけっぱなしにすると2つだけで1か月（730時間）約13.6USDになり、ルートボリュームのEBS料金はこれとは別にかかります（[EC2オンデマンド料金](https://aws.amazon.com/ec2/pricing/on-demand/)・[VPC料金](https://aws.amazon.com/vpc/pricing/)）。2025年7月15日以降に初めてAWSに登録した場合は、サインアップ時に100USD、対象のアクティビティで最大100USDのクレジットが付き、登録時に無料プランか有料プランを選びます（[AWSの告知](https://aws.amazon.com/about-aws/whats-new/2025/07/aws-free-tier-credits-month-free-plan/)）。[無料プラン](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/free-tier-plans.html)は6か月経過かクレジット消化の早いほうで終わり、そのままにするとアカウントが自動で閉じられ、AWS Organizationsへの参加などで有料プランへ自動で切り替わります。無料利用枠の対象として表示されるのは、t3.microやt4g.microを含む6種類のインスタンスタイプです（[EC2のドキュメント](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-free-tier-usage.html)）。検証が終わったら`terraform destroy`で消すところまでを1回の作業にしてください。実AWSへapplyせずに試したい場合は、[LocalStackとは？AWSをローカルで動かす手順と有料化後の料金・判断基準](/tech/details/17634/)のtflocalでエンドポイントをローカルへ向ける方法があります。

applyで作ったEC2の中身をAnsibleで構成する手順は[TerraformとAnsibleの連携手順：インベントリの受け渡しとactionsでの自動実行](https://www.issoh.co.jp/tech/details/18475/)で解説しています。

## terraform planで止まるエラーと対処

### 認証エラー3種の文言と原因の切り分け

最初のplanが認証エラーで止まったら、プロファイルの選択、認証情報の有無、認証情報の有効性の順に切り分けます。Terraform 1.16.5とaws 6.68.0で、それぞれの条件を作って実行したときの文言は次の3種でした。

```
# 1. AWS_PROFILE（またはprofile）の名前が ~/.aws/config に無い
Error: failed to get shared config profile, example-dev

# 2. profile指定も環境変数も無い
Error: No valid credential sources found

# 3. アクセスキーが無効
Error: Retrieving AWS account details: validating provider credentials:
retrieving caller identity from STS: operation error STS: GetCallerIdentity,
https response error StatusCode: 403, ... api error InvalidClientTokenId:
The security token included in the request is invalid.
```

1は`AWS_PROFILE`や`profile`に書いた名前の綴りか`AWS_CONFIG_FILE`の向き先、2は`aws sso login`の実行忘れかCIでロールを引き受けるステップの欠落を疑います。3はSTSがアクセスキーを有効と認めなかったという意味で、失効・削除済みのキーや、想定と違う場所から読まれたキーが候補です。`aws configure list`を実行すると、キーをマスクした状態で取得元（環境変数か設定ファイルか）が表示されます。環境変数が原因なら`unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN`で外します。

### 権限不足のUnauthorizedOperationを復号して読む手順

認証は通ったのにapplyの途中で`UnauthorizedOperation`が出た場合、EC2系のAPIはエラーにエンコードされた認可メッセージを付けて返します。そのままでは読めないので、STSで復号します。

```
aws sts decode-authorization-message \
  --encoded-message "<エラーに含まれる文字列>" \
  --query DecodedMessage --output text
```

復号結果には、明示的なDenyによる拒否か許可が無いことによる拒否か、リクエストしたプリンシパル、アクション、リソース、条件キーの値が入ります（[DecodeAuthorizationMessageのAPIリファレンス](https://docs.aws.amazon.com/STS/latest/APIReference/API%5FDecodeAuthorizationMessage.html)）。復号を実行するプリンシパルには`sts:DecodeAuthorizationMessage`の権限が必要なので、調査を担当するロールに付けておきます。

### ロックが残ったときのforce-unlock

applyの途中でPCがスリープした、CIのジョブが強制終了された、といった場合はロックが残り、次の実行が`Error: Error acquiring the state lock`で止まります。エラーには`Lock Info`としてID、Path、Operation、Who、Version、Createdが並ぶので、まずWhoとCreatedで「本当に誰も実行していないか」を確かめます。誰も動かしていないと確認できたら、表示されたIDを渡して解除します。

```
terraform force-unlock 7f6394d0-ce97-b9d4-c337-c1737062e1b2
```

[force-unlockのドキュメント](https://developer.hashicorp.com/terraform/cli/commands/force-unlock)のとおり、このコマンドは現在の構成のロックだけを外し、インフラには触りません。IDは誤って別のロックを外さないためのnonceとして働きます。実行中の相手がいる状態で外すとstateが壊れるので、`-lock=false`で回避する運用は採りません。

## terraform-aws-modulesのVPCモジュールと自作モジュールの分岐

### 公開VPCモジュールで複数AZのネットワークを組む設定例

実運用のVPCは、サブネットを複数のアベイラビリティゾーンへ並べ、NATゲートウェイとルートテーブルまで揃えるとリソースブロックが数十本になります。この定型部分は[terraform-aws-modules/terraform-aws-vpc](https://github.com/terraform-aws-modules/terraform-aws-vpc)に任せる方法があり、最新は6.7.3（2026年9月18日UTC公開）です。次の例はaws 6.68.0と組み合わせて`terraform validate`が通っています。`.terraform.lock.hcl`が記録するのはプロバイダの版だけでモジュールの版は記録しないため、モジュールは`version = "6.7.3"`と完全に固定し、上げるときは値の変更をレビューに通します。

```
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "6.7.3"

  name = "sample-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true
}
```

`single_nat_gateway = true`はNATゲートウェイを1台に絞る指定です。NATゲートウェイは稼働時間と処理データ量の両方で課金されるため検証環境では台数を減らしますが、そのAZが落ちると全プライベートサブネットの外向き通信が止まります。AZ障害時も他のAZの外向き通信を保ちたい本番では、`single_nat_gateway = false`と`one_nat_gateway_per_az = true`でAZごとに1台置きます。READMEには、ルートモジュールでのVPCフローログ作成が6系では非推奨で7.0.0で削除予定とあるため、フローログは同じリポジトリの`modules/flow-log`を使います。

### 自作モジュールへ寄せる条件と1.16のモジュール内import

公開モジュールが向くのは、VPCのように構成の型がほぼ決まっていて自社固有の要件が少ないリソースです。社内の命名規則・タグ規約・セキュリティ基準をモジュールの境界で強制したい場合や、入力変数の多さがレビューの負担になっている場合は、必要な引数だけを出す自作モジュールで包みます。分割の粒度は[Terraform moduleの分割粒度とoutput設計｜自作とRegistryの判断](/tech/details/16527/)で扱っています。

手作りの既存VPCをモジュール管理へ移すときは、1.16.0から`import`ブロックをモジュールの内側に書けます。1.15系まではルートモジュールに置く必要があったため、取り込み用の記述をモジュールごとにまとめられるようになりました。手順は[terraform importで既存リソースをコード化する手順｜importブロックとCLIの使い分け](/tech/details/16523/)にあります。

## CloudFormation・AWS CDK・CDKTFと比べたTerraformの選び方

### AWS単独ならCloudFormationとCDKを選ぶ条件

管理対象がAWSだけで、stateファイルの保管と排他制御を自分たちで運用したくないなら、CloudFormationかAWS CDKが有利です。状態はAWS側が持つため、tfstateの置き場もロックも検討から外れます。AWS Organizations配下へStackSetsで一括展開する要件も純正の土俵です。ただしCDKは、`cdk bootstrap`でS3バケット・ECRリポジトリ・IAMロールを含むCDKToolkitスタックを先に作ります。

### Terraformを選ぶ管理対象と、見送るべき体制

Terraformを選ぶ理由が立つのは、管理対象がAWSの外へ広がるときです。AzureやGCP、Datadog、GitHub、CloudflareのようなSaaSまで同じ記法と`plan`・`apply`の流れで管理できます。他クラウドの手順は[TerraformでAzureを構築する手順｜azurerm 5系の認証とbackend設計](/tech/details/16542/)と[TerraformでGCPを構築する手順｜google 7系の認証とGCSバックエンド設計](/tech/details/16544/)で扱っています。AWS自身も[Best practices for using the Terraform AWS Provider](https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/introduction.html)を公開しており、Security・Backends・AWS Provider version managementなどの章立ては設計レビューの観点に使えます。

逆に、対象がAWS単独で、tfstateの保管・ロック・版の更新を担える人が1人しかいない体制では、Terraformは見送るのが妥当です。stateが壊れたときに直せる人がいない構成は、手作業より復旧が難しくなります。プロバイダの変更履歴やセキュリティ修正を確認し、必要な更新を検証して適用する担当を決められない体制も同じです。

### CDKTFを新規採用しない理由と既存構成の移行経路

TypeScriptやPythonでTerraformを書くCDK for Terraform（CDKTF）は、新規採用の候補から外します。HashiCorpは2025年12月10日にサンセットとアーカイブを実施し、[GitHubのリポジトリ](https://github.com/hashicorp/terraform-cdk)は読み取り専用です。最終リリースはv0.21.0（2025年6月4日）で、既存プロジェクトは`cdktf synth --hcl`でHCLを書き出して離脱します。判断材料は[CDK for Terraform（CDKTF）とは？提供終了後の移行判断を解説](/tech/details/16553/)にまとめています。

## よくある質問

### AWSでTerraformを使うメリットは何ですか？

構成をコードの差分としてレビューでき、同じ定義からステージングと本番を同じ形で作れる点です。この利点はCloudFormationでも得られるため、本記事ではAWSとその外側のサービスを1つの記法とワークフローで管理できる点を、Terraformを選ぶ判断で最も重く見ています。既存のモジュールやチームの習熟を理由にAWS単独で選ぶ判断もあります。

### TerraformとTera Termの違いは何ですか？

名前が似ているだけで別物です。TerraformはAWSなどのリソースをコードから作るIaCツール、Tera TermはWindowsでSSH接続などに使う端末エミュレータです。本記事の最小構成はSession Managerで接続するため、SSHクライアントは使いません。

### Terraformの実行にAWS CLIのインストールは必要ですか？

Terraformの実行そのものにAWS CLIは不要です。awsプロバイダは共有config・credentialsファイルや環境変数から直接読みます。ただしIAM Identity Centerのプロファイルを使う場合、`aws configure sso`と`aws sso login`にCLIが要るため、実務ではCLIも入れる構成になります。

### tfstateのロックにDynamoDBはもう不要ですか？

新規構築では不要です。1.11未満のCLIが1台でも残っている間はテーブルを残し、全員の版が上がってから外します。手順はbackendの章にまとめています。

### IaC（インフラのコード化）とTerraformの関係は？

IaC（Infrastructure as Code）は、サーバーやネットワークの構成を手作業でなく定義ファイルで管理する考え方です。TerraformはそのためのツールでHCLという記法で書いた定義を読み、プロバイダと呼ばれるプラグインを介してAWSなどのAPIを呼びます。AWSを扱うときに読み込まれるのがawsプロバイダです。

## 関連記事

- [Terraformのインストール手順をOS別に整理｜tfenv・CIでの版固定まで](/tech/details/16545/)
- [Terraform stateとは？tfstateの構造とS3バックエンド・移動削除の安全手順](/tech/details/16525/)
- [terraform planの差分の読み方｜init・apply・destroyの安全手順](/tech/details/16531/)
- [GitHub ActionsとTerraformでAWSのCI/CDを構築する手順（OIDC・S3ネイティブロック対応）](/tech/details/2715/)
- [TerraformでAzureを構築する手順｜azurerm 5系の認証とbackend設計](/tech/details/16542/)

---

出典: [TerraformでAWSを構築する手順｜aws 6系の認証とS3バックエンド・エラー対処](<https://www.issoh.co.jp/tech/details/1900/>)（株式会社一創）
