Terraform Config Generator

Sketch starter HCL in this tab. Pick a provider, stack resources, variables and outputs, then copy a main.tf. Plan and apply still belong on your machine, pointed at an account you intend to pay for.

Terraform HCL workbench

Pin before you add objects

main.tf
  • 0resources
  • 0variables
  • 0outputs
  • Local state

Read this before apply

terraform apply spends money. Treat the file as a bill.

A terraform config generator in a browser tab has one honest job: emit a starter .tf file you still have to fmt, init, and plan yourself. The file is not documentation. terraform apply talks to a cloud API and creates billable objects. A weekend lab with one instance and a public IP looks cheap until the instance stays up for six weeks because nobody ran destroy.

This page never calls plan. This page never calls apply. Those commands belong on your machine.

If you are replacing a live VPC, close this tab. Open the existing state instead. The builder has never seen your account, your backend, or the IDs already in the world. Sketching a throwaway lab is the job. Rewriting production from a blank form is how duplicate NAT gateways get born.

A concrete walk-through

What the default AWS sketch will try to buy

Leave AWS selected. Keep the seeded aws_region variable. Add one aws_instance named web from the sample. Copy main.tf, drop the file in an empty folder, run terraform init, then terraform plan.

Init downloads hashicorp/aws at the pinned 5.x line and writes .terraform.lock.hcl. Commit the lockfile. Plan then stalls on ami, because the sample points at var.ami_id and no such variable exists yet. That stall is useful. A hardcoded AMI from a blog post is usually from another region, already deregistered, or both.

Fix the stall with a data source, not a guessed AMI string:

data "aws_ami" "debian" {most_recent = true
owners = ["136693071363"]filter {name = "name"
values = ["debian-12-amd64-*"]}}
resource "aws_instance" "web" {ami = data.aws_ami.debian.id
instance_type = "t3.micro"}

Plan then shows one instance in us-west-2. Apply creates the instance. Destroy is the only cleanup. Close the laptop without destroy and the instance keeps billing. The sample tags the instance toolexe-lab so the stray is easy to find later in the console.

The part people skip

The state file is the inventory. HCL is the wish list.

Teams new to Terraform treat main.tf as the source of truth. The source of truth is terraform.tfstate. HCL describes the shape you want. The state file records what Terraform believes exists, including IDs you never typed. Delete the state while the resources stay up and the next apply tries to create duplicates. Keep the state and delete the HCL, and Terraform wants to destroy everything in the next plan.

Where the truth lives after the first apply
You changeTerraform believesUsual result
HCL onlyState still matches the old worldPlan proposes create, update, or destroy
Cloud console onlyState is staleNext plan fights you, or ignores the drift until refresh
State file deletedEmpty workspaceApply recreates objects already in the account
Backend pointed at the wrong bucketSomeone else's inventory, or noneTwo workspaces managing one account

Remote backends exist for this reason. S3 with a lock table, Terraform Cloud, GCS, Azure Blob. They store state off the laptop and take a lock so two applies do not collide. The generator writes no backend block on purpose. A guessed bucket name is worse than local state, because the next init silently points at the wrong place. After you copy the file, add a backend yourself, run terraform init -migrate-state, and keep state out of git.

Need names for the secrets the backend will read? Sort them in the environment variable manager rather than committing a terraform.tfvars full of keys.

Language, not a form

Four blocks, four jobs

resource
Create and manage an object. aws_instance.web is a resource. Terraform owns the lifecycle. Changing the local name is a destroy and create, not a rename in the cloud.
data
Read an object you do not own. An AMI lookup belongs here. Putting a lookup in a resource block creates something you meant to find. This page does not emit data sources. Add them after copy, the way the Debian example above does.
variable
Input. A default makes apply quiet. No default means Terraform prompts, or a .tfvars file must supply the value. The receipt under the HCL lists variables with no default so you see the prompts before you run plan.
output
Values other workspaces or humans need, such as an instance id. An output is not a cloud object. Remove one and nothing in AWS dies. Marking one sensitive blanks CLI prints. The value still sits in state.

A fifth type exists. module wraps a folder of the four above. This page never writes module calls. A generated module would guess your interface and then fight you in review. Write the root file here. Extract a module once the same pattern appears three times.

Pick the right generator

Terraform, CloudFormation, and Ansible are not substitutes

Three files, three jobs. Mixing them because "infrastructure as code" is a category in a sidebar wastes a week.

Which file you should be generating
The workReach forLeave Terraform alone when
AWS objects, or two cloudsThis page, then real modulesYou already have a CloudFormation stack with drift you understand
AWS objects, AWS only, org already on stacksAWS CloudFormation generatorYou need a provider outside AWS
Packages, files, services on a boxAnsible playbook generatorYou wanted a VM more than you wanted nginx.conf
Cluster objectsKubernetes YAML generatorYou do not already use Terraform as the cluster's source of truth

A Compose file for local services belongs in the Docker Compose generator. The Docker provider on this page talks to a daemon. Compose talks to a project. Different files, different commands, different failure modes.

A destroy you did not mean

count will retire the wrong machine

