tfstateファイルをS3に保管する

tfstateファイルは実際の環境がterraformの管理下にあるのかを記述している情報が記載されている。個人で運用する分には問題ないが、複数人で運用する際はtfstateファイルのズレが発生する。terraform applyでコードを環境に反映した際にtfstateファイルも更新されるからだ。

そうなると各開発者のローカル環境で統一されたtfstateファイルを運用することは不可能であり、もし運用したとしたら、おそらく環境が壊れる。

GitHubなどのリモートリポジトリ管理も選択肢に入るかもしれないが、これもコンフリクトや機密情報を含む可能性があるため、非推奨の管理となる。

と言ったところで選択肢に入るのがS3での管理である。 ただしここにも注意点があり、Terraform管理対象環境でのS3管理は避けた方が良い。Terraform自身によって環境を変化させてしまう危険性があるからだ。 またS3のみを使用しても今度は同時更新のリスクも出てきてしまう。それらを完全回避するにはDynamoDBを使用した排他制御が必要になる。 今回は練習のためTerraform管理環境でS3+DynamoDBを使用した管理を記載していく。

※tfstateファイルの管理が多い、もしくはS3などを使用したくない場合はTerraform Cloudを使用するのが楽かもしれない。 Terraform Cloud

前提

TerraformでAWS環境を変更できること(AWS CLIやterrafrom init済みなこと)

main.tfを作成

main.tfは下記のように作成しておく。 この後、S3を作成する際に再度編集する。 main.tf

terraform {
	required_version = "1.12.1"
	required_providers {
		aws = {
			source = "hashicorp/aws"
			version = "5.99.1"
		}
	}
}
 
provider "aws" {
	region = "ap-northeast-1"
	profile = "admin"
	default_tags {
		tags = {
			Env = var.environment
			Project = var.project
		}
	}
}

variables.tf 変数定義

variable "project" {
	type = string
	description = "プロジェクト名"
}
 
  
 
variable "environment" {
	type = string
	description = "環境名"
}

terraform.tfvars 変数への値設定

project = "tfstate-storage"
environment = "prod"

S3を作成

まずは本体を作成する s3.tfを作成。 バケット名は一意である必要があるため、randomリソースを使用して命名に使用している

# ランダム値生成
resource "random_string" "s3_unique_key" {
	length = 6
	upper = false
	lower = true
	numeric = true
	special = false
} 
 
# 本体
resource "aws_s3_bucket" "tfstate_storage" {
	bucket = "tfstate-storage-${random_string.s3_unique_key.result}"
	tags = {
		Name = "${var.project}-${var.environment}-s3"
	}
}

S3のバージョニングを有効にする。 こうすることでtfstateの上書きを避けて、ロールバックが可能な状態にできる。(バージョニングを使用しな場合はそのまま上書きされてしまうため、絶対に必要な設定である)

# バージョニング設定
resource "aws_s3_bucket_versioning" "tfstate_storage" {
	bucket = aws_s3_bucket.tfstate_storage.id
	versioning_configuration {
		status = "Enabled"
	}
}

アクセス設定を行う。publicアクセスは認めない設定で作成

# アクセス設定(publicアクセスをできないようにした)
resource "aws_s3_bucket_public_access_block" "tfstate_storage" {
	bucket = aws_s3_bucket.tfstate_storage.id
 
	block_public_acls = true # パブリックACLの設定を拒否する
	block_public_policy = true # すでに存在するパブリックACLを無視する
	ignore_public_acls = true # パブリックポリシー(Principal: "*")の設定をブロック
	restrict_public_buckets = true # パブリックポリシーが存在しても実際のアクセスを制限
}

ここまでで一旦、S3の作成は完了である(この後も追記はする) 全体像

# ランダム値生成
resource "random_string" "s3_unique_key" {
	length = 6
	upper = false
	lower = true
	numeric = true
	special = false
} 
 
# 本体
resource "aws_s3_bucket" "tfstate_storage" {
	bucket = "tfstate-storage-${random_string.s3_unique_key.result}"
	tags = {
		Name = "${var.project}-${var.environment}-s3"
	}
}
 
# バージョニング設定
resource "aws_s3_bucket_versioning" "tfstate_storage" {
	bucket = aws_s3_bucket.tfstate_storage.id
	versioning_configuration {
		status = "Enabled"
	}
}
 
