| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/local 2.9.0 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform and a free HCP Terraform account — Terraform lab environment on Ubuntu |
| Privilege | Normal user |
| Scope | Connecting the Terraform CLI to HCP Terraform for remote runs and remote state — authentication, the cloud block, initializing a CLI-driven workspace, remote plan and apply, execution modes, workflow types, and first-run troubleshooting. Does not cover project administration, variable sets, policies, or migrating an existing state file. |
| Related guides | Terraform backends and remote state Terraform state explained terraform init terraform apply Terraform workspaces |
Everything you have run so far kept two things on your own machine:
- The state file that records what Terraform built
- The process that runs plan and apply
That works until a second person needs to run terraform apply, or until you want an audit trail of who changed what. HCP Terraform moves both of those off your laptop while leaving the commands you type exactly as they are:
Ubuntu Terraform CLI
│
▼
HCP Terraform workspace
│
├── remote state
└── remote plan/apply environmentYou still run terraform plan and terraform apply in a terminal. The work happens somewhere else, and the logs come back to you.
What HCP Terraform adds to the Terraform CLI
HCP Terraform is HashiCorp's hosted service for running Terraform. It is the same product that was called Terraform Cloud until the 2023 rename, which is worth knowing because plenty of documentation and Stack Overflow answers still use the old name. The hostname stayed app.terraform.io through the rename.
Terraform on its own is a binary that reads configuration and calls provider APIs. Everything around that binary — where state lives, who is allowed to apply, what happened last Tuesday — is left to you. HCP Terraform fills in that surrounding layer:
- Managed state — state is stored, versioned, and locked for you, with no S3 bucket or DynamoDB table to build first
- Remote runs — plan and apply execute on HCP Terraform's workers rather than on your machine
- Workspaces — each workspace holds one state file, its own variables, and its own run history
- Variables and secrets — workspace values are encrypted at rest and can be marked Sensitive so the stored value cannot be read back through the UI or the Variables API
- Three ways to trigger a run — from the CLI, from a connected Git repository, or through the API
- Governance — policy checks, cost estimation, and team permissions on the paid tiers
The Sensitive setting on a workspace variable is about storage, not run output:
- Sensitive stops the stored value being read back through the UI or Variables API.
sensitive = truein configuration marks a variable or output for redaction in plan and apply output.- Treat output redaction as best-effort — a provider or unusual expression can still surface the value.
The free tier covers up to 500 managed resources per organization with unlimited users. This lab starts with the built-in terraform_data resource, which HashiCorp excludes from the managed-resource count, and later creates one local_file resource that does count — briefly, since everything is destroyed at the end.
What HCP Terraform does not change is the language or the command set:
- Your
.tffiles stay the same. terraform planstill means plan.- Plan output looks the same.
That is the whole point of the CLI-driven workflow: you get the managed pieces without learning a new tool.
Create an HCP Terraform account, organization, and workspace
Sign up for a free account at app.terraform.io and confirm the email that arrives. The confirmation link lands you on the Organizations page, where you create your first organization.
An organization is the billing and membership boundary. Inside it, projects group workspaces, and a workspace holds one state file:
Organization (golinuxcloud-lab)
└── Project (Default Project)
└── Workspace (hcp-terraform-tutorial)Organization names are unique across all of HCP Terraform, so a personal lab name such as golinuxcloud-lab saves you from fighting over terraform-test. Every command below uses that name; substitute your own.
You do not need to create the workspace by hand. A workspace named in a cloud block is created automatically the first time you initialize, as a CLI-driven workspace with remote execution — which is exactly the shape this article wants. Creating it in the UI first also works if you:
- Pick the CLI-driven workflow
- Do not connect a Git repository yet
Before going further, check that your CLI is new enough. The cloud block needs Terraform 1.1 or later:
terraform versionSample output:
Terraform v1.15.8
on linux_amd64Anything from 1.1 onward will work. Older releases have to use the remote backend instead, which is the predecessor of the cloud block.
Authenticate the Terraform CLI with terraform login
The CLI needs an API token before it can talk to your organization. The terraform login command handles the whole exchange:
terraform loginIt explains what it is about to do and waits for you to agree, because it is about to write a credential to disk:
Terraform will request an API token for app.terraform.io using your browser.
If login is successful, Terraform will store the token in plain text in
the following file for use by subsequent commands:
/root/.terraform.d/credentials.tfrc.json
Do you want to proceed?
Only 'yes' will be accepted to confirm.
Enter a value: yes
---------------------------------------------------------------------------------
Terraform must now open a web browser to the tokens page for app.terraform.io.
If a browser does not open this automatically, open the following URL to proceed:
https://app.terraform.io/app/settings/tokens?source=terraform-login
---------------------------------------------------------------------------------
Generate a token using your browser, and copy-paste it into this prompt.
Token for app.terraform.io:
Enter a value:
Retrieved token for user golinuxcloud
Welcome to HCP Terraform!
Documentation: terraform.io/docs/cloudOn a desktop the browser opens by itself; on a headless VM you copy the printed URL. Click Generate token on that page, copy the value, and paste it at the prompt — nothing echoes back as you paste, which is expected. The Retrieved token for user line names the account the token belongs to, so read it as a confirmation that you authenticated as the right person.
The path in that message is the home directory of whoever ran the command, so yours will read /home/yourname/.terraform.d/ unless you are working as root the way this lab did. Terraform itself needs no elevated privileges here.
You can inspect the credentials file without exposing the secret by asking for the keys rather than the values:
jq '.credentials | keys' ~/.terraform.d/credentials.tfrc.jsonSample output:
[
"app.terraform.io"
]One entry per host, which is what makes the same mechanism work for Terraform Enterprise: you run terraform login tfe.example.com and a second entry appears. Since the token sits there in clear text:
- Keep the file out of any repository
- Keep it out of dotfile sync tools that might expose it
For CI systems, writing a file into a home directory is awkward. Terraform also reads a token from an environment variable named after the host, with dots converted to underscores:
TF_TOKEN_app_terraform_iotakes precedence over the credentials fileterraform logoutremoves the stored credential locally- Deleting the token under User settings → Tokens in the web UI revokes it everywhere
Connect your configuration with the cloud block
Work in a directory of its own so this exercise does not collide with your other labs:
mkdir -p ~/terraform-labs/hcp-terraform-tutorial && cd ~/terraform-labs/hcp-terraform-tutorialThe cloud block is what turns an ordinary working directory into one that is bound to a workspace. Write a configuration with that block and a single built-in terraform_data resource, so nothing here needs a cloud provider account:
cat > main.tf <<'EOF'
terraform {
cloud {
organization = "golinuxcloud-lab"
workspaces {
name = "hcp-terraform-tutorial"
}
}
}
variable "environment" {
type = string
description = "Label applied to the demo resource"
default = "lab"
}
resource "terraform_data" "greeting" {
input = "Hello from HCP Terraform (${var.environment})"
}
output "greeting" {
value = terraform_data.greeting.output
}
EOFThree things in that block decide where your run goes:
organization— names the organization you createdworkspaces.name— binds this directory to exactly one workspaceworkspaces.tags— alternative that maps one directory onto several workspaces (for example dev and prod)hostname— optional; defaults toapp.terraform.ioand only needs setting for Terraform Enterprise
The cloud block replaces a backend block rather than joining it. A configuration cannot have both.
Now initialize. This is the step that contacts HCP Terraform, checks your token, and binds the directory to the workspace:
terraform initSample output:
Initializing HCP Terraform...
Initializing provider plugins...
- terraform.io/builtin/terraform is built in to Terraform
HCP Terraform has been successfully initialized!
You may now begin working with HCP Terraform. Try running "terraform plan" to
see any changes that are required for your infrastructure.The first line is the tell: an ordinary directory prints Initializing the backend... instead. The workspace did not exist a moment ago, and Terraform created it without asking. Confirm the CLI is pointed at it:
terraform workspace listSample output:
* hcp-terraform-tutorialThe asterisk marks the current workspace, and the name is the remote one rather than the default you would see with local state. Reload the workspace list in the HCP Terraform UI and it appears there too, with no runs yet.
Run a remote plan and apply from the CLI
Nothing about the next command is new, which is the interesting part:
terraform planSample output:
Running plan in HCP Terraform. Output will stream here. Pressing Ctrl-C
will stop streaming the logs, but will not stop the plan running remotely.
Preparing the remote plan...
To view this run in a browser, visit:
https://app.terraform.io/app/golinuxcloud-lab/hcp-terraform-tutorial/runs/run-aHXb57jf6eqGpXQ6
Waiting for the plan to start...
Terraform v1.15.8
on linux_amd64
Initializing plugins and modules...
Terraform will perform the following actions:
# terraform_data.greeting will be created
+ resource "terraform_data" "greeting" {
+ id = (known after apply)
+ input = "Hello from HCP Terraform (lab)"
+ output = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ greeting = (known after apply)Read the top three lines carefully, because they describe something quite different from a local plan:
- Terraform uploaded the contents of your directory and queued a run
- Your terminal is now tailing the remote run logs
- The second
Terraform v1.15.8banner is the remote worker introducing itself
The run URL is a permanent record you can open, share, or link from a ticket.
Because the run lives on the server, Ctrl-C only detaches your terminal. The plan keeps going, and you can watch the rest of it in the browser.
Applying works the same way, with the confirmation prompt still on your side of the connection:
terraform applySample output:
Running apply in HCP Terraform. Output will stream here.
To view this run in a browser, visit:
https://app.terraform.io/app/golinuxcloud-lab/hcp-terraform-tutorial/runs/run-RFgCegsVGGMdcAqs
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions in workspace "hcp-terraform-tutorial"?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
terraform_data.greeting: Creating...
terraform_data.greeting: Creation complete after 0s [id=ce0bcf4b-7418-b9c8-abbd-223726987744]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
greeting = "Hello from HCP Terraform (lab)"Note the wording of the prompt: it names the workspace you are about to change, not just the resources. That phrasing exists because a CLI-driven apply can affect infrastructure the rest of your team shares. Answering yes releases the run on the server, and the creation log streams back.
Outputs are stored with the state rather than on your disk, so reading one is a remote call:
terraform output greetingSample output:
"Hello from HCP Terraform (lab)"Open the workspace in the browser and the Runs tab now lists both operations:
- A CLI
terraform planis recorded as a plan-only run - A CLI
terraform applyis recorded as plan and apply
The history shows every speculative check as well as every real change.
Confirm that state now lives in HCP Terraform
The clearest evidence is what is missing from your working directory:
ls -aSample output:
.
..
.terraform
main.tfThere is no terraform.tfstate and no backup file, even though you just created a resource. Yet Terraform still knows about that resource:
terraform state listSample output:
terraform_data.greetingThat answer took a few seconds rather than being instant, because it came over the network. The .terraform directory holds only a pointer to the workspace — the state itself is on the server.
You can fetch the whole state document and look at its metadata:
terraform state pull | jq '{terraform_version, serial, lineage}'Sample output:
{
"terraform_version": "1.15.8",
"serial": 1,
"lineage": "b3656a95-d714-f8d2-7233-37129e49669c"
}The serial number increments on every state write, and HCP Terraform keeps each version rather than overwriting. The workspace's States tab lists them, so you can:
- See which run produced which state version
- Roll back if a change goes wrong
Run a second plan to be sure the recorded state matches reality:
terraform planSample output:
Running plan in HCP Terraform. Output will stream here.
To view this run in a browser, visit:
https://app.terraform.io/app/golinuxcloud-lab/hcp-terraform-tutorial/runs/run-Utf3331YjAnPxMVU
terraform_data.greeting: Refreshing state... [id=ce0bcf4b-7418-b9c8-abbd-223726987744]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.No changes from a machine that holds no state file is the whole feature in one line. Any colleague who clones this configuration and runs terraform init gets the same answer, because they read the same state.
Remote execution, local execution, and the disposable run environment
Every workspace has an execution mode, and it decides which machine actually runs Terraform:
| Execution mode | Where plan and apply run | What HCP Terraform provides | How you recognize it |
|---|---|---|---|
| Remote (default) | On HCP Terraform's workers | State, run history, variables, logs, policy checks | Output starts with Running plan in HCP Terraform and prints a run URL |
| Local | On your own machine | State storage and locking only | No run URL; you see Acquiring state lock instead |
Remote is the default and the mode everything above used. The property that surprises people is that the remote run environment is disposable:
- It is created for one run
- It is thrown away afterwards
- Anything a run writes to its own filesystem goes with it
That is easiest to believe once you watch it happen. Rewrite the configuration with a local_file resource, which writes a file next to your configuration, and declare the provider it comes from:
cat > main.tf <<'EOF'
terraform {
cloud {
organization = "golinuxcloud-lab"
workspaces {
name = "hcp-terraform-tutorial"
}
}
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
variable "environment" {
type = string
description = "Label applied to the demo resource"
default = "lab"
}
resource "terraform_data" "greeting" {
input = "Hello from HCP Terraform (${var.environment})"
}
resource "local_file" "note" {
filename = "${path.module}/generated/hello.txt"
content = "Written during the run\n"
}
output "greeting" {
value = terraform_data.greeting.output
}
EOFlocal_file comes from a real provider rather than being built in, so the working directory needs the plugin before the next run:
terraform initSample output:
Initializing HCP Terraform...
Initializing provider plugins...
- terraform.io/builtin/terraform is built in to Terraform
- Finding hashicorp/local versions matching "~> 2.5"...
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above.
HCP Terraform has been successfully initialized!Terraform installed the provider locally so it can parse and validate your configuration. The remote worker downloads its own copy when the run starts. Apply the addition:
terraform applySample output:
# local_file.note will be created
+ resource "local_file" "note" {
+ content = <<-EOT
Written during the run
EOT
+ filename = "./generated/hello.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Enter a value: yes
local_file.note: Creating...
local_file.note: Creation complete after 0s [id=e954becebab751d8ee58496db9c2cbe7795d1a73]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.Terraform reports that it created the file. Look for it where the configuration said it would go:
ls -l generated/hello.txtSample output:
ls: cannot access 'generated/hello.txt': No such file or directoryThe file was real, and it was created — inside the run environment, which no longer exists. This is the single most common surprise when a working local configuration is moved to HCP Terraform. Rethink configurations that:
- Read or write local paths
- Shell out to scripts on disk
- Expect an SSH key or other file in your home directory
The run happens on a machine you never see.
Switching the workspace to local execution makes the contrast obvious. In the UI, open Settings → General, choose Local under Execution Mode, and save. Then plan again:
terraform planSample output:
Acquiring state lock. This may take a few moments...
terraform_data.greeting: Refreshing state... [id=ce0bcf4b-7418-b9c8-abbd-223726987744]
local_file.note: Refreshing state... [id=e954becebab751d8ee58496db9c2cbe7795d1a73]
# local_file.note will be created
+ resource "local_file" "note" {
+ filename = "./generated/hello.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Releasing state lock. This may take a few moments...Two details changed:
- There is no run URL and no streaming banner — your own machine did the work
- You see state lock messages — HCP Terraform still holds the state
That is local execution in one transcript: the CLI executes, the service stores.
The plan itself makes the disposable filesystem point a second time. The local_file provider checks whether the file exists during refresh, finds nothing, and concludes the resource was deleted — so Terraform offers to create it again. Under remote execution that would happen on every single run, forever, because each run starts with an empty disk.
Set the execution mode back to Remote in the same settings page before continuing.
CLI-driven, VCS-driven, and API-driven workflows
The workspace you built is CLI-driven, which is one of three ways HCP Terraform can start a run:
| Workflow | What triggers a run | Where the configuration comes from | Typical use |
|---|---|---|---|
| CLI-driven | terraform plan or terraform apply on your machine, on a workspace with no linked repository |
Uploaded from your working directory | Learning, day-to-day development, teams not ready to gate on Git |
| VCS-driven | A commit or pull request in a connected repository | Cloned by HCP Terraform from the repo | Production pipelines where every change is reviewed and merged |
| API-driven | An HTTP call that uploads a configuration and queues a run | Uploaded by your own tooling | Custom pipelines and platform teams building on top of Terraform |
These workflows overlap for speculative planning, but not for applying:
- A VCS-connected workspace still accepts
terraform planfrom the CLI against your local configuration — handy before you commit - CLI-driven remote
terraform applyis only available on workspaces with no linked VCS repository - Once a workspace is connected to a repository, real changes must come through the VCS workflow — the repository is the source of truth
Every workflow ends up in the same run history against the same state.
Pick CLI-driven while you are learning:
- Keeps the feedback loop short
- Matches the certification exam workflow for Terraform CLI integration
- Does not require a Git repository before you can see a run
Common HCP Terraform CLI problems
The failures below are the ones that stop a first run, and each has a message specific enough to identify it.
An invalid or expired token fails during init, before Terraform looks at anything else:
terraform initSample output:
Error: Failed to read organization "golinuxcloud-lab" at host app.terraform.io
on main.tf line 3, in terraform:
3: organization = "golinuxcloud-lab"
Encountered an unexpected error while reading the organization settings:
unauthorizedThe heading points at your organization line, which sends people off checking a name that is perfectly correct. The word that matters is unauthorized on the last line — the credential was rejected. Run terraform login again, or check whether a stale TF_TOKEN_app_terraform_io in your shell is overriding the credentials file.
A misspelled organization produces an almost identical heading with a very different ending:
terraform initSample output:
Error: Failed to read organization "golinuxcloud-labs" at host app.terraform.io
Encountered an unexpected error while reading the organization settings:
organization "golinuxcloud-labs" at host app.terraform.io not found.
Please ensure that the organization and hostname are correct and that your
API token for app.terraform.io is valid.not found rather than unauthorized means the token was accepted and the name was wrong. Compare the string against the organization name in the URL when you are signed in to the UI — a trailing s is enough to break it.
When init fails on the organization line, read the last line of the error:
unauthorized— token problemnot found— organization name problem
Editing the cloud block after a directory is initialized stops the next command immediately:
terraform planSample output:
Error: HCP Terraform or Terraform Enterprise initialization required: please run "terraform init"
Reason: HCP Terraform configuration block has changed.
Changes to the HCP Terraform configuration block require reinitialization,
to discover any changes to the available workspaces.
To re-initialize, run:
terraform initTerraform refuses to guess whether you meant to switch workspaces or made a typo. Re-run terraform init, and add -reconfigure when you want to discard the existing binding rather than carry it over.
The last common one only happens remotely. A variable with no default fails the run instead of prompting, because the worker has no terminal to prompt with:
terraform planSample output:
Error: No value for required variable
on main.tf line 11:
11: variable "owner" {
The root module input variable "owner" is not set, and has no default
value. Use a -var or -var-file command line argument to provide a value for
this variable.The suggested fix is written for local runs. With the CLI-driven workflow the natural home for the value is the workspace itself: open the Variables tab, add owner as a Terraform variable, and run again. Passing -var on the command line also works, and a terraform.tfvars file in the directory is uploaded along with everything else.
A few more that are quicker to recognize than to reproduce:
| Symptom | Likely cause | Fix |
|---|---|---|
| Run fails reaching a provider API or private registry | The HCP Terraform worker has no route to a host that your laptop can reach | Switch the workspace to local execution, or use an HCP Terraform agent inside your network |
| Plan succeeds locally but the run never appears in the UI | Workspace is in local execution mode | Set Execution Mode back to Remote under Settings → General |
| Provider authentication fails only on remote runs | Credentials come from environment variables on your machine, which the worker never sees | Add them as environment variables on the workspace and mark them sensitive |
| Configuration reads or writes local files, or shells out to scripts | The run environment is disposable and starts empty | Redesign around remote-friendly resources, or run that workspace in local execution mode |
terraform apply is rejected after a successful plan |
Your team role does not include apply permission on that workspace | Ask an owner for apply access, or approve the run in the UI |
Clean up the lab
Nothing here costs money, but the local_file resource does count against your organization's managed resource total for as long as it exists, and a stale workspace is clutter. Destroy the lab the same way you would anything else:
terraform destroySample output:
Plan: 0 to add, 0 to change, 1 to destroy.
Do you really want to destroy all resources in workspace "hcp-terraform-tutorial"?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
terraform_data.greeting: Destroying... [id=ce0bcf4b-7418-b9c8-abbd-223726987744]
terraform_data.greeting: Destruction complete after 0s
Apply complete! Resources: 0 added, 0 changed, 1 destroyed.Only one resource was destroyed even though two were applied, and that is the disposable filesystem again — the refresh at the start of the destroy found no file on the fresh worker, dropped local_file.note from state, and left terraform_data.greeting as the only thing to remove. Confirm nothing is left:
terraform state listThe command prints nothing at all when the state is empty, which is the result you want. The workspace itself still exists; delete it from Settings → Destruction and Deletion in the UI if you do not plan to reuse it.
Finally, remove the stored credential from the machine:
terraform logoutSample output:
Removing the stored credentials for app.terraform.io from the following file:
/root/.terraform.d/credentials.tfrc.json
Success! Terraform has removed the stored API token for app.terraform.io.Read that message precisely: it removed the stored token, which is the local copy. The token itself is still valid on the server, so if the machine was shared or temporary, delete the token under User settings → Tokens in the UI as well.
References
- Use HCP Terraform with the Terraform CLI — CLI integration overview
- The CLI-driven run workflow — how remote runs are triggered and approved
- The cloud block — configuration reference
- terraform login — token retrieval and credential storage
- CLI configuration file — credentials file and environment variable tokens
- HCP Terraform workspace state — state versions and managed resource counts
Summary
You connected a local Terraform CLI to HCP Terraform and ran the ordinary workflow against it. Three pieces did the work:
terraform login— stored an API token on diskcloudblock — named your organization and workspaceterraform init— bound the directory and created the workspace on the spot
From there, terraform plan and terraform apply behaved as always, except each one:
- Queued a run on HCP Terraform workers
- Streamed logs back to your terminal
- Printed a URL to a permanent run record
The terraform_data resource kept the exercise free of cloud credentials.
The change worth internalizing is where things now live:
- Your working directory has no state file
terraform state listandterraform outputare network calls against shared remote state- The remote run environment is disposable —
local_filereported success but left nothing on disk - Configurations that touch the local filesystem, read shell environment variables, or expect an SSH agent need rethinking — or the workspace needs local execution mode
When a first run fails, read past the error heading to the last line:
unauthorized— token problemnot found— organization name problem- A variable with no default fails outright on remote runs — set it in the workspace Variables tab
From here, the natural next steps are:
- Organizing workspaces into projects
- Moving variables and provider credentials into the workspace for remote authentication
- Bringing an existing local
terraform.tfstateunder HCP Terraform management
All three build on the connection you just made.

