Lab 2: Building Basic Terraform Configurations
Lab Overview
This hands-on lab guides you through building real Azure infrastructure using Terraform. You'll create a complete web application stack including resource groups, storage accounts, app services, and databases, while learning Terraform best practices and state management.
Lab Objectives
- Build a complete web application infrastructure stack
- Implement proper resource dependencies and naming conventions
- Practice Terraform workflow:
init,plan,apply,destroy - Understand Terraform state management
- Configure variables and outputs for reusability
- Implement security best practices in Terraform configurations
Prerequisites
- Completed Lab 1: Environment Setup & Validation
- Terraform development environment configured
- Azure CLI authenticated
- VS Code with Terraform extensions
Part 1: Web Application Infrastructure Planning
Architecture Overview
We'll build infrastructure for a modern web application with the following components:
graph TB
subgraph "Resource Group"
A[App Service Plan]
B[Web App]
C[Storage Account]
D[SQL Server]
E[SQL Database]
F[Application Insights]
G[Log Analytics Workspace]
end
B --> A
E --> D
F --> G
B --> C
B --> E
B --> F
style A fill:#e1f5fe
style B fill:#c8e6c9
style C fill:#fff3e0
style D fill:#fce4ec
style E fill:#fce4ec
style F fill:#f3e5f5
style G fill:#f3e5f5
Resource Requirements
| Component | Purpose | Configuration |
|---|---|---|
| Resource Group | Container for all resources | Standard naming convention |
| Storage Account | Static files, backups | Standard LRS, secure config |
| App Service Plan | Compute platform | B1 for dev, P1v3 for prod |
| Web App | Application hosting | Linux, Node.js runtime |
| SQL Server | Database server | Azure AD auth, no public access |
| SQL Database | Application data | Basic tier for dev, S2 for prod |
| Log Analytics | Monitoring workspace | 30-day retention |
| Application Insights | APM and monitoring | Connected to Log Analytics |
Part 2: Project Structure Setup
Step 1: Create Enhanced Project Structure
# Navigate to your workshop directory
cd terraform-workshop
# Create enhanced directory structure
mkdir -p terraform/environments/{dev,staging,prod}
# Create shared information files
touch terraform/locals.tf
touch terraform/data.tf
# Create environment-specific variable files
touch terraform/environments/dev/terraform.tfvars
touch terraform/environments/staging/terraform.tfvars
touch terraform/environments/prod/terraform.tfvars
# Show structure
tree terraform/
Step 2: Environment-Specific Variables
Create dev environment variables:
# terraform/environments/dev/terraform.tfvars
project_name = "webapp"
environment = "dev"
location = "East US 2"
# App Service Configuration
app_service_sku = "S1"
# Database Configuration
database_sku = "GP_S_Gen5_1"
database_max_size_gb = 2
database_backup_retention_days = 7
database_geo_redundant_backup = false
# Storage Configuration
storage_replication_type = "LRS"
storage_blob_retention_days = 7
# Monitoring Configuration
log_retention_days = 30
# Tags
tags = {
Project = "WebAppDemo"
Environment = "Development"
Owner = "DevTeam"
ManagedBy = "Terraform"
CostCenter = "Engineering"
}
Create prod environment variables:
# terraform/environments/prod/terraform.tfvars
project_name = "webapp"
environment = "prod"
location = "East US 2"
# App Service Configuration
app_service_sku = "B1"
# Database Configuration
database_sku = "GP_S_Gen5_1"
database_max_size_gb = 2
database_backup_retention_days = 7
database_geo_redundant_backup = false
# Storage Configuration
storage_replication_type = "GRS"
storage_blob_retention_days = 7
# Monitoring Configuration
log_retention_days = 90
# Tags
tags = {
Project = "WebAppDemo"
Environment = "Production"
Owner = "DevTeam"
ManagedBy = "Terraform"
CostCenter = "Engineering"
Criticality = "High"
}
Create staging environment variables:
# terraform/environments/staging/terraform.tfvars
project_name = "webapp"
environment = "staging"
location = "East US 2"
# App Service Configuration
app_service_sku = "B1"
# Database Configuration
database_sku = "GP_S_Gen5_1"
database_max_size_gb = 2
database_backup_retention_days = 7
database_geo_redundant_backup = false
# Storage Configuration
storage_replication_type = "ZRS"
storage_blob_retention_days = 7
# Monitoring Configuration
log_retention_days = 60
# Tags
tags = {
Project = "WebAppDemo"
Environment = "Staging"
Owner = "DevTeam"
ManagedBy = "Terraform"
CostCenter = "Engineering"
}
Part 3: Core Infrastructure Configuration
Step 1: Update Variables Definition
# terraform/variables.tf
variable "project_name" {
description = "Name of the project (used in resource naming)"
type = string
validation {
condition = can(regex("^[a-z0-9]+$", var.project_name))
error_message = "Project name must contain only lowercase letters and numbers."
}
}
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be one of: dev, staging, prod."
}
}
variable "location" {
description = "Azure region for all resources"
type = string
default = "West US 2"
}
# App Service Variables
variable "app_service_sku" {
description = "SKU for the App Service Plan"
type = string
default = "B1"
validation {
condition = contains(["B1", "B2", "B3", "S1", "S2", "S3", "P1v3", "P2v3", "P3v3"], var.app_service_sku)
error_message = "App Service SKU must be a valid Azure App Service Plan SKU."
}
}
# Database Variables
variable "database_sku" {
description = "SKU for the SQL Database"
type = string
default = "Basic"
}
variable "database_max_size_gb" {
description = "Maximum size of the database in GB"
type = number
default = 2
validation {
condition = var.database_max_size_gb >= 1 && var.database_max_size_gb <= 1024
error_message = "Database size must be between 1 and 1024 GB."
}
}
variable "database_backup_retention_days" {
description = "Number of days to retain database backups"
type = number
default = 7
validation {
condition = var.database_backup_retention_days >= 7 && var.database_backup_retention_days <= 35
error_message = "Backup retention must be between 7 and 35 days."
}
}
variable "database_geo_redundant_backup" {
description = "Enable geo-redundant database backups"
type = bool
default = false
}
# Storage Variables
variable "storage_replication_type" {
description = "Storage account replication type"
type = string
default = "LRS"
validation {
condition = contains(["LRS", "GRS", "RAGRS", "ZRS", "GZRS", "RAGZRS"], var.storage_replication_type)
error_message = "Storage replication type must be a valid Azure storage replication option."
}
}
variable "storage_blob_retention_days" {
description = "Number of days to retain deleted blobs"
type = number
default = 7
}
# Monitoring Variables
variable "log_retention_days" {
description = "Number of days to retain logs"
type = number
default = 30
}
# Tags
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
# Generated Variables (optional overrides)
variable "admin_username" {
description = "SQL Server administrator username"
type = string
default = "sqladmin"
sensitive = true
}
Step 2: Shared information
# terraform/locals.tf
locals {
# Generate unique suffix for globally unique resources
unique_suffix = random_string.unique.result
# Naming conventions
resource_prefix = "${var.project_name}-${var.environment}-${local.unique_suffix}"
# Common tags merged with user-provided tags
common_tags = merge(var.tags, {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
CreatedDate = formatdate("YYYY-MM-DD", timestamp())
LastModified = formatdate("YYYY-MM-DD hh:mm:ss ZZZ", timestamp())
})
# Environment-specific configurations
environment_config = {
is_production = var.environment == "prod"
is_development = var.environment == "dev"
# Features enabled by environment
features = {
always_on_webapp = var.environment != "dev"
geo_redundant_backup = var.environment == "prod"
advanced_monitoring = var.environment == "prod"
}
# Security settings
security = {
min_tls_version = "1.2"
https_only = true
storage_public_access = false
}
}
}
# Generate unique identifiers
resource "random_string" "unique" {
length = 6
upper = false
special = false
}
resource "random_password" "sql_admin_password" {
length = 24
special = true
# Ensure password meets Azure SQL requirements
min_upper = 2
min_lower = 2
min_numeric = 2
min_special = 2
}
# terraform/data.tf
# Current Azure client configuration
data "azurerm_client_config" "current" {}
# Current Azure subscription
data "azurerm_subscription" "current" {}
Step 3: Main Infrastructure Configuration
# terraform/main.tf
# Resource Group
resource "azurerm_resource_group" "main" {
name = "rg-${local.resource_prefix}"
location = var.location
tags = local.common_tags
}
# Storage Account
resource "azurerm_storage_account" "main" {
name = "st${replace(local.resource_prefix, "-", "")}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = var.storage_replication_type
account_kind = "StorageV2"
# Security settings
public_network_access_enabled = false
allow_nested_items_to_be_public = false
shared_access_key_enabled = true
https_traffic_only_enabled = true
min_tls_version = "TLS1_2"
# Infrastructure encryption for production
infrastructure_encryption_enabled = local.environment_config.is_production
# Blob properties
blob_properties {
# Enable versioning
versioning_enabled = true
# Change feed for audit trail
change_feed_enabled = local.environment_config.is_production
# Retention policy
delete_retention_policy {
days = var.storage_blob_retention_days
}
# Container retention policy
container_delete_retention_policy {
days = var.storage_blob_retention_days
}
}
# Network rules (restrictive by default)
network_rules {
default_action = "Deny"
ip_rules = [] # Add your IP addresses as needed
# Allow access from the same virtual network (when implemented)
virtual_network_subnet_ids = []
}
tags = merge(local.common_tags, {
Component = "Storage"
Service = "DataStorage"
})
}
# Log Analytics Workspace
resource "azurerm_log_analytics_workspace" "main" {
name = "log-${local.resource_prefix}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
sku = "PerGB2018"
retention_in_days = var.log_retention_days
# Data export and linked services
daily_quota_gb = local.environment_config.is_development ? 1 : null
tags = merge(local.common_tags, {
Component = "Monitoring"
Service = "LogAnalytics"
})
}
# Application Insights
resource "azurerm_application_insights" "main" {
name = "appi-${local.resource_prefix}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
workspace_id = azurerm_log_analytics_workspace.main.id
application_type = "web"
# Retention and sampling
retention_in_days = var.log_retention_days
daily_data_cap_in_gb = local.environment_config.is_development ? 1 : null
tags = merge(local.common_tags, {
Component = "Monitoring"
Service = "ApplicationInsights"
})
}
# App Service Plan
resource "azurerm_service_plan" "main" {
name = "asp-${local.resource_prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux"
sku_name = var.app_service_sku
# Auto-scaling settings for production
maximum_elastic_worker_count = local.environment_config.is_production ? 10 : null
worker_count = local.environment_config.is_production ? 2 : 1
tags = merge(local.common_tags, {
Component = "Compute"
Service = "AppServicePlan"
})
}
# Web App
resource "azurerm_linux_web_app" "main" {
name = "app-${local.resource_prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_service_plan.main.location
service_plan_id = azurerm_service_plan.main.id
# Application configuration
site_config {
# Always on for non-development environments
always_on = local.environment_config.features.always_on_webapp
# Runtime configuration
application_stack {
node_version = "18-lts"
}
# Security settings
http2_enabled = true
minimum_tls_version = local.environment_config.security.min_tls_version
scm_minimum_tls_version = local.environment_config.security.min_tls_version
ftps_state = "FtpsOnly"
# Health check
health_check_path = "/health"
health_check_eviction_time_in_min = 2
# CORS settings (configure as needed)
cors {
allowed_origins = ["*"] # Restrict in production
support_credentials = false
}
}
# HTTPS configuration
https_only = local.environment_config.security.https_only
# Application settings
app_settings = {
# Runtime environment
NODE_ENV = var.environment == "prod" ? "production" : "development"
PORT = "8080"
# Application configuration
ENVIRONMENT = var.environment
WEBSITE_RUN_FROM_PACKAGE = "1"
WEBSITE_ENABLE_SYNC_UPDATE_SITE = "true"
# Storage connection
STORAGE_CONNECTION_STRING = azurerm_storage_account.main.primary_connection_string
# Application Insights
APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.main.instrumentation_key
APPLICATIONINSIGHTS_CONNECTION_STRING = azurerm_application_insights.main.connection_string
# Database connection (will be added after SQL resources)
DATABASE_SERVER = azurerm_mssql_server.main.fully_qualified_domain_name
DATABASE_NAME = azurerm_mssql_database.main.name
DATABASE_USERNAME = var.admin_username
# The application will fetch the password from Key Vault using its Managed Identity
KEY_VAULT_URI = azurerm_key_vault.main.vault_uri
}
# Managed identity for secure access
identity {
type = "SystemAssigned"
}
# Connection strings
connection_string {
name = "DefaultConnection"
type = "SQLAzure"
value = "Server=${azurerm_mssql_server.main.fully_qualified_domain_name};Database=${azurerm_mssql_database.main.name};User ID=${var.admin_username};Password=${random_password.sql_admin_password.result};Encrypt=True;TrustServerCertificate=False;"
}
tags = merge(local.common_tags, {
Component = "Compute"
Service = "WebApp"
Runtime = "Node.js"
})
}
# SQL Server
resource "azurerm_mssql_server" "main" {
name = "sql-${local.resource_prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = var.admin_username
administrator_login_password = random_password.sql_admin_password.result
# Security settings
public_network_access_enabled = true
minimum_tls_version = "1.2"
# Azure AD authentication (recommended)
azuread_administrator {
login_username = data.azurerm_client_config.current.object_id
object_id = data.azurerm_client_config.current.object_id
tenant_id = data.azurerm_client_config.current.tenant_id
azuread_authentication_only = true # Set to true after setting up Azure AD users
}
tags = merge(local.common_tags, {
Component = "Database"
Service = "SQLServer"
})
}
# SQL Database
resource "azurerm_mssql_database" "main" {
name = "sqldb-${local.resource_prefix}"
server_id = azurerm_mssql_server.main.id
# SKU configuration
sku_name = var.database_sku
# Storage configuration
max_size_gb = var.database_max_size_gb
# Backup configuration
short_term_retention_policy {
retention_days = var.database_backup_retention_days
}
# Maintenance and performance
auto_pause_delay_in_minutes = local.environment_config.is_development ? 60 : 120
min_capacity = local.environment_config.is_development ? 0.5 : 1
tags = merge(local.common_tags, {
Component = "Database"
Service = "SQLDatabase"
})
}
# Firewall rule for Azure services (adjust as needed)
resource "azurerm_mssql_firewall_rule" "azure_services" {
name = "AllowAzureServices"
server_id = azurerm_mssql_server.main.id
start_ip_address = "0.0.0.0"
end_ip_address = "0.0.0.0"
}
# Key Vault for secrets management
resource "azurerm_key_vault" "main" {
name = "kv-${local.resource_prefix}"
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"
# Security settings
enabled_for_disk_encryption = true
enabled_for_deployment = true
enabled_for_template_deployment = true
rbac_authorization_enabled = true
public_network_access_enabled = true # Set to false and configure private endpoints for production
# Purge protection for production
purge_protection_enabled = local.environment_config.is_production
soft_delete_retention_days = local.environment_config.is_production ? 90 : 7
tags = merge(local.common_tags, {
Component = "Security"
Service = "KeyVault"
})
}
# RBAC role assignment for current user/service principal - Key Vault Administrator
resource "azurerm_role_assignment" "current_user_kv_admin" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Administrator"
principal_id = data.azurerm_client_config.current.object_id
}
# RBAC role assignment for current user/service principal - Key Vault Secrets Officer (for setting secrets)
resource "azurerm_role_assignment" "current_user_kv_secrets_officer" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets Officer"
principal_id = data.azurerm_client_config.current.object_id
}
# Store SQL password in Key Vault
resource "azurerm_key_vault_secret" "sql_password" {
name = "sql-admin-password"
value = random_password.sql_admin_password.result
key_vault_id = azurerm_key_vault.main.id
depends_on = [
azurerm_role_assignment.current_user_kv_admin,
azurerm_role_assignment.current_user_kv_secrets_officer
]
tags = merge(local.common_tags, {
Component = "Security"
Service = "Secret"
})
}
# RBAC role assignment for Web App managed identity - Key Vault Secrets User (scoped to specific secret)
resource "azurerm_role_assignment" "webapp_kv_secrets_user" {
scope = azurerm_key_vault_secret.sql_password.resource_versionless_id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_web_app.main.identity[0].principal_id
}
Part 4: Enhanced Outputs Configuration
Step 1: Comprehensive Outputs
# terraform/outputs.tf
# Infrastructure Information
output "resource_group_name" {
description = "Name of the created resource group"
value = azurerm_resource_group.main.name
}
output "resource_group_id" {
description = "ID of the created resource group"
value = azurerm_resource_group.main.id
}
output "location" {
description = "Azure region where resources are deployed"
value = azurerm_resource_group.main.location
}
# Storage Account Information
output "storage_account_name" {
description = "Name of the storage account"
value = azurerm_storage_account.main.name
}
output "storage_account_primary_endpoint" {
description = "Primary blob endpoint of the storage account"
value = azurerm_storage_account.main.primary_blob_endpoint
sensitive = true
}
output "storage_account_connection_string" {
description = "Connection string for the storage account"
value = azurerm_storage_account.main.primary_connection_string
sensitive = true
}
# Web Application Information
output "web_app_name" {
description = "Name of the web application"
value = azurerm_linux_web_app.main.name
}
output "web_app_url" {
description = "URL of the web application"
value = "https://${azurerm_linux_web_app.main.default_hostname}"
}
output "web_app_identity_principal_id" {
description = "Principal ID of the web app's managed identity"
value = azurerm_linux_web_app.main.identity[0].principal_id
}
# Database Information
output "sql_server_name" {
description = "Name of the SQL server"
value = azurerm_mssql_server.main.name
}
output "sql_server_fqdn" {
description = "Fully qualified domain name of the SQL server"
value = azurerm_mssql_server.main.fully_qualified_domain_name
}
output "sql_database_name" {
description = "Name of the SQL database"
value = azurerm_mssql_database.main.name
}
output "sql_connection_string" {
description = "SQL database connection string"
value = "Server=${azurerm_mssql_server.main.fully_qualified_domain_name};Database=${azurerm_mssql_database.main.name};User ID=${var.admin_username};Encrypt=True;TrustServerCertificate=False;"
sensitive = true
}
# Monitoring Information
output "application_insights_instrumentation_key" {
description = "Application Insights instrumentation key"
value = azurerm_application_insights.main.instrumentation_key
sensitive = true
}
output "application_insights_connection_string" {
description = "Application Insights connection string"
value = azurerm_application_insights.main.connection_string
sensitive = true
}
output "log_analytics_workspace_id" {
description = "ID of the Log Analytics workspace"
value = azurerm_log_analytics_workspace.main.workspace_id
}
# Security Information
output "key_vault_name" {
description = "Name of the Key Vault"
value = azurerm_key_vault.main.name
}
output "key_vault_uri" {
description = "URI of the Key Vault"
value = azurerm_key_vault.main.vault_uri
}
# Configuration Summary
output "deployment_summary" {
description = "Summary of the deployed infrastructure"
value = {
project_name = var.project_name
environment = var.environment
location = var.location
resources = {
resource_group = azurerm_resource_group.main.name
web_app = azurerm_linux_web_app.main.name
sql_server = azurerm_mssql_server.main.name
storage_account = azurerm_storage_account.main.name
key_vault = azurerm_key_vault.main.name
}
endpoints = {
web_app_url = "https://${azurerm_linux_web_app.main.default_hostname}"
storage_url = azurerm_storage_account.main.primary_blob_endpoint
}
unique_suffix = local.unique_suffix
tags = local.common_tags
}
sensitive = true
}
Part 5: Development Workflow
Step 1: Initialize and Validate Development Environment
# Navigate to terraform directory
cd terraform
# Initialize Terraform
terraform init
# Validate configuration
terraform validate
# Format code
terraform fmt -recursive
# Check formatting
terraform fmt -check=true -diff=true
Step 2: Plan Development Environment
# Plan with development variables
terraform plan -var-file="environments/dev/terraform.tfvars" -out=dev.tfplan
# Review the plan carefully
# Should show approximately 12-15 resources to be created
Expected Plan Summary:
Plan: 15 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ deployment_summary = {
+ environment = "dev"
+ location = "East US"
+ project_name = "webapp"
+ resources = {
+ key_vault = (known after apply)
+ resource_group = "rg-webapp-dev"
+ sql_server = (known after apply)
+ storage_account = (known after apply)
+ web_app = (known after apply)
}
+ unique_suffix = (known after apply)
}
+ web_app_url = (known after apply)
# ... additional outputs
Step 3: Deploy Development Environment
# Apply the development plan
terraform apply dev.tfplan
# This will take approximately 3-5 minutes to complete
Monitor the deployment progress:
- ✅ Random resources created first
- ✅ Resource group created
- ✅ Storage account and Key Vault created in parallel
- ✅ Log Analytics and Application Insights created
- ✅ App Service Plan created
- ✅ SQL Server and Database created
- ✅ Web App created last (depends on all other resources)
Step 4: Verify Deployment
# View all outputs
terraform output
# View specific outputs
terraform output web_app_url
terraform output deployment_summary
# View sensitive outputs
terraform output -raw storage_account_connection_string
terraform output -raw sql_connection_string
Verify in Azure:
Either by using the Azure Portal or Azure CLI:
# List all resources in the resource group
az resource list --resource-group "$(terraform output -raw resource_group_name)" --output table
# Check web app status
az webapp show --name "$(terraform output -raw web_app_name)" --resource-group "$(terraform output -raw resource_group_name)" --query "state" --output tsv
# Check SQL server status
az sql server show --name "$(terraform output -raw sql_server_name)" --resource-group "$(terraform output -raw resource_group_name)" --query "state" --output tsv
Part 6: Multi-Environment Testing
Step 1: Compare Environments
# To compare environments, simply use the appropriate var-file for each plan.
# For example, to see the plan for the staging environment:
terraform plan -var-file="environments/staging/terraform.tfvars" -out=staging.tfplan
# And to see the plan for production:
terraform plan -var-file="environments/prod/terraform.tfvars" -out=prod.tfplan
# You can apply these plans just as you did for the dev environment.
# Note: Each environment will be deployed into a different, isolated resource group
# based on the naming convention "rg-${var.project_name}-${var.environment}".
Costs and quotas
Deploying (not planning) multiple environments will incur additional costs. Ensure you will delete resources after the lab if not needed.
There also may be subscription limits on certain SKUs or resource types. If you encounter quota issues, consider using lower-tier SKUs or deleting unused resources.
Part 7: State Management and Best Practices
Step 1: Understanding Terraform State
# Examine state file structure
terraform show
# List resources in state
terraform state list
# Show specific resource
terraform state show azurerm_linux_web_app.main
# Show resource dependencies
terraform graph | dot -Tsvg > infrastructure-graph.svg
Step 2: State Operations
# Refresh state (sync with actual Azure resources)
terraform refresh -var-file="environments/dev/terraform.tfvars"
# Import existing resource (example)
# az group create --name "external-rg" --location "East US"
# terraform import azurerm_resource_group.external /subscriptions/xxx/resourceGroups/external-rg
# Move resource in state
# terraform state mv azurerm_resource_group.main azurerm_resource_group.primary
Step 3: Backup and Recovery
# Backup current state
cp terraform.tfstate terraform.tfstate.backup
# Show state backup versions
terraform state pull > state-backup.json
# Restore from backup (if needed)
# terraform state push state-backup.json
Part 8: Testing and Validation
Step 1: Security testing
# Check for security issues (if tfsec is installed)
# Install tfsec: curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash
tfsec .
Analyze the output to consider the security posture of your Terraform code.
Part 9: Troubleshooting Common Issues
Issue 1: Resource Name Conflicts
Problem: "A resource with this name already exists"
Solution:
# Check existing resources
az resource list --name "*webapp*" --output table
# Update unique_suffix or change project_name
# Edit terraform/variables.tf or terraform.tfvars
# Force new unique suffix
terraform apply -replace="random_string.unique"
Issue 2: Authentication Errors
Problem: "Error building AzureRM Client"
Solution:
# Re-authenticate
az logout
az login
# Verify subscription
az account show
# Check service principal (if used)
az ad sp show --id $ARM_CLIENT_ID
Issue 3: State Lock Issues
Problem: "Error acquiring the state lock"
Solution:
# List lock info (look for lock ID in error message)
terraform force-unlock <LOCK_ID>
# If lock is stuck, verify no other Terraform processes
ps aux | grep terraform
# Clean restart
terraform init -reconfigure
Issue 4: SQL Authentication Issues
Problem: Cannot connect to SQL database
Solution:
# Check SQL server firewall rules
az sql server firewall-rule list --server "$(terraform output -raw sql_server_name)" --resource-group "$(terraform output -raw resource_group_name)" --output table
# Add your IP to firewall (temporary)
MY_IP=$(curl -s ifconfig.me)
az sql server firewall-rule create --server "$(terraform output -raw sql_server_name)" --resource-group "$(terraform output -raw resource_group_name)" --name "TempAccess" --start-ip-address "$MY_IP" --end-ip-address "$MY_IP"
# Test connection
sqlcmd -S "$(terraform output -raw sql_server_fqdn)" -d "$(terraform output -raw sql_database_name)" -U "$admin_username" -P "$sql_password"
Part 10: Cleanup and Resource Management
Step 1: Planned Destruction
# Plan destruction
terraform plan -destroy -var-file="environments/dev/terraform.tfvars" -out=destroy.tfplan
# Review what will be destroyed
terraform show destroy.tfplan
# Apply destruction
terraform apply destroy.tfplan
Step 2: Verify Cleanup
# Check if resource group still exists (should fail)
az group show --name "$(terraform output -raw resource_group_name)"
# List any remaining resources
az resource list --query "[?resourceGroup=='rg-webapp-dev']" --output table
# Clean up any orphaned resources manually if needed
Step 3: Clean Terraform State
# Remove state files (if everything is destroyed)
rm -f terraform.tfstate*
rm -f *.tfplan
# Clean .terraform directory
rm -rf .terraform
# Reinitialize for next use
terraform init
Lab Completion Checklist
✅ Core Infrastructure
- Resource Group created with proper naming
- Storage Account with security settings
- App Service Plan and Web App
- SQL Server and Database with authentication
- Key Vault with proper access policies
- Application Insights and Log Analytics
✅ Configuration Management
- Variables properly defined and validated
- Environment-specific configurations
- Outputs provide useful information
- Tags applied consistently
- Secrets managed securely
✅ Terraform Operations
-
terraform initsuccessful -
terraform validatepasses -
terraform plangenerates correct plan -
terraform applydeploys successfully -
terraform destroycleans up properly
✅ Best Practices
- Code formatted consistently
- Resource dependencies properly defined
- Security settings implemented
- State management understood
- Testing and validation performed
Success Criteria
You have successfully completed Lab 2 if you:
- ✅ Built a complete web application stack with all required components
- ✅ Implemented proper Terraform practices including variables, outputs, and state management
- ✅ Deployed to multiple environments with different configurations
- ✅ Validated infrastructure through testing and Azure CLI commands
- ✅ Managed the complete lifecycle including deployment and destruction
Next Steps
Immediate Actions:
- Save your configuration for use in the next lab
- Document any customizations you made
- Note performance observations and improvement ideas
Next Lab: Lab 3: Azure Verified Modules (AVM)
Lab 3 Preview
In the next lab, you'll learn how to replace your custom resource configurations with Azure Verified Modules (AVM), dramatically reducing code complexity while improving security and maintainability.