# アクセス設定(publicアクセスをできないようにした)
resource "aws_s3_bucket_public_access_block" "tfstate_storage" {
	bucket = aws_s3_bucket.tfstate_storage.id
 
	block_public_acls = true # パブリックACLの設定を拒否する
	block_public_policy = true # すでに存在するパブリックACLを無視する
	ignore_public_acls = true # パブリックポリシー(Principal: "*")の設定をブロック
	restrict_public_buckets = true # パブリックポリシーが存在しても実際のアクセスを制限
}

Key Management Serviceでキーの作成

tfstateファイルにはVPC IDやDBのパスワードなどが含まれる。 そのため、 S3に保存されたtfstateファイルを暗号化しておきたい。 KMSは暗号鍵を安全に生成・管理・利用するためのマネージドサービスであり、これを連携していく。(実際に暗号化と複合化はS3で行う。そのために設定も追加していく)

key.tfを作成する

resource "aws_kms_key" "tsstate_storage_key" {
	description = "description" # 説明
	deletion_window_in_days = 7 # 削除する際は7日後に削除される
	enable_key_rotation = true # キーのローテションをするか
	tags = {
		Name = "${var.project}-${var.environment}-kms"
	}
} 
 
resource "aws_kms_alias" "tsstate_storage_key_alias" {
	name = "alias/tsstate_storage_key_alias"
	target_key_id = aws_kms_key.tsstate_storage_key.id
}

エイリアスをつける理由として key-id(uuid)で呼び出しを行わなければならないため、可読性が非常に低い。かつ、エイリアス先は変更できるため、鍵の変更も容易になる。

# エイリアスあり
kms_master_key_id = "alias/terraform-tfstate"
# エイリアスなし
kms_master_key_id = "c8cf2cdc-399f-42fa-8b76-c01efff5f855"

S3への追記 S3の暗号化設定で先ほどのKMSの鍵を使用するように指定。 s3.tf

# ランダム値生成
resource "random_string" "s3_unique_key" {
	length = 6
	upper = false
	lower = true
	numeric = true
	special = false
} 
 
# 本体
resource "aws_s3_bucket" "tfstate_storage" {
	bucket = "tfstate-storage-${random_string.s3_unique_key.result}"
	tags = {
		Name = "${var.project}-${var.environment}-s3"
	}
}
 
# 省略~~~~~~~~~~
 
# 追記
# 暗号化設定
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate_storage" {
	bucket = aws_s3_bucket.tfstate_storage.id
	
	rule {
		apply_server_side_encryption_by_default {
			kms_master_key_id = aws_kms_key.tsstate_storage_key.arn
			sse_algorithm = "aws:kms"
		}
	}
}

ここまででS3の準備は完了である。 これでもtfstateファイルは運用可能な状態であるが、複数人で行う際には問題が発生するケースががある。 それは同時に変更を行った場合である。同時変更が記録され、tfstateファイルの中身が壊れてしまう恐れがある。 これを回避するためにDynamoDBを使用し、terraform plan / apply 開始時に DynamoDB からロック取得できるようにする。

DynamoDBを作成する

db.tfを作成する

resource "aws_dynamodb_table" "tfstate-lock-db" {
	name = "tfstate-lock"
	billing_mode = "PAY_PER_REQUEST"
	
	hash_key = "LockID" # 主キーの名前
	attribute { # 属性定義
		name = "LockID"
		type = "S" # String
	}
}

コマンド実行

terraformコマンドを実行し、S3、KMS、DynamoDBを実施に反映していく

terraform apply

main.tfに追記を行う

実際にAWS上にリソースが作成されたのを確認したら、main.tfにtfstateファイルの保存先の設定を記載してく terraformブロックのbackendでtfstateファイルの保存先を指定できる

backendの中では変数や使えないのでハードコーディングをすることになる main.tf

terraform {
	required_version = "1.12.1"
	required_providers {
		aws = {
			source = "hashicorp/aws"
			version = "5.99.1"
		}
	}
	# 設定を追記
	backend "s3" {
		bucket = "tfstate-storage-icpnvm"
		key = "state/terraform.tfsstate"
		region = "ap-northeast-1"
		encrypt = true
		kms_key_id = "alias/tsstate_storage_key_alias"
		dynamodb_table = "tfstate-lock"
	}
}

記載し終わったら、

terraform init

を行い

terraform apply

をすれば完了である。

CLIアカウントに適切なポリシーをアタッチしていない場合はエラーになるため、エラー内容に従ってポリシーをアタッチしていけば良い。 筆者は S3への権限、DynamoDBへの権限でエラーになったのを確認できたため、設定を行い無事にtfstateファイルの保存をs3にすることができた。

簡単ではあるが、以上とする。