count = length(var.names) looks neat. Then you remove the second name from the list. Terraform sees index 1 vanish and index 2 slide into its place. The remaining instances get new addresses. One of them is destroyed and rebuilt. The "wrong" box is the one whose index moved, not the one you deleted from the list.

# address today address after you drop "api"
aws_instance.web[0] web aws_instance.web[0] web
aws_instance.web[1] api aws_instance.web[1] worker # was [2]aws_instance.web[2] worker

for_each keyed on a map or a set of strings survives the edit, because addresses use the key, not a position. Use count for identical objects with no identity of their own. Use for_each once names, AZs, or environment keys exist. The samples in the builder use neither. Add for_each after you copy, once you know the keys.

Pins

~> 5.0 is a contract with the registry

required_providers plus required_version stop a teammate on Terraform 0.14 from running a file written for 1.6. ~> 5.0 means 5.x, not 6.x. The day the AWS provider ships 6.0, 6.0 stays out until you raise the pin on purpose. Leave the constraint off and init pulls whatever is newest, including a major with renamed resources.

The Azure button says Azure. The file says azurerm. GCP is google. Docker comes from kreuzwerker/docker, not hashicorp/docker. Mixing product names with registry names is the first error most people hit after a copy-paste from two tutorials.

Commit .terraform.lock.hcl. Skip the lockfile and every laptop resolves a slightly different 5.x, then CI fails on an argument your laptop never saw.

A print filter, not a vault

sensitive = true hides CLI output. Encryption lives elsewhere.

Marking an output sensitive blanks the value in terraform output. The value still sits in state in plaintext unless the backend encrypts at rest. Same story for variables. sensitive = true on a variable stops the value echoing in logs. Anyone with the state file still reads the secret.

Do not paste live keys into the form. Feed credentials from the environment with TF_VAR_, or from a vault your runner already trusts. If apply runs in CI, put those keys in repository secrets and write the workflow with the GitHub Actions workflow generator. The HCL should name the variable. The HCL should not contain the password.

Honest limits

Blocks this page refuses to invent

  • backend. Bucket names, DynamoDB tables, and Terraform Cloud org names are yours. Guessing them is how two laptops share the wrong state.
  • module calls. Source, version, and the variable interface belong to a module you already trust, not a form.
  • import, moved, removed. State surgery is a review, not a dropdown.
  • provider aliases. Multi-region AWS needs provider "aws" { alias = "use1" }. Add aliases after you copy, once you know the regions.
  • IAM policy JSON. A generated policy is either too open or too wrong. Write the JSON by hand, then attach the document to the role.
  • plan and apply. No credentials leave this tab, so no API call happens. Run those on a machine you control.

Download main.tf. Run terraform fmt. Run terraform init. Run terraform plan against an empty workspace. Read the plan like a receipt. Then apply, or do not.

Questions after the first plan

Plan, state, backends, pins, and what this tab will not do.

Does this page run terraform plan or apply?

No. The tab assembles HCL in JavaScript. Init, plan, apply, and destroy still run on your machine against an account you choose. Treat the download as a starting file, then fmt the file and read the plan before you apply.

Why is there no backend block in the output?

A backend needs a bucket, a prefix, or a Terraform Cloud organization already in use. Inventing those names here would point init at the wrong place. Add the backend after you copy, then run terraform init -migrate-state if you already applied with local state.

Where do AWS keys and Azure subscriptions go?

Out of the file. The AWS provider reads the environment, a shared credentials file, or an instance role. Azurerm 4 reads ARM_SUBSCRIPTION_ID or a subscription_id argument. Google reads GOOGLE_CREDENTIALS or application default credentials. Put secrets in the environment or a vault, not in main.tf.

Why does the Azure button write azurerm instead of azure?

The registry address is hashicorp/azurerm. The provider block label must match the registry name. GCP is google. Docker is docker from kreuzwerker, not hashicorp. The buttons use product names. The file uses registry names.

Does sensitive = true encrypt the output?

No. The flag blanks the value in CLI prints and some logs. State still stores the value. Use a backend with encryption at rest, restrict who reads the state, and keep secrets out of git. Sensitive is a print filter.

Should I split the download into versions.tf, variables.tf, and outputs.tf?

Terraform loads every .tf file in the folder, so the split is for humans. One main.tf is fine for a lab. Split once the file is long enough for review to hurt. The annotate option writes comments naming those files so the cut is obvious.

Why did init install a different provider version than I typed?

The version field is a constraint, not a pin to one build. ~> 5.0 allows any 5.x. The exact build is recorded in .terraform.lock.hcl after the first init. Commit the lockfile so CI and laptops share the same 5.x.

How do I attach this file to objects already in the cloud?

terraform import, or an import block in Terraform 1.5 and later. The generator does not write import blocks because the object id has to come from your account. Write the resource to match reality, then import, then plan and confirm the plan is empty before you change anything.

Does any of this HCL leave the browser?

No. Generation, copy, and download all run in this tab. Close the tab and the stack is gone. Still avoid pasting production keys into any web form. Local JavaScript is not a secret store.