Sitemap

Part 1: Provisioning Azure Infrastructure with Terraform and Setting Up CI Using Azure DevOps

11 min read5 days ago

--

What Are We Building?

Running an application locally is one thing, but deploying it to the cloud is another challenge. And ideally, we want a deployment process that’s easy to manage and repeat.

In this article, I’ll walk you through how I’m deploying an e-commerce application built with Spring Boot and React on Azure Kubernetes Service (AKS). We’ll use tools like Terraform, Ansible, Helm, and Argo CD to automate different parts of the deployment.

The application is based on a microservices architecture, with separate services for products, orders, payments, and notifications. It uses PostgreSQL for data storage and Kafka for asynchronous communication between services. We also have an API Gateway to handle incoming requests, Eureka for service discovery, and Spring Cloud Config for centralized configuration.

Press enter or click to view image in full size
Microservices application

Here’s how we’ll approach the deployment:

  • Terraform: Provision the Azure infrastructure, including networking, the AKS cluster, Azure Container Registry (ACR), and PostgreSQL.
  • Docker: Build container images for our services and push them to ACR.
  • Ansible: Automate the installation and initial setup of Argo CD.
  • Helm & Argo CD: Deploy our application to Kubernetes and manage deployments using GitOps.

Once everything is deployed, we’ll check that our services are working properly. We’ll also make a change in Git and see how Argo CD picks it up and applies it to our cluster.

I’ll share the commands, configurations, and any issues I run into along the way, including how I fix them. The goal is to keep things practical and make it easy for you to try the same setup yourself.

Let’s get started with the prerequisites and our first Terraform configuration.

2. Prerequisites

Before getting started, make sure you have an Azure account with an active subscription, an Azure DevOps organization, and a Git repository for your project.

We’ll be using a few tools throughout this tutorial:

  • Terraform to provision our Azure infrastructure.
  • Azure CLI to interact with Azure from the terminal.
  • Docker to build and package our application into container images.
  • kubectl & Helm to deploy and manage resources in Kubernetes.
  • Ansible to automate the installation of Argo CD.
  • Git to manage our source code and Kubernetes configurations.

If you already have these tools installed, you’re good to go, if not yet, you can install Terraform and Azure CLI by following the official installtion guide.

Let’s first check that Terraform and Azure CLI are working:

terraform version
az version

Next, log in to your Azure account:

az login

This will open your browser and ask you to sign in with your Azure credentials.

That’s it for the prerequisites. We can now move on to setting up Terraform and provisioning our Azure infrastructure.

3. Provision Azure Infrastructure with Terraform

Now that our tools are ready, let’s start setting up our Azure infrastructure using Terraform.

Step 1: Prepare the Project Structure

First, we need to organize our Terraform files. I’ll keep everything inside an iac/terraform directory in the application repository.

From the root of the project, run:

mkdir -p iac/terraform/bootstrap
mkdir -p iac/terraform/environments/dev

Our Terraform directory will look like this:

iac/
└── terraform/
├── bootstrap/
└── environments/
└── dev/

We’ll use these two folders for different purposes:

  • bootstrap: To create the Azure Storage Account and container where we’ll store our Terraform state.
  • environments/dev: To define our Azure resources, including the virtual network, AKS cluster, container registry, and PostgreSQL.

You might be wondering why we need a separate folder for the Terraform state.

Terraform uses a state file to keep track of the resources it creates and manages. Instead of keeping this file locally, we’ll store it in Azure Blob Storage. This way, we can access the same state from our local machine and later from Azure DevOps Pipelines.

Before moving on, let’s update our .gitignore file to avoid committing Terraform state files and other local or potentially sensitive files to Git:

# Terraform working directories
**/.terraform/

# State files
*.tfstate
*.tfstate.*

# Local variable files and saved plans
*.tfvars
*.tfvars.json
*.tfplan

With our project structure ready, let’s configure the Azure provider and create the storage for our Terraform state.

Step 2: Configure the Azure Provider

Now, let’s configure Terraform to work with Azure using the AzureRM provider.

First, go to the bootstrap folder:

cd iac/terraform/bootstrap

Create a file named providers.tf:

terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "5.12.0"
}
}
}

provider "azurerm" {
features {}
}

This tells Terraform to use AzureRM version 5.x to manage our Azure resources.

Next, set your Azure subscription ID:

export ARM_SUBSCRIPTION_ID=$(az account show --query id --output tsv)

Terraform will use our existing Azure CLI login to authenticate.

Now, initialize Terraform to download the required provider:

terraform init

