You are currently viewing Building and deploying a Cloud-Native E-Commerce Platform on Azure Kubernetes Service (AKS): Provisioning Infrastructure with Terraform and Automating Deployments with Jenkins CI/CD

Building and deploying a Cloud-Native E-Commerce Platform on Azure Kubernetes Service (AKS): Provisioning Infrastructure with Terraform and Automating Deployments with Jenkins CI/CD

Introduction

In today’s fast-paced world of software development, building scalable, reliable, and automated infrastructure is very crucial. This blog summarizes creating a multi-cloud, fully automated e-commerce platform from Infrastructure setup to deployment. By leveraging Terraform, I provisioned essential infrastructure components across Google Cloud Platform (GCP) and Microsoft Azure. The sample application itself is an e-commerce solution, featuring services for authentication, payments, product management, and order processing.

To ensure seamless deployments, I integrated Jenkins for running CI/CD pipeline, enabling continuous integration and delivery of the application across environments. This project showcases a robust workflow combining the power of Infrastructure as Code (IaC), containerized microservices, CI/CD and cloud automation.

Let’s dive into the technical details.

This will be broken down into three parts:

  • Infrastructure provisioning using Terraform
  • Application deployment to Azure Kubernetes Service (AKS)
  • Automated deployment using Jenkins CI/CD

Prerequisites

1. Azure CLI installed

2. kubectl installed

3. Docker installed

4. Node.js 18+ installed

5. Azure subscription

6. Google Cloud subscription

7. Google Cloud CLI

8. Terraform

Infrastructure

  • Azure Kubernetes Service (AKS)
  • Azure Container Registry (ACR)
  • Azure Key Vault
  • Azure Application Gateway
  • Azure DNS
  • Google Cloud VM Instance
  • Jenkins
  • Other components provisioned and configured which will be later used in our App but not used in this specific project: Microsoft Azure PostgreSQL – Flexible Server, Azure Cache for Redis

Local Development Setup

# Clone the repository

git clone <GitHub URL>
cd ecomm-app

# Install dependencies for all services

npm install

# Set up environment variables

cp .env.example .env

# Start all services locally – from the root folder

npm run dev

Step 1: Infrastructure setup using Terraform

Jenkins Server on GCP VM Instances

We will setup Jenkins server on Google Cloud (Ok, you may be wondering why not use Azure platform, since other components will be there, well, I wanted to leverage on the Google Cloud free credits, and I intend to run the Jenkins server for a little bit longer).

Authenticate to Google Cloud, there are a couple of methods, the common ones are:

  1. Authenticate using personal account
gcloud auth login
gcloud config set project PROJECT_ID

2. Authenticate to Google cloud using ADC

gcloud auth application-default login

Below is the terraform script – main.tf, you can download the rest from here.

# Create a Google Cloud Network
resource "google_compute_network" "jenkins_nkw" {
  name                    = "jenkins-nkw"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "jenkins_subnet" {
  name          = "jenkins-subnet"
  ip_cidr_range = "10.0.0.0/24"
  network       = google_compute_network.jenkins_nkw.self_link
  region        = var.region
}

# Create a Firewall Rule to allow Jenkins port
resource "google_compute_firewall" "jenkins_firewall" {
  name    = "jenkins-firewall"
  network = google_compute_network.jenkins_nkw.id
  allow {
    protocol = "tcp"
    ports    = ["22", "8080"]
  }

  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["jenkins"]
}

# Create a Google Compute Instance
resource "google_compute_instance" "jenkins_server" {
  name         = "jenkins-server"
  machine_type = var.machine_type
  tags         = ["jenkins"]
  boot_disk {
    initialize_params {
      image = "ubuntu-os-cloud/ubuntu-2004-lts"
      size  = 50
    }
  }

  network_interface {
    subnetwork = google_compute_subnetwork.jenkins_subnet.self_link
    access_config {
      // Ephemeral public IP
    }
  }

  metadata_startup_script = file("${path.module}/scripts/install_jenkins.sh")
  service_account {
    email  = google_service_account.jenkins_sa.email
    scopes = ["cloud-platform"]
  }
}

resource "google_service_account" "jenkins_sa" {
  account_id   = "jenkins-service-account"
  display_name = "Jenkins Service Account"
}

resource "google_project_iam_member" "jenkins_storage_admin" {
  project = var.project_id
  role    = "roles/storage.admin"
  member  = "serviceAccount:${google_service_account.jenkins_sa.email}"
}

}

First, create bucket on google cloud to store terraform state

