Skip to content

Migrate the Terraform S3 backends off dynamodb_table onto use_lockfile and delete the lock tables #218

Open

Description

@ale210

Overview

We need to replace the deprecated dynamodb_table backend parameter with use_lockfile = true in hackforla/incubator and hackforla/devops-security, then delete the two now-unused DynamoDB lock tables, because HashiCorp deprecated DynamoDB-based state locking in Terraform 1.11 and will remove it in a future minor version. Both repos already run 1.16.1, so every plan and apply in both is printing the deprecation warning today.

Action Items

Facts verified 2026-09-07, so you do not have to re-derive them:

  • The warning fires 3x per plan run and 4x per apply, in every run sampled in both repos back to 2026-08-08. It reads Warning: Deprecated Parameter / The parameter "dynamodb_table" is deprecated. Use parameter "use_lockfile" instead. and points at backend.tf line 7, which is misleading — that line is an empty backend "s3" {} block. The parameter is in terraform/prod.backend.tfvars, which the workflows pass as backend_config_file.
  • use_lockfile went generally available in Terraform 1.11, in the same release that deprecated the DynamoDB arguments. Both repos resolve required_version = "~> 1.16.0" to 1.16.1, so there is no version work.
  • No new IAM permission is needed. S3-native locking needs s3:GetObject, s3:PutObject and s3:DeleteObject on <key>.tflock. The apply roles (incubator-tf-apply, devops-security-tf-apply) hold AdministratorAccess. The plan roles never take a lock at all — dflook/terraform-plan passes -lock=false unconditionally (image/entrypoints/plan.sh:15, checked at both the pinned v1.49.0 and at main). That is also why the plan roles work today holding only ReadOnlyAccess over DynamoDB.
  • Both state buckets (hfla-incubator-terraform-state, hfla-ops-terraform-state) have versioning Enabled.
  • Both lock tables hold exactly one item and it is not a lock — it is the …/terraform.tfstate-md5 digest row. No lock is currently held in either.
  • Both tables have DeletionProtectionEnabled: true, so deleting them is a two-call operation. Neither has point-in-time recovery.
  • These are the only two DynamoDB tables in account 035866691871. After step 3 the account has none.

1. hackforla/incubator — 1 PR, terraform/prod.backend.tfvars:

  • Delete the dynamodb_table = "hfla_incubator_terraform_table" line and add use_lockfile = true.
  • Add a no-op comment line to any .tf file in the same PR. Both workflows filter on paths: ['**/*.tf', '**/*.yaml'], and prod.backend.tfvars matches neither pattern — so a PR that changes only the tfvars triggers no workflow at all, on the PR or on the merge. This is not optional: without it there is nothing to verify against and the merge produces no apply run.
  • Treat a non-empty plan as a hard gate. A backend change produces zero resource changes. If the plan shows any, stop and raise it rather than merging.
  • After the PR merges, confirm the Terraform apply (OIDC) run succeeds and that no Deprecated Parameter warning appears anywhere in its log.

2. hackforla/devops-security — 1 PR. This one is bigger than incubator's, because three other things in this repo reference the lock table:

  • terraform/prod.backend.tfvars — delete dynamodb_table = "hfla_ops_terraform_table", add use_lockfile = true.
  • terraform/aws-custom-policies/tf-plan-scoped.json — delete the whole AllowDynamoDBLockTableAccessForTerraformPlan statement. This file is live: terraform/aws-custom-policies.tf feeds it to modules/aws-policies, which deploys it as the IAM policy IncubatorTfPlanSecretsRead, currently attached to the incubator-tf-plan role. Leaving the statement would leave a live policy granting DynamoDB actions on a table that no longer exists.
    • Do not add .tflock permissions to replace it. The statement immediately above it already grants s3:PutObject/s3:DeleteObject on arn:aws:s3:::hfla-ops-terraform-state/*, which covers the lock file — and the plan role does not lock anyway.
    • Do not "fix" the fact that a policy named IncubatorTfPlanSecretsRead, attached to incubator's plan role, grants access to devops-security's backend. That cross-wiring predates this work. If it looks wrong to you, raise it as its own issue.
  • CONTRIBUTING.md — delete the #### Set up DynamoDB to store the backend state section (lines 157–174, through the *** after Back to Top), and in the Creating Local tfvars file example (~line 251) swap dynamodb_table = "hfla_ops_terraform_table" for use_lockfile = true. This is the section that tells every new member to create a lock table by hand.
  • .github/ISSUE_TEMPLATE/pre-work-template-devops-security.md — delete the - [ ] Create the DynamoDB table sub-item (line 40) and reword its parent (line 38) so it no longer promises a DynamoDB step.
  • Same no-op .tf comment requirement, and it is worse here: this repo's filter is paths: ['**/*.tf'] only, so .tfvars, .json, .md and the issue template all fail to trigger it.
  • The plan gate is different in this repo. Expect exactly one change: an in-place update of module.aws_custom_policies.aws_iam_policy.custom_policy["IncubatorTfPlanSecretsRead"], from the JSON edit. Anything else — any replacement, any second resource — is a stop-and-raise.
  • After the PR merges, confirm Apply Terraform changes on merge succeeds with no Deprecated Parameter warning, and that the policy's new default version no longer contains the DynamoDB statement.
  • After the merge, re-open /issues/new/choose and confirm the pre-work template renders without the DynamoDB step. GitHub renders issue templates from the default branch only, so this genuinely cannot be checked from the branch.

