Files
DevOps-Security-Agent-Skills/infrastructure/cloud-azure/terraform-azure/SKILL.md
T

612 lines
16 KiB
Markdown
Raw Normal View History

2026-01-27 17:35:45 -05:00
---
name: terraform-azure
description: Provision Azure infrastructure with Terraform. Configure providers, manage state, and deploy resources. Use when implementing IaC for Azure.
license: MIT
metadata:
author: devops-skills
version: "1.0"
---
# Terraform Azure
2026-03-24 18:02:50 -04:00
Provision and manage Azure infrastructure with Terraform using the AzureRM provider. Covers provider configuration, remote state, resource groups, VNets, AKS, Key Vault, complete .tf file examples, and production workflows.
## When to Use
- You need multi-cloud or cloud-agnostic Infrastructure as Code.
- Your team standardizes on Terraform across AWS, Azure, and GCP.
- You need plan/apply workflows with change preview before deployment.
- You want modular, reusable infrastructure components.
- You need state locking and drift detection for production infrastructure.
## Prerequisites
```bash
# Install Terraform
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# Verify installation
terraform version
# Install Azure CLI and login
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
az login
az account set --subscription "my-subscription-id"
# Create storage account for remote state
az group create --name tfstate-rg --location eastus
az storage account create \
--name tfstate$(openssl rand -hex 4) \
--resource-group tfstate-rg \
--sku Standard_LRS \
--encryption-services blob
az storage container create \
--name tfstate \
--account-name tfstateXXXXXXXX
```
2026-01-27 17:35:45 -05:00
## Provider Configuration
2026-03-24 18:02:50 -04:00
### providers.tf
2026-01-27 17:35:45 -05:00
```hcl
terraform {
2026-03-24 18:02:50 -04:00
required_version = ">= 1.5.0"
2026-01-27 17:35:45 -05:00
required_providers {
azurerm = {
source = "hashicorp/azurerm"
2026-03-24 18:02:50 -04:00
version = "~> 3.80"
}
azuread = {
source = "hashicorp/azuread"
version = "~> 2.47"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
2026-01-27 17:35:45 -05:00
}
}
2026-03-24 18:02:50 -04:00
2026-01-27 17:35:45 -05:00
backend "azurerm" {
2026-03-24 18:02:50 -04:00
resource_group_name = "tfstate-rg"
storage_account_name = "tfstate12345abc"
2026-01-27 17:35:45 -05:00
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}
provider "azurerm" {
2026-03-24 18:02:50 -04:00
features {
key_vault {
purge_soft_delete_on_destroy = false
recover_soft_deleted_key_vaults = true
}
resource_group {
prevent_deletion_if_contains_resources = true
}
}
# Optional: use a specific subscription
# subscription_id = var.subscription_id
}
provider "azuread" {}
```
### variables.tf
```hcl
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "location" {
description = "Azure region for all resources"
type = string
default = "eastus"
}
variable "project_name" {
description = "Project name used in resource naming"
type = string
default = "myapp"
}
variable "tags" {
description = "Tags applied to all resources"
type = map(string)
default = {}
}
variable "sql_admin_password" {
description = "SQL Server admin password"
type = string
sensitive = true
}
variable "aks_admin_group_id" {
description = "Azure AD group ID for AKS admin access"
type = string
}
locals {
name_prefix = "${var.project_name}-${var.environment}"
common_tags = merge(var.tags, {
environment = var.environment
project = var.project_name
managed_by = "terraform"
})
2026-01-27 17:35:45 -05:00
}
```
2026-03-24 18:02:50 -04:00
### terraform.tfvars (per environment)
```hcl
# terraform.prod.tfvars
environment = "prod"
location = "eastus"
project_name = "myapp"
aks_admin_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
tags = {
cost_center = "engineering"
owner = "platform-team"
}
```
## Resource Group
### resource-group.tf
2026-01-27 17:35:45 -05:00
```hcl
resource "azurerm_resource_group" "main" {
2026-03-24 18:02:50 -04:00
name = "${local.name_prefix}-rg"
location = var.location
tags = local.common_tags
2026-01-27 17:35:45 -05:00
}
```
2026-03-24 18:02:50 -04:00
## Virtual Network
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
### network.tf
```hcl
resource "azurerm_virtual_network" "main" {
name = "${local.name_prefix}-vnet"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
address_space = ["10.0.0.0/16"]
tags = local.common_tags
}
resource "azurerm_subnet" "aks" {
name = "aks-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.1.0/22"]
}
resource "azurerm_subnet" "app" {
name = "app-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.8.0/24"]
}
resource "azurerm_subnet" "data" {
name = "data-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.9.0/24"]
private_endpoint_network_policies_enabled = true
}
resource "azurerm_network_security_group" "app" {
name = "${local.name_prefix}-app-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "AllowHTTPS"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = local.common_tags
}
resource "azurerm_subnet_network_security_group_association" "app" {
subnet_id = azurerm_subnet.app.id
network_security_group_id = azurerm_network_security_group.app.id
}
```
## AKS Cluster
### aks.tf
```hcl
resource "azurerm_log_analytics_workspace" "aks" {
name = "${local.name_prefix}-law"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
sku = "PerGB2018"
retention_in_days = 30
tags = local.common_tags
}
resource "azurerm_kubernetes_cluster" "main" {
name = "${local.name_prefix}-aks"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
dns_prefix = "${var.project_name}-${var.environment}"
kubernetes_version = "1.28"
default_node_pool {
name = "system"
vm_size = "Standard_D4s_v5"
enable_auto_scaling = true
min_count = 2
max_count = 5
zones = [1, 2, 3]
vnet_subnet_id = azurerm_subnet.aks.id
os_disk_size_gb = 128
os_disk_type = "Managed"
max_pods = 50
node_labels = {
role = "system"
}
}
identity {
type = "SystemAssigned"
}
network_profile {
network_plugin = "azure"
network_policy = "calico"
service_cidr = "10.1.0.0/16"
dns_service_ip = "10.1.0.10"
load_balancer_sku = "standard"
}
azure_active_directory_role_based_access_control {
managed = true
azure_rbac_enabled = true
admin_group_object_ids = [var.aks_admin_group_id]
}
oms_agent {
log_analytics_workspace_id = azurerm_log_analytics_workspace.aks.id
}
key_vault_secrets_provider {
secret_rotation_enabled = true
secret_rotation_interval = "2m"
}
tags = local.common_tags
}
resource "azurerm_kubernetes_cluster_node_pool" "app" {
name = "app"
kubernetes_cluster_id = azurerm_kubernetes_cluster.main.id
vm_size = "Standard_D8s_v5"
enable_auto_scaling = true
min_count = 2
max_count = 20
zones = [1, 2, 3]
vnet_subnet_id = azurerm_subnet.aks.id
max_pods = 50
node_labels = {
workload = "app"
}
node_taints = [
"dedicated=app:NoSchedule"
]
tags = local.common_tags
}
```
## Key Vault
### keyvault.tf
```hcl
data "azurerm_client_config" "current" {}
resource "azurerm_key_vault" "main" {
name = "${var.project_name}-${var.environment}-kv"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
soft_delete_retention_days = 90
purge_protection_enabled = true
enabled_for_disk_encryption = true
network_acls {
default_action = "Deny"
bypass = "AzureServices"
ip_rules = var.allowed_ip_ranges
virtual_network_subnet_ids = [
azurerm_subnet.app.id,
azurerm_subnet.aks.id,
]
}
tags = local.common_tags
}
resource "azurerm_key_vault_access_policy" "terraform" {
key_vault_id = azurerm_key_vault.main.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
secret_permissions = [
"Get", "List", "Set", "Delete", "Purge", "Recover"
]
key_permissions = [
"Get", "List", "Create", "Delete", "Purge", "Recover",
"WrapKey", "UnwrapKey"
]
}
resource "azurerm_key_vault_access_policy" "aks" {
key_vault_id = azurerm_key_vault.main.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_kubernetes_cluster.main.key_vault_secrets_provider[0].secret_identity[0].object_id
secret_permissions = ["Get", "List"]
}
resource "azurerm_key_vault_secret" "sql_password" {
name = "sql-admin-password"
value = var.sql_admin_password
key_vault_id = azurerm_key_vault.main.id
depends_on = [azurerm_key_vault_access_policy.terraform]
}
```
## SQL Database
### database.tf
```hcl
resource "azurerm_mssql_server" "main" {
name = "${local.name_prefix}-sql"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_admin_password
minimum_tls_version = "1.2"
azuread_administrator {
login_username = "SQL Admins"
object_id = var.aks_admin_group_id
}
tags = local.common_tags
}
resource "azurerm_mssql_database" "main" {
name = "${var.project_name}-db"
server_id = azurerm_mssql_server.main.id
collation = "SQL_Latin1_General_CP1_CI_AS"
sku_name = var.environment == "prod" ? "BC_Gen5_4" : "GP_S_Gen5_2"
max_size_gb = var.environment == "prod" ? 256 : 32
zone_redundant = var.environment == "prod"
short_term_retention_policy {
retention_days = var.environment == "prod" ? 14 : 7
}
tags = local.common_tags
}
resource "azurerm_private_endpoint" "sql" {
name = "${local.name_prefix}-sql-pe"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
subnet_id = azurerm_subnet.data.id
private_service_connection {
name = "sql-connection"
private_connection_resource_id = azurerm_mssql_server.main.id
subresource_names = ["sqlServer"]
is_manual_connection = false
}
tags = local.common_tags
}
```
## Outputs
### outputs.tf
```hcl
output "resource_group_name" {
value = azurerm_resource_group.main.name
}
output "aks_cluster_name" {
value = azurerm_kubernetes_cluster.main.name
}
output "aks_kube_config" {
value = azurerm_kubernetes_cluster.main.kube_config_raw
sensitive = true
}
output "key_vault_uri" {
value = azurerm_key_vault.main.vault_uri
}
output "sql_server_fqdn" {
value = azurerm_mssql_server.main.fully_qualified_domain_name
}
output "vnet_id" {
value = azurerm_virtual_network.main.id
}
```
## Terraform Workflow Commands
```bash
# Initialize (download providers, configure backend)
terraform init
# Validate configuration syntax
terraform validate
# Format all .tf files
terraform fmt -recursive
# Plan changes for a specific environment
terraform plan \
-var-file="terraform.prod.tfvars" \
-var="sql_admin_password=$(az keyvault secret show --vault-name ops-vault --name sql-pass --query value -o tsv)" \
-out=tfplan
# Apply the saved plan
terraform apply tfplan
# Apply with auto-approve (CI/CD pipelines only)
terraform apply \
-var-file="terraform.prod.tfvars" \
-auto-approve
# Destroy infrastructure (careful!)
terraform plan -destroy -var-file="terraform.prod.tfvars" -out=destroyplan
terraform apply destroyplan
# Import existing resources into state
terraform import azurerm_resource_group.main /subscriptions/{sub}/resourceGroups/myapp-prod-rg
# Show current state
terraform state list
terraform state show azurerm_kubernetes_cluster.main
# Move resources in state (renaming)
terraform state mv azurerm_resource_group.old azurerm_resource_group.new
# Refresh state from real infrastructure
terraform refresh -var-file="terraform.prod.tfvars"
# Unlock stuck state
terraform force-unlock LOCK_ID
# Use workspaces for environment isolation
terraform workspace new prod
terraform workspace select prod
terraform workspace list
```
## Module Structure
```
project/
modules/
networking/
main.tf
variables.tf
outputs.tf
aks/
main.tf
variables.tf
outputs.tf
database/
main.tf
variables.tf
outputs.tf
environments/
dev/
main.tf
terraform.tfvars
backend.tf
prod/
main.tf
terraform.tfvars
backend.tf
```
### Using Modules
```hcl
# environments/prod/main.tf
module "networking" {
source = "../../modules/networking"
environment = var.environment
location = var.location
project_name = var.project_name
address_space = ["10.0.0.0/16"]
}
module "aks" {
source = "../../modules/aks"
environment = var.environment
location = var.location
project_name = var.project_name
resource_group_name = module.networking.resource_group_name
subnet_id = module.networking.aks_subnet_id
admin_group_id = var.aks_admin_group_id
}
module "database" {
source = "../../modules/database"
environment = var.environment
location = var.location
project_name = var.project_name
resource_group_name = module.networking.resource_group_name
subnet_id = module.networking.data_subnet_id
admin_password = var.sql_admin_password
}
```
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| `Error acquiring state lock` | Previous run crashed or concurrent access | Run `terraform force-unlock LOCK_ID` after confirming no other run is active |
| `Provider version constraint error` | Version conflict in required_providers | Run `terraform init -upgrade` to fetch compatible versions |
| `Resource already exists` | Resource created outside Terraform | Import with `terraform import` to bring it under management |
| `Cycle detected` in plan | Circular dependency between resources | Restructure references or use `depends_on` carefully |
| State file corruption | Concurrent writes or manual edits | Restore from state backup in the storage account versioning |
| `AuthorizationFailed` during apply | Service principal lacks RBAC permissions | Assign Contributor role on subscription or resource group |
| Plan shows unexpected changes | Drift from manual portal changes | Run `terraform refresh` then `terraform plan` to reconcile |
| Module source not found | Incorrect relative path or registry reference | Verify path in `source` attribute; run `terraform init` again |
## Related Skills
- `arm-templates` -- Azure-native IaC alternative with Bicep.
- `azure-aks` -- AKS cluster details and kubectl operations.
- `azure-networking` -- VNet and NSG design referenced in Terraform configs.
- `azure-sql` -- Database provisioning and security configurations.
- `azure-vms` -- VM sizing and scale set configurations.