gsutil mb -p PROJECT_ID gs://BUCKET_NAME

Run the below commands to provision the server:

Initialize and apply Terraform:

terraform init
terraform plan -out=tfplan
terraform apply

Outputs:

jenkins_public_ip = “<public-ip-jenkins>”
jenkins_url = “http://<public-ip-jenkins>:8080”

  • Retrieve the password
  • Install suggested Plugin.

Setup AKS Cluster, Application Gateway, Redis, PostgreSQL Infra on Azure

Provision the AKS cluster using terraform. You can view and download the terraform scripts from Here:

Authenticate to Azure

Login to Azure cli:

az login

To explicitly login with Service Principal:

az login --service-principal \
    --username APP_ID \
    --password PASSWORD \
    --tenant TENANT_ID

First, create the storage account for Terraform state:

az group create --name terraform-state-rg --location uksouth
az storage account create --name tfstatedev24 --resource-group terraform-state-rg --location eastus --sku Standard_LRS
az storage container create --name tfstate --account-name tfstatedev24

Run the terraform scripts by navigating to folder environment/dev, to download all the scripts, visit the GitHub link, clone or download to your machine.

Here is a sample config from one of the modules:

# modules/aks/main.tf
resource "azurerm_kubernetes_cluster" "aks" {
  name                = "${var.prefix}-aks"
  location            = var.location
  resource_group_name = var.resource_group_name
  dns_prefix          = "${var.prefix}-aks"
  kubernetes_version  = var.kubernetes_version

  default_node_pool {
    name                = "system"
    node_count          = var.system_node_count
    vm_size             = var.system_node_vm_size
    vnet_subnet_id      = var.subnet_id
    enable_auto_scaling = true
    min_count           = var.system_node_min_count
    max_count           = var.system_node_max_count
  }

  identity {
    type = "SystemAssigned"
  }

  key_vault_secrets_provider {
    secret_rotation_enabled  = var.enable_secret_rotation
    #secret_rotation_interval = "2m"    # Optional: Configure rotation interval
  }

  # Enable OIDC and Workload Identity
  oidc_issuer_enabled       = true
  workload_identity_enabled = true

  network_profile {
    network_plugin    = "azure"
    network_policy    = "calico"
    load_balancer_sku = "standard"

    service_cidr       = "172.16.0.0/16"
    dns_service_ip     = "172.16.0.10"
  }

  oms_agent {
    log_analytics_workspace_id = var.log_analytics_workspace_id
  }

  tags = var.tags
}

# On-demand node pool for critical workloads
resource "azurerm_kubernetes_cluster_node_pool" "ondemand" {
  name                  = "ondemand"
  kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id
  vm_size               = var.ondemand_node_vm_size
  vnet_subnet_id        = var.subnet_id
  priority              = "Regular"

  enable_auto_scaling  = true
  min_count = var.ondemand_node_min_count
  max_count = var.ondemand_node_max_count

  node_labels = {
    "nodepool-type" = "ondemand"
    "environment"   = var.environment
  }

  node_taints = []

  tags = var.tags
}

# Spot instance node pool for cost optimization
resource "azurerm_kubernetes_cluster_node_pool" "spot" {
  name                  = "spot"
  kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id
  vm_size               = var.spot_node_vm_size
  vnet_subnet_id        = var.subnet_id
  priority              = "Spot"
  eviction_policy       = "Delete"
  spot_max_price        = var.spot_price_max

  enable_auto_scaling  = true
  min_count = var.spot_node_min_count
  max_count = var.spot_node_max_count

  node_labels = {
    "nodepool-type" = "spot"
    "environment"   = var.environment
  }

  node_taints = [
    "kubernetes.azure.com/scalesetpriority=spot:NoSchedule"
  ]

  tags = var.tags
}


# Role assignment for Key Vault access
resource "azurerm_role_assignment" "aks_keyvault" {
  scope                = var.key_vault_id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_kubernetes_cluster.aks.key_vault_secrets_provider[0].secret_identity[0].object_id

  depends_on = [
    azurerm_kubernetes_cluster.aks
  ]
}

# Additional role assignment for CSI Driver
resource "azurerm_role_assignment" "aks_csi_keyvault" {
  scope                = var.key_vault_id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_kubernetes_cluster.aks.kubelet_identity[0].object_id

  depends_on = [
    azurerm_kubernetes_cluster.aks
  ]
}

Other modules present for this project are: Network, acr, app-gw, keyvault, monitoring, redis, postgreSQL.

