DynamoDBさようなら、S3だけでTerraformのステートロックが完結する時代の到来へ

# はじめに
- 先日(2025.3.13)、JAWS-UG東京のIaC Nightというイベントに参加してきました
  - [JAWS-UG東京 IaC Night 〜入門から上級まで！AWSをコードで構築しよう〜](https://jawsug.connpass.com/event/344954/)
- その中で、Terraformの固定概念を覆すすごい機能の紹介がされていましたので、試してみました
  - [該当ページはこちら](https://speakerdeck.com/casek/enhancing-axa-data-platform-with-terraform?slide=29)
# 紹介する機能概要
## 公式リリースノート
- [v1.11.0](https://github.com/hashicorp/terraform/releases/tag/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ファイル

  ```hcl
  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バケットの中身
  ![S3バケットの中身](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276335/b2a086e4-c115-491e-ac01-62dcb1490f23.png)
  - あれ、中身は今までのstateファイルがあるだけに見えます、、
- stateファイルの中身
  - ロックしているような文字列はなさそうです
  <details><summary>stateファイルの中身</summary>

  ```
  {
    "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
  }
  ```

  </details>
## Second Try
- 何が変わるのかを調べていたところ、以下ブログに必要なS3バケットの権限が記載されていました。
  ```
  s3:ListBucket: バケット上で状態が保存されるパスをリストできる必要があります。
  s3:GetObject: 状態ファイルの読み書きが必要です。
  s3:PutObject: 状態ファイルの読み書きが必要です。
  s3:DeleteObject: ロックファイルを利用する場合に必要です。
  ```
  - [S3バケットに対して必要な権限](https://zenn.dev/ldr/articles/626cbbed8a96ed#s3%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%81%AB%E5%AF%BE%E3%81%97%E3%81%A6%E5%BF%85%E8%A6%81%E3%81%AA%E6%A8%A9%E9%99%90)
- 前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の段階でエラーが出るようになりました

  ```shell
  % 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権限が必要だったんですね、、
- ということで、S3バケットを確認すると、しっかりロックファイルが作成されていることがわかりました。
  ![S3バケットのロックファイル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276335/f842b5d9-2023-492f-ab6c-022931be868c.png)
- ロックファイルの中身
  - terraform plan実行時のエラーに記載されていた内容がjson形式で記載されているようです。

  ```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オプションにて強制的にロックを解除して進んでいます。

    ```shell
    # 今回の場合は以下
    % 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は問題なく実行できました

  ```shell
  % 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バケットを確認したところ、ロックファイルができていました

![ThirdTryS3Bucket.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276335/e6745e29-8580-43e8-ac3e-4aad6e4a47c5.png)

  ```shell
  % 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の時も内容は差は無いようです。
  ```json
  {"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"を入力して進んで見たところ、問題なく実行でき、また完了後にはロックファイルは削除されていました。

  ```shell
  # 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.
  % 
  ```

  ![apply完了後のS3バケット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276335/c6e4b7b3-da5e-41d0-973b-046fc8f76767.png)
## 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 ←これが忘れやすそうな印象
  ```