DEV Community

Cover image for Deploying Infrastructure to Azure Using HCP Terraform and GitHub VCS
Bernard Chika Uwaezuoke
Bernard Chika Uwaezuoke

Posted on

Deploying Infrastructure to Azure Using HCP Terraform and GitHub VCS

INTRODUCTION

Every Cloud Engineer eventually reaches the same point.
At first, running Terraform from your laptop feels exciting. You write your .tf files, run:

terraform init
terraform plan
terraform apply

and Azure resources appear like magic.

But as projects grow, that approach becomes risky.
What happens when another engineer needs to review your changes?
What happens when the state file is sitting on someone’s laptop?
What happens when a junior engineer accidentally applies changes to production without approval?

That is where HCP Terraform and GitHub VCS integration come in.
In this project, I will walk you through how to deploy Azure infrastructure using Terraform code stored in GitHub, with HCP Terraform handling remote runs, state management, plan review, and controlled apply operations.

As a Cloud Engineer and technical instructor, this is the kind of workflow I encourage students and teams to learn early because it reflects how real infrastructure should be managed: version-controlled, reviewable, auditable, and repeatable.

HCP Terraform supports a VCS-driven workflow where it fetches Terraform configuration from a connected repository and automatically starts plan and apply operations when changes are pushed. This allows GitHub to act as the source of truth for infrastructure code.

Project Goal

We are going to deploy basic Azure infrastructure using:

  • Azure as the cloud provider
  • Terraform as the Infrastructure as Code tool
  • GitHub as the version control system
  • HCP Terraform as the remote execution and state management platform

The deployment will create:

  • Azure Resource Group
  • Virtual Network
  • Subnet
  • Network Security Group
  • Public IP
  • Network Interface
  • Linux Virtual Machine

Architecture Overview

Architectural Overview

In simple terms:

  1. We write Terraform code locally.
  2. We push the code to GitHub.
  3. HCP Terraform detects the change.
  4. HCP Terraform runs terraform plan.
  5. After approval, HCP Terraform runs terraform apply.
  6. Azure resources are deployed.

This is a professional DevOps workflow because no one is manually creating resources in the Azure portal, and no one is running production infrastructure from their laptop (Minimum DevOps requirements).

Prerequisites

Before starting, make sure you have:

  1. An Azure subscription
  2. A GitHub account
  3. An HCP Terraform account
  4. Terraform installed locally
  5. Azure CLI installed locally
  6. Basic knowledge of Git and Terraform

You also need permission to create resources in Azure and permission to create or use a service principal click here to know more.

Here We Go!

Step 1: Create a GitHub Repository

  • Create a new GitHub repository.
    azure-hcp-terraform-vcs

  • Clone the repository locally
    git clone https://github.com/YOUR_USERNAME/azure-hcp-terraform-vcs.git

  • Switch to the cloned repository
    cd azure-hcp-terraform-vcs

Create a repo
Creating a Repo in GitHub

For details on how to create a GitHub repository, click here

  • Create the Terraform project structure azure-hcp-terraform-vcs/

├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
└── README.md
Enter fullscreen mode Exit fullscreen mode

Project Structure

The above structure is simple enough for beginners to understand, but also professional enough to build on later.

Step 2: Configure the Azure Provider

  • Create providers.tf.
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}

Enter fullscreen mode Exit fullscreen mode

This tells Terraform to use the AzureRM provider.
The features {} block is required by the AzureRM provider.

Step 3: Create Terraform Variables

  • Create variables.tf.
variable "resource_group_name" {
  description = "Name of the Azure Resource Group"
  type        = string
}

variable "location" {
  description = "Azure region where resources will be deployed"
  type        = string
}

variable "vnet_name" {
  description = "Name of the Virtual Network"
  type        = string
}

variable "vnet_address_space" {
  description = "Address space for the Virtual Network"
  type        = list(string)
}

variable "subnet_name" {
  description = "Name of the subnet"
  type        = string
}

variable "subnet_address_prefixes" {
  description = "Address prefixes for the subnet"
  type        = list(string)
}

variable "vm_name" {
  description = "Name of the Linux Virtual Machine"
  type        = string
}

variable "admin_username" {
  description = "Admin username for the virtual machine"
  type        = string
}

variable "public_key" {
  description = "SSH public key content for the virtual machine"
  type        = string
}


