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
In simple terms:
- We write Terraform code locally.
- We push the code to GitHub.
- HCP Terraform detects the change.
- HCP Terraform runs terraform plan.
- After approval, HCP Terraform runs terraform apply.
- 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:
- An Azure subscription
- A GitHub account
- An HCP Terraform account
- Terraform installed locally
- Azure CLI installed locally
- 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-vcsClone the repository locally
git clone https://github.com/YOUR_USERNAME/azure-hcp-terraform-vcs.gitSwitch to the cloned repository
cd azure-hcp-terraform-vcs
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
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 {}
}
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
}
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"
}
}
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
}
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..."
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 loginConfirm your subscription:
az account showSet 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>"
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"
}
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
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.
- The files in the repository in GitHub
Step 9: Create an HCP Terraform Organization
Go to HCP Terraform and create an organization. Click here
Click on
Create Organization
SelectPersonal
A good organization naming pattern could be:
peabsmart-cloud-labs
- On the left pane inside the organization click on
Projects, then click onNew projectand enter the project detail and Click onCreate. For this project, we will useAzure-Training-Projects
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 workspaceHCP Terraform:
Select
Version Control WorkflowChoose GitHub.
Authorize HCP Terraform to access your GitHub account or organization.
Select the repository:
azure-hcp-terraform-vcs-
Click on
CreateTerraform 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.
The Terraform variable page will popup
Enter the required variable values from terraform.tfvars file we created earlier and click
Save variables.
Step 11: Add Azure Credentials to HCP Terraform
Go to the workspace:
Workspace > VariablesAdd 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.
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.
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
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>
- Apply completed
- Prove of authorization after review (Reviewer's Comment)
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
You can also verify using Azure CLI:
az resource list \
--resource-group rg-hcp-terraform-demo \
--output table
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
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
- 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)