Finally, validate the configuration:

terraform validate

If everything is correct, you’ll see:

Success! The configuration is valid.

Our Azure provider is ready. Next, we’ll create the storage for our Terraform state.

Step 3: Create the Terraform State Storage

Now, let’s create an Azure Storage Account to store our Terraform state files.

We’ll need three resources: a resource group, a storage account, and a private container.

Inside iac/terraform/bootstrap, create a file named main.tf:

resource "azurerm_resource_group" "tfstate" {
name = "rg-ecomm-tfstate"
location = "France Central"
}

resource "azurerm_storage_account" "tfstate" {
name = "saecommtfstatev0"
resource_group_name = azurerm_resource_group.tfstate.name
location = azurerm_resource_group.tfstate.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
environment = "dev"
}
}
resource "azurerm_storage_container" "tfstate" {
name = "sc-ecomm-tfstate"
storage_account_id = azurerm_storage_account.tfstate.id
container_access_type = "private"
}

Before deploying, let’s make sure the Microsoft.Storage provider is registered:

az provider register --namespace Microsoft.Storage --wait

Next, let’s format and validate and preview our configuration:

terraform fmt
terraform validate
terraform plan

If everything looks good, let’s deploy:

terraform apply

Step 4: Configure Remote State

Now that our storage account is ready, let’s configure Terraform to store our development environment’s state in Azure.

First, navigate to the dev folder and copy our provider configuration:

cd ../environments/dev
cp ../../bootstrap/providers.tf .

Next, create a file named backend.tf:

terraform {
backend "azurerm" {
resource_group_name = "rg-ecomm-tfstate"
storage_account_name = "saecommtfstatev0"
container_name = "sc-ecomm-tfstate"
key = "dev.terraform.tfstate"
}
}

Make sure to use the same storage account name you created in the previous step.

This configuration tells Terraform where to store its state file. We’ll use our existing Azure CLI login to authenticate, so our account needs permission to access the storage account keys.

Now, initialize Terraform:

terraform init

If everything is configured correctly, you should see:

Successfully configured the backend "azurerm"!

That’s it! Our development environment will now store its Terraform state in Azure Blob Storage instead of locally.

The bootstrap state will remain local, so make sure to keep it safe.

Next, we’ll start provisioning our Azure infrastructure.

Step 5: Create the Network

Now, let’s set up our Azure network. We’ll create a Virtual Network (VNet) with two subnets: one for AKS and another for PostgreSQL.

First, inside iac/terraform/environments/dev, create a file named main.tf:

resource "azurerm_resource_group" "ecomm" {
name = "rg-ecomm-dev"
location = "France Central"
}

This creates a resource group to organize our Azure resources.

Get Abdessamad Abidar’s stories in your inbox

Join Medium for free to get updates from this writer.

Next, create network.tf:

resource "azurerm_virtual_network" "ecomm" {
name = "vnet-ecomm-dev"
location = azurerm_resource_group.ecomm.location
resource_group_name = azurerm_resource_group.ecomm.name
address_space = ["10.0.0.0/16"]
}

resource "azurerm_subnet" "aks" {
name = "snet-aks"
resource_group_name = azurerm_resource_group.ecomm.name
virtual_network_name = azurerm_virtual_network.ecomm.name
address_prefixes = ["10.0.1.0/24"]
}

resource "azurerm_subnet" "postgres" {
name = "snet-postgres"
resource_group_name = azurerm_resource_group.ecomm.name
virtual_network_name = azurerm_virtual_network.ecomm.name
address_prefixes = ["10.0.2.0/24"]
delegation {
name = "postgres"
service_delegation {
name = "Microsoft.DBforPostgreSQL/flexibleServers"
actions = [
"Microsoft.Network/virtualNetworks/subnets/join/action"
]
}
}
}

Here, we’re creating:

  • Virtual Network: Our main Azure network [10.0.0.0/16].
  • AKS subnet: The network for our Kubernetes cluster [10.0.1.0/24].
  • PostgreSQL subnet: A dedicated subnet for our database [10.0.2.0/24].

We’ve also added a subnet delegation, which is required for Azure PostgreSQL Flexible Server.

Before deploying, let’s make sure the Azure networking provider is registered:

az provider register --namespace Microsoft.Network --wait

Now, from our dev folder, let's validate and preview our configuration:

terraform fmt
terraform validate
terraform plan

If everything looks good, apply the changes:

terraform apply

Our network is ready! Next, we’ll create an Azure Container Registry (ACR) to store our Docker images.

Step 6: Create Azure Container Registry

