実践で学ぶDevOpsツールの使いこなし術 17

「Terraform」で状態ファイルに「リモートバックエンド」を設定してみよう

第17回の今回は、「Terraform」の状態ファイルをS3に保存するための「リモートバックエンド」の設定方法について解説します。

田中 智明

2025年12月16日 6:30

はじめに

第16回では「Terraform」のコードをモジュール化し、ディレクトリを分離して開発環境と本番環境を適切に管理する方法を学びました。VPC、EKS、ECRのモジュールを作成し、各環境で異なるパラメータを渡すことで、コードの重複を避けつつ環境を分離できました。

しかし、現在の構成では状態ファイル(terraform.tfstate)がローカルに保存されているため、チーム開発で問題が発生します。例えば、AさんとBさんがそれぞれ自分のPCからterraform applyを実行すると状態ファイルはAさんBさんそれぞれのPC上で作成、または更新されます。この状態ファイルは同期されず、リソースの重複作成や予期しない削除が発生する可能性があります。また、複数人が同時にterraform applyを実行するとインフラのリソースに競合が発生し、インフラが壊れる危険性があります。

これらの問題を解決するために、本記事では「リモートバックエンド」を設定します。リモートバックエンドとは、状態ファイルをローカルではなくS3などのリモートストレージに保存する仕組みです。これにより、以下のメリットが得られます。

  • 状態の共有: チームメンバー全員が同じ状態ファイルにアクセスできる
  • ロック機構: 複数人が同時にterraform applyを実行することを防ぐ
  • バージョン管理: S3のバージョニング機能により状態ファイルの変更履歴が保存される
  • 暗号化: 機密情報を安全に保護できる

従来、TerraformのS3バックエンドではDynamoDBテーブルを使った排他ロック機能が使用されていました。しかし、Terraform公式ドキュメントによるとDynamoDBベースのロックは非推奨(deprecated)となり、将来のマイナーバージョンで削除される予定です。Terraform v1.10で導入され、v1.11で一般提供(GA)として正式サポートされた「S3 native state locking」では、S3上にロックファイル(.tflock)を作成します。本記事では、この新しい方法(use_lockfile)を使用します。

リモートバックエンドの設定

前回作成したディレクトリ構成を前提に環境ごとにS3バケットを作成し、リモートバックエンドを設定していきます。

S3バケットの作成

環境ごとにS3バケットを作成します。開発環境用と本番環境用にそれぞれバケットを用意しましょう。

開発環境用バケット
$ aws s3api create-bucket \
  --bucket my-terraform-state-dev \
  --region ap-northeast-1 \
  --create-bucket-configuration LocationConstraint=ap-northeast-1

$ aws s3api put-bucket-versioning \
  --bucket my-terraform-state-dev \
  --versioning-configuration Status=Enabled

$ aws s3api put-bucket-encryption \
  --bucket my-terraform-state-dev \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      }
    }]
  }'

$ aws s3api put-public-access-block \
  --bucket my-terraform-state-dev \
  --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
本番環境用バケット
$ aws s3api create-bucket \
  --bucket my-terraform-state-prod \
  --region ap-northeast-1 \
  --create-bucket-configuration LocationConstraint=ap-northeast-1

$ aws s3api put-bucket-versioning \
  --bucket my-terraform-state-prod \
  --versioning-configuration Status=Enabled

$ aws s3api put-bucket-encryption \
  --bucket my-terraform-state-prod \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      }
    }]
  }'

$ aws s3api put-public-access-block \
  --bucket my-terraform-state-prod \
  --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

バケット名は全世界で一意である必要があるため、my-terraform-state-devmy-terraform-state-prodの部分は適切な名前に変更してください。

各コマンドの意味を確認しておきましょう。

create-bucketコマンドで指定したリージョンにS3バケットを作成します。

put-bucket-versioningでバージョニングを有効化します。これにより、状態ファイルの変更履歴が保存されるため、誤った変更があった場合でも以前のバージョンに復旧できます。

