With Terraform installed on RHEL 8, the next step is writing real-world HCL configurations that provision cloud infrastructure. HashiCorp Configuration Language 2 (HCL2) provides a clean, human-readable syntax for defining providers, resources, variables, and outputs that can be version-controlled alongside your application code. This tutorial walks through provisioning an AWS EC2 instance — covering input variables, output values, execution plans, state management, and remote state storage with an S3 backend — using patterns you can adapt to any cloud provider Terraform supports.
Prerequisites
- Terraform installed on RHEL 8 (see the previous tutorial in this series)
- An AWS account with an IAM user that has
AmazonEC2FullAccessandAmazonS3FullAccesspermissions - AWS CLI installed and configured (
aws configure) with a valid access key and secret - An existing AWS key pair or one created with
aws ec2 create-key-pair - Familiarity with basic Terraform workflow (
init,plan,apply)
Step 1 — Write the Provider and Resource Configuration
Create a project directory and define the AWS provider alongside an EC2 resource using HCL2 syntax. All block types follow the pattern block_type "type_label" "name_label" { ... }.
mkdir ~/tf-aws-demo && cd ~/tf-aws-demo
cat > main.tf < 5.0"
}
}
required_version = ">= 1.5.0"
}
provider "aws" {
region = var.aws_region
}
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
key_name = var.key_pair_name
tags = {
Name = "rhel8-terraform-web"
Environment = var.environment
ManagedBy = "Terraform"
}
}
EOF
Step 2 — Define Input Variables
Input variables decouple configuration values from the resource definitions, making configurations reusable across regions and environments without editing main.tf.
cat > variables.tf < terraform.tfvars <<'EOF'
aws_region = "us-east-1"
instance_type = "t3.micro"
key_pair_name = "my-keypair"
environment = "staging"
EOF
Step 3 — Define Output Values
Outputs expose useful attributes of provisioned resources — such as an instance’s public IP — so other Terraform configurations or CI/CD pipelines can reference them.
cat > outputs.tf <<'EOF'
output "instance_id" {
description = "EC2 instance ID"
value = aws_instance.web.id
}
output "public_ip" {
description = "Public IP address of the web instance"
value = aws_instance.web.public_ip
}
output "public_dns" {
description = "Public DNS name of the web instance"
value = aws_instance.web.public_dns
}
EOF
Step 4 — Plan and Apply with a Saved Plan File
Saving the plan to a file before applying is a best practice in CI/CD pipelines: it ensures that exactly the reviewed changes are applied, with no drift if the configuration is modified between the plan and apply steps.
# Initialise the working directory
terraform init
# Generate and save the execution plan
terraform plan -out=plan.tfplan
# Review the plan output, then apply the saved plan
terraform apply plan.tfplan
# After apply, display all outputs
terraform output
# Display a specific output value (useful in CI scripts)
terraform output -raw public_ip
Step 5 — Inspect and Manage State
Terraform state is the source of truth for what infrastructure exists. The terraform state subcommands let you inspect, move, and remove resources without touching the actual infrastructure.
# List all resources tracked in state
terraform state list
# Show full details of a specific resource
terraform state show aws_instance.web
# Remove a resource from state without destroying it
# (useful if you want Terraform to stop managing a resource)
terraform state rm aws_instance.web
# Import an existing resource into state
# terraform import aws_instance.web i-0abcd1234efgh5678
# Destroy all managed resources
terraform destroy -auto-approve
Step 6 — Configure Remote State with an S3 Backend
Storing state locally is fine for personal projects, but teams need a shared, locking-enabled backend. The AWS S3 backend with a DynamoDB lock table is the most common choice for AWS-based infrastructure.
# First, create the S3 bucket and DynamoDB table manually or via a bootstrap config
# Then add the backend block to main.tf (or a separate backend.tf):
cat > backend.tf <<'EOF'
terraform {
backend "s3" {
bucket = "my-org-terraform-state"
key = "rhel8-demo/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-locks"
}
}
EOF
# Re-initialise to migrate local state to S3
terraform init -migrate-state
# Verify the backend is active
terraform state list
Conclusion
You have written an HCL2 Terraform configuration that provisions an AWS EC2 instance, separated concerns using input variables and terraform.tfvars, exposed resource attributes with output values, applied a saved plan file, managed state using the terraform state subcommands, and configured a remote S3 backend with DynamoDB locking for team use. These patterns scale from small demos to production multi-account infrastructure.
Next steps: Integrate Terraform into a Jenkins CI/CD Pipeline on RHEL 8, Manage Terraform Workspaces for Multi-Environment Deployments, and Register a GitHub Actions Self-Hosted Runner on RHEL 8.