Building Azure Infrastructure with Terraform and Configuring It with Ansible

This article explores how to combine Terraform and Ansible to create and configure an Azure environment seamlessly. We'll provision the infrastructure, deploy virtual machines (VMs), and configure them with NGINX using Ansible. The solution is ideal for automating deployments while ensuring consistency and scalability.


Overview of the Project

Tools and Frameworks:

  • Terraform: Automates infrastructure provisioning.

  • Ansible: Manages configuration and deployments.

Steps:

  1. Use Terraform to provision:

    • Virtual Network (VNet).

    • Subnets.

    • Virtual Machines with Public IPs.

    • Secure configurations.

  2. Use Ansible to:

    • Install NGINX.

    • Deploy a sample webpage.


Terraform Configuration

1. The vm.tf File

The vm.tf file provisions the virtual network, subnets, public IPs, and virtual machines based on the environment (dev, staging, or production). Here's the breakdown:

# Define Resource Group
data "azurerm_resource_group" "test" {
  name = "tfmulti"
}

# Define Location Dynamically Based on Environment
locals {
  region = var.environment == "prd" ? "East US" : var.environment == "stg" ? "West US" : var.environment == "dev" ? "Central US" : "East US"
}

# Provision Virtual Network
resource "azurerm_virtual_network" "tfvnetmulti" {
  name                = "${var.environment}-tfvnet"
  address_space       = ["10.0.0.0/16"]
  location            = local.region
  resource_group_name = data.azurerm_resource_group.test.name
}

# Provision Subnet
resource "azurerm_subnet" "tfvnetsub" {
  name                 = "internal"
  resource_group_name  = data.azurerm_resource_group.test.name
  virtual_network_name = azurerm_virtual_network.tfvnetmulti.name
  address_prefixes     = ["10.0.2.0/24"]
}

# Provision Public IPs for Each VM
resource "azurerm_public_ip" "testpubid" {
  count               = var.instance_count
  name                = "${var.environment}-tflinuxpublicip-${count.index + 1}"
  location            = azurerm_virtual_network.tfvnetmulti.location
  resource_group_name = data.azurerm_resource_group.test.name
  allocation_method   = "Static"
}

# Provision Network Interfaces
resource "azurerm_network_interface" "testnic" {
  count               = var.instance_count
  name                = "${var.environment}-tflinuxnic-${count.index + 1}"
  location            = azurerm_virtual_network.tfvnetmulti.location
  resource_group_name = data.azurerm_resource_group.test.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.tfvnetsub.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.testpubid[count.index].id
  }
}

# Provision Virtual Machines
resource "azurerm_linux_virtual_machine" "testvm" {
  count               = var.instance_count
  name                = "${var.environment}-tflinuxvm-${count.index + 1}"
  resource_group_name = data.azurerm_resource_group.test.name
  location            = azurerm_virtual_network.tfvnetmulti.location
  size                = "Standard_F2"
  admin_username      = "adminuser"

  network_interface_ids = [
    azurerm_network_interface.testnic[count.index].id,
  ]

  admin_ssh_key {
    username   = "adminuser"
    public_key = file("tf-key.pub")
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts"
    version   = "latest"
  }

  tags = {
    Environment = var.environment
    Project     = "TerraformDemo"
  }
}

2. The main.tf File

The main.tf file uses Terraform modules to define the number of instances and environments:

module "dev_infra" {
  source         = "./infra"
  instance_count = 2
  environment    = "dev"
}

module "stg_infra" {
  source         = "./infra"
  instance_count = 2
  environment    = "stg"
}

module "prd_infra" {
  source         = "./infra"
  instance_count = 1
  environment    = "prd"
}

output "dev_infra_vmpubip" {
  value = module.dev_infra.vmpubips
}

output "stg_infra_vmpubip" {
  value = module.stg_infra.vmpubips
}

output "prd_infra_vmpubip" {
  value = module.prd_infra.vmpubips
}

Ansible Configuration

Once the infrastructure is provisioned, Ansible is used to install and configure NGINX on the VMs.

Tasks File for NGINX Role

# tasks file for nginx-role
---
- name: Install nginx
  apt:
    name: nginx
    state: latest