put-bucket-encryptionでは、AES256アルゴリズムによるサーバーサイド暗号化を有効化します。状態ファイルにはAWSリソースの設定情報やシークレット情報が含まれる可能性があります。暗号化によりS3のディスク上では暗号化された状態で保存されるため、物理的なストレージへの不正アクセスやバックアップデータの盗難から保護されます。適切なIAM権限を持つユーザーがS3 APIを通じてアクセスする場合は自動的に復号されますが、権限のないユーザーや物理的なアクセスからは内容を読み取ることができません。

put-public-access-blockでバケットへの公開アクセスを完全に遮断します。これによりインターネットからの直接アクセスを防ぎ、セキュリティを確保します。

backend設定の追加

S3バケットが作成できたので、各環境のprovider.tfにbackend設定を追加します。

environments/dev/provider.tf(backend設定を追加)
terraform {
  required_version = "~> 1.13.5"

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

  # リモートバックエンドの設定を追加
  backend "s3" {
    bucket       = "my-terraform-state-dev"  # 作成したバケット名に変更
    key          = "eks-cluster/terraform.tfstate"
    region       = "ap-northeast-1"
    encrypt      = true
    use_lockfile = true
  }
}

provider "aws" {
  region = "ap-northeast-1"
  # 開発環境用のAWSアカウント
}
environments/prod/provider.tf(backend設定を追加)
terraform {
  required_version = "~> 1.13.5"

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

  # リモートバックエンドの設定を追加
  backend "s3" {
    bucket       = "my-terraform-state-prod"  # 作成したバケット名に変更
    key          = "eks-cluster/terraform.tfstate"
    region       = "ap-northeast-1"
    encrypt      = true
    use_lockfile = true
  }
}

provider "aws" {
  region = "ap-northeast-1"
  # 本番環境用のAWSアカウント
}

backend設定の各パラメータを確認しておきましょう。bucketには作成したS3バケット名を指定します。環境ごとに異なるバケットを使用することで、開発環境と本番環境の状態ファイルを完全に分離できます。

keyはS3内での状態ファイルのパスを指定します。プロジェクト名やサービス名などを含めることで、複数のTerraformプロジェクトで同じバケットを使用する場合でも状態ファイルを整理しやすくなります。

regionはバケットが作成されているAWSリージョンを指定します。encrypttrueに設定することで状態ファイルの暗号化を有効化します。

use_lockfiletrueに設定することで、S3ベースのロック機能を有効化します。これにより、DynamoDBテーブルを使用せずにロック機構を実現できます。

このuse_lockfileが従来のDynamoDBベースのロック機構に代わる新しい方法です。S3上に.tflockファイルを作成することで、DynamoDBテーブルを使用せずにロックを実現します。

リモートバックエンドの初期化とリソース作成

backend設定を追加したので、各環境でTerraformを初期化してリソースを作成します。

開発環境の構築
$ cd environments/dev
$ terraform init
$ terraform plan
$ terraform apply

terraform initコマンドを実行するとbackend設定が読み込まれ、S3バックエンドが初期化されます。その後terraform planで変更内容を確認し、terraform applyでリソースを作成します。

実行すると、S3バケットに状態ファイル(eks-cluster/terraform.tfstate)が作成されます。ローカルには状態ファイルが保存されず、すべてS3で管理されるようになります。

開発環境で問題なくリソースが作成されることを確認したら、本番環境を構築します。

本番環境の構築
$ cd ../prod
$ terraform init
$ terraform plan
$ terraform apply

このように、環境ごとに独立してTerraformを実行できます。各環境は異なるS3バケットに状態ファイルを保存するため、互いに干渉することなく管理できます。チームメンバー全員が同じS3バケットにアクセスすることで、状態ファイルを共有できるようになりました。

ロック機構の確認

リモートバックエンドが正しく設定されているか、ロック機構を確認してみましょう。terraform planを実行中に、別のターミナルで同じ環境に対してterraform planを実行すると、以下のようなロックエラーが表示されます。

Error: Error acquiring the state lock

Error message: state is already locked
Lock Info:
  ID:        xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  Path:      my-terraform-state-dev/eks-cluster/terraform.tfstate
  Operation: OperationTypePlan
  Who:       user@hostname
  Version:   1.13.5
  Created:   2025-01-15 12:34:56.789 +0000 UTC

ロックエラーの表示

