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, and bool as fundamental data types
  • Complex Data Types: Supports list, set, map, object, and tuple for structured data
  • Type Validation: Type constraints help catch errors early and provide better documentation for expected input formats

Variable assignment methods:

  1. Command line: terraform apply -var="region=us-east-1"
  2. Environment variable: export TF_VAR_region="us-east-1" (TF_VAR_ + VariableName)
  3. Values defined in terraform.tfvars file
  4. 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 β€” use data 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 and remote-exec:
    • local-exec runs commands on your local machine
    • remote-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
}