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 AmazonEC2FullAccess and AmazonS3FullAccess permissions
  • 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.