3. Delete both tables — only after both PRs have merged and both apply runs are green:

  • Confirm no lock is held in either table. aws dynamodb scan --table-name <table> --region us-west-2 --query 'Items[].LockID.S' must return only the -md5 row.

  • Record both table definitions in a comment on this issue before deleting, so the change is reversible without a PR trail to read. Capture aws dynamodb describe-table and aws dynamodb list-tags-of-resource for each.

  • Disable deletion protection, then delete. Both calls need admin in 035866691871 — if you do not have it, hand this step off rather than working around it.

    for t in hfla_incubator_terraform_table hfla_ops_terraform_table; do
      aws dynamodb update-table --table-name "$t" --region us-west-2 --no-deletion-protection-enabled
      aws dynamodb delete-table --table-name "$t" --region us-west-2
    done
  • Verify: aws dynamodb list-tables --region us-west-2 returns an empty list, and the next apply in each repo still succeeds.

  • To reverse, recreate the table and re-enable protection. Terraform rewrites the -md5 digest row itself on the next apply, so the row does not need restoring. hfla_ops_terraform_table additionally carried tags managed-by=exempt and project=devops-security; hfla_incubator_terraform_table carried none.

    aws dynamodb create-table --table-name <table> --region us-west-2 \
        --attribute-definitions AttributeName=LockID,AttributeType=S \
        --key-schema AttributeName=LockID,KeyType=HASH \
        --billing-mode PAY_PER_REQUEST
    aws dynamodb update-table --table-name <table> --region us-west-2 --deletion-protection-enabled

Do the backend swap in one step, not a two-phase migration — but know why:

  • Terraform documents that dynamodb_table and use_lockfile may be set simultaneously, and that is the official migration path. Its only purpose is to keep two clients that disagree about the locking mechanism from both acquiring a lock. Since holding both keeps the deprecation warning firing, a phased migration means four PRs to clear a warning that two will clear.
  • The risk it covers is real but small here: while this is in flight, a terraform apply from a local checkout that still has dynamodb_table would not see a CI lock taken through S3, or vice versa. Mitigate by not running a local apply against either repo on the day the PRs merge, and by the lock check in step 3.
  • Note for anyone with an existing local checkout: CI initialises from a clean container every run, so there is no cached backend config and no migration prompt. A local .terraform/ directory will prompt Backend configuration changed and needs terraform init -reconfigure.

Out of scope — do not do these here:

Resources/Instructions

  • Files, all on main in both repos. Line numbers were accurate 2026-09-07 and may drift — locate each by its content rather than by position.
    • hackforla/incubator: terraform/prod.backend.tfvars
    • hackforla/devops-security: terraform/prod.backend.tfvars, terraform/aws-custom-policies/tf-plan-scoped.json, CONTRIBUTING.md, .github/ISSUE_TEMPLATE/pre-work-template-devops-security.md
  • S3 backend documentation — the use_lockfile argument, the statement that DynamoDB locking "is deprecated and will be removed in a future minor version", the simultaneous-configuration note, and the .tflock permission list.
  • Terraform 1.11 CHANGELOG — "S3 native state locking is now generally available", the release that introduced the deprecation.
  • dflook/terraform-plan's -lock=false behaviour: image/entrypoints/plan.sh line 15.
  • Runs showing the warning, for comparison after the fix: incubator apply 34061802778, devops-security apply 34009820416.

