For Terraform remote state, availability and consistency come first. On AWS, the standard pattern is to store state in S3 and acquire locks via DynamoDB.
This article focuses on S3-side encryption design and DynamoDB-side lock design, balancing exam-relevant talking points with safe operational practices in the real world.
Terraform state may contain sensitive values, and accidental deletion or contention can be catastrophic. Durability, encryption, prevention of concurrent runs, and auditability are therefore non-negotiable.
S3 stores the state. DynamoDB is the single source of truth for locking. S3 versioning enables rollbacks, and DynamoDB's mutual exclusion prevents concurrent applies. The network path is protected by TLS to the AWS API.
From an exam perspective, the high-yield topics are S3 encryption method selection, KMS key permissions, bucket versioning, the DynamoDB lock table key schema, and handling force-unlock.
Design your keys first and foremost to avoid workspace collisions. Using workspace_key_prefix and separating keys by env/workspace name/module is both readable and safe. Since 2020, S3 guarantees strong object consistency, but you still need locking.
Versioning is mandatory; it makes recovery from accidental deletes or overwrites realistic. Combine it with lifecycle rules to set retention for older versions and control costs. MFA Delete carries significant operational overhead, so it is commonly substituted with auditing plus versioning.
Think of encryption in two layers. Terraform's backend s3 setting encrypt=true requires SSE-S3 with S3-managed keys. If organizational policy mandates customer-managed KMS keys, specify the CMK in the bucket's default encryption and enforce SSE-KMS via the bucket policy and KMS key policy. The backend has no option to directly specify a particular KMS key.
| Aspect | SSE-S3 (S3-managed keys) | SSE-KMS (customer-managed keys) | Client-side encryption |
|---|---|---|---|
| Key management | AWS side. Users do not directly manage the keys | Users manage the CMK; policies and rotation are supported | The app encrypts; key distribution is an operational challenge |
| Ease of implementation | Very easy; the backend's encrypt=true is enough to get you there | Requires designing S3 bucket default encryption and KMS policies | Significant implementation burden; you must also consider decryption compatibility |
| Audit and control | Limited; detailed KMS auditing is not available | KMS provides key-usage logs; IAM/KMS policies enable fine-grained control | Left to the application; cloud-side auditing is limited |
| Cost | Virtually no additional cost | KMS requests and CMK costs apply | Encryption itself is free, but operational costs grow |
| Exam focus | The fact that encrypt=true means SSE-S3 | S3 default encryption plus KMS permissions are required | Generally discouraged; only for special requirements |
Before write operations like apply, Terraform reaches out to DynamoDB to acquire a lock. The table has a simple schema with only a LockID partition key, and the lock is acquired via a conditional PutItem on LockID. On failure, Terraform assumes an existing lock and either waits or fails.
PAY_PER_REQUEST is the recommended throughput mode; it handles the intermittent load from multiple teams and CI runners naturally. SSE is recommended, though sensitive values generally do not end up in the table. In the rare case of a failure you can use terraform force-unlock, but only after confirming there are truly no concurrent runs.
Lock and write flow during a Terraform state update
Keeping IAM permissions for state to a strict minimum is the safest approach. For S3, grant GetObject and PutObject on the specified prefix of the target bucket, plus ListBucket (limited by prefix), and typically do not grant DeleteObject. When using SSE-KMS, grant KMS Encrypt/Decrypt/GenerateDataKey/DescribeKey scoped to the key.
DynamoDB needs GetItem/PutItem/DeleteItem and DescribeTable for acquiring and releasing locks. Table-scan permissions are not required.
Backend configuration values do not support variable references, so write them statically or externalize them via -backend-config. The safe procedure is to first create the bucket, table, and so on with local state, then migrate with terraform init -migrate-state.
An example of an S3 bucket, KMS, DynamoDB lock table, and least-privilege policy (HCL, abridged)
# 先に local backend で作成し、その後 remote backend に移行すること。
terraform {
required_version = ">= 1.3"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.0"
}
}
backend "s3" {
bucket = "my-tf-state-bucket"
key = "env/prod/app1/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-lock"
encrypt = true # SSE-S3 を要求。SSE-KMS はバケット側で設定する
# 変数参照不可。変更時は terraform init -migrate-state
}
}
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_kms_key" "tf_state" {
description = "Terraform state at rest encryption"
enable_key_rotation = true
}
resource "aws_s3_bucket" "tf_state" {
bucket = "my-tf-state-bucket"
}
resource "aws_s3_bucket_versioning" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.tf_state.arn
}
}
}
resource "aws_s3_bucket_lifecycle_configuration" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
rule {
id = "expire-old-versions"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 90
}
}
}
resource "aws_dynamodb_table" "tf_lock" {
name = "terraform-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
server_side_encryption {
enabled = true
}
}
# ステート操作ロール用 IAM ポリシー例(必要最小限)
data "aws_iam_policy_document" "tf_state_access" {
statement {
sid = "S3StateReadWrite"
actions = ["s3:GetObject", "s3:PutObject"]
resources = [
"${aws_s3_bucket.tf_state.arn}/env/prod/app1/*"
]
}
statement {
sid = "S3List"
actions = ["s3:ListBucket"]
resources = [aws_s3_bucket.tf_state.arn]
condition {
test = "StringLike"
variable = "s3:prefix"
values = ["env/prod/app1/*"]
}
}
statement {
sid = "DynamoDBLock"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:DescribeTable"]
resources = [aws_dynamodb_table.tf_lock.arn]
}
statement {
sid = "KMSForS3State"
actions = ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey"]
resources = [aws_kms_key.tf_state.arn]
}
}
resource "aws_iam_policy" "tf_state_access" {
name = "tf-state-access"
policy = data.aws_iam_policy_document.tf_state_access.json
}As a rule, avoid manipulating the same state from multiple accounts. The safer pattern is to consolidate into a single account and cross account boundaries via AssumeRole. Keep the backend in one place and have working roles access it.
S3 cross-region replication is useful for disaster recovery, but state requires a single authoritative copy. Disallow writes on the replica and treat it strictly as a read-only recovery option. When using SSE-KMS, set up a KMS Grant for the replication role and configure the KMS key on the destination side.
Avoid multi-regionalizing the lock table with DynamoDB global tables; that breaks the singularity of the lock. As a rule, place the lock in a single location in the same region as the authoritative S3 copy.
In day-to-day operations, regularly check S3 versioning and lifecycle, KMS key expiration and rotation, and CloudTrail data events. Follow your naming conventions for workspaces and keys to prevent collisions.
If a stale lock prevents apply from proceeding, first confirm via your monitoring stack or CI that no running session exists, then inspect the lock item in DynamoDB. Use terraform force-unlock only when truly necessary. When state is corrupted, roll back to the most recent S3 version and verify health with terraform state pull.
Permission errors most often stem from either the KMS policy or the S3 bucket policy. When using a CMK via S3 default encryption, always confirm that the IAM role in question is also permitted by the KMS key policy.
Associate / Pro
問題 1
Organizational policy requires Terraform state to always be encrypted with a customer-managed KMS key. Which is the correct S3 backend design?
正解: A
The S3 backend's encrypt=true is a flag for enabling SSE-S3 and cannot specify a particular KMS key. To require SSE-KMS, configure the CMK in the S3 bucket's default encryption and use the bucket policy and KMS key policy together so that only the target role can use the key.
Is the key schema for the DynamoDB lock table fixed?
Yes. It must be a single-key schema with a partition key named LockID. No sort key is required. Terraform uses LockID with a conditional PutItem and rejects the lock acquisition if a duplicate exists. TTL is not used for Terraform's lock control.
What happens if you do not specify DynamoDB with the S3 backend?
Locking is disabled and the risk of state corruption from concurrent runs increases significantly. Outside of small or personal use cases, always configure dynamodb_table in production.
What KMS permissions are required when using SSE-KMS?
Typically you need kms:Encrypt, kms:Decrypt, kms:GenerateDataKey, and kms:DescribeKey. Even when using a CMK via the S3 default encryption, grant the caller permission to use the key in both the IAM policy and the KMS key policy.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...