Run the below commands to provision all the required resources:

cd environments/dev

terraform init
terraform plan -out=tfplan
terraform apply (Or run: terraform apply -auto-approve) 

Step 2: Application deployment to Azure Kubernetes Service (AKS)

Our application has a frontend and backend services which we are going to deploy to AKS:

Note that in this implementation, I have used two namespaces – ecomm-frontend and ecomm-backend.

Further enhancement could involve using separate namespaces for each service in production environment (Auth, catalog, orders and payment). This makes it modular, easily managed, secure and scalable.


### Microservices

1. Auth Service

2. Catalog Service

3. Payment Service

4. Orders Service

5. Frontend

You can view and download all the manifest files from Here:

Before applying our Kubernetes configurations, we need build our images and push them to ACR.

Build and Push Docker Images

Login to ACR

# Login to ACR
az acr login --name ecomdevacr

You can go into the folder directly to build each image:

Build the image

docker build -t ecomdevacr.azurecr.io/frontend:v1 .

Push to ACR

# Push to ACR
docker push ecomdevacr.azurecr.io/frontend:v1

For Services:

cd services/auth
docker build -t ecomdevacr.azurecr.io/auth:v1 .
docker push ecomdevacr.azurecr.io/auth:v1

Do same for other service

Or build while pointing to the folder, for example:

docker build -t ecommacr.azurecr.io/frontend:v1 ./frontend
docker build -t ecomm.azurecr.io/orders-service:v1 ./services/orders

Keyvault

Go to Azure Portal to add your env variables in keyvault or you could use CLI to add it from your local machine. Ensure you are authenticated to Azure before addition.

az keyvault secret set --vault-name "ecom-dev-kv216" \
   --name "STRIPE-SECRET-KEY" \
    --value "<STRIPE_SECRET_KEY>"

az keyvault secret set \
  --vault-name ecom-dev-kv216 \
  --name "REDIS-CONNECTION" \
  --value "your-connection-string"

Add ALL the required envs.

Get AKS Credentials

Let us get AKS credentials which retrieves the kubeconfig file, that is, retrieves access credentials for the Kubernetes API server of the specified AKS cluster and allows you to interact with the AKS cluster using Kubectl.

az aks get-credentials \
      --resource-group ${RESOURCE_GROUP} \
      --name ${AKS_CLUSTER} \
      --overwrite-existing

Apply K8 Configs

# Create namespace

kubectl apply -f k8s/namespace.yaml

# Apply individual configs

kubectl apply -f k8s/ServiceAccount.yaml
kubectl apply -f k8s/auth-service.yaml

kubectl apply -f k8s/ingress.yaml

# Apply all configs at once

Or to apply all at once

kubectl apply -f .

or the directory
kubectl apply -f K8/

Check deployment status

kubectl get deployments -n ecomm-backend

Check pods

kubectl get pods -n ecomm-backend

Check services

kubectl get services -n ecomm-backend

Check ingress

kubectl get ingress -n ecomm-backend

Describe a deployment

kubectl describe deployment auth-service -n ecomm-backend

Additional step to use KeyVault with our cluster

I performed this additional setup to connect AKS with Azure Keyvault to download environment variables from Keyvault into our cluster which will be used by our application.

If you check the terraform scripts for AKS cluster, you will notice that we have enabled CSI driver add-on.

key_vault_secrets_provider {
    secret_rotation_enabled  = var.enable_secret_rotation
    #secret_rotation_interval = "2m"    # Optional: Configure rotation interval
  }

  # Enable OIDC and Workload Identity
  oidc_issuer_enabled       = true
  workload_identity_enabled = true

 

If you are using this method, you also have to configure ServiceAccount and SecretProviderClass which will map our objects pointing to the keyvault, keyvault name and IDs to enable the communication. Please check the K8 manifest files for the config – you would have applied them in the previous step.

Configure workload identity

az identity federated-credential create --name $FEDERATED_IDENTITY_NAME --identity-name $UAMI --resource-group $RESOURCE_GROUP --issuer ${AKS_OIDC_ISSUER} --subject system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}

Ensure to export your variables before running the above commands, for example:

export AKS_OIDC_ISSUER="$(az aks show --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --query "oidcIssuerProfile.issuerUrl" -o tsv)"
export SERVICE_ACCOUNT_NAME="workload-identity-sa" or "workload-identity-sa-fe"
export SERVICE_ACCOUNT_NAMESPACE="ecomm-backend" or "ecomm-frontend"
export RESOURCE_GROUP=<Your-resource-group>
export UAMI=azurekeyvaultsecretsprovider-ecom-dev-aks
export FEDERATED_IDENTITY_NAME="aksfederatedidentity"