Next, let’s create an Azure Container Registry (ACR) to store our Docker images.

Azure DevOps will push our images to ACR, and AKS will pull them when deploying the application.

Inside iac/terraform/environments/dev, create a file named acr.tf:

resource "azurerm_container_registry" "ecomm" {
name = "EcommContainerRegistry1"
resource_group_name = azurerm_resource_group.ecomm.name
location = azurerm_resource_group.ecomm.location
sku = "Basic"
admin_enabled = false
}

We’ll use the Basic tier, which is enough for our project.

Before deploying, register the Azure Container Registry provider:

az provider register --namespace Microsoft.ContainerRegistry --wait

Then apply the configuration!

Our container registry is ready. Next, let’s create our AKS cluster.

Step 7: Create the AKS cluster

AKS will run our microservices, frontend, Kafka, and Argo CD.

Inside iac/terraform/environments/dev, create aks.tf:

resource "azurerm_user_assigned_identity" "aks" {
location = azurerm_resource_group.ecomm.location
name = "id-aks-ecomm-dev"
resource_group_name = azurerm_resource_group.ecomm.name
}

resource "azurerm_role_assignment" "aks_network" {
scope = azurerm_subnet.aks.id
role_definition_name = "Network Contributor"
principal_id = azurerm_user_assigned_identity.aks.principal_id
}

resource "azurerm_kubernetes_cluster" "ecomm" {
name = "aks-ecomm-dev"
location = azurerm_resource_group.ecomm.location
resource_group_name = azurerm_resource_group.ecomm.name
dns_prefix = "aks-ecomm-dev"

default_node_pool {
name = "default"
node_count = 1
vm_size = "Standard_D4as_v7"
vnet_subnet_id = azurerm_subnet.aks.id
}

identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.aks.id]
}

network_profile {
network_plugin = "azure"
network_plugin_mode = "overlay"
pod_cidr = "10.244.0.0/16"
service_cidr = "10.1.0.0/16"
dns_service_ip = "10.1.0.10"
}

node_provisioning_profile {
default_node_pools = "Auto"
}

depends_on = [azurerm_role_assignment.aks_network]
}

resource "azurerm_role_assignment" "example" {
scope = azurerm_container_registry.ecomm.id
role_definition_name = "AcrPull"
principal_id = azurerm_kubernetes_cluster.ecomm.kubelet_identity[0].object_id
}

We’ll start with two nodes, each with 4 vCPUs and 16 GB of memory. The VM size must be available within your subscription’s regional quota.

The managed identity lets AKS use our subnet. The AcrPull role lets its nodes download images from ACR.

Register the required Azure resource providers:

az provider register --namespace Microsoft.ContainerService --wait
az provider register --namespace Microsoft.Compute --wait
az provider register --namespace Microsoft.ManagedIdentity --wait

Once the cluster is created, the node should show Ready.

Step 8: Create PostgreSQL

Next, let’s set up our PostgreSQL database using Azure Database for PostgreSQL Flexible Server.

We’ll create one PostgreSQL server with three databases for our product, order, and payment services. Our AKS cluster will connect to it through the private network.

Inside iac/terraform/environments/dev, create a file named postgres.tf:

variable "db_psswd" {
type = string
sensitive = true
}

resource "azurerm_private_dns_zone" "db" {
name = "ecomm.postgres.database.azure.com"
resource_group_name = azurerm_resource_group.ecomm.name
}

resource "azurerm_private_dns_zone_virtual_network_link" "db" {
name = "postgres-vnet-link"
private_dns_zone_id = azurerm_private_dns_zone.db.id
virtual_network_id = azurerm_virtual_network.ecomm.id
}

resource "azurerm_postgresql_flexible_server" "ecomm" {
name = "pg-ecomm-devv0"
resource_group_name = azurerm_resource_group.ecomm.name
location = azurerm_resource_group.ecomm.location
version = "16"
delegated_subnet_id = azurerm_subnet.pg.id
private_dns_zone_id = azurerm_private_dns_zone.db.id
public_network_access_enabled = false
administrator_login = "ecommadmin"
administrator_password = var.db_psswd

storage_mb = 32768

sku_name = "B_Standard_B1ms"
depends_on = [azurerm_private_dns_zone_virtual_network_link.db]

}

resource "azurerm_postgresql_flexible_server_database" "ecomm" {

for_each = toset([
"ecomm_product_db",
"ecomm_order_db",
"ecomm_payment_db"
])


name = each.value
server_id = azurerm_postgresql_flexible_server.ecomm.id
collation = "en_US.utf8"
charset = "UTF8"

}