これは正常な動作です。ロック機構が正しく働いていることを確認できました。S3バケットを確認すると、ロック中はeks-cluster/terraform.tfstate.tflockというファイルが作成されていることが分かります。このファイルは操作が完了すると自動的に削除されます。

S3バケット内の.tflockファイル

この.tflockファイルベースのロック機構は、従来のDynamoDBロックと比べて大きなメリットがあります。

従来のDynamoDBロックでは、dynamodb_tableパラメータでDynamoDBテーブルを指定する必要がありました。そのため、事前にDynamoDBテーブルを作成して管理する必要があり、追加のコストも発生していました。

新しいuse_lockfile方式ではDynamoDBテーブルが不要になり、S3だけでバックエンドが完結します。use_lockfile方式は2024年に追加されたS3の新機能「S3 Conditional Writes」のIf-None-Matchヘッダーを使って実装されたロック機能です。

Terraformが.tflockファイルをIf-None-Match: "*"ヘッダー付きでS3に書き込みリクエストを行います。バケットにファイルがなければロックを取得し、すでにファイルが存在していれば412 Precondition Failedを返答しロック取得失敗となります。これによりS3だけで排他ロックを実現できるようになったため、DynamoDBが不要となりました。S3の仕組みによりロックファイルの読み書きが確実に動作するため、信頼性も高くなっています。

既存のローカル状態をリモートに移行する

もし、すでにローカルでterraform applyを実行済みで、状態ファイルがローカルに存在する場合は、以下の手順でリモートバックエンドに移行できます。

  1. backend設定を追加
    前述の通り、provider.tfにbackend設定を追加します。
  2. terraform init -migrate-stateを実行
    $ terraform init -migrate-state
    バックエンド設定を変更する際は-migrate-stateオプションを指定して状態を移行します。このオプションにより、既存のローカル状態ファイルをS3にコピーできます。

    実行すると、移行の確認が表示されます。
    Do you want to copy existing state to the new backend?
      Pre-existing state was found while migrating the previous "local" backend to the
      newly configured "s3" backend. No existing state was found in the newly
      configured "s3" backend. Do you want to copy this state to the new "s3"
      backend? Enter "yes" to copy and "no" to start with an empty state.
    
      Enter a value: yes
    yesと入力すると、ローカルの状態ファイルがS3にコピーされます。
  3. 移行の確認
    S3バケットに状態ファイルが作成されていることを確認します。
    $ aws s3 ls s3://my-terraform-state-dev/eks-cluster/
  4. ローカルの状態ファイルを削除
    リモートバックエンドへの移行が完了したら、ローカルの状態ファイル(terraform.tfstate)は不要になります。今後はすべてS3から状態が読み込まれるため、ローカルのファイルは削除して構いません。
    $ rm terraform.tfstate terraform.tfstate.backup

リモートバックエンドのセキュリティ強化

本番環境では、さらにセキュリティを強化することが推奨されます。

KMS暗号化の使用

先ほど設定したAES256暗号化では、S3が自動的に暗号化キーを管理するため設定は簡単ですが、キーの使用履歴を追跡することはできません。本番環境でより厳格なセキュリティが求められる場合は「KMS」(Key Management Service)を使用することを検討しましょう。

KMSを使用する主なメリットは、CloudTrailで誰がいつ状態ファイルを復号したかを記録できることです。これにより、状態ファイルへのアクセスを監査できます。

また、KMSではS3のアクセス権限に加えてKMSキーの使用権限も必要になります。管理は複雑になりますが、誤って広範なS3アクセス権限を付与してもKMSキーの権限がなければ復号できません。例えば、開発者全員にS3のReadアクセスを付与しているが、本番環境の状態ファイルは特定の管理者だけが見られるようにしたい場合に有効です。

さらに、万が一状態ファイルに機密情報が含まれていることが判明し、それが外部に漏洩した場合、KMSキーを無効化することで漏洩したファイルを読めなくできます。通常運用では使わない機能ですが、セキュリティインシデント対応の最後の手段として有効です。キーを無効化した後はバックアップから復旧し、新しいキーで再暗号化することになります。