Check if the CSI driver addon is enabled with managed identity:

az aks show -g hub-ecommdev-rg -n ecom-dev-aks --query "addonProfiles.azureKeyvaultSecretsProvider.identity"

To confirm that your csi driver-secret store provider is enabled and running:

kubectl get pods -n kube-system -l 'app in (secrets-store-csi-driver,secrets-store-provider-azure)'

NAME                                     READY   STATUS    RESTARTS   AGE
aks-secrets-store-csi-driver-wrwkw       3/3     Running   0          10h
aks-secrets-store-csi-driver-zr2gq       3/3     Running   0          10h
aks-secrets-store-provider-azure-5vdmc   1/1     Running   0          10h
aks-secrets-store-provider-azure-b8dvf   1/1     Running   0          10h

CONFIGURE DNS

If you are using Azure DNS, this is a brief summary of how you can set it up.
Retrieve your App GW Public IP, or visit Azure Portal to get it.

Create DNS Zone

az network dns zone create --resource-group hub-ecommdev-rg --name example.co.uk

Verify creation

az network dns zone show --resource-group hub-ecommdev-rg --name example.co.uk

Create DNS records using A-records

az network dns record-set a add-record --resource-group hub-ecommdev-rg \
    --zone-name "example.co.uk" \
    --record-set-name "@" \
    --ipv4-address x.x.x.104

Also create similar record for your api. subdomain.

Create self-signed certificate or import your SSL certificate (Note that you can use other methods)

az network application-gateway ssl-cert create \
    --gateway-name "ecom-dev-appgw" \
    --name "ecomm-cert" \
    --resource-group hub-ecommdev-rg \
    --cert-file "../cert.pfx" \
    --cert-password "<your-password>"

Go to the site of your domain name host provider and add Azure nameservers in your DNS settings.

After the completion of the setup and there are no errors, all the pods should be running like below output.

Checks

$ kubectl get pods -n ecomm-backend
NAME                               READY   STATUS             RESTARTS      AGE
auth-service-69f4dc4695-2tt8r      1/1     Running            0             34m
auth-service-69f4dc4695-z8m56      1/1     Running            0             34m
catalog-service-6c66bbd588-gwlgw   1/1     Running            0             20m
catalog-service-6c66bbd588-xmz92   1/1     Running            0             20m
orders-service-8c5f8656c-2k7vb     1/1     Running            0             4h11m
orders-service-8c5f8656c-kpbmk     1/1     Running            0             4h11m
payment-service-76f49d4d9-fgkms    1/1     Running            0             4m30s
payment-service-76f49d4d9-lp9mb    1/1     Running            0             4m30s

$ kubectl get pods -n ecomm-frontend
NAME                        READY   STATUS    RESTARTS         AGE
frontend-744c4474c5-9lx6r   1/1     Running   0                130m
frontend-744c4474c5-g4v4t   1/1     Running   0                130m

$ kubectl get ingress -n ecomm-frontend
NAME               CLASS    HOSTS              ADDRESS   PORTS   AGE
frontend-ingress   <none>   <domain-name>   <app-gw-pip>          80      150m

Troubleshooting tips

kubectl logs <pod-name> -n ecomm-backend
kubectl get pods -n ecomm-backend
kubectl logs orders-service-b578c578f-5pj68 -n ecomm-backend --previous
kubectl describe pods orders-service-b578c578f-mkrjs -n ecomm-backend

Cluster-wide resource usage

kubectl top nodes
kubectl top pods --all-namespaces

Check ingress status

kubectl get ingress -A

Check AGIC pods

kubectl get pods -n kube-system -l app=ingress-appgw

Step 3: Automated deployment using Jenkins CI/CD

Earlier we provisioned Jenkins server on Google cloud VM instance, logged in and installed suggested plugins.

Next, we will install additional plugins and configure some tools.

Plugins

  • Azure credentials
  • Azure cli
  • Pipeline stage view
  • NodeJS
  • Docker pipeline
  • parameterized trigger
  • Image tag parameter

Configure Tools

In Dashboard –> Manage Jenkins –> Tools: Configure git, NodeJS, JDK (JAVA_HOME)

In Dashboard –> Manage Jenkins –> Credentials: Add all the required credentials to enable authentication and to hide our secrets – envs.

