Writing Configuration Files (HCL)
1. Understanding HCL Syntax
Terraform Configuration Language (HCL) is a domain-specific language used to define infrastructure as code. HCL supports both a native syntax and JSON syntax, though the native syntax is preferred for its readability.
| Element | Description | Example |
|---|---|---|
| Blocks | Structural elements that define resources, data sources, providers, etc. | |
| Attributes | Simple key-value pairs within blocks. | ami = "ami-12345678" |
| Lists | Ordered collections of values. | tags = ["web", "production"] |
| Maps | Key-value collections with string keys. | labels = { env = "prod", team = "backend" } |
| Expressions | Values computed from other values using operators or functions. | count = length(var.instances) |
Example: Basic HCL Block
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-unique-bucket-name"
acl = "private"
tags = {
Name = "My S3 Bucket"
Environment = "Production"
}
}
Note: HCL supports interpolation using
${} syntax, which allows referencing values from other parts of the configuration.Warning: HCL is case-sensitive. Attribute names and resource types must match exactly.
Key Concepts
- Blocks define the structure of your configuration.
- Attributes are key-value pairs inside blocks.
- Lists and Maps are used to represent collections of data.
- Expressions allow dynamic computation within the configuration.
2. Creating Terraform Files
Example: Basic Terraform Configuration File
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
}
| Element | Description | Required |
|---|---|---|
| resource | Defines a resource to be created | Yes |
| data | Defines a data source to be read | No |
| variable | Defines an input variable for the configuration | No |
| output | Defines an output value to be displayed after apply | No |
| module | Includes configuration from another directory or module | No |
Note: Terraform configuration files use the
.tf extension.Warning: Avoid hardcoding sensitive values like passwords or API keys in configuration files.
3. Organizing Configuration Files
Directory Structure Best Practices
- Create a root directory for your Terraform project
- Organize configuration files by environment (e.g.,
dev/, stg/, prod/) - Use modules to encapsulate reusable components
- Place backend configuration in
.terraformrcorbackend.tf - Store sensitive data in
terraform.tfvarsor external secret stores
| Directory | Description | Usage |
|---|---|---|
| main.tf | Main configuration file | Define resources and providers |
| variables.tf | Variable definitions | Externalize configuration values |
| outputs.tf | Output definitions | Expose resource values to users |
| modules/ | Module directory | Reusable, encapsulated configurations |
| backend.tf | Backend configuration | Define state storage and locking |
| terraform.tfvars | Variable values | Environment-specific variable overrides |
Example: Module Organization
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
region = "us-west-2"
}
Note: Use
terraform init after organizing files to ensure all modules are properly initialized.
Key Principles
- Keep configurations DRY (Don't Repeat Yourself)
- Use
moduleblocks to compose complex infrastructure - Separate concerns by environment and resource type
- Use
terraform.tfvarsfor environment-specific values
4. Using Comments (# single-line, /* */ multi-line)
| Comment Type | Syntax | Usage |
|---|---|---|
| Single-line comment | # This is a single-line comment |
Used to add brief notes or disable lines of configuration |
| Multi-line comment | /* This is a multi-line comment */ |
Used for longer explanations or temporarily disabling blocks of code |
Example: Using Comments in HCL
resource "aws_instance" "web" {
# Create an EC2 instance
ami = "ami-12345678"
instance_type = "t2.micro"
/*
* This block is commented out
* and will not be processed
*
* root_block_device {
* volume_size = 20
* }
*/
}
5. Defining Resource Blocks
| Property | Description | Example |
|---|---|---|
| type | Resource type (e.g., aws_instance, google_compute_instance) | aws_instance |
| name | Unique name for the resource within the configuration | web_server |
| depends_on | List of resources this resource depends on | ["aws_security_group.web"] |
| count | Number of instances to create (optional) | 3 |
| provider | Provider to use for this resource (optional) | aws.west |
Example: Basic Resource Block
resource "aws_instance" "web_server" {
ami = "ami-12345678"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
}
}
Example: Resource with Count
resource "aws_instance" "web_server" {
count = 3
ami = "ami-12345678"
instance_type = "t2.micro"
tags = {
Name = "WebServer-${count.index}"
}
}
Example: Resource with Dependencies
resource "aws_security_group" "web" {
name = "web-sg"
}
resource "aws_instance" "web_server" {
depends_on = [aws_security_group.web]
ami = "ami-12345678"
instance_type = "t2.micro"
}
6. Setting Block Labels
| Block Type | Label Syntax | Description |
|---|---|---|
| resource | resource "aws_instance" "web" { } |
Defines a resource with type aws_instance and label web |
| data | data "aws_ami" "ubuntu" { } |
Defines a data source with type aws_ami and label ubuntu |
| variable | variable "instance_type" { } |
Defines a variable with label instance_type |
| output | output "public_ip" { } |
Defines an output with label public_ip |
| module | module "vpc" { } |
Defines a module with label vpc |
Example: Resource with Labels
resource "aws_instance" "web_server" {
ami = "ami-12345678"
instance_type = "t2.micro"
}
Example: Data Source with Labels
data "aws_ami" "latest_ubuntu" {
most_recent = true
owners = ["099720109473"]
filters {
name = "ubuntu/images/*"
values = ["18.04"]
}
}
Note: Labels must be unique within the scope of a block type and must follow HCL identifier rules (alphanumeric and underscore only, cannot start with a number).
7. Using Arguments and Attributes (key = value)
| Concept | Description | Example |
|---|---|---|
| Arguments | Values provided to a block or function, typically used for configuration | count = 2 |
| Attributes | Named values within a block that define properties of the resource or module | name = "web-server" |
Example: Resource with Arguments and Attributes
resource "aws_instance" "web" {
count = 2
ami = "ami-12345678"
instance_type = "t2.micro"
tags = {
Name = "web-server"
}
}
Note: Arguments and attributes are used to configure resources, data sources, and modules in HCL. Arguments can be expressions or literals, while attributes are key-value pairs that define properties.
8. Understanding Meta-Arguments
| Argument | Description | Values | Notes |
|---|---|---|---|
| count | Number of instances to create | number |
Creates multiple instances of a resource. Use count.index to reference each instance. |
| for_each | Creates instances for each key-value pair in a map or set | map(string) or set(string) |
Useful for creating multiple resources with different configurations. Iterates over keys and values. |
| depends_on | Explicitly declares dependencies between resources | list(resource) |
Ensures that Terraform applies resources in the correct order. Not required if dependencies are inferred from references. |
| provider | Specifies the provider for a resource | string |
Useful when multiple providers are configured. Example: provider = "aws". |
| lifecycle | Controls lifecycle behaviors of a resource | object |
Allows customization of creation, update, and destruction behavior. Supports ignore_changes, create_before_destroy, and prevent_destroy. |
| validation | Validates input values for variables or attributes | object |
Enforces constraints on variable values. Requires condition and error_message. |
Example: Using count
resource "aws_instance" "web" {
count = 3
ami = "ami-12345678"
instance_type = "t2.micro"
tags = {
Name = "web-${count.index}"
}
}
Example: Using for_each
resource "aws_instance" "web" {
for_each = {
dev = "t2.micro"
prod = "t3.large"
}
ami = "ami-12345678"
instance_type = each.value
tags = {
Name = "web-${each.key}"
}
}
Example: Using depends_on
resource "aws_s3_bucket" "bucket" {
bucket = "my-bucket"
}
resource "aws_s3_bucket_policy" "bucket_policy" {
depends_on = [aws_s3_bucket.bucket]
bucket = aws_s3_bucket.bucket.id
policy = data.aws_iam_policy_document.bucket_policy.json
}
Example: Using lifecycle
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
lifecycle {
ignore_changes = [ami]
create_before_destroy = true
}
}
9. Formatting Code
| Rule | Description | Example |
|---|---|---|
| Indentation | Use 2 spaces for indentation | |
| Spacing | One blank line between top-level blocks | |
| Quotes | Use double quotes for strings | "ami-12345678" |
| Line Length | Lines should not exceed 100 characters | |
Note: The
terraform fmt command automatically formats your HCL files according to these standards.
10. Validating Syntax
Use the terraform validate command to check the syntax and structure of your Terraform configuration files.
| Command | Description | Example |
|---|---|---|
| terraform validate | Validates the syntax and structure of Terraform configuration files. | terraform validate |
Note: The
terraform validate command checks for syntax errors and ensures that all required attributes are present. It does not interact with remote systems or resources.
11. Using Override Files
Terraform allows you to use override files to customize configuration for different environments or scenarios without modifying the main configuration files.
| File Name Pattern | Description | Usage |
|---|---|---|
| terraform.tfvars | Default variable file for setting values | Used to override variables in root module |
| terraform.tfvars.json | JSON format variable file | Alternative to HCL format for variables |
| *.auto.tfvars | Auto-loaded variable files | Loaded automatically if present |
| *.auto.tfvars.json | Auto-loaded JSON variable files | Loaded automatically if present |
Example: Using override files for environments
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
}
Note: Override files are loaded in a specific order and can be overridden by command-line flags or environment variables.