Activity

  1. moved this from New Issue Review to Prioritized Backlog in CoP: DevOps: Project Boardon Sep 10, 2026
  2. ale210 commented on Sep 21, 2026

    @ale210
    MemberAuthor

    Quick note - we need to update the onboarding instructions if we are dropping dynamodb for locks

  3. moved this from Prioritized Backlog to In progress (actively working) in CoP: DevOps: Project Boardon Sep 22, 2026
  4. here commented on Sep 22, 2026

    @here
    Member

    Taking a look at this one as I just made the same change on a local project and noticed the pattern here.

  5. here commented on Sep 23, 2026

    @here
    Member

    Made progress on this and ran into permissions errors on pushing branch to incubator repo and aws when running terraform init and plan.

    Errors possibly related to my GitHub and aws identities being inactive with hfla for some time. I'll join the 6pm call tomorrow and/or comment here or reach out on Slack to @mikewaggoner if helpful.

    Local initial diff for reference:

    diff --git a/terraform/backend.tf b/terraform/backend.tf
    index c6bc2b2..bc59834 100644
    --- a/terraform/backend.tf
    +++ b/terraform/backend.tf
    @@ -4,6 +4,7 @@ terraform {
       # 1.12 to 1.16 unnoticed. The dflook plan/apply actions resolve this to the
       # latest matching release, so this line alone chooses the CI Terraform version.
       required_version = "~> 1.16.0"
    +  # Locking config lives in terraform/prod.backend.tfvars (use_lockfile).
       backend "s3" {
       }
       required_providers {
    diff --git a/terraform/prod.backend.tfvars b/terraform/prod.backend.tfvars
    index a74f3c1..ac4b4b8 100644
    --- a/terraform/prod.backend.tfvars
    +++ b/terraform/prod.backend.tfvars
    @@ -1,5 +1,5 @@
     bucket         = "hfla-incubator-terraform-state"
     key            = "incubator/terraform.tfstate"
     region         = "us-west-2"
    -dynamodb_table = "hfla_incubator_terraform_table"
    +use_lockfile   = true
     encrypt        = true
    \ No newline at end of file
    
  6. self-assigned this
    on Sep 24, 2026
  7. here commented on Oct 1, 2026

    @here
    Member

    Opened hackforla/devops-security#204 to Re-onboarding self for aws permissions to continue on this issue.

  8. here commented on Oct 1, 2026

    @here
    Member

    Local incubator terraform plan and init fail with missing local resources. Pushing changes to PR to review terraform plan output from CI/CD without local setup.

    ╷
    │ Error: reading Route53 Hosted Zone (Z099349812ZUUFQEPL51Q): operation error Route 53: GetHostedZone, https response error StatusCode: 403, RequestID: 9f848ebf-b66a-4eac-a1ac-e5e7981b48c4, api error AccessDenied: User: arn:aws:iam::390979745100:user/herehfla is not authorized to access this resource
    │
    │
    ╵
    ...
    │ Error: reading ELBv2 Load Balancers: couldn't find resource
    │
    │   with module.civic-tech-jobs.module.certificate_stage.data.aws_lb.this,
    │   on modules/acm-certificate/main.tf line 44, in data "aws_lb" "this":
    │   44: data "aws_lb" "this" {
    │
    ╵
    
  9. here commented on Oct 1, 2026

    @here
    Member

    Changing status to Review for confirmation and to merge hackforla/incubator#249 as next step.

  10. moved this from In progress (actively working) to Questions/Review in CoP: DevOps: Project Boardon Oct 1, 2026
  11. hfla-graphql-app commented on Oct 14, 2026

    @hfla-graphql-app

    Hello @here -

    Please add an update using the below template (even if you have a pull request). Afterwards, remove
    the status: to update! label and add the status: updated label.

    1. Progress: What is the current status of this issue? What have you completed and what is left to do?
    2. Blockers: Explain any difficulties or errors encountered.
    3. Availability: How much time will you have this week to work on this issue?
    4. ETA: When do you expect this issue to be completed?
    5. Pictures (optional): Add any pictures of the visual changes made to the site so far.

    If you need help, be sure to either: 1) place your issue in the "Questions/Review" status-column of the
    Project Board and ask for help at your next meeting; 2) put a status: help wanted label on your issue
    and pull request; or 3) put up a request for assistance on the team's Slack channel.

    You are receiving this comment because your last update was before 10/07/2026, 02:55 PM PDT.

  12. added
    status: updatedNo blockers and update is ready for review
    and removed on Oct 14, 2026
  13. moved this from In progress (actively working) to Done in CoP: DevOps: Project Boardon Oct 17, 2026
  14. here commented on Oct 21, 2026

    @here
    Member

    Moving back to in progress to begin additional items in description,

    1. hackforla/devops-security — 1 PR. This one is bigger than incubator's, because three other things in this repo reference the lock table:

    Progress: Task 1 of 3 completed
    Blockers: No known blockers
    Availability: 1-2 hours
    ETA: 2-3 weeks

  15. moved this from Done to In progress (actively working) in CoP: DevOps: Project Boardon Oct 21, 2026
  16. here commented on Oct 22, 2026

    @here
    Member

    Requesting merge of hackforla/devops-security#219 after peer review. I'll continue on with next steps as detailed above after merge completes successfully.

    Confirmed terraform plan in MR is as expected with one change at https://github.com/hackforla/devops-security/actions/runs/39697096038/job/119557015040?pr=219

    Terraform will perform the following actions:
      # module.aws_custom_policies.aws_iam_policy.custom_policy["IncubatorTfPlanSecretsRead"] will be updated in-place
    ...
    Plan: 0 to add, 1 to change, 0 to destroy.
    
  17. moved this from In progress (actively working) to Questions/Review in CoP: DevOps: Project Boardon Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions