はじめに
- 先日(2025.3.13)、JAWS-UG東京のIaC Nightというイベントに参加してきました
- その中で、Terraformの固定概念を覆すすごい機能の紹介がされていましたので、試してみました
紹介する機能概要
公式リリースノート
- v1.11.0
- 原文説明
S3 native state locking is now generally available. The use_lockfile argument enables users to adopt the S3-native mechanism for state locking. As part of this change, we've deprecated the DynamoDB-related arguments in favor of this new locking mechanism. While you can still use DynamoDB alongside S3-native state locking for migration purposes, we encourage migrating to the new state locking mechanism. (#36338)
- DeepL翻訳
S3ネイティブのステートロックが一般的に利用可能になった。use_lockfile引数を指定することで、S3ネイティブの状態ロック機構を採用することができます。この変更に伴い、DynamoDB関連の引数は非推奨となりました。移行目的でDynamoDBをS3ネイティブのステートロックと並行して使用することは可能ですが、新しいステートロックメカニズムへの移行を推奨します。(#36338)
- 注意:今回ご紹介するS3 Backend Native Lock機能は、v1.11.0で正式リリース(GA:generally available)された機能のため、それより新しいバージョンを利用する必要があります。
ざっくり解説
- Terraformの同一コードを複数人でデプロイする場合、S3などのオブジェクトストレージをバックエンドに設定し、かつ修正中であることを示すため、DynamoDBを用いてロックする、ということをよくやってきました。
- しかし上記手順では、言ってしまえばstateファイルの管理だけにS3とDynamoDBの2つの管理が必要になってしまいます。
- それが、S3だけでStateファイルの管理とロック機構を備えることができるようになった、というのが今回ご紹介する素晴らしい新機能です。
- ちなみに前述のリリースノートを見て驚いたのですが、DynamoDBによるロック機能はまさかの 非推奨 という扱いになったんですね。。
バージョン情報
- 使用するterraformバージョン
- v1.11.2
- 2025.3.14時点最新
お試しする構成
- 今回はstateファイルの確認だけなので、構成としては非常にシンプルに、VPCとそこにEC2インスタンスを作成するという内容にしています。
Let's Go!!
First Try
-
使用したtfファイル
terraform { required_providers { aws = { source = "hashicorp/aws" version = "5.91.0" } } # S3 Backendを設定 backend "s3" { bucket = "terraform-backend-73m3j2gank8zpymc8yyn" key = "terraform.tfstate" region = "ap-northeast-1" # S3 Backend Native Lock機能を有効にする設定箇所 use_lockfile = true } } provider "aws" { region = "ap-northeast-1" } resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_support = true enable_dns_hostnames = true tags = { Name = "main-vpc" } } resource "aws_subnet" "private" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "ap-northeast-1a" tags = { Name = "private-subnet" } } resource "aws_instance" "al2023" { ami = "ami-0599b6e53ca798bb2" instance_type = "t2.micro" subnet_id = aws_subnet.private.id }
-
上記でinit ⇒ plan ⇒ applyした後のS3バケットの中身
- あれ、中身は今までのstateファイルがあるだけに見えます、、
-
stateファイルの中身
- ロックしているような文字列はなさそうです
stateファイルの中身
{ "version": 4, "terraform_version": "1.11.2", "serial": 14, "lineage": "27dd03bf-de24-a8c4-cef6-24bb0f5d9ea1", "outputs": {}, "resources": [ { "mode": "managed", "type": "aws_instance", "name": "al2023", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ { "schema_version": 1, "attributes": { "ami": "ami-0599b6e53ca798bb2", "arn": "arn:aws:ec2:ap-northeast-1:xxxxxxxxxxxxxx:instance/i-0559650665901c325", "associate_public_ip_address": false, "availability_zone": "ap-northeast-1a", "capacity_reservation_specification": [ { "capacity_reservation_preference": "open", "capacity_reservation_target": [] } ], "cpu_core_count": 1, "cpu_options": [ { "amd_sev_snp": "", "core_count": 1, "threads_per_core": 1 } ], "cpu_threads_per_core": 1, "credit_specification": [ { "cpu_credits": "standard" } ], "disable_api_stop": false, "disable_api_termination": false, "ebs_block_device": [], "ebs_optimized": false, "enable_primary_ipv6": null, "enclave_options": [ { "enabled": false } ], "ephemeral_block_device": [], "get_password_data": false, "hibernation": false, "host_id": "", "host_resource_group_arn": null, "iam_instance_profile": "", "id": "i-0559650665901c325", "instance_initiated_shutdown_behavior": "stop", "instance_lifecycle": "", "instance_market_options": [], "instance_state": "running", "instance_type": "t2.micro", "ipv6_address_count": 0, "ipv6_addresses": [], "key_name": "", "launch_template": [], "maintenance_options": [ { "auto_recovery": "default" } ], "metadata_options": [ { "http_endpoint": "enabled", "http_protocol_ipv6": "disabled", "http_put_response_hop_limit": 2, "http_tokens": "required", "instance_metadata_tags": "disabled" } ], "monitoring": false, "network_interface": [], "outpost_arn": "", "password_data": "", "placement_group": "", "placement_partition_number": 0, "primary_network_interface_id": "eni-068602ad1dc9e91cb", "private_dns": "ip-10-0-1-107.ap-northeast-1.compute.internal", "private_dns_name_options": [ { "enable_resource_name_dns_a_record": false, "enable_resource_name_dns_aaaa_record": false, "hostname_type": "ip-name" } ], "private_ip": "10.0.1.107", "public_dns": "", "public_ip": "", "root_block_device": [ { "delete_on_termination": true, "device_name": "/dev/xvda", "encrypted": false, "iops": 3000, "kms_key_id": "", "tags": {}, "tags_all": {}, "throughput": 125, "volume_id": "vol-012ffa7e907939d2c", "volume_size": 8, "volume_type": "gp3" } ], "secondary_private_ips": [], "security_groups": [], "source_dest_check": true, "spot_instance_request_id": "", "subnet_id": "subnet-09590a8d132549503", "tags": null, "tags_all": {}, "tenancy": "default", "timeouts": null, "user_data": null, "user_data_base64": null, "user_data_replace_on_change": false, "volume_tags": null, "vpc_security_group_ids": [ "sg-0c5c5552b7b035dbf" ] }, "sensitive_attributes": [], "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwicmVhZCI6OTAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", "dependencies": [ "aws_subnet.private", "aws_vpc.main" ] } ] }, { "mode": "managed", "type": "aws_subnet", "name": "private", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ { "schema_version": 1, "attributes": { "arn": "arn:aws:ec2:ap-northeast-1:xxxxxxxxxxxxxx:subnet/subnet-09590a8d132549503", "assign_ipv6_address_on_creation": false, "availability_zone": "ap-northeast-1a", "availability_zone_id": "apne1-az4", "cidr_block": "10.0.1.0/24", "customer_owned_ipv4_pool": "", "enable_dns64": false, "enable_lni_at_device_index": 0, "enable_resource_name_dns_a_record_on_launch": false, "enable_resource_name_dns_aaaa_record_on_launch": false, "id": "subnet-09590a8d132549503", "ipv6_cidr_block": "", "ipv6_cidr_block_association_id": "", "ipv6_native": false, "map_customer_owned_ip_on_launch": false, "map_public_ip_on_launch": false, "outpost_arn": "", "owner_id": "xxxxxxxxxxxxxx", "private_dns_hostname_type_on_launch": "ip-name", "tags": { "Name": "private-subnet" }, "tags_all": { "Name": "private-subnet" }, "timeouts": null, "vpc_id": "vpc-09e7884c692cf0e78" }, "sensitive_attributes": [], "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMSJ9", "dependencies": [ "aws_vpc.main" ] } ] }, { "mode": "managed", "type": "aws_vpc", "name": "main", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ { "schema_version": 1, "attributes": { "arn": "arn:aws:ec2:ap-northeast-1:xxxxxxxxxxxxxx:vpc/vpc-09e7884c692cf0e78", "assign_generated_ipv6_cidr_block": false, "cidr_block": "10.0.0.0/16", "default_network_acl_id": "acl-030e608ea9d931f02", "default_route_table_id": "rtb-097d751ddf7a6606a", "default_security_group_id": "sg-0c5c5552b7b035dbf", "dhcp_options_id": "dopt-ee2d0e89", "enable_dns_hostnames": true, "enable_dns_support": true, "enable_network_address_usage_metrics": false, "id": "vpc-09e7884c692cf0e78", "instance_tenancy": "default", "ipv4_ipam_pool_id": null, "ipv4_netmask_length": null, "ipv6_association_id": "", "ipv6_cidr_block": "", "ipv6_cidr_block_network_border_group": "", "ipv6_ipam_pool_id": "", "ipv6_netmask_length": 0, "main_route_table_id": "rtb-097d751ddf7a6606a", "owner_id": "xxxxxxxxxxxxxx", "tags": { "Name": "main-vpc" }, "tags_all": { "Name": "main-vpc" } }, "sensitive_attributes": [], "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==" } ] } ], "check_results": null }
Second Try
-
何が変わるのかを調べていたところ、以下ブログに必要なS3バケットの権限が記載されていました。
s3:ListBucket: バケット上で状態が保存されるパスをリストできる必要があります。 s3:GetObject: 状態ファイルの読み書きが必要です。 s3:PutObject: 状態ファイルの読み書きが必要です。 s3:DeleteObject: ロックファイルを利用する場合に必要です。
-
前3つはstateファイルをGet/Pushするとか考えると確かに必要そうです。
-
ただ、DeleteObjectってなんで必要なんだろう???と疑問に思い、この権限を省いた以下のポリシーを適用した状態でterraformコードを実行してみました
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "ec2:DeleteSubnet", "ec2:DescribeInstances", "ec2:DeleteTags", "ec2:DescribeInstanceAttribute", "ec2:CreateVpc", "s3:ListBucket", "ec2:DescribeVpcAttribute", "ec2:ModifySubnetAttribute", "ec2:DescribeNetworkInterfaces", "ec2:DescribeAvailabilityZones", "ec2:DescribeVolumes", "ec2:ModifyVpcAttribute", "ec2:DescribeRouteTables", "ec2:DescribeInstanceStatus", "ec2:TerminateInstances", "ec2:DescribeTags", "ec2:CreateTags", "ec2:DeleteNetworkInterface", "ec2:RunInstances", "ec2:DescribeInstanceCreditSpecifications", "ec2:DescribeSecurityGroups", "ec2:CreateNetworkInterface", "ec2:DescribeImages", "s3:PutObject", "s3:GetObject", "ec2:DescribeVpcs", "ec2:DescribeInstanceTypes", "ec2:DeleteVpc", "ec2:CreateSubnet", "ec2:DescribeSubnets" ], "Resource": "*" } ] }
-
ということで、plan ⇒ apply、、をしようとしたところ、planの段階でエラーが出るようになりました
% terraform plan Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: (略) Plan: 3 to add, 0 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. ╷ │ Error: Error releasing the state lock │ │ Error message: failed to delete the lock file: operation error S3: DeleteObject, https response error StatusCode: 403, RequestID: │ KYYW6B7FTA1QS1DX, HostID: │ PUaKZP++7gBpF6mAMNyFN4TuqyEfy20uUke7GlyXHQgS3mHFGylVPanHSVUI+krbZN4LDm6xEdlXrIHvUWKhgG9DeZk7emUlvl20tZ2GLWw=, api error │ AccessDenied: User: arn:aws:iam::xxxxxxxxxxxxxx:user/terraform-user is not authorized to perform: s3:DeleteObject on resource: │ "arn:aws:s3:::terraform-backend-73m3j2gank8zpymc8yyn/terraform.tfstate.tflock" because no identity-based policy allows the │ s3:DeleteObject action │ Lock Info: │ ID: dc059dd5-55bb-ca42-24e1-2a4a4628cfcf │ Path: terraform-backend-73m3j2gank8zpymc8yyn/terraform.tfstate │ Operation: OperationTypePlan │ Who: xxxxxxxxxxxxxx@MacBookAir.local │ Version: 1.11.2 │ Created: 2025-03-15 03:49:13.359389 +0000 UTC │ Info: │ │ │ Terraform acquires a lock when accessing your state to prevent others │ running Terraform to potentially modify the state at the same time. An │ error occurred while releasing this lock. This could mean that the lock │ did or did not release properly. If the lock didn't release properly, │ Terraform may not be able to run future commands since it'll appear as if │ the lock is held. │ │ In this scenario, please call the "force-unlock" command to unlock the │ state manually. This is a very dangerous operation since if it is done │ erroneously it could result in two people modifying state at the same time. │ Only call this command if you're certain that the unlock above failed and │ that no one else is holding a lock. ╵ %
-
エラーをまとめると、以下が記されています。
- "DeleteObject"が権限として足りない
- 権限が足りないことによってロックファイルが削除できない
- ロックファイルの中身らしき内容情報
- まさかMacBookAirから実行していることまでバレるとは、、
- 末尾のメッセージ、2セクションを翻訳すると以下
Terraformは、他のユーザーが同時にTerraformを実行して状態を変更する可能性を防ぐため、状態にアクセスする際にロックを取得します。 このロックを解放する際にエラーが発生しました。 これは、ロックが正しく解放されたかされなかったかを意味します。 ロックが正しく解放されなかった場合、ロックが保持されているように見えるため、Terraformは将来のコマンドを実行できない可能性があります。 このシナリオでは、「force-unlock」コマンドを呼び出して、状態を手動でロック解除してください。 これは非常に危険な操作です。 誤って実行すると、2人が同時に状態を変更する結果になる可能性があるためです。 上記のロック解除が失敗したことと、他に誰もロックを保持していないことが確実な場合にのみ、このコマンドを呼び出してください。
-
どうやらstateファイルはapplyの際に作成されるはずですが、ロックファイルはplanの際にも作成され、実行が完了したタイミングで削除されるという挙動のようです。
- だからDeleteObject権限が必要だったんですね、、
-
ロックファイルの中身
- terraform plan実行時のエラーに記載されていた内容がjson形式で記載されているようです。
{"ID":"dc059dd5-55bb-ca42-24e1-2a4a4628cfcf","Operation":"OperationTypePlan","Info":"","Who":"xxxxxxxxxxxxxxxxx@MacBookAir.local","Version":"1.11.2","Created":"2025-03-15T03:49:13.359389Z","Path":"terraform-backend-73m3j2gank8zpymc8yyn/terraform.tfstate"}
Third Try
-
Second Tryでは"s3:DeleteObject"権限が足りない、、というエラーだったので、今度は"s3:DeleteObject"を付与して実行してみました。
-
なお、ロックファイルがある状態だとplanもapplyも実行できなくなってしまうため、force-unlockオプションにて強制的にロックを解除して進んでいます。
# 今回の場合は以下 % terraform force-unlock dc059dd5-55bb-ca42-24e1-2a4a4628cfcf Do you really want to force-unlock? Terraform will remove the lock on the remote state. This will allow local Terraform commands to modify this state, even though it may still be in use. Only 'yes' will be accepted to confirm. Enter a value: yes Terraform state has been successfully unlocked! The state has been unlocked, and Terraform commands should now be able to obtain a new lock on the remote state. %
-
-
まず、planは問題なく実行できました
% terraform plan Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: (略) Plan: 3 to add, 0 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. %
-
次いでapplyなのですが、このまま実行してよいかの確認である"yes"入力待機の状態にしてS3バケットを確認したところ、ロックファイルができていました
% terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
+ create
Terraform will perform the following actions:
(略)
Plan: 3 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
-
ロックファイルの中身はこちら
- タイムスタンプやIDなどは当然異なっていますが、基本的にはplanの時もapplyの時も内容は差は無いようです。
{"ID":"3655d2d0-bf7a-b0bd-f656-a88a0ee41b4f","Operation":"OperationTypeApply","Info":"","Who":"xxxxxxxxxxxxxxxxx@MacBookAir.local","Version":"1.11.2","Created":"2025-03-15T04:07:57.50775Z","Path":"terraform-backend-73m3j2gank8zpymc8yyn/terraform.tfstate"}
-
"yes"を入力して進んで見たところ、問題なく実行でき、また完了後にはロックファイルは削除されていました。
# yesの入力から Enter a value: yes aws_vpc.main: Creating... aws_vpc.main: Still creating... [10s elapsed] aws_vpc.main: Creation complete after 12s [id=vpc-0340fdd81e0aaa05f] aws_subnet.private: Creating... aws_subnet.private: Creation complete after 0s [id=subnet-049c57836b03b36e9] aws_instance.al2023: Creating... aws_instance.al2023: Still creating... [10s elapsed] aws_instance.al2023: Creation complete after 13s [id=i-0b0dac72a335642be] Apply complete! Resources: 3 added, 0 changed, 0 destroyed. %
Final Try
-
では、ロックファイルができることはわかったので、ロック機構がちゃんと動作するかどうかを確認してみました。
-
Aユーザーのターミナルでapplyを実行、実行してよいかのyes入力のプロンプト状態で、Bユーザーのターミナルでplanを実行してみる、、という確認方法です。
-
Aユーザーターミナル
% terraform apply Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: (略) Plan: 3 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value:
-
Bユーザーターミナル
$ terraform plan ╷ │ Error: Error acquiring the state lock │ │ Error message: operation error S3: PutObject, https response error StatusCode: 412, RequestID: W063VH1HMY6QPHYG, │ HostID: X+O5z4tJ9IkbsegqOGCGc9oER4oG5qHzy3zWIaUP/kInZDP3c2cs7ui7bIA+7C6dstAMWxitqFx924sW5xLsB+r9VOz9DOmP0/mwNlTluQ4=, │ api error PreconditionFailed: At least one of the pre-conditions you specified did not hold │ Lock Info: │ ID: 53111bb8-05e8-a8be-1ba9-6f1340772fe9 │ Path: terraform-backend-73m3j2gank8zpymc8yyn/terraform.tfstate │ Operation: OperationTypeApply │ Who: xxxxxxxxxxxxxxxxx@MacBookAir.local │ Version: 1.11.2 │ Created: 2025-03-15 04:18:47.374204 +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. ╵ $
-
ちゃんとロックされていることがわかりますね。
まとめ
- 従来S3とDynamoDBという2つのサービスを使用しなければstateのロックが実現できなかったのに対し、S3バケットのみでロックが完結するというのは非常にシンプルで使い勝手が良いと感じました。
- 更には機能を使用する際に、バックエンドのセクションに以下を追加するだけ!というのも素晴らしいです。
use_lockfile = true
- 注意事項として、以下の権限付与が必要なため、お忘れなく!
s3:ListBucket s3:GetObject s3:PutObject s3:DeleteObject ←これが忘れやすそうな印象