🦍

tfstateをAWS S3で管理する

2023/12/15に公開

TerraformのtfstateファイルをS3で管理する方法

概要

個人開発でTerraformを利用する場合は、ローカルPCなどのTerraform実行環境でtfstateを管理しても特に問題ないですが、複数人のチームでTerraformを利用しインフラを構築・運用していく場合では、各メンバーが同じtfstateにアクセスする必要があります。また、そのファイルにアクセスできたとしてもファイルロックを行わない限り、複数人で同時に編集・実行を行った際に競合が発生してしまいます。これらの事象を解決するために、よくAWSのS3とDynamoDBを利用したtfstate管理方法が用いられることがあります。

今回はそのS3とDynamoDBを利用したtfstate管理方法と、本当にそれで競合を防ぐことができるのか?を検証してみたいと思います。

手順

  1. S3とDynamoDB Tableの作成
  2. TerraformのBackend指定
  3. tfstateのロック状態を再現してみる

1. S3とDynamoDB Tableの作成

S3バケット作成

まずはtfstateを保管するためのS3を作成します。私は、以下のコードを・・・/backend/s3.tfというファイル名で保存しました。バケット名は適宜変更してください。

s3.tf
# プロバイダー指定
provider "aws" {
  region = "ap-northeast-1"
}

# tfstate管理バケット
resource "aws_s3_bucket" "terraform_state" {
  bucket = "your-bucket-name"
  lifecycle {
    prevent_destroy = true
  }
}

# バージョニング有効化
resource "aws_s3_bucket_versioning" "versioning_enabled" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

# 暗号化
resource "aws_s3_bucket_server_side_encryption_configuration" "default" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# パブリックアクセスをブロック
resource "aws_s3_bucket_public_access_block" "public_access" {
  bucket                  = aws_s3_bucket.terraform_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

DynamoDB Table作成

次に・・・/backend/dynamodb.tfを作成します。テーブル名は適宜変更してください。

dynamodb.tf
resource "aws_dynamodb_table" "terraform_locks" {
  name = "your-table-name"
  billing_mode = "PAY_PER_REQUEST"
  hash_key = "LockID"
  attribute {
    name = "LockID"
    type = "S"
  }
}

BucketとTableの作成

・・・/backend/dynamodb.tf・・・/backend/s3.tfとを作成したら、/backendディレクトリに移動してterraformを初期化します。

terraform init

以下のような実行結果がでればOKです。

Terraform has been successfully initialized!

続いてapplyを行い、実際にバケットとテーブルを作成します。
ここではapplyしてますが、事前にplanを行い必要なリソースが作成されるかを確認して下さい。

terraform apply

以下のようにapplyが成功すればOKです。マネコンからリソースが作成されているかを確認してください。

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

この段階ではまだtfstateはローカルにあリます。・・・/backend/terraform.tfstateが確認できるかと思います。次に、TerraformのBackendをS3に指定して、S3上でtfstateを管理できるようします。

2. TerraformのBackend指定

Backendを指定するには、Terraformブロックを作成してそこに設定を書き込んでいきます。今回は、・・・/backend/main.tfを作成しました。また、先ほど・・・/backend/s3.tfに記載していたproviderブロックもこちらに移動してきています。

main.tf
provider "aws" {
  region = "ap-northeast-1"
}

terraform {
  backend "s3" {
    bucket         = "your-bucket-name"
    key            = "backend/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "your-table-name"
    encrypt        = true
  }
}

上記のファイルを作成したら、再度Terraformを初期化します。

terraform init

そうすると以下のようなメッセージが出力されます。

Initializing the backend...
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:

Terraformがローカルにあるtfstateファイルを検知し、指定したバケットにtfstateを移動させるか聞いてきます。yesを入力すると以下のように初期化が完了します。

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.30.0

Terraform has been successfully initialized!

手順としては以上です。

3. tfstateのロック状態を再現してみる

これまでの手順を実施しておくと、次回terraformを実行する際に、自動でバックエンドのS3にあるtfstateを確認しにいくようになります。バケットのバージョニングも設定しているので、S3上でtfstateのバージョン管理も行われています。ただ、本当にロックできてるの?という心配性なあなたに朗報です。
競合状態を再現してみましょう!

ロック状態を再現する

まず、適当にS3バケットを先ほど書いたコードに追加します。
・・・/backend/s3.tfに以下を追記してください。

resource "aws_s3_bucket" "terraform_state_2" {
  bucket = "your-bucket-name-2"
  lifecycle {
    prevent_destroy = true
  }
}

planを実行すると、新しいバケット"your-bucket-name-2"が差分として検出されます。そのままapply→yesでバケットを作成しますが、そこで!!!!!!

ここ重要
yesでapplyを実行し、ターミナル上でapplyが実行されているまさにその途中でターミナルを閉じてください。
control + cではなく、ターミナルをぶちってください。

そうすると、DynamoDBにロック項目が作成されたまま、tfstateがlockされている状況を再現できます。control + cだとできませんでした。

ちなみにその時のDynamoDBを確認するとこんな感じ

このロックがかかった状態で再度planを実行するとこのようになります。

╷
│ Error: Error acquiring the state lock
│ 
│ Error message: ConditionalCheckFailedException: The conditional request failed
│ Lock Info:
│   ID:        ******************
│   Path:      ***************/backend/terraform.tfstate
│   Operation: OperationTypeApply
│   Who:       **********************
│   Version:   1.5.7
│   Created:   2023-12-14 22:57:08.431619 +0000 UTC
│   Info:      
│ 
│ 
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.

(必要に応じて「****」でマスキングしてます)

ロックがかかった状態(=誰かがterraformでリソースを変更している)ではtfstateに変更がかかるような操作はできないということが確認できました。これで安心して複数人のチームでtfstateを共有できますね。

一応、ロックを解除しておきましょう。

terraform force-unlock <Lock ID>
上記のコマンドを実行すると、先ほど確認したDynamoDBのロック項目が消えており、planも問題なく実行できるようになります。

4. まとめ

クラウドのインフラ構築ではスタンダードになりつつあるTerraformの初歩的な部分ですが、検証してみると案外楽しいですね。

Discussion