Why did HashiCorp create the HCL (HashiCorp Configuration Language)? #
- Understanding Need for a Purpose-Built Language: General-purpose languages/formats (like YAML, JSON, or Python) are not optimized for describing infrastructure
- HCL (HashiCorp Configuration Language): HCL was designed specifically to be human-readable, machine-friendly, and domain-specific for Infrastructure as Code (IaC)
- Easy to Learn, Hard to Misuse: Simple for beginners, with guardrails to prevent critical errors
- Improve Readability for Operators and DevOps Teams: HCL uses a clean, block-based syntax that's easier to read and understand - even for folks unfamiliar with programming
- Provide Strong Static Validation: Terraform can detect syntax and type errors before applying any changes β reducing costly mistakes
- Enable Rich, Custom Types and Expressions: HCL supports built-in functions, conditionals, for-loops, maps, lists, and interpolation
What are the core concepts of HCL? Explain with a practical example. #
- Providers: Allows Terraform to connect to and manage resources on a specific cloud or service (e.g., AWS, Azure, Google Cloud, Docker, Kubernetes)
- Resources: Allows Terraform to create, update, or delete actual infrastructure components (e.g., IAM user, S3 bucket)
- Variables: Allows users to input values from the outside, making configurations flexible and reusable
- Locals: Allows defining reusable internal constants or computed values (CANNOT be overridden from outside)
- Output: Allows Terraform to display and share useful result values
# π§ VARIABLE BLOCK
# WHAT: Accepts IAM user name input
# WHY: Makes the config reusable
variable "iam_user_name" {
type = string
default = "default_iam_user_name_value"
# Can be overridden using terraform.tfvars or CLI
# Example:
# terraform apply -var="iam_user_name=something_else"
}
# π LOCALS BLOCK
# WHAT: Used for internal computed values
# WHY: Avoid repetition
locals {
organization = "in28minutes" # Local value. Will NOT be changed from outside
}
# βοΈ PROVIDER BLOCK
# WHAT: Configures AWS access
# WHY: Required to manage AWS resources
provider "aws" {
region = "us-east-1"
}
# π¦ RESOURCE BLOCK
# WHAT: Creates a single IAM user
# WHY: Provision identity in AWS
resource "aws_iam_user" "my_user" {
name = var.iam_user_name + "_" + local.organization
# Interpolation syntax NOT necessary for simple things
# ${var.iam_user_name} + "_" + ${local.organization}
}
# π€ OUTPUT BLOCK
# WHAT: Displays the IAM user name
# WHY: Helps confirm what was created
# Share output with other projects
output "iam_user_name_value" {
value = aws_iam_user.my_user.name
description = "The IAM user that was created"
}
Why do we need variables in Terraform? #
- Variable Declaration: Variables are declared using
variable
blocks with optional type constraints, default values, descriptions, and validation rules - Variable Reference: Variables are referenced using
var.<variable_name>
syntax throughout the configuration - Primitive Data Types: Supports
string
,number
, andbool
as fundamental data types - Complex Data Types: Supports
list
,set
,map
,object
, andtuple
for structured data - Type Validation: Type constraints help catch errors early and provide better documentation for expected input formats
Variable assignment methods:
- Command line:
terraform apply -var="region=us-east-1"
- Environment variable:
export TF_VAR_region="us-east-1"
(TF_VAR_ + VariableName) - Values defined in
terraform.tfvars file
- Custom Variable file:
terraform apply -var-file="prod.tfvars"
# π§ VARIABLE DECLARATION
# WHAT: Declares a variable named "region"
# WHY: Makes config reusable with flexible input
variable "region" {
type = string
default = "us-east-1"
description = "AWS region to deploy into"
# Variation: Add validation rules (optional)
validation {
condition = contains(
["us-east-1", "us-west-1"], var.region)
error_message = "Allowed values: us-east-1, us-west-1"
}
}
# π·οΈ VARIABLE REFERENCE
# WHAT: Refer to variables using var.<name>
provider "aws" {
region = var.region
}
# π€ PRIMITIVE DATA TYPES
# string, number, bool
variable "app_name" {
type = string
default = "my-app"
}
variable "instance_count" {
type = number
default = 2
}
variable "enable_monitoring" {
type = bool
default = true
}
# π§± EXAMPLES OF COMPLEX DATA TYPES
# list of strings
variable "azs" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
# map of string values
variable "tags" {
type = map(string)
default = {
owner = "team-a"
env = "dev"
}
}
# set of strings
variable "unique_zones" {
type = set(string)
default = ["us-east-1a", "us-east-1b"]
}
# π‘ TYPE VALIDATION BENEFIT
# Ensures incorrect input types fail early
# terraform validate will catch mismatches
# πΎ VARIABLE ASSIGNMENT METHODS (IN PRIORITY ORDER)
# 1οΈβ£ Command Line
# terraform apply -var="region=us-east-1"
# terraform apply -var-file="prod.tfvars"
# 2οΈβ£ Environment Variable
# export TF_VAR_region="us-east-1"
# 3οΈβ£ terraform.tfvars
# region = "us-east-1"
# 4οΈβ£ Default in variable block
# variable "region" {
# default = "us-west-1"
# }
What is the difference between locals and variables in Terraform? #
- Locals Block: A
locals
block lets you define one or more named local values (locals) within a module - Purpose and Benefits
- Avoid Repetition: Define complex expressions or values once instead of repeating them across resources in the same module
- Enhance Readability: Give meaningful names to complex expressions making code self-documenting
- (Key Difference) Variables Are Directly Configurable From Outside:
- Variables = external input (flexible)
- Locals = internal values or constants
##############################################
# π LOCALS BLOCK
# WHAT: Internal reusable values
# WHY: Avoid repetition, simplify logic
# WHEN: Use for naming, tagging, timestamps
##############################################
locals {
# App name used across resources
app_name = "inventory"
# IAM role name: app + env + suffix
lambda_role_id = "${local.app_name}-\
${var.environment}-lambda-role"
# Timestamp in YYYYMMDDhhmmss format
timestamp = formatdate(
"YYYYMMDDhhmmss",
timestamp()
)
# List of standard ports (SSH, HTTP, HTTPS)
default_ports = [22, 80, 443]
# Shared tagging across all AWS resources
common_tags = {
Owner = "ranga"
Environment = var.environment
Project = var.project
}
}
What is a Data Source in Terraform? #
- Need For Data Source: Sometimes we need to reference resources that were not created by Terraform β like an existing VPC, subnet, or AMI
- What is a Data Source in Terraform: A read-only block that allows you to fetch and use information about existing resources in your configuration
- Helps You Integrate with Existing Infrastructure: Enables re-usability, avoids duplication
- Common Use Cases:
- Get latest AMI ID from AWS
- Look up existing VPCs, subnets, or security groups
- Share information between separate Terraform projects (You want to get information about a resource created in a different project)
- Read-Only Access to External Resources: Data sources never modify infrastructure β they only retrieve data
- Reduces Hard-coding: Avoids hard-coding of resource details
# π DATA SOURCE SYNTAX:
# data "<provider>_<type>" "<name>" { ... }
# π§ WHEN: Use when you need to reference existing
# resources not created in your Terraform code.
# π§ DATA SOURCE EXAMPLE: AWS AMI Lookup
# WHAT: Find the latest Amazon Linux AMI
# WHY: Use it for EC2 instance launch
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["137112412989"] # Official Amazon Linux AMI owner
filter {
name = "name" # Which AMI attribute to filter on
values = ["al2023-ami-*-x86_64"] # Defining a Wild Card Search
}
# ADD OTHER FILTERS AS NEEDED
}
# β
The data block does NOT create the AMI
# It just fetches its ID and metadata
# π¦ RESOURCE USING DATA SOURCE
# Launch an EC2 instance using that AMI
resource "aws_instance" "example" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
tags = {
Name = "data-source-example"
}
}
# π€ OUTPUT: Print the AMI ID fetched via data source
output "selected_ami_id" {
value = data.aws_ami.amazon_linux.id
description = "AMI ID fetched using data source"
}
# π§ͺ VARIATION: Lookup existing VPC
# data "aws_vpc" "default" {
# default = true
# }
# π§ͺ VARIATION: Lookup S3 bucket (already created)
# data "aws_s3_bucket" "mybucket" {
# bucket = "my-existing-bucket-name"
# }
How do resources compare to data sources in Terraform? #
resource
β Actively Manages Infrastructure:- Creates, updates, or destroys infrastructure
- Terraform tracks and owns the complete lifecycle
- Example:
resource "aws_s3_bucket" "my_bucket" { bucket = "my-app-bucket" }
data
β Reads Existing Information:- Fetches details about resources not created by Terraform
- Does not change or manage the resource
- Read ONLY!
- Example:
data "aws_s3_bucket" "existing_bucket" { bucket = "shared-logs" }
- (Best Practice) Use
data
blocks to Dynamically Fetch IDs: Avoid hardcoding values like AMI IDs, subnet IDs, or security group names β usedata
instead for flexibility and maintainability
Aspect | Resource Block | Data Block |
---|---|---|
Keyword | resource |
data |
Primary Action | create , update , delete real infrastructure |
Read existing infrastructure |
Lifecycle Phases | Planned in plan , executed in apply |
Fetched in refresh/plan most of the times |
Typical Use Cases | Provision VMs, networks, DNS records, managed services | Lookup existing VPCs, AMI IDs, .. |
What is the purpose of Provisioners in Terraform? #
- Understanding Need for Custom Configuration After Provisioning: Sometimes, we need to install packages, run scripts, or perform setup steps after a VM or resource is created
- Provisioners: Provisioners allow you to run scripts or commands on a resource after it's created or before it's destroyed
- Used to Perform Setup Tasks Not Supported Natively by Terraform: Like bootstrapping applications, setting file permissions, or executing install commands
- Simple for Quick Use Cases: Easy to install packages or echo output without external tooling
- (Feature) Supports
local-exec
andremote-exec
:local-exec
runs commands on your local machineremote-exec
runs commands inside the created VM using SSH or ..
- Helpful When Configuration Tools Arenβt Used: Good for lightweight setup tasks when Ansible, Chef, or Puppet are not being used
- (Best Practice) Use Sparingly and Prefer External Tools: Use Ansible or similar options when possible; reserve provisioners for unavoidable tasks only
resource "aws_instance" "web_server" {
ami = var.latest_ami
instance_type = "t3.micro"
key_name = "default-ec2"
vpc_security_group_ids = [aws_security_group
.web_sg.id]
# π§ HOW: Terraform connects to instance via SSH
connection {
type = "ssh"
host = self.public_ip
user = "ec2-user"
private_key = file(var.aws_key_pair)
}
# π¦ PROVISIONER BLOCK - remote-exec
# WHAT: Run commands inside the EC2
# WHY: Automate post-launch setup
# WHEN: Runs only during `terraform apply`
# WHERE: Runs from Terraform machine to EC2
provisioner "remote-exec" {
inline = [
# Install Apache
"sudo yum install httpd -y",
# Start Apache service
"sudo service httpd start",
# Create basic web page
"echo Welcome to in28minutes - Virtual \
Server is at ${self.public_dns} | sudo tee \
/var/www/html/index.html"
]
}
}
# π¬ LOCAL-EXEC ON BUCKET CREATION
# WHAT: Runs echo locally when S3 is created
# HOW: Uses a null_resource with a trigger
# WHEN: After new bucket creation
resource "aws_s3_bucket" "my_bucket" {
bucket = "in28minutes-demo-bucket-1234"
# Replace with unique name (globally unique)
}
resource "null_resource" "after_s3_created" {
provisioner "local-exec" {
command = "echo Hello from Terraform!"
}
# π Trigger rerun when bucket id changes
triggers = {
bucket_id = aws_s3_bucket.my_bucket.id
}
}
How do you create an EC2 instance using Terraform? #
- Provision an EC2 instance: In the default VPC with a security group
- Install and start Apache Web Server: Host a simple welcome page
provider "aws" {
region = "us-east-1"
}
# π DATA SOURCES
# WHAT: Fetch existing resources (read-only)
# β
Gets the default VPC (used in SG & subnet)
resource "aws_default_vpc" "default" {}
# β
Get all default subnets from the default VPC
data "aws_subnets" "default_subnets" {
filter {
name = "vpc-id"
values = [aws_default_vpc.default.id]
}
}
# β
Get the latest Amazon Linux AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["137112412989"] # Official Amazon Linux AMI owner
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
# β
Use wildcard for versioning like:
# "al2023-ami-2023.*-x86_64"
}
# ADD OTHER FILTERS AS NEEDED
}
# π SECURITY GROUP
# WHAT: Allow HTTP and SSH into the instance
# WHY: Without this, the server is unreachable
resource "aws_security_group" "http_server_sg" {
name = "http_server_sg"
vpc_id = aws_default_vpc.default.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
# Allows HTTP from anywhere
# BEST PRACTICE: RESTRICT IPS π
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
# Allows SSH access (Use carefully)
# BEST PRACTICE: RESTRICT IPS π
}
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
# Allows all outbound traffic
}
tags = {
name = "http_server_sg"
}
}
##############################################
# π₯οΈ EC2 INSTANCE
# WHAT: Launches a virtual server in AWS
# WHY: Host a sample web page using Apache
##############################################
resource "aws_instance" "http_server" {
ami = data.aws_ami
.amazon_linux.id
key_name = "default-ec2"
instance_type = "t3.micro"
vpc_security_group_ids = [
aws_security_group.http_server_sg.id
]
subnet_id = data.aws_subnets
.default_subnets.ids[0]
# π‘ Uses the first subnet from default VPC
# π SSH connection setup
connection {
type = "ssh"
host = self.public_ip
user = "ec2-user"
private_key = file(var.aws_key_pair)
}
# π¦ Provisioner installs & starts Apache
provisioner "remote-exec" {
inline = [
"sudo yum install httpd -y",
"sudo service httpd start",
"echo Welcome to in28minutes - Virtual\
Server is at ${self.public_dns} | \
sudo tee /var/www/html/index.html"
]
}
}
variable "aws_key_pair" {
# Provide value while executing
# `terraform apply -var="aws_key_pair=PATH_TO_FILE\NAME.pem"`
}
output "http_server_public_dns" {
value = aws_instance.http_server.public_dns
# π Use this DNS to access the webpage
}