linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/lessons/tf-intermediate-06-remote-backend

lesson ── terraform-intermediate ── ~18 мин ── 5 шагов

Remote state in S3 (on LocalStack)

Until now the state lived in terraform.tfstate next to your HCL. That works while you are alone. A team of two means two state files, and they drift apart within the first week.

Now you will build the boilerplate setup: one root creates the S3 bucket for the state and a DynamoDB table for the lock. A second root uses them as a remote backend. This is the standard layout: bootstrap separate from the application.

▶ интерактивный sandbox

Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    Bootstrap: HCL for the bucket and the table

    Create the files in /home/student/tf-backend/bootstrap/:

    main.tf:

    hcl
    resource "aws_s3_bucket" "state" {
      bucket = "linuxlab-tfstate"
    }
    resource "aws_s3_bucket_versioning" "state" {
      bucket = aws_s3_bucket.state.id
      versioning_configuration {
        status = "Enabled"
      }
    }
    resource "aws_dynamodb_table" "lock" {
      name         = "linuxlab-tflocks"
      billing_mode = "PAY_PER_REQUEST"
      hash_key     = "LockID"
      attribute {
        name = "LockID"
        type = "S"
      }
    }

    This is a separate root. Its state is local (by default). The bucket and the table are the only resources, and they exist to hold the state of the other roots.

    S3 versioning is mandatory: it saves you when the state gets overwritten by accident. See tf-remote-backend-s3.

    ✓ Bootstrap is described. Init plus apply will create the backend infra.

  2. 02

    Apply the bootstrap

    bash
    cd /home/student/tf-backend/bootstrap
    terraform init
    terraform apply -auto-approve

    After apply:

    bash
    aws --endpoint-url=http://localstack:4566 s3 ls
    aws --endpoint-url=http://localstack:4566 dynamodb list-tables

    You see linuxlab-tfstate and linuxlab-tflocks. Ready to use.

    ✓ The backend infra is created. Now the app root will point at it.

  3. 03

    App root with backend configuration

    In /home/student/tf-backend/app/ create main.tf:

    hcl
    terraform {
      backend "s3" {
        bucket         = "linuxlab-tfstate"
        key            = "app/terraform.tfstate"
        region         = "us-east-1"
        dynamodb_table = "linuxlab-tflocks"
        # For LocalStack
        endpoints = {
          s3       = "http://localstack:4566"
          dynamodb = "http://localstack:4566"
        }
        skip_credentials_validation = true
        skip_metadata_api_check     = true
        skip_requesting_account_id  = true
        use_path_style              = true
        encrypt                     = false
      }
    }
    resource "random_id" "suffix" {
      byte_length = 4
    }
    resource "aws_s3_bucket" "demo" {
      bucket = "linuxlab-app-${random_id.suffix.hex}"
    }

    Notice:

    • backend "s3" goes inside the terraform { } block.
    • key = "app/terraform.tfstate" is the path inside the bucket. The convention is <project>/<env>/terraform.tfstate.
    • endpoints. Needed only for LocalStack. On real AWS you drop it.
    • encrypt = false, for LocalStack. On real AWS, set it to true, preferably with KMS.

    Backend parameters cannot be interpolated through var.*. They are static, or you use a partial backend.

    ✓ The app root is described with an S3 backend. Init will pick it up.

  4. 04

    Init the app: state goes to S3

    bash
    cd /home/student/tf-backend/app
    terraform init

    The output will show:

    Initializing the backend...
    Successfully configured the backend "s3"! Terraform will automatically
    use this backend unless the backend configuration changes.

    There will be no local terraform.tfstate file. The state goes to S3. Check it:

    bash
    ls -la /home/student/tf-backend/app/
    aws --endpoint-url=http://localstack:4566 s3 ls s3://linuxlab-tfstate/

    In the directory, there is no terraform.tfstate. In the bucket, still empty (no apply yet).

    ✓ The app sees the S3 backend. State will be written to the bucket.

    The same thing on OpenTofu

    OpenTofu keeps its CLI and state compatible with Terraform for the commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. But on the first switch, back up your state and run it on a feature branch first. The differences cluster in the newer features (variables in the backend, state encryption, OCI registry-backed modules). See tf-opentofu-parity for the full matrix.

    • → OpenTofu parity
  5. 05

    Apply: the state appears in S3

    bash
    terraform apply -auto-approve

    After apply, look at what is in the bucket:

    bash
    aws --endpoint-url=http://localstack:4566 s3 ls s3://linuxlab-tfstate/app/

    You will see terraform.tfstate. Download it and look:

    bash
    aws --endpoint-url=http://localstack:4566 s3 cp s3://linuxlab-tfstate/app/terraform.tfstate /tmp/state.json
    cat /tmp/state.json | jq '.serial, .lineage'

    That is your state. If a colleague in another terminal runs terraform init with the same backend parameters, they get the same state.

    Now check the lock through DynamoDB:

    bash
    aws --endpoint-url=http://localstack:4566 dynamodb scan --table-name linuxlab-tflocks

    After apply there are no records (the lock is released). If someone tries to run in parallel with your apply, they will hit the lock and fail with Error acquiring the state lock. See tf-common-errors.

    ✓ The state lives in S3. Ready for teamwork, ready for CI/CD.

    Partial backend and multi-env

    All four or five backend parameters duplicated across prod/stage/dev? That is bad: edit one parameter, and you get N PRs.

    The fix is a partial backend:

    hcl
    terraform {
      backend "s3" {}   # empty block
    }

    And you pass the parameters at init:

    bash
    # backends/prod.hcl
    bucket         = "linuxlab-tfstate"
    key            = "app/prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "linuxlab-tflocks"
    # backends/dev.hcl
    bucket         = "linuxlab-tfstate"
    key            = "app/dev/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "linuxlab-tflocks"
    bash
    terraform init -backend-config=backends/prod.hcl
    # or
    terraform init -backend-config=backends/dev.hcl

    One HCL. N environments. This is the typical multi-env layout. In detail in lesson 12.

    • → Remote state in S3
    • → Init and backends

Что ты узнал

The S3 backend keeps the state file in a bucket and the lock record in DynamoDB. You configure it in terraform { backend "s3" { ... } }. Backend values cannot be interpolated, so the parameters are either static or passed through -backend-config at init.

команды

  • terraform init -migrate-statemove from the old backend to a new one
  • terraform init -backend-config=prod.hclpartial backend, parameters from a file
  • aws --endpoint-url=$LS s3 ls s3://BUCKET/look at the state files in the bucket

концепции

  • · Bootstrap (bucket and table): a separate root, created once
  • · The app root configures the backend, and its state lives in the bucket you created
  • · The lock works only if everyone goes through the terraform CLI

← предыдущий

Destroy: tear down infrastructure safely

следующий →

OPA + Rego, gating plan.json

Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies