Curious Devs Corner

Curious Devs Corner

Share this post

Curious Devs Corner
Curious Devs Corner
Terraform Quickstart: Part 3 - Getting Started with Terraform
Mini-Courses

Terraform Quickstart: Part 3 - Getting Started with Terraform

Install Terraform, Write Your First Configuration, and Learn the Basics of Terraform Workflow

KirshiYin's avatar
KirshiYin
Jun 05, 2025
∙ Paid

Share this post

Curious Devs Corner
Curious Devs Corner
Terraform Quickstart: Part 3 - Getting Started with Terraform
Share

In this chapter, you’ll install Terraform and start using it. You’ll learn the basics of HCL syntax and write your first configuration. You’ll also get familiar with the Terraform workflow. By the end, you’ll have deployed your first resource using Terraform.

Installing Terraform

Terraform runs as a command-line tool, and the installation process is straightforward.

The examples in this course have been tested on Ubuntu OS. For other installation options, head over to the official documentation.

$ wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

$ sudo apt update && sudo apt install terraform

Now, let’s check if the installation is successful:

$ terraform version

You should see something like this:

Terraform v1.6.6

You can now use Terraform CLI to deploy resources.

Curious Devs Corner is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.

HCL Syntax and Structure

As mentioned in the previous lesson, Terraform uses the HashiCorp Configuration Language (HCL). It’s a declarative language designed to be both human-readable and machine-friendly.

Let’s have a look into the basics:

Blocks

Blocks are containers for related configurations. This is an example of a resource block:

resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  
  tags = {
    Name = "Web Server"
  }
}

The main block types include:

  • terraform: Core Terraform settings.

  • provider: Configuration for a provider.

  • resource: Defines an infrastructure component.

  • data: References existing infrastructure.

  • variable: Defines input variables.

  • output: Defines output values.

  • module: References reusable modules.

  • locals: Defines local variables.

We’ll review each type in more detail later in this chapter.

Arguments and Expressions

Arguments assign values to names inside blocks:

resource "aws_instance" "example" {
  # Simple argument assignment
  instance_type = "t2.micro"
  
  # Expression using a variable
  ami = var.image_id
  
  # Conditional expression
  monitoring = var.environment == "production" ? true : false
  
  # Function call
  subnet_id = element(aws_subnet.example[*].id, 0)
  
  # For expressions
  dynamic "ebs_block_device" {
    for_each = var.block_devices
    content {
      device_name = ebs_block_device.value.name
      volume_size = ebs_block_device.value.size
    }
  }
}

Comments

HCL supports several comment styles:

# This is a single-line comment

// This is also a single-line comment

/*
   This is a
   multi-line
   comment
*/

Terraform File Structure

A typical Terraform project is organized into multiple files:

project/
├── main.tf        # Main resources
├── variables.tf   # Input variable declarations
├── outputs.tf     # Output values
├── providers.tf   # Provider configurations
├── versions.tf    # Terraform and provider versions
└── terraform.tfvars # Variable values (gitignored)

This is just a convention and not a requirement. Terraform reads all .tf files in a directory.

Terraform Components

Terraform uses a few key building blocks. Before we jump into practice, let's go through them.

Providers

Providers are plugins that allow Terraform to interact with APIs. They include:

  1. Cloud providers: AWS, Azure, GCP, DigitalOcean.

  2. Infrastructure services: GitHub, Cloudflare, Kubernetes.

  3. Local utilities: Random, Local, Template, HTTP.

You specify a provider in your configuration to tell Terraform what to manage.

Example AWS Provider configuration:

provider "aws" {
  region = "us-west-2"
  profile = "production"
}

provider "aws" {
  alias = "east"
  region = "us-east-1"
  profile = "production"
}

The alias parameter allows multiple configurations of the same provider.

Terraform has hundreds of providers. Feel free to browse the official docs. Each provider has documentation that explains how to use it.

Resources

A resource is an infrastructure component Terraform manages. It defines what Terraform should create, modify, or delete.

Example: Creating a S3 Bucket AWS resource:

resource "aws_s3_bucket" "example" {
  bucket = "my-tf-test-bucket"

  tags = {
    Name        = "My bucket"
    Environment = "Dev"
  }
}

Config overview:

  • Resource type (aws_s3_bucket).

  • Local name (example).

  • Arguments like buckets and tags.

All supported resource types and arguments can be found in the provider’s documentation.

State Files

What is Terraform State?

State is Terraform's way of mapping real-world resources to your configuration. It tracks:

  1. Resource metadata.

  2. Dependencies between resources.

  3. Resource attributes and properties.

  4. Mappings from resource names to provider IDs.

The state is stored in a file called terraform.tfstate by default.

