Guide · Terraform

Run Terraform against a local AWS

HomeCloud speaks the AWS APIs, so the stock hashicorp/aws provider works against it unchanged. You point the provider at a local endpoint, turn on path-style S3, and terraform apply creates real containers, databases and load balancers on your machine.

HomeCloud v0.4.0Updated 2026-10-118 min read

Most "local AWS" setups for Terraform are about testing configuration without a bill or a sandbox account. The catch with emulators is that a successful apply often means only that the API accepted the call. With HomeCloud, an aws_db_instance is a PostgreSQL container you can connect to, an aws_lambda_function runs on AWS's own runtime image, and an aws_lb is nginx forwarding to your tasks. This guide sets up the provider, runs a complete example stack, and explains what the nightly module tests cover.

1. Start HomeCloud and export credentials

You need Docker (Engine on Linux, Docker Desktop or OrbStack on macOS and Windows) and Terraform 1.5+ or OpenTofu.

shell
curl -fsSL https://homecloud.pages.dev/scripts/install.sh | sh
homecloud serve                    # first start prints the root console password once

# in another terminal
eval "$(homecloud aws-env)"        # AWS_ENDPOINT_URL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
export AWS_ENDPOINT_URL=http://localhost:8080   # a host name for Terraform, see below
aws sts get-caller-identity

The keys belong to the account HomeCloud created on first start. They are real credentials: HomeCloud verifies SigV4 signatures and evaluates IAM policies on every call, so dummy keys such as test/test are rejected.

2. Configure the provider

There are two ways to point the provider at HomeCloud. The simplest relies on AWS_ENDPOINT_URL, which the provider reads for every service. This is what HomeCloud's own module test suite does:

providers.tfendpoint from the environment
provider "aws" {
  region = "us-east-1"

  # HomeCloud serves buckets by path, so bucket names need not resolve as host names.
  s3_use_path_style = true
}

If you prefer the configuration to say where it deploys, list the services in an endpoints block. This is the form used by the shop example; delete the block and the same files target AWS:

main.tfexplicit endpoints
variable "endpoint" {
  default = "http://localhost:8080"   # a host name, not an IP address
}

provider "aws" {
  region            = "us-east-1"
  s3_use_path_style = true

  endpoints {
    s3     = var.endpoint
    sts    = var.endpoint
    iam    = var.endpoint
    lambda = var.endpoint
    sqs    = var.endpoint
    rds    = var.endpoint
    ec2    = var.endpoint
    # ...one line per service the configuration uses
  }
}

Three details matter:

  • s3_use_path_style = true is the only HomeCloud-specific setting. Without it the provider puts the bucket name in the host name.
  • Use localhost, not 127.0.0.1, in the endpoint. The provider's S3 client prefixes the host with the account ID, which does not work with an IP address. homecloud aws-env prints 127.0.0.1, which is fine for the AWS CLI and SDKs, hence the extra export above.
  • No skip_credentials_validation, skip_requesting_account_id or skip_metadata_api_check. HomeCloud implements STS, so the provider's start-up checks pass as they do on AWS.

There is one region, us-east-1, and one account. Configurations that create resources in a second region (Secrets Manager replicas, multi-Region KMS replicas, cross-region RDS replicas) get an AWS-shaped error.

3. Apply a complete stack

The repository ships a realistic example in examples/terraform/shop: a VPC with public subnets, an application load balancer with HTTP and HTTPS (an ACM certificate validated through Route 53), an ECS Fargate service running nginx, RDS PostgreSQL with the master password managed in Secrets Manager, S3, DynamoDB, SNS to SQS to a Lambda consumer, and an API Gateway HTTP API.

shell
eval "$(homecloud aws-env)"
export TF_VAR_endpoint=http://localhost:8080      # the variable's default; set it for another host or port

cd examples/terraform/shop
terraform init
terraform apply -auto-approve       # about four minutes, mostly the database and load balancer

Then check that the pieces actually work, not just that they exist:

shell
curl "$(terraform output -raw api_url)"    # {"service": "shop-api", "path": "/hello"}

aws sqs send-message --queue-url "$(terraform output -raw queue_url)" \
  --message-body '{"order":1}'
aws logs tail "$(terraform output -raw consumer_log_group)" --since 5m   # "order received: ..."

docker port hc-elb-shop-alb             # the host port nginx is published on

terraform plan                         # No changes.
terraform destroy -auto-approve         # removes the containers, networks and volumes too

The example's README also shows how to connect to the database with the password Terraform never saw, read from Secrets Manager.

What the module tests cover

The shop stack uses plain resources. Real projects lean on community modules, so HomeCloud runs 17 scenarios built from terraform-aws-modules at pinned versions, nightly, with OpenTofu and the stock provider (6.x). Each scenario has to apply, pass a check that the resources really work (an object read back, a message delivered, a function invoked through its alias and its queue, nginx served through the load balancer, a DNS answer, a decrypted ciphertext), re-plan with no changes, and destroy cleanly. The last run passed all 17.

terraform-aws-modules scenarios that pass on HomeCloud
AreaModules
Networkvpc, security-group, alb, route53, acm
Computelambda (with alias), ecs (Fargate behind an ALB), apigateway-v2
Datas3-bucket, dynamodb-table, rds (PostgreSQL 16)
Messagingsqs, sns, eventbridge
Securityiam (policy, role, user, group), kms, secrets-manager, ssm-parameter
Observabilitycloudwatch (log groups, metric filters, alarms, composite alarms, saved queries)

Module versions, per-service scores and how to run the suite yourself (compat/run.sh) are in docs/compatibility.md.

What is stored but not enforced

Some resources apply, re-plan and destroy as on AWS but have no single-host equivalent, so HomeCloud keeps them as records. Plan your tests around these:

  • NAT gateways: a VPC with an internet gateway and a default route reaches the internet from every subnet; private subnets do not need the NAT gateway.
  • Network ACLs: stored and reported, never enforced. Security groups are enforced, as iptables rules inside the VPC.
  • Application Auto Scaling: targets and policies are stored; the ECS desired count does not change.
  • S3 AbortIncompleteMultipartUpload lifecycle rules are stored; MinIO cannot run them.

Coverage beyond these modules is listed per service and per operation in docs/aws-compat.md. CloudFormation works too, including aws_cloudformation_stack, minus nested stacks and custom resources.

In CI

The same flow runs on a stock GitHub Actions runner. The HomeCloud action downloads a checksum-verified release, starts the server and exports the AWS_* variables; a post step removes every container it started.

.github/workflows/terraform.yml
steps:
  - uses: actions/checkout@v4
  - uses: solinode/homecloud/integrations/github-action@main
    with:
      services-wait: s3                  # wait for MinIO too
  - uses: hashicorp/setup-terraform@v3
  - env:
      AWS_ENDPOINT_URL: http://localhost:8080   # a host name, as above
    run: |
      terraform init
      terraform apply -auto-approve
      terraform plan -detailed-exitcode   # fails the job if the re-plan shows drift
      terraform destroy -auto-approve

More options, including testcontainers for Go and Python, are in the testing and CI guide. If you are moving from another emulator, read the LocalStack alternative guide; for Lambda-heavy stacks, testing Lambda locally.