Automate Velero AKS Backups with Terraform and Ansible

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 Group
resource "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 account
resource "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" = <<EOF
AZURE_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=AzurePublicCloud
EOF
}
}
# Current subscription data
data "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

# Initialize
terraform init
# Preview changes
terraform plan -out=tfplan
# Apply
terraform apply tfplan
# Get kubeconfig
terraform output -raw kube_config > ~/.kube/config
# Verify Velero
kubectl get pods -n velero
velero 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 Settings
resource_group: "myResourceGroup"
location: "eastus"
aks_cluster_name: "myAKSCluster"
# Storage Settings
storage_account_name: "velerobackupstorage"
blob_container_name: "velero-backups"
# Velero Settings
velero_namespace: "velero"
velero_version: "v1.12.0"
velero_azure_plugin_version: "v1.8.0"
velero_chart_version: "5.0.0"
# Backup Settings
backup_schedule: "0 2 * * *"
backup_ttl: "720h"
backup_name: "daily-backup"
# Namespaces to exclude
excluded_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/v1
kind: Schedule
metadata:
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 collections
ansible-galaxy collection install azure.azcollection
ansible-galaxy collection install kubernetes.core
# Install Python dependencies
pip install ansible[azure] kubernetes
# Run the playbook
ansible-playbook site.yml -v
# Run specific role only
ansible-playbook site.yml --tags "velero" -v
# Dry run
ansible-playbook site.yml --check -v

Comparison: Terraform vs Ansible

FeatureTerraformAnsible
Best ForInfrastructure provisioningConfiguration & app deployment
State ManagementYes (tfstate file)No native state
IdempotencyBuilt-inTask-level
Azure ResourcesExcellentGood
Kubernetes ResourcesGoodExcellent
Learning CurveMediumLow
RollbackVia stateManual
Recommended UseCreate AKS + StorageInstall & 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.

Leave a comment