Enter fullscreen mode Exit fullscreen mode

Important Note

In HCP Terraform, avoid using local file paths like this:
file("C:/Users/USER/.ssh/azure_rsa.pub")
That path exists on your laptop, not inside the HCP Terraform remote execution environment.
Instead, paste the actual public key content into an HCP Terraform variable and check the sensitive atribute box.

Step 4: Create the Azure Infrastructure Code

  • Create main.tf.
resource "azurerm_resource_group" "rg" {
  name     = var.resource_group_name
  location = var.location
}

resource "azurerm_virtual_network" "vnet" {
  name                = var.vnet_name
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = var.vnet_address_space
}

resource "azurerm_subnet" "subnet" {
  name                 = var.subnet_name
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = var.subnet_address_prefixes
}

resource "azurerm_network_security_group" "nsg" {
  name                = "${var.vm_name}-nsg"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  security_rule {
    name                       = "Allow-SSH"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "public_ip" {
  name                = "${var.vm_name}-public-ip"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "nic" {
  name                = "${var.vm_name}-nic"
  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
  }
}

resource "azurerm_network_interface_security_group_association" "nsg_association" {
  network_interface_id      = azurerm_network_interface.nic.id
  network_security_group_id = azurerm_network_security_group.nsg.id
}

resource "azurerm_linux_virtual_machine" "vm" {
  name                = var.vm_name
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  size                = "Standard_D2s_v3"
  admin_username      = var.admin_username

  network_interface_ids = [
    azurerm_network_interface.nic.id
  ]

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.public_key
  }

  os_disk {
    name                 = "${var.vm_name}-osdisk"
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
}
Enter fullscreen mode Exit fullscreen mode

This code builds a simple Azure VM environment with networking, security, and SSH access.
This is a good starter project because it touches the core Azure infrastructure components: compute, networking, identity, and security.

Step 5: Create Outputs

  • Create outputs.tf.
output "resource_group_name" {
  description = "The name of the created resource group"
  value       = azurerm_resource_group.rg.name
}

output "public_ip_address" {
  description = "The public IP address of the virtual machine"
  value       = azurerm_public_ip.public_ip.ip_address
}

output "vm_name" {
  description = "The name of the virtual machine"
  value       = azurerm_linux_virtual_machine.vm.name
}
Enter fullscreen mode Exit fullscreen mode

Outputs are useful because after deployment, HCP Terraform will show important values such as the public IP address of the VM.

Step 6: Create an Example tfvars File

  • Create terraform.tfvars
resource_group_name      = "rg-hcp-terraform-demo"
location                 = "westeurope"
vnet_name                = "vnet-hcp-demo"
vnet_address_space       = ["10.0.0.0/16"]
subnet_name              = "subnet-hcp-demo"
subnet_address_prefixes  = ["10.0.1.0/24"]
vm_name                  = "vm-hcp-demo"
admin_username           = "azureuser"
public_key               = "ssh-rsa AAAA..."
Enter fullscreen mode Exit fullscreen mode

Do not commit real secrets or sensitive values into GitHub.
For public_key, use the content of your .pub file.
For example:
cat ~/.ssh/azure_rsa.pub
On Windows PowerShell:
Get-Content C:\Users\USER\.ssh\azure_rsa.pub
Copy the full public key content.

Step 7: Create an Azure Service Principal

HCP Terraform needs permission to deploy resources into Azure.
The common approach is to create a service principal and store its values as environment variables in HCP Terraform. Microsoft recommends using service principals for automated tools such as Terraform instead of using a fully privileged personal user account.

  • Login to Azure:
    az login

  • Confirm your subscription:
    az account show

  • Set the subscription:
    az account set --subscription "<SUBSCRIPTION_ID>"

  • Create a service principal:

az ad sp create-for-rbac \
  --name "sp-hcp-terraform-azure" \
  --role "Contributor" \
  --scopes "/subscriptions/<SUBSCRIPTION_ID>"
Enter fullscreen mode Exit fullscreen mode

The output will look similar to this:

{
  "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "displayName": "sp-hcp-terraform-azure",
  "password": "xxxxxxxxxxxxxxxxxxxx",
  "tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
Enter fullscreen mode Exit fullscreen mode

Save these values securely.
You will need them in HCP Terraform.

Step 8: Push Your Code to GitHub

  • In your working director, run:
git status
git add .
git commit -m "Initial Azure infrastructure with Terraform"
git push origin main
Enter fullscreen mode Exit fullscreen mode

At this point, GitHub becomes the source of truth for your infrastructure code.
This is a major shift from manual cloud administration to real Infrastructure as Code practice.

Push to GitHub

  • The files in the repository in GitHub Terraform Config Files in GitHub

Step 9: Create an HCP Terraform Organization

  • Go to HCP Terraform and create an organization. Click here

  • Click on Create Organization
    Select Personal
    A good organization naming pattern could be:
    peabsmart-cloud-labs

HCP Organization

  • On the left pane inside the organization click on Projects, then click on New project and enter the project detail and Click on Create. For this project, we will use Azure-Training-Projects

Create a Project

Projects in HCP Terraform help organize related workspaces, especially when you are managing multiple environments or cloud providers. HashiCorp’s HCP Terraform learning path includes project and variable set organization as part of the recommended collaboration workflow.

Step 10: Connect HCP Terraform to GitHub and Create a HCP Terraform Workspace

  • In the project you created, Click on Create a workspace HCP Terraform:

Create a workspace

  • Select Version Control Workflow

  • Choose GitHub.

Connect GitHub

  • Authorize HCP Terraform to access your GitHub account or organization.

  • Select the repository: azure-hcp-terraform-vcs

  • Click on Create

    Terraform working directory: /

    Use / if your .tf files are in the root of the repository.
    If your files are inside a folder liketerraform/, then set the working directory to: terraform

This is a common mistake. If the working directory is wrong, HCP Terraform will not find your Terraform configuration.

Complete Workspace

  • The Terraform variable page will popup

  • Enter the required variable values from terraform.tfvars file we created earlier and click Save variables.

Variables

Step 11: Add Azure Credentials to HCP Terraform

  • Go to the workspace:
    Workspace > Variables

  • Add the following as Environment Variables:
    ARM_CLIENT_ID
    ARM_CLIENT_SECRET
    ARM_SUBSCRIPTION_ID
    ARM_TENANT_ID

Use these values:

ARM_CLIENT_ID        = appId from the service principal output
ARM_CLIENT_SECRET    = password from the service principal output
ARM_SUBSCRIPTION_ID  = your Azure subscription ID
ARM_TENANT_ID        = tenant from the service principal output
Mark ARM_CLIENT_SECRET as Sensitive.
Enter fullscreen mode Exit fullscreen mode

Env Variables

Microsoft documents these Azure Terraform authentication variables as the standard service-principal environment variables for Terraform automation.

Step 12: Queue the First Terraform Plan

Once the workspace is connected to GitHub and variables are added, HCP Terraform will usually trigger a run automatically.

You can also manually queue a run:
Workspace > Runs > Queue plan
HCP Terraform will run:
terraform init
terraform plan
remotely.
This is where the value becomes clear.
The plan is no longer hidden on one person’s machine. It is visible, reviewable, and part of the deployment workflow.

runs

Step 13: Review the Plan

A good Cloud Engineer does not blindly apply infrastructure changes.
Review the plan carefully.
Look for:

  • Resources to be created
  • Resources to be modified
  • Resources to be destroyed
  • Unexpected region changes
  • Unexpected VM sizes
  • Public exposure
  • Security group rules
  • Naming mistakes

For example, if the plan says:
Plan: 8 to add, 0 to change, 0 to destroy.
that means Terraform intends to create 8 resources.
A depicted in the image below

Step 14: Apply the Infrastructure

If the plan looks correct, click: Confirm & Apply

Apply

HCP Terraform will run the apply operation remotely.
After a successful apply, you should see your outputs:

resource_group_name = rg-hcp-terraform-demo
vm_name             = vm-hcp-demo
public_ip_address   = x.x.x.x
You can now test SSH access:
ssh azureuser@<PUBLIC_IP_ADDRESS>
Enter fullscreen mode Exit fullscreen mode
  • Apply completed

Apply completed

  • Prove of authorization after review (Reviewer's Comment)

Reviewers

Step 15: Verify Resources in Azure

  • Go to the Azure portal and check:
    Resource Groups > rg-hcp-terraform-demo
    You should see:

  • Virtual Machine

  • Virtual Network

  • Subnet

  • Network Security Group

  • Public IP

  • Network Interface

  • OS Disk

As Depicted below

Azure Portal

You can also verify using Azure CLI:

az resource list \
  --resource-group rg-hcp-terraform-demo \
  --output table
Enter fullscreen mode Exit fullscreen mode

Step 16: Make a Change Through GitHub

Imagine your team wants to change the VM size.

  • Update this line in main.tf:
    size = "Standard_D2s_v3"
    Change it to:
    size = "Standard_B1s"

  • Commit and push:

git add .
git commit -m "Update VM size"
git push origin main
Enter fullscreen mode Exit fullscreen mode

HCP Terraform detects the GitHub change and starts a new run.
This is the power of VCS-driven infrastructure.
You are no longer clicking around in Azure. You are managing infrastructure through code, Git history, and controlled deployment runs.

Step 17: Destroy the Infrastructure Safely

When you are done with the demo, destroy the resources to avoid unnecessary Azure cost.

  • In HCP Terraform: Workspace > Settings > Destruction and Deletion > Queue destroy plan

Queue destroy plan

  • Confirm by typing delete
    delete

  • Review the destroy plan, then confirm.

Confirm destroy

  • Deletion Completed Deletion completed

HCP Terraform supports destroy operations from the workspace, and HashiCorp warns that deleting a workspace does not automatically destroy the infrastructure it manages. You must queue a destroy plan if you want the actual cloud resources removed.

Common Errors and How to Fix Them

Error 1: Unsupported Operator for public_key_path

Example error:
invalid HCL for variable "public_key_path" at 1,1: Unsupported operator
Cause:
You are probably passing a Windows path like this directly into HCP Terraform:
C:/Users/USER/.ssh/azure_rsa.pub
HCP Terraform runs remotely and cannot access files on your local machine.
Fix:
Use the actual public key content instead:
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ..."

Error 2: List of String Required, But Have String

Bad value:
vnet_address_space = "[\"10.0.0.0/16\"]"
Correct value:
vnet_address_space = ["10.0.0.0/16"]
Cause:
The first example is a string. The second example is a real HCL list.

Error 3: Azure Authentication Failed

Check that these environment variables exist in HCP Terraform:
ARM_CLIENT_ID
ARM_CLIENT_SECRET
ARM_SUBSCRIPTION_ID
ARM_TENANT_ID

Also confirm that the service principal has enough permissions on the subscription or resource group.

Error 4: Terraform Cannot Find Configuration

Cause:
Your HCP Terraform workspace working directory is wrong.
If your files are here:
repo/main.tf
use:
/
If your files are here:
repo/terraform/main.tf
use:
terraform

Best Practices I Teach My Students

1. Do Not Store Secrets in GitHub

Never commit:

  • client secrets
  • passwords
  • private keys
  • .tfstate files Use HCP Terraform sensitive variables for secrets.

2. Use Git Branches and Pull Requests

For production teams, avoid pushing directly to main.
Use:
feature branch > pull request > review > merge > HCP Terraform run
This improves collaboration and prevents careless infrastructure changes.

3. Keep State Remote

One of the strongest benefits of HCP Terraform is remote state management.
Your state is not scattered across laptops.
It is centrally managed, protected, and connected to the workspace.

4. Separate Environments

For serious projects, create separate workspaces:

  • azure-dev
  • azure-staging
  • azure-prod Each workspace should have its own variables and approval controls.

5. Use Clear Naming Standards

Bad names:

  • test-rg
  • vm1
  • network-demo

Better names:

  • rg-dev-hcp-demo-weu
  • vnet-dev-hcp-demo-weu
  • vm-dev-hcp-demo-01 Good naming helps with operations, cost tracking, troubleshooting, and governance.

Final Reflection

This project is more than just deploying a virtual machine to Azure.
It demonstrates a professional Infrastructure as Code workflow.
As a Cloud Engineer, I see Terraform as more than a provisioning tool. It is a discipline. It teaches structure, repeatability, version control, collaboration, and accountability.

As a technical instructor, I also see this workflow as one of the best ways to help students understand how modern cloud teams work. They learn that cloud engineering is not about clicking buttons in the portal. It is about building systems that are predictable, secure, reviewable, and easy to reproduce.

Top comments (0)