Kahibaro
Discord Login Register

Cloud provisioning with Terraform

Why Use Terraform for Cloud Provisioning?

Using Terraform for cloud provisioning means using code (HCL) to create and manage cloud resources in a repeatable, version-controlled way. In this chapter, the focus is specifically on using Terraform with real cloud providers (AWS, Azure, GCP), not on generic Terraform features or HCL syntax that were covered earlier.

Key benefits specific to cloud provisioning:

Basic Cloud Provisioning Workflow

The high-level workflow doesn’t change much between clouds:

  1. Configure the provider (credentials, region, etc.).
  2. Declare resources (VMs/instances, networks, storage, etc.).
  3. Initialize the working directory: terraform init
  4. Preview changes: terraform plan
  5. Apply the configuration: terraform apply
  6. Destroy resources when they’re no longer needed: terraform destroy

The details—provider blocks, resource types, and arguments—differ per cloud.

Authenticating Terraform to the Cloud

Each cloud has several authentication methods. The most important rule: prefer official, tool-friendly methods over hardcoding credentials in HCL.

Typical patterns:

For real projects, combine:

Minimal Cloud Examples

The goal of these examples is to show how Terraform translates into concrete cloud resources for a very small “hello world” infrastructure. They omit many options on purpose.

AWS: Provisioning a Simple EC2 Instance

Prerequisites:

Example layout:

# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.5.0"
}
provider "aws" {
  region = "us-east-1"
}
# 1. Create a security group allowing SSH
resource "aws_security_group" "web_sg" {
  name        = "example-web-sg"
  description = "Allow SSH inbound"
  ingress {
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # For demos only; not safe for production
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
# 2. Create an EC2 instance
resource "aws_instance" "web" {
  ami                         = "ami-0c02fb55956c7d316" # Example Amazon Linux 2 AMI; region-specific
  instance_type               = "t3.micro"
  vpc_security_group_ids      = [aws_security_group.web_sg.id]
  associate_public_ip_address = true
  key_name                    = "my-keypair"           # Must exist in AWS
  tags = {
    Name = "example-web"
  }
}
# 3. Output the instance public IP
output "instance_public_ip" {
  value = aws_instance.web.public_ip
}

Workflow:

  1. terraform init
  2. terraform plan
  3. terraform apply
  4. Use the output IP to SSH: ssh ec2-user@<ip> -i /path/to/key.pem
  5. When done: terraform destroy

This example shows:

Azure: Provisioning a Linux Virtual Machine

Prerequisites:

Typical minimal setup (real Azure VM configs are more verbose):

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}
provider "azurerm" {
  features {}
}
# 1. Resource group (logical container)
resource "azurerm_resource_group" "rg" {
  name     = "rg-terraform-example"
  location = "eastus"
}
# 2. Virtual network and subnet
resource "azurerm_virtual_network" "vnet" {
  name                = "vnet-example"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_subnet" "subnet" {
  name                 = "subnet-example"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}
# 3. Public IP and NIC
resource "azurerm_public_ip" "public_ip" {
  name                = "pip-example"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  allocation_method   = "Dynamic"
}
resource "azurerm_network_interface" "nic" {
  name                = "nic-example"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.public_ip.id
  }
}
# 4. Linux VM
resource "azurerm_linux_virtual_machine" "vm" {
  name                  = "vm-example"
  location              = azurerm_resource_group.rg.location
  resource_group_name   = azurerm_resource_group.rg.name
  size                  = "Standard_B1s"
  admin_username        = "azureuser"
  network_interface_ids = [azurerm_network_interface.nic.id]
  admin_ssh_key {
    username   = "azureuser"
    public_key = file("~/.ssh/id_rsa.pub")
  }
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-focal"
    sku       = "20_04-lts"
    version   = "latest"
  }
}
output "vm_public_ip" {
  value = azurerm_public_ip.public_ip.ip_address
}

This illustrates:

GCP: Provisioning a Compute Engine Instance

Prerequisites:

Example:

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 6.0"
    }
  }
}
provider "google" {
  project = "my-gcp-project-id"
  region  = "us-central1"
  zone    = "us-central1-a"
}
# Simple Compute Engine instance
resource "google_compute_instance" "vm" {
  name         = "tf-example-vm"
  machine_type = "e2-micro"
  zone         = "us-central1-a"
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }
  network_interface {
    network       = "default"
    access_config {} # Needed for external IP
  }
  metadata = {
    ssh-keys = "gcpuser:${file("~/.ssh/id_rsa.pub")}"
  }
  labels = {
    environment = "demo"
  }
}
output "vm_external_ip" {
  value = google_compute_instance.vm.network_interface[0].access_config[0].nat_ip
}

Notable points:

Managing State for Cloud Environments

Cloud provisioning quickly outgrows local terraform.tfstate files. For meaningful cloud use, you almost always want a remote backend:

Common patterns:

Example: AWS remote state backend:

terraform {
  backend "s3" {
    bucket         = "my-tf-state-bucket"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

This enables:

Configuration of backends is usually done once per project and then reused.

Structuring Terraform for Multiple Environments

When provisioning cloud infrastructure, you typically need dev, staging, and production. For beginners, two simple patterns suffice:

Pattern 1: Directory Per Environment

Example structure:

terraform/
  modules/
    network/
      main.tf
      variables.tf
      outputs.tf
    compute/
      main.tf
      variables.tf
      outputs.tf
  envs/
    dev/
      main.tf
      backend.tf
      dev.tfvars
    prod/
      main.tf
      backend.tf
      prod.tfvars

Pattern 2: Workspaces (For Simpler Cases)

Workspaces allow reusing the same configuration for multiple instances (e.g., default, dev, prod) with workspace-aware naming.

Caveats:

Provisioning Common Cloud Building Blocks

Beyond a single VM, real-world Terraform usage involves assembling compositions of typical cloud components. At a beginner level, you should be comfortable with:

Networking

Expect these primitives:

Pattern:

  1. Create a virtual network (VPC/vNet/Network).
  2. Create one or more subnets.
  3. Create firewall/security rules to allow SSH/HTTP/etc.
  4. Attach compute resources to subnets and security groups.

Compute + Storage

Typical resources:

Look out for:

Load Balancers and Managed Services (High-Level View)

Even if you don’t fully implement them yet, know that Terraform can manage:

These follow the same pattern: provider block → resource declarations → outputs.

Using Variables and Outputs for Cloud Reuse

For cloud provisioning, variables and outputs are essential for making configurations reusable and composable.

Minimal practical rules:

Example (generic pattern):

# variables.tf
variable "region" {
  description = "Cloud region"
  type        = string
  default     = "us-east-1"
}
variable "instance_type" {
  description = "Instance size"
  type        = string
  default     = "t3.micro"
}
# main.tf (AWS example fragment)
provider "aws" {
  region = var.region
}
resource "aws_instance" "web" {
  instance_type = var.instance_type
  # ...
}
# outputs.tf
output "web_public_ip" {
  value = aws_instance.web.public_ip
}

Cloud provisioning almost always ends up parameterized this way so you can adjust cost, performance, and regions without rewriting resources.

Integrating Terraform with DevOps Workflows

Since this course lives in a DevOps and Cloud part, it’s worth highlighting how cloud provisioning with Terraform fits in a broader workflow:

Practical Tips and Common Pitfalls

When you start using Terraform for real cloud provisioning, a few recurring issues appear:

Where to Go Next

After understanding basic cloud provisioning with Terraform, the natural next steps are:

Views: 20

Comments

Please login to add a comment.

Don't have an account? Register now!