Add the following:

  • AKS Credentials: (Kind: Azure Service Principal)
  • Azure Container Registry Credentials (Kind: Username with password – SP ID/Secret)
  • Registry name (Kind: secret)
  • Resource group (Kind: secret)
  • CSI_CLIENT_ID (Kind: secret)
  • TENANT_ID (Kind: secret)
  • KeyVault name (Kind: secret)
  • AKS_CLUSTER (Kind: secret)

Note: I had to install additional packages (Azure cli, git, terraform, npm) on Jenkins server to support our build, you can include them as part of the installation steps in the start-up script – install_jenkins.sh.

Lets automate the provisioning and deployment process using Jenkins CI/CD solutions

We are going for Pipeline for the sake of practice otherwise you can configure Jenkins webhook in your GitHub to automatically trigger pipeline build when changes are made to the codes/scripts in the repo.

Infrastructure pipeline setup

Go to Dashboards –> New Item –> Enter an item name –> Pipeline –> Ok.

Go to Pipeline –> select “Pipeline script” and paste in content of the Jenkinsfile –> Save.
Now you can manually start the build. As it is a parameterized build, you choose apply to run the scripts and provision resources or choose destroy to delete all resources.

pipeline {
    agent any
    
    parameters {
        choice(name: 'ACTION', choices: ['apply', 'destroy'], description: 'Select Terraform action')
    }
    
    environment {
        TERRAFORM_DIR = 'Infrastructure-app-terraform/environments/dev'
        AZURE = credentials('aks-cred')
    }
    
    stages {
        stage('Checkout Infrastructure') {
            steps {
                checkout([$class: 'GitSCM',
                    branches: [[name: '*/master']],
                    userRemoteConfigs: [[
                        url: 'https://github.com/<user-name>/ecomm-hub-infra.git',
                        credentialsId: 'github'
                    ]]
                ])
            }
        }
        
        stage('Terraform Operations') {
            steps {
                withCredentials([azureServicePrincipal('aks-cred')]) {
                    dir(TERRAFORM_DIR) {
                        script {
                            // Set Azure credentials for all Terraform operations
                            def azureEnv = """
                                export ARM_CLIENT_ID=\${AZURE_CLIENT_ID}
                                export ARM_CLIENT_SECRET=\${AZURE_CLIENT_SECRET}
                                export ARM_SUBSCRIPTION_ID=\${AZURE_SUBSCRIPTION_ID}
                                export ARM_TENANT_ID=\${AZURE_TENANT_ID}
                            """
                            
                            // Terraform Init
                            stage('Terraform Init') {
                                sh """
                                    ${azureEnv}
                                    terraform init
                                """
                            }
                            
                            if (params.ACTION == 'destroy') {
                                // Terraform Destroy
                                stage('Terraform Destroy') {
                                    sh """
                                        ${azureEnv}
                                        terraform plan -destroy -out=tfplan
                                        terraform apply -auto-approve tfplan
                                    """
                                }
                            } else {
                                // Terraform Plan
                                stage('Terraform Plan') {
                                    sh """
                                        ${azureEnv}
                                        terraform plan -out=tfplan
                                    """
                                }
                                
                                // Terraform Apply
                                stage('Terraform Apply') {
                                    //sh """
                                        //${azureEnv}
                                        //terraform apply -auto-approve tfplan
                                    //"""
                                    sh "ls -lrth"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    post {
        always {
            sh '''
                # Logout from Azure
                #az logout
                
                # Clear Azure CLI cache
                rm -rf ~/.azure
            '''
            cleanWs()
        }
        success {
            echo 'Infrastructure deployment successful!'
        }
        failure {
            echo 'Infrastructure deployment failed!'
        }
    }
}

Application pipeline setup

Go to Dashboards > New Item > Enter an item name > Pipeline > Ok

Go to Pipeline –> select Pipeline script and paste in content of the Jenkinsfile –> Save.

Now, manually start the build.

Our pipeline finally got completed after dealing with some Jenkins errors.

Conclusion

In this blog, we worked on building and deploying a cloud-native e-commerce platform using Azure Kubernetes Service (AKS). By leveraging Terraform for infrastructure provisioning and Jenkins CI/CD for deployment automation, we successfully demonstrated the power of combining modern DevOps practices with scalable cloud solutions.

We have explored how to streamline the deployment of a microservices-based e-commerce application together with integrated CI/CD pipelines and robust infrastructure as code, this project highlights how to achieve operational efficiency, scalability, and maintainability in cloud-native application development.

Leave a Reply