- name: Enable nginx
  service:
    name: nginx
    enabled: yes

- name: Deploy webpage
  copy:
    src: index.html
    dest: /var/www/html

Automation Workflow

  1. Provision Infrastructure:

    • Run Terraform to create the infrastructure:

        terraform init
        terraform apply -auto-approve
      
  2. Extract Terraform Outputs:

    • Use the script to dynamically update Ansible inventory with public IPs:

        #!/bin/bash
      
        # Paths to the inventory files
        INVENTORY_DEV="/Users/snehsrivastava/terraform_practice/ansible/inventories/dev"
        INVENTORY_STG="/Users/snehsrivastava/terraform_practice/ansible/inventories/stg"
        INVENTORY_PRD="/Users/snehsrivastava/terraform_practice/ansible/inventories/prd"
      
        # Terraform output JSON file
        TERRAFORM_OUTPUT="terraform_output.json"
      
        # Step 1: Fetch Terraform outputs in JSON format
        terraform output -json > "$TERRAFORM_OUTPUT"
        if [ $? -ne 0 ]; then
          echo "Error: Failed to fetch Terraform outputs. Ensure Terraform has been applied successfully."
          exit 1
        fi
      
        echo "Terraform outputs saved to $TERRAFORM_OUTPUT"
      
        # Step 2: Function to update inventory file
        generate_inventory() {
          local env="$1"  # Environment (e.g., dev, stg, prd)
          local inventory_file="$2"  # Path to the inventory file
      
          # Extract the IP addresses from Terraform outputs
          ips=$(jq -r ".${env}_infra_vmpubip.value[]" "$TERRAFORM_OUTPUT")
      
          # Check if there are any IPs for this environment
          if [ -z "$ips" ]; then
            echo "No IPs found for environment: $env"
            return
          fi
      
          # Start generating the inventory file content
          echo "[${env}servers]" > "$inventory_file"
          server_index=1
          for ip in $ips; do
            echo "server${server_index} ansible_host=${ip}" >> "$inventory_file"
            server_index=$((server_index + 1))
          done
      
          # Add group variables
          echo -e "\n[${env}servers:vars]" >> "$inventory_file"
          echo "ansible_user=adminuser" >> "$inventory_file"
          echo "ansible_ssh_private_key_file=/Users/snehsrivastava/terraform_practice/secrets/tf-key" >> "$inventory_file"
          echo "ansible_python_interpreter=/usr/bin/python3" >> "$inventory_file"
      
          echo "Updated inventory for $env: $inventory_file"
        }
      
        # Step 3: Update inventory files for each environment
        generate_inventory "dev" "$INVENTORY_DEV"
        generate_inventory "stg" "$INVENTORY_STG"
        generate_inventory "prd" "$INVENTORY_PRD"
      
        # Cleanup
        rm -f "$TERRAFORM_OUTPUT"
        echo "Terraform output file removed. All inventory files updated successfully."
      
  3. Run Ansible Playbook:

    • Configure the VMs:

        ansible-playbook -i inventories/dev nginx-playbook.yml
      

Challenges and Resolutions

  1. Dynamic Inventory:

    • Parsing Terraform outputs into Ansible inventory was automated using a Python script integrated into the shell workflow.
  2. Terraform Module Management:

    • Separate modules were used for dev, staging, and production environments for better scalability.
  3. Firewall Rules:

    • Ensured port 22 (SSH) and port 80 (HTTP) were open in NSGs.
  4. Configuration Drift:

    • Ansible was used to standardize configuration across VMs.

Diagram of the Architecture

  1. Terraform-Provisioned Azure Infrastructure:

    • Virtual Network with Subnets.

    • Public IPs and Network Interfaces.

    • Linux VMs tagged by environment.

  2. NGINX Deployment via Ansible:

    • Automated installation and configuration on provisioned VMs.

Conclusion: This project demonstrates how Terraform and Ansible can work together to create a highly automated and scalable infrastructure on Azure. Terraform handles the provisioning, while Ansible ensures consistent configurations across environments.

Feel free to explore the GitHub repository for the complete code!