Hardcoding values directly in main.tf works for a quick test but breaks down as soon as a project grows: credentials end up in source control, switching between environments requires editing source files, and configuration becomes hard to review. Terraform input variables solve all three problems by separating values from logic — secrets stay out of source files, environments switch by swapping a single .tfvars file, and the configuration itself stays readable.
Recommended project structure
Instead of one monolithic main.tf, split configuration into purpose-specific files. Terraform loads all .tf files in a directory automatically, so the split is purely organizational.
gcore-terraform/
├── providers.tf # terraform block, required_providers, provider config
├── variables.tf # all variable declarations
├── main.tf # resources and data sources
├── outputs.tf # output values
├── terraform.tfvars # variable values (not committed to version control)
└── .terraform.lock.hcl # committed to version control
terraform.tfvars often contains API keys and other secrets. Add it to .gitignore. Commit .terraform.lock.hcl to lock provider versions for the whole team.
Provider configuration
Move the provider configuration out of main.tf into a dedicated providers.tf so it is easy to find and update:
terraform {
required_version = ">= 1.11"
required_providers {
gcore = {
source = "G-Core/gcore"
version = "~> 2.0"
}
}
}
provider "gcore" {
api_key = var.api_key
}
The api_key value now comes from a variable instead of being hardcoded.
Define variables
All variable declarations go in variables.tf. A variable block has four optional fields:
variable "project_name" {
description = "Name of the Gcore project to look up."
type = string
default = "Default"
}
| Field | Purpose |
|---|
description | Documents what the variable is for. Shown when running terraform plan and in generated documentation. |
type | Constrains the accepted value type. Terraform rejects a wrong type before connecting to any API. |
default | Makes the variable optional. If omitted, Terraform requires a value to be supplied. |
sensitive | When true, masks the value in all command output and state display. |
Variable types
Terraform supports simple types and collection types.
Simple types
variable "api_key" {
type = string
sensitive = true
}
variable "instance_count" {
type = number
default = 2
}
variable "enable_monitoring" {
type = bool
default = false
}
Collection types
variable "allowed_regions" {
description = "List of Gcore regions to deploy into."
type = list(string)
default = ["Luxembourg-2", "Amsterdam"]
}
variable "tags" {
description = "Tags applied to all resources."
type = map(string)
default = {
environment = "dev"
team = "platform"
}
}
Reference individual elements in configuration:
# first region from the list
region = var.allowed_regions[0]
# value from a map
environment_label = var.tags["environment"]
Sensitive variables
Mark variables that hold secrets with sensitive = true:
variable "api_key" {
description = "Gcore permanent API token."
type = string
sensitive = true
}
Terraform masks the value everywhere — in plan output, apply output, and the interactive console:
$ echo 'var.api_key' | terraform console
(sensitive value)
The actual value is still stored in terraform.tfstate. Do not commit the state file to version control.
Add a validation block to catch bad values before Terraform makes any API call:
variable "instance_count" {
description = "Number of instances to provision."
type = number
default = 1
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "instance_count must be between 1 and 10."
}
}
If the value fails the condition, Terraform prints the error message and stops:
│ Error: Invalid value for variable
│
│ on variables.tf line 1:
│ 1: variable "instance_count" {
│
│ instance_count must be between 1 and 10.
Assign values to variables
Terraform resolves variable values in this order (later sources override earlier ones):
| Priority | Method | Best for |
|---|
| 1 (lowest) | default in variable block | Safe fallback values |
| 2 | terraform.tfvars | Local development |
| 3 | Named .tfvars file with -var-file | Multiple environments |
| 4 | -var flag | One-off overrides |
| 5 (highest) | TF_VAR_* environment variable | CI/CD pipelines, secrets managers |
Auto-loaded file
Create terraform.tfvars in the project directory. Terraform loads it automatically without any flags:
api_key = "YOUR_API_KEY"
project_name = "Default"
Run terraform plan — Terraform reads the file automatically:
$ terraform plan
data.gcore_cloud_project.project: Reading...
data.gcore_cloud_project.project: Read complete after 1s [name=Default]
Changes to Outputs:
+ project_id = 1186668
+ project_name = "Default"
Files per deployment stage
For separate environments (dev, staging, production), create one .tfvars file per environment:
gcore-terraform/
├── dev.tfvars
├── staging.tfvars
└── production.tfvars
staging.tfvars:
api_key = "YOUR_API_KEY"
project_name = "Staging"
region_name = "Amsterdam"
Pass the file explicitly with -var-file:
terraform plan -var-file="staging.tfvars"
terraform apply -var-file="production.tfvars"
Command-line override
Override a single variable without editing any file:
terraform plan -var="project_name=Staging"
Use multiple -var flags to override several variables at once:
terraform plan -var="project_name=Staging" -var="region_name=Amsterdam"
The -var flag takes the highest priority among file-based methods — it overrides values from .tfvars files.
CI/CD injection
Any variable named example can be set via the environment variable TF_VAR_example. This is the preferred method in CI/CD pipelines and secret managers where writing files is impractical.
$env:TF_VAR_api_key = "YOUR_API_KEY"
$env:TF_VAR_project_name = "Default"
terraform plan
export TF_VAR_api_key="YOUR_API_KEY"
export TF_VAR_project_name="Default"
terraform plan
Terraform picks up the values without any flags or files.
Define outputs
Outputs expose values from the Terraform state — useful for reading resource IDs after apply, or for passing values between configurations. All output declarations go in outputs.tf:
output "project_id" {
description = "Numeric ID of the Gcore project."
value = data.gcore_cloud_project.project.id
}
output "project_name" {
description = "Name of the resolved Gcore project."
value = data.gcore_cloud_project.project.name
}
After terraform apply, outputs print automatically:
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
project_id = 1186668
project_name = "Default"
Print outputs at any time without re-running apply:
project_id = 1186668
project_name = "Default"
For scripting and automation, use JSON format:
{
"project_id": {
"sensitive": false,
"type": "number",
"value": 1186668
},
"project_name": {
"sensitive": false,
"type": "string",
"value": "Default"
}
}
Mark an output as sensitive to prevent it from printing in plain text:
output "api_key_used" {
value = var.api_key
sensitive = true
}
Inspect variables interactively
terraform console opens an interactive prompt that evaluates expressions against the current configuration and state. Use it to check variable values and test expressions before using them in resources.
> var.project_name
"Default"
> var.api_key
(sensitive value)
> var.allowed_regions[0]
"Luxembourg-2"
> var.tags["environment"]
"dev"
Type exit or press Ctrl+D to close the console.
terraform console loads the configuration once at startup. After editing .tf files, exit and restart the console to see updated values.
.gitignore recommendations
# Provider binary and plugins
.terraform/
# State files — may contain secrets
*.tfstate
*.tfstate.backup
# Override files
*.tfvars
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Crash logs
crash.log
crash.*.log
Commit .terraform.lock.hcl — it locks provider versions for the team and does not contain secrets.
Tag configuration versions
Tagging significant commits gives rollbacks a reliable target. Tag each release or major configuration change after applying it:
git tag -a v1.0.1 -m "Release version 1.0.1 with updated resource configuration"
git push origin main --tags
List all available tags at any time:
Test rollback procedures periodically so the process is already verified before a real incident occurs.
Roll back the configuration
Configuration changes can sometimes cause unexpected behavior. A rollback strategy allows quick reversion to the last known good state, minimizing downtime.
Step 1. Identify the target version
Run git log or git tag to browse available checkpoints and identify the version to restore.
Step 2. Check out the version
Step 3. Apply the rollback
The infrastructure is now running the configuration from the selected version.