We’re using a small Burstable instance with 32 GB of storage for this project. We’ve also disabled public access, so our database will only be accessible through the private network.

Before continuing, register the PostgreSQL resource provider:

az provider register --namespace Microsoft.DBforPostgreSQL --wait

Then apply the configuration!

Our PostgreSQL configuration is ready. Next, we’ll move on to the remaining infrastructure setup.

Press enter or click to view image in full size
Provisioned Azure Infrastructure

4. Set Up Azure DevOps

Step 9: Connect Azure DevOps to Azure

We’ll use Azure DevOps Pipelines to test our application, build Docker images, and push them to ACR.

Open your Azure DevOps project, or create a project named ecomm-microservices.

Go to Project settings → Service connections → New service connection:

  1. Select Azure Resource Manager.
  2. Choose App registration (automatic).
  3. Select Workload identity federation.
  4. Choose your Azure subscription and the resource group rg-ecomm-dev.
  5. Name the connection azure-ecomm-dev and save it.
Press enter or click to view image in full size

This connects the pipeline to Azure without storing a client secret. The automatic setup requires subscription Owner access and permission to create app registrations.

Next, we need to give our service connection permission to push Docker images to Azure Container Registry (ACR).

To do this, follow these steps:

  1. In Azure DevOps, open the azure-ecomm-dev service connection and click Manage App registration.
  2. Copy the application’s Display name or Client ID.
  3. Go to the Azure portal and open EcommContainerRegistry1 → Access control (IAM) → Add role assignment.
  4. Select the AcrPush role and click Next.
  5. Choose User, group, or service principal, then click Select members.
  6. Search for your application, select it, and click Select.
  7. Finally, click Review + assign.
Press enter or click to view image in full size

That’s it! Azure DevOps now has permission to push Docker images to our container registry.

Step 10: Create the First Pipeline

Now, let’s create our first Azure DevOps pipeline.

In Azure DevOps, follow these steps:

  1. Go to Pipelines → New pipeline.
  2. Select GitHub and authorize access to your repository.
  3. Select your application repository.
  4. Choose Starter pipeline.

Next, commit the generated pipeline (Just for test 😉). Azure DevOps will save it and start our first pipeline.

Press enter or click to view image in full size

Next, we’ll add the steps to test our application and build its Docker images.

Step 11: Run the backend tests

Before building Docker images, we’ll run the tests for our Spring Boot services.


trigger:
- master

pool:
vmImage: ubuntu-latest

steps:
- checkout: self

- task: JavaToolInstaller@0
displayName: Use Java 21
inputs:
versionSpec: '21'
jdkArchitectureOption: x64
jdkSourceOption: PreInstalled

- bash: |
set -e
for service in config-server discovery-service product-service \
order-service payment-service notification-serivce \
api-gateway
do
mvn -B -f "$service/pom.xml" clean verify
done
displayName: Build and test backend services

The pipeline builds and tests each service. If a command fails, set -e stops the script and the pipeline won’t continue to the next task.

Commit and push the changes to trigger the pipeline.

Step 12: Build and push the backend images

Once the tests pass, we’ll build the Docker images and push them to ACR.

- task: AzureCLI@2
displayName: Build and push application images
inputs:
azureSubscription: azure-ecomm-dev
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
set -e

ACR_NAME="EcommContainerRegistry1"
REGISTRY=$(az acr show --name "$ACR_NAME" \
--query loginServer --output tsv)

az acr login --name $ACR_NAME"

for service in config-server discovery-service product-service \
order-service payment-service notification-service \
api-gateway
do
IMAGE="$REGISTRY/$service/$Build.BuildId"

docker build -t "$IMAGE" "./$service"

docker push "$IMAGE"
done

IMAGE="$REGISTRY/frontend:$BUILD_BUILDID"

docker build -t "$IMAGE" ./frontend
docker push "$IMAGE"

Trigger the pipeline and, once it completes successfully, check that the images are available in ACR.

Press enter or click to view image in full size

Conclusion

That’s it for Part 1!

We’ve set up our Azure infrastructure using Terraform and created an Azure DevOps pipeline to test our Spring Boot services, build Docker images, and push them to Azure Container Registry (ACR).

In Part 2, we’ll use Ansible to install Argo CD and deploy our application to AKS using Helm and GitOps.

See you in Part 2!

--

--

Abdessamad Abidar
Abdessamad Abidar

Written by Abdessamad Abidar

Software Engineer | DevOps & Cloud Enthusiast