KMSキーの作成
$ KEY_ID=$(aws kms create-key --description "Terraform state encryption key" --query 'KeyMetadata.KeyId' --output text)
$ aws kms create-alias --alias-name alias/terraform-state --target-key-id $KEY_ID
S3バケットでKMSを有効化
 aws s3api put-bucket-encryption \
  --bucket my-terraform-state-prod \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "alias/terraform-state"
      }
    }]
  }'
backend設定でKMSキーを指定
backend "s3" {
  bucket       = "my-terraform-state-prod"
  key          = "eks-cluster/terraform.tfstate"
  region       = "ap-northeast-1"
  encrypt      = true
  kms_key_id   = "alias/terraform-state"
  use_lockfile = true
}

CloudTrailでの監査ログの確認

KMSを使用する最大のメリットは、状態ファイルへのアクセスを監査できることです。CloudTrailで誰がいつ状態ファイルを復号したかを確認してみましょう。

CloudTrailコンソールでの確認手順
  1. AWSマネジメントコンソールでCloudTrailサービスを開く
  2. 左メニューから「イベント履歴」を選択する
  3. フィルタを設定して、KMSキーの使用履歴を検索する
    • 「イベント名」で「Decrypt」を選択(状態ファイルの読み取り時に復号される)
    • または「リソース名」でKMSキーのARNやエイリアスを指定する

    CloudTrailでのイベント履歴検索

  4. 検索結果から特定のイベントをクリックすると詳細情報が表示される
    • ユーザー名: 誰が操作したか(IAMユーザー、ロール)
    • イベント時刻: いつアクセスしたか
    • 送信元IPアドレス: どこからアクセスしたか
    • リクエストパラメータ: どのKMSキーを使用したか
    • イベントの詳細情報

例えば、terraform planterraform applyを実行すると、状態ファイルを読み取るためにDecryptイベントが記録されます。この履歴を定期的に確認することで不正なアクセスがないかを監査できます。

また、CloudTrailのログをS3に保存して長期保管したり、CloudWatch Logsに送信してアラートを設定することもできます。これにより、特定のKMSキーが使用された際にSlackやメールで通知を受け取ることも可能です。

バケットポリシーによるアクセス制御

特定のIAMロールやユーザーのみがバケットにアクセスできるように、バケットポリシーを設定します。

まず、以下の内容でbucket-policy.jsonというファイルを作成します。AWSアカウントIDとIAMロール/ユーザー名は、実際の環境に合わせて変更してください。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowTerraformStateAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::123456789012:role/TerraformExecutionRole",
          "arn:aws:iam::123456789012:user/terraform-user"
        ]
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::my-terraform-state-prod/*"
    },
    {
      "Sid": "AllowListBucket",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::123456789012:role/TerraformExecutionRole",
          "arn:aws:iam::123456789012:user/terraform-user"
        ]
      },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-terraform-state-prod"
    }
  ]
}

次に、このポリシーをバケットに適用します。

$ aws s3api put-bucket-policy --bucket my-terraform-state-prod --policy file://bucket-policy.json

このポリシーを適用することで、承認されたIAMプリンシパルのみが状態ファイルにアクセスできるようになります。

おわりに

本記事では、Terraformの状態ファイルをS3に保存するリモートバックエンドの設定方法を学びました。

従来のDynamoDBベースのロック機構ではなく、use_lockfile方式を使用することで、シンプルかつ効率的なロック機構を実現できました。S3上に.tflockファイルを作成することでDynamoDBテーブルを使用せずにロックを実現し、コストと管理の負担を軽減できます。

リモートバックエンドにより、以下のメリットが得られます。

  • チーム全体での状態共有: 全員が同じ状態ファイルを参照できる
  • 同時実行の防止: ロック機構により競合を回避できる
  • 変更履歴の保存: S3のバージョニングにより誤った変更から復旧できる
  • セキュリティの向上: 暗号化とアクセス制御により機密情報を保護できる

次回は「GitHub Actions」における「CI/CD統合」について解説します。プルリクエスト時にterraform planを自動実行してレビューし、mainブランチへのマージ後にterraform applyを自動実行するワークフローを構築します。また、本番環境への変更には承認フローを設け、安全なデプロイを実現します。

この記事をシェアしてください

人気記事トップ10

人気記事ランキングをもっと見る

企画広告も役立つ情報バッチリ! Sponsored