Automating Velero AKS Backup with Ansible & Terraform
Option 1: Terraform
Project Structure
velero-terraform/├── main.tf├── variables.tf├── outputs.tf├── providers.tf└── modules/ ├── storage/ │ ├── main.tf │ └── variables.tf ├── identity/ │ ├── main.tf │ └── variables.tf └── velero/ ├── main.tf └── variables.tf
providers.tf
terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 3.0" } helm = { source = "hashicorp/helm" version = "~> 2.0" } kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.0" } }}provider "azurerm" { features {}}provider "helm" { kubernetes { host = azurerm_kubernetes_cluster.aks.kube_config.0.host client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.client_certificate) client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.client_key) cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.cluster_ca_certificate) }}provider "kubernetes" { host = azurerm_kubernetes_cluster.aks.kube_config.0.host client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.client_certificate) client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.client_key) cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config.0.cluster_ca_certificate)}
variables.tf
variable "resource_group_name" { description = "Resource group name" type = string default = "myResourceGroup"}variable "location" { description = "Azure region" type = string default = "eastus"}variable "aks_cluster_name" { description = "AKS cluster name" type = string default = "myAKSCluster"}variable "storage_account_name" { description = "Storage account for Velero backups" type = string default = "velerobackupstorage"}variable "blob_container_name" { description = "Blob container for Velero backups" type = string default = "velero-backups"}variable "velero_namespace" { description = "Kubernetes namespace for Velero" type = string default = "velero"}variable "backup_retention_hours" { description = "Backup TTL in hours" type = number default = 720 # 30 days}variable "backup_schedule" { description = "Cron schedule for backups" type = string default = "0 2 * * *" # Daily at 2am}
main.tf
# Resource Groupresource "azurerm_resource_group" "rg" { name = var.resource_group_name location = var.location}# ── STORAGE ──────────────────────────────────────────────resource "azurerm_storage_account" "velero" { name = var.storage_account_name resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location account_tier = "Standard" account_replication_type = "LRS" blob_properties { delete_retention_policy { days = 30 } } tags = { purpose = "velero-backup" }}resource "azurerm_storage_container" "velero" { name = var.blob_container_name storage_account_name = azurerm_storage_account.velero.name container_access_type = "private"}# ── IDENTITY ─────────────────────────────────────────────resource "azurerm_user_assigned_identity" "velero" { name = "velero-identity" resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location}# Assign Contributor role to storage accountresource "azurerm_role_assignment" "velero_storage" { scope = azurerm_storage_account.velero.id role_definition_name = "Storage Blob Data Contributor" principal_id = azurerm_user_assigned_identity.velero.principal_id}# Assign Contributor role to resource group (for disk snapshots)resource "azurerm_role_assignment" "velero_rg" { scope = azurerm_resource_group.rg.id role_definition_name = "Contributor" principal_id = azurerm_user_assigned_identity.velero.principal_id}# ── AKS CLUSTER ──────────────────────────────────────────resource "azurerm_kubernetes_cluster" "aks" { name = var.aks_cluster_name location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name dns_prefix = var.aks_cluster_name default_node_pool { name = "default" node_count = 2 vm_size = "Standard_D2_v2" } identity { type = "SystemAssigned" } tags = { environment = "production" }}# ── VELERO NAMESPACE ─────────────────────────────────────resource "kubernetes_namespace" "velero" { metadata { name = var.velero_namespace } depends_on = [azurerm_kubernetes_cluster.aks]}# ── VELERO CREDENTIALS SECRET ────────────────────────────resource "kubernetes_secret" "velero_credentials" { metadata { name = "velero-credentials" namespace = kubernetes_namespace.velero.metadata[0].name } data = { "cloud" = <<EOFAZURE_SUBSCRIPTION_ID=${data.azurerm_subscription.current.subscription_id}AZURE_TENANT_ID=${data.azurerm_subscription.current.tenant_id}AZURE_CLIENT_ID=${azurerm_user_assigned_identity.velero.client_id}AZURE_RESOURCE_GROUP=${azurerm_resource_group.rg.name}AZURE_CLOUD_NAME=AzurePublicCloudEOF }}# Current subscription datadata "azurerm_subscription" "current" {}# ── VELERO HELM RELEASE ──────────────────────────────────resource "helm_release" "velero" { name = "velero" repository = "https://vmware-tanzu.github.io/helm-charts" chart = "velero" namespace = kubernetes_namespace.velero.metadata[0].name version = "5.0.0" values = [ yamlencode({ configuration = { provider = "azure" backupStorageLocation = { name = "default" provider = "velero.io/azure" bucket = azurerm_storage_container.velero.name config = { resourceGroup = azurerm_resource_group.rg.name storageAccount = azurerm_storage_account.velero.name subscriptionId = data.azurerm_subscription.current.subscription_id } } volumeSnapshotLocation = { name = "default" provider = "velero.io/azure" config = { resourceGroup = azurerm_resource_group.rg.name subscriptionId = data.azurerm_subscription.current.subscription_id } } } credentials = { existingSecret = kubernetes_secret.velero_credentials.metadata[0].name } initContainers = [ { name = "velero-plugin-for-azure" image = "velero/velero-plugin-for-microsoft-azure:v1.8.0" imagePullPolicy = "IfNotPresent" volumeMounts = [ { mountPath = "/target" name = "plugins" } ] } ] schedules = { daily-backup = { schedule = var.backup_schedule template = { ttl = "${var.backup_retention_hours}h0m0s" includeClusterResources = true excludedNamespaces = ["kube-system", "velero"] } } } }) ] depends_on = [ kubernetes_secret.velero_credentials, azurerm_role_assignment.velero_storage, azurerm_role_assignment.velero_rg ]}
outputs.tf
output "storage_account_name" { value = azurerm_storage_account.velero.name}output "blob_container_name" { value = azurerm_storage_container.velero.name}output "velero_identity_client_id" { value = azurerm_user_assigned_identity.velero.client_id}output "aks_cluster_name" { value = azurerm_kubernetes_cluster.aks.name}output "kube_config" { value = azurerm_kubernetes_cluster.aks.kube_config_raw sensitive = true}
Deploy with Terraform
# Initializeterraform init# Preview changesterraform plan -out=tfplan# Applyterraform apply tfplan# Get kubeconfigterraform output -raw kube_config > ~/.kube/config# Verify Velerokubectl get pods -n velerovelero backup-location get
Option 2: Ansible
Project Structure
velero-ansible/├── inventory/│ └── hosts.yml├── group_vars/│ └── all.yml├── roles/│ ├── azure_storage/│ │ └── tasks/│ │ └── main.yml│ ├── azure_identity/│ │ └── tasks/│ │ └── main.yml│ └── velero/│ ├── tasks/│ │ └── main.yml│ └── templates/│ ├── credentials.j2│ └── backup-schedule.yml.j2└── site.yml
group_vars/all.yml
# Azure Settingsresource_group: "myResourceGroup"location: "eastus"aks_cluster_name: "myAKSCluster"# Storage Settingsstorage_account_name: "velerobackupstorage"blob_container_name: "velero-backups"# Velero Settingsvelero_namespace: "velero"velero_version: "v1.12.0"velero_azure_plugin_version: "v1.8.0"velero_chart_version: "5.0.0"# Backup Settingsbackup_schedule: "0 2 * * *"backup_ttl: "720h"backup_name: "daily-backup"# Namespaces to excludeexcluded_namespaces: - kube-system - velero
roles/azure_storage/tasks/main.yml
---- name: Create Resource Group azure.azcollection.azure_rm_resourcegroup: name: "{{ resource_group }}" location: "{{ location }}" state: present- name: Create Storage Account azure.azcollection.azure_rm_storageaccount: resource_group: "{{ resource_group }}" name: "{{ storage_account_name }}" type: Standard_LRS kind: StorageV2 state: present register: storage_account_result- name: Create Blob Container azure.azcollection.azure_rm_storageblob: resource_group: "{{ resource_group }}" storage_account_name: "{{ storage_account_name }}" container: "{{ blob_container_name }}" state: present- name: Get Storage Account Keys azure.azcollection.azure_rm_storageaccount_info: resource_group: "{{ resource_group }}" name: "{{ storage_account_name }}" register: storage_info- name: Set Storage Key Fact set_fact: storage_account_key: "{{ storage_info.storageaccounts[0].primary_endpoints.key }}"
roles/azure_identity/tasks/main.yml
---- name: Get Azure Subscription Info azure.azcollection.azure_rm_subscription_info: register: subscription_info- name: Set Subscription Facts set_fact: subscription_id: "{{ subscription_info.subscriptions[0].subscription_id }}" tenant_id: "{{ subscription_info.subscriptions[0].tenant_id }}"- name: Create Service Principal for Velero azure.azcollection.azure_rm_adserviceprincipal: app_id: "velero-sp" state: present register: sp_result- name: Assign Contributor Role to Service Principal azure.azcollection.azure_rm_roleassignment: scope: "/subscriptions/{{ subscription_id }}/resourceGroups/{{ resource_group }}" assignee_object_id: "{{ sp_result.object_id }}" role_definition_name: Contributor state: present- name: Assign Storage Blob Contributor Role azure.azcollection.azure_rm_roleassignment: scope: "/subscriptions/{{ subscription_id }}/resourceGroups/{{ resource_group }}/providers/Microsoft.Storage/storageAccounts/{{ storage_account_name }}" assignee_object_id: "{{ sp_result.object_id }}" role_definition_name: "Storage Blob Data Contributor" state: present
roles/velero/templates/credentials.j2
AZURE_SUBSCRIPTION_ID={{ subscription_id }}AZURE_TENANT_ID={{ tenant_id }}AZURE_CLIENT_ID={{ client_id }}AZURE_CLIENT_SECRET={{ client_secret }}AZURE_RESOURCE_GROUP={{ resource_group }}AZURE_CLOUD_NAME=AzurePublicCloud
roles/velero/templates/backup-schedule.yml.j2
apiVersion: velero.io/v1kind: Schedulemetadata: name: {{ backup_name }} namespace: {{ velero_namespace }}spec: schedule: "{{ backup_schedule }}" template: ttl: "{{ backup_ttl }}" includeClusterResources: true excludedNamespaces:{% for ns in excluded_namespaces %} - {{ ns }}{% endfor %}
roles/velero/tasks/main.yml
---- name: Create Velero Namespace kubernetes.core.k8s: name: "{{ velero_namespace }}" api_version: v1 kind: Namespace state: present- name: Create Velero Credentials File template: src: credentials.j2 dest: /tmp/credentials-velero mode: '0600'- name: Create Kubernetes Secret for Velero Credentials kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: velero-credentials namespace: "{{ velero_namespace }}" stringData: cloud: | AZURE_SUBSCRIPTION_ID={{ subscription_id }} AZURE_TENANT_ID={{ tenant_id }} AZURE_CLIENT_ID={{ client_id }} AZURE_CLIENT_SECRET={{ client_secret }} AZURE_RESOURCE_GROUP={{ resource_group }} AZURE_CLOUD_NAME=AzurePublicCloud- name: Add Velero Helm Repository kubernetes.core.helm_repository: name: vmware-tanzu repo_url: "https://vmware-tanzu.github.io/helm-charts" state: present- name: Install Velero via Helm kubernetes.core.helm: name: velero chart_ref: vmware-tanzu/velero chart_version: "{{ velero_chart_version }}" namespace: "{{ velero_namespace }}" state: present values: configuration: provider: azure backupStorageLocation: name: default provider: velero.io/azure bucket: "{{ blob_container_name }}" config: resourceGroup: "{{ resource_group }}" storageAccount: "{{ storage_account_name }}" subscriptionId: "{{ subscription_id }}" volumeSnapshotLocation: name: default provider: velero.io/azure config: resourceGroup: "{{ resource_group }}" subscriptionId: "{{ subscription_id }}" credentials: existingSecret: velero-credentials initContainers: - name: velero-plugin-for-azure image: "velero/velero-plugin-for-microsoft-azure:{{ velero_azure_plugin_version }}" imagePullPolicy: IfNotPresent volumeMounts: - mountPath: /target name: plugins- name: Wait for Velero Pod to be Ready kubernetes.core.k8s_info: kind: Pod namespace: "{{ velero_namespace }}" label_selectors: - app.kubernetes.io/name=velero register: velero_pod until: velero_pod.resources[0].status.phase == "Running" retries: 10 delay: 15- name: Apply Backup Schedule kubernetes.core.k8s: state: present template: backup-schedule.yml.j2- name: Verify Backup Location command: velero backup-location get register: backup_location_status changed_when: false- name: Display Backup Location Status debug: msg: "{{ backup_location_status.stdout }}"- name: Clean Up Credentials File file: path: /tmp/credentials-velero state: absent
site.yml (Main Playbook)
---- name: Setup Velero Backup for AKS hosts: localhost connection: local gather_facts: false pre_tasks: - name: Verify required tools are installed command: "{{ item }} --version" loop: - az - kubectl - helm register: tool_check changed_when: false - name: Verify AKS context command: kubectl cluster-info register: cluster_info changed_when: false - name: Display cluster info debug: msg: "{{ cluster_info.stdout_lines[0] }}" roles: - azure_storage - azure_identity - velero post_tasks: - name: Trigger initial manual backup command: > velero backup create initial-backup --include-cluster-resources=true --wait register: initial_backup changed_when: true - name: Display backup result debug: msg: "{{ initial_backup.stdout }}"
Run the Ansible Playbook
# Install required collectionsansible-galaxy collection install azure.azcollectionansible-galaxy collection install kubernetes.core# Install Python dependenciespip install ansible[azure] kubernetes# Run the playbookansible-playbook site.yml -v# Run specific role onlyansible-playbook site.yml --tags "velero" -v# Dry runansible-playbook site.yml --check -v
Comparison: Terraform vs Ansible
| Feature | Terraform | Ansible |
|---|---|---|
| Best For | Infrastructure provisioning | Configuration & app deployment |
| State Management | Yes (tfstate file) | No native state |
| Idempotency | Built-in | Task-level |
| Azure Resources | Excellent | Good |
| Kubernetes Resources | Good | Excellent |
| Learning Curve | Medium | Low |
| Rollback | Via state | Manual |
| Recommended Use | Create AKS + Storage | Install & configure Velero |
Best Practice: Combine Both
Terraform → Creates Azure infrastructure (AKS, Storage, Identity) ↓Ansible → Installs and configures Velero on the cluster ↓Velero → Runs scheduled backups automatically
This gives you the best of both worlds — Terraform for infrastructure and Ansible for application configuration.