Important: Never manually edit the state file! Terraform compares your code with the state file to detect changes. If you edit the state file manually, Terraform might not recognize existing resources.

Why State Matters

It serves several critical purposes:

  1. Mapping Configuration to Resources: Connects your Terraform config to real-world resources.

  2. Tracking Metadata: Stores resource dependencies.

  3. Performance: Improves plan speed by caching resource attributes.

  4. Collaboration: Enables team members to work on the same infrastructure.

State Storage Options

  1. Local State (default):

    • Simple, but problematic for teams.

    • No native locking mechanism.

    • Must be manually backed up.

  2. Remote State:

    • Stored on a shared location (S3, Azure Blob, etc.).

    • Supports locking to prevent concurrent modifications.

    • Team-friendly.

    • Better security for sensitive data.

State Locking

State locking prevents concurrent modifications that could corrupt the state. For example, consider two developers trying to modify the same resource at the same time:

State Security Considerations

The state often contains sensitive data such as:

  1. Database passwords.

  2. Private keys.

  3. Access tokens and secrets.

Best practices:

  • Enable encryption for the remote state.

  • Restrict access to state files.

  • Consider using a partial state with the -target flag.

  • Use sensitive output values (sensitive = true).

Variables and Outputs

Variables and outputs allow Terraform configurations to be flexible and reusable.

Variables

An example config to reuse the repo_name variable:

variable "repo_name" {
  default = "my-repo"
}

resource "github_repository" "example" {
  name = var.repo_name
}

You can define variables in several ways:

  • In the configuration (default).

  • In a .tfvars file: terraform.tfvars or *.auto.tfvars.

  • On the command line: terraform apply -var="region=us-east-1".

  • As environment variables: TF_VAR_region=us-east-1.

Advanced Variable Types

Terraform also supports complex data types:

# List of objects
variable "subnets" {
  type = list(object({
    cidr_block = string
    az = string
    public = bool
  }))
  default = [
    {
      cidr_block = "10.0.1.0/24"
      az = "us-west-2a"
      public = true
    },
    {
      cidr_block = "10.0.2.0/24"
      az = "us-west-2b"
      public = false
    }
  ]
}

# Map of objects
variable "users" {
  type = map(object({
    role = string
    permissions = list(string)
  }))
  default = {
    alice = {
      role = "admin"
      permissions = ["read", "write", "delete"]
    },
    bob = {
      role = "developer"
      permissions = ["read", "write"]
    }
  }
}

Outputs

They allow Terraform to display important information after running.

output "repo_url" {
  value = github_repository.example.html_url
}

output "load_balancer_dns" {
  value = aws_lb.web.dns_name
  description = "The DNS name of the load balancer"
  sensitive = true # Marks this output as sensitive
}

Outputs are displayed after the terraform apply command and can be queried using terraform output. This is helpful when you want to directly click on a URL.

After running terraform apply, you’ll see:

Apply complete! Resources: 1 added, 0 changed, 0 destroyed. Outputs: repo_url = "https://github.com/user/my-repo"

Note that output marked as sensitive = true will be hidden:

Outputs:

load_balancer_dns = <sensitive>

Data Sources

Data sources let you fetch information from outside Terraform and use it in your configuration without creating or modifying resources. While resources create things, data sources look up existing things.

Data sources are read-only. You use them to query external data, like:

  • Getting details of an existing AWS VPC

  • Fetching a GitHub user or repository

  • Looking up AMIs (Amazon Machine Images)

  • Reading from files or APIs

They do not create or change infrastructure.

Example data source to check an existing GitHub repo and make its data available (e.g., name, visibility, etc.) inside your config:

data "github_repository" "example" {
  full_name = "some-org/some-repo"
}

Modules

Modules are reusable Terraform configurations.

Why Use Modules?

Consider this real-world problem:

A team member expresses frustration that their Terraform code is becoming unwieldy and hard to navigate with hundreds of resources in a single file.

How the solution could look like:

Refactor the code into multiple logical files based on resource types or workloads. Create a consistent naming convention for resources so they can be easily located across files. Use modules to encapsulate related resources.

When you incorporate modules, you:

  • Avoid repetition: Instead of copying code, use a module.

  • Improve maintainability: Updates apply to all instances.

  • Encapsulate logic: Keep things modular.

Example: A module to create multiple repositories.

module "repos" {
  source  = "./modules/github-repos"
  repo_names = ["repo1", "repo2"]
}

We will cover modules in more detail later.

Terraform Workflow Overview

Keep reading with a 7-day free trial

Subscribe to Curious Devs Corner to keep reading this post and get 7 days of free access to the full post archives.

Already a paid subscriber? Sign in
© 2025 Kirshi Yin
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share