Project 5: Infrastructure as Code Pipeline β
Overview β
Build a complete Infrastructure as Code (IaC) pipeline using Bicep templates and Azure DevOps/GitHub Actions. This project covers template development, modularization, testing, and automated deployment.
Difficulty: Intermediate to Advanced
Duration: 4-6 hours
Cost: ~$10-20/month (minimal - mostly free tier)
Architecture Diagram β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DEVELOPMENT WORKFLOW β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β VS Code βββββΆβ Git Repo βββββΆβ CI/CD βββββΆβ Azure β β
β β + Bicep β β (GitHub) β β Pipeline β β Deploy β β
β β Extension β β β β β β β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β β β β β
β βΌ βΌ βΌ βΌ β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Write Bicep β β Push Changes β β Build & Test β β What-If & β β
β β Templates β β to Branch β β Templates β β Deploy β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CI/CD PIPELINE STAGES β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β BUILD STAGE β β
β β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββ β β
β β β Lint βββΆβ Build βββΆβ Test βββΆβ Publish βββΆβ Artifacts β β β
β β β Check β β Bicep β β Pester β β ARM β β β β β
β β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β DEPLOY TO DEV STAGE β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β What-If βββΆβ Manual βββΆβ Deploy β β β
β β β Preview β β Approval β β to Dev β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β DEPLOY TO PROD STAGE β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β What-If βββΆβ Manual βββΆβ Deploy βββΆβ Smoke β β β
β β β Preview β β Approval β β to Prod β β Tests β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BICEP MODULE STRUCTURE β
β β
β infra/ β
β βββ main.bicep # Main entry point β
β βββ main.parameters.dev.json # Dev environment parameters β
β βββ main.parameters.prod.json # Prod environment parameters β
β β β
β βββ modules/ # Reusable modules β
β β βββ network/ β
β β β βββ vnet.bicep # Virtual Network β
β β β βββ nsg.bicep # Network Security Group β
β β β βββ bastion.bicep # Azure Bastion β
β β β β
β β βββ compute/ β
β β β βββ vm.bicep # Virtual Machine β
β β β βββ vmss.bicep # VM Scale Set β
β β β βββ appservice.bicep # App Service β
β β β β
β β βββ storage/ β
β β β βββ storage.bicep # Storage Account β
β β β β
β β βββ security/ β
β β βββ keyvault.bicep # Key Vault β
β β β
β βββ tests/ # Pester tests β
β βββ main.tests.ps1 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββWhat You'll Learn β
- Write modular Bicep templates
- Create reusable modules
- Implement parameter files for different environments
- Set up CI/CD pipeline with GitHub Actions
- Use what-if deployments for validation
- Implement approval gates
Prerequisites β
- Azure subscription
- GitHub account
- VS Code with Bicep extension
- Azure CLI installed
Phase 1: Project Setup β
Step 1.1: Create Project Structure β
bash
# Create project directory
mkdir -p infra-as-code/{modules/{network,compute,storage,security},tests}
cd infra-as-code
# Initialize git
git init
# Create .gitignore
cat > .gitignore << 'EOF'
# Azure
*.parameters.local.json
.azure/
# IDE
.vscode/
.idea/
# Build artifacts
*.arm.json
# OS
.DS_Store
Thumbs.db
EOFStep 1.2: Create Resource Group for Testing β
bash
# Set variables
LOCATION="eastus"
RG_DEV="rg-iac-dev-eastus"
RG_PROD="rg-iac-prod-eastus"
# Create resource groups
az group create --name $RG_DEV --location $LOCATION --tags Environment=Dev
az group create --name $RG_PROD --location $LOCATION --tags Environment=ProdPhase 2: Create Bicep Modules β
Step 2.1: Network Module - Virtual Network β
bash
cat > modules/network/vnet.bicep << 'EOF'
@description('Name of the virtual network')
param vnetName string
@description('Location for the virtual network')
param location string = resourceGroup().location
@description('Address prefix for the virtual network')
param addressPrefix string = '10.0.0.0/16'
@description('Array of subnet configurations')
param subnets array = [
{
name: 'default'
addressPrefix: '10.0.1.0/24'
}
]
@description('Tags for the resource')
param tags object = {}
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: vnetName
location: location
tags: tags
properties: {
addressSpace: {
addressPrefixes: [addressPrefix]
}
subnets: [for subnet in subnets: {
name: subnet.name
properties: {
addressPrefix: subnet.addressPrefix
networkSecurityGroup: contains(subnet, 'nsgId') ? {
id: subnet.nsgId
} : null
privateEndpointNetworkPolicies: contains(subnet, 'privateEndpointNetworkPolicies') ? subnet.privateEndpointNetworkPolicies : 'Disabled'
}
}]
}
}
@description('Resource ID of the virtual network')
output vnetId string = vnet.id
@description('Name of the virtual network')
output vnetName string = vnet.name
@description('Array of subnet resource IDs')
output subnetIds array = [for (subnet, i) in subnets: vnet.properties.subnets[i].id]
EOFStep 2.2: Network Module - NSG β
bash
cat > modules/network/nsg.bicep << 'EOF'
@description('Name of the network security group')
param nsgName string
@description('Location for the NSG')
param location string = resourceGroup().location
@description('Array of security rules')
param securityRules array = []
@description('Tags for the resource')
param tags object = {}
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-05-01' = {
name: nsgName
location: location
tags: tags
properties: {
securityRules: [for rule in securityRules: {
name: rule.name
properties: {
priority: rule.priority
direction: rule.direction
access: rule.access
protocol: rule.protocol
sourcePortRange: contains(rule, 'sourcePortRange') ? rule.sourcePortRange : '*'
destinationPortRange: contains(rule, 'destinationPortRange') ? rule.destinationPortRange : '*'
destinationPortRanges: contains(rule, 'destinationPortRanges') ? rule.destinationPortRanges : []
sourceAddressPrefix: contains(rule, 'sourceAddressPrefix') ? rule.sourceAddressPrefix : '*'
destinationAddressPrefix: contains(rule, 'destinationAddressPrefix') ? rule.destinationAddressPrefix : '*'
}
}]
}
}
@description('Resource ID of the NSG')
output nsgId string = nsg.id
@description('Name of the NSG')
output nsgName string = nsg.name
EOFStep 2.3: Compute Module - Virtual Machine β
bash
cat > modules/compute/vm.bicep << 'EOF'
@description('Name of the virtual machine')
param vmName string
@description('Location for the VM')
param location string = resourceGroup().location
@description('Size of the VM')
@allowed([
'Standard_B2s'
'Standard_D2s_v3'
'Standard_D4s_v3'
])
param vmSize string = 'Standard_B2s'
@description('OS type')
@allowed([
'Windows'
'Linux'
])
param osType string = 'Linux'
@description('Admin username')
param adminUsername string
@description('Admin password or SSH key')
@secure()
param adminPasswordOrKey string
@description('Authentication type')
@allowed([
'password'
'sshPublicKey'
])
param authenticationType string = 'password'
@description('Subnet ID for the VM')
param subnetId string
@description('Tags for the resource')
param tags object = {}
var imageReference = osType == 'Windows' ? {
publisher: 'MicrosoftWindowsServer'
offer: 'WindowsServer'
sku: '2022-datacenter-g2'
version: 'latest'
} : {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
var linuxConfiguration = {
disablePasswordAuthentication: authenticationType == 'sshPublicKey'
ssh: authenticationType == 'sshPublicKey' ? {
publicKeys: [
{
path: '/home/${adminUsername}/.ssh/authorized_keys'
keyData: adminPasswordOrKey
}
]
} : null
}
resource nic 'Microsoft.Network/networkInterfaces@2023-05-01' = {
name: '${vmName}-nic'
location: location
tags: tags
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: {
id: subnetId
}
privateIPAllocationMethod: 'Dynamic'
}
}
]
}
}
resource vm 'Microsoft.Compute/virtualMachines@2023-07-01' = {
name: vmName
location: location
tags: tags
properties: {
hardwareProfile: {
vmSize: vmSize
}
storageProfile: {
imageReference: imageReference
osDisk: {
name: '${vmName}-osdisk'
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Premium_LRS'
}
}
}
osProfile: {
computerName: vmName
adminUsername: adminUsername
adminPassword: authenticationType == 'password' ? adminPasswordOrKey : null
linuxConfiguration: osType == 'Linux' ? linuxConfiguration : null
}
networkProfile: {
networkInterfaces: [
{
id: nic.id
}
]
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: true
}
}
}
}
@description('Resource ID of the VM')
output vmId string = vm.id
@description('Private IP address of the VM')
output privateIpAddress string = nic.properties.ipConfigurations[0].properties.privateIPAddress
EOFStep 2.4: Storage Module β
bash
cat > modules/storage/storage.bicep << 'EOF'
@description('Name of the storage account')
param storageAccountName string
@description('Location for the storage account')
param location string = resourceGroup().location
@description('SKU for the storage account')
@allowed([
'Standard_LRS'
'Standard_GRS'
'Standard_ZRS'
'Premium_LRS'
])
param sku string = 'Standard_LRS'
@description('Kind of storage account')
@allowed([
'StorageV2'
'BlobStorage'
'FileStorage'
])
param kind string = 'StorageV2'
@description('Enable blob soft delete')
param enableBlobSoftDelete bool = true
@description('Soft delete retention days')
param softDeleteRetentionDays int = 7
@description('Tags for the resource')
param tags object = {}
resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
tags: tags
sku: {
name: sku
}
kind: kind
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
networkAcls: {
defaultAction: 'Allow'
bypass: 'AzureServices'
}
}
}
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storage
name: 'default'
properties: {
deleteRetentionPolicy: {
enabled: enableBlobSoftDelete
days: softDeleteRetentionDays
}
containerDeleteRetentionPolicy: {
enabled: enableBlobSoftDelete
days: softDeleteRetentionDays
}
}
}
@description('Resource ID of the storage account')
output storageAccountId string = storage.id
@description('Name of the storage account')
output storageAccountName string = storage.name
@description('Primary blob endpoint')
output primaryBlobEndpoint string = storage.properties.primaryEndpoints.blob
EOFStep 2.5: Security Module - Key Vault β
bash
cat > modules/security/keyvault.bicep << 'EOF'
@description('Name of the Key Vault')
param keyVaultName string
@description('Location for the Key Vault')
param location string = resourceGroup().location
@description('Tenant ID')
param tenantId string = subscription().tenantId
@description('SKU for the Key Vault')
@allowed([
'standard'
'premium'
])
param sku string = 'standard'
@description('Enable soft delete')
param enableSoftDelete bool = true
@description('Soft delete retention days')
param softDeleteRetentionDays int = 90
@description('Enable purge protection')
param enablePurgeProtection bool = true
@description('Array of access policies')
param accessPolicies array = []
@description('Tags for the resource')
param tags object = {}
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: keyVaultName
location: location
tags: tags
properties: {
tenantId: tenantId
sku: {
family: 'A'
name: sku
}
enabledForDeployment: true
enabledForDiskEncryption: true
enabledForTemplateDeployment: true
enableSoftDelete: enableSoftDelete
softDeleteRetentionInDays: softDeleteRetentionDays
enablePurgeProtection: enablePurgeProtection
enableRbacAuthorization: true
networkAcls: {
defaultAction: 'Allow'
bypass: 'AzureServices'
}
accessPolicies: accessPolicies
}
}
@description('Resource ID of the Key Vault')
output keyVaultId string = keyVault.id
@description('URI of the Key Vault')
output keyVaultUri string = keyVault.properties.vaultUri
EOFPhase 3: Create Main Template β
Step 3.1: Main Bicep File β
bash
cat > main.bicep << 'EOF'
targetScope = 'resourceGroup'
// ============================================================================
// PARAMETERS
// ============================================================================
@description('Environment name')
@allowed([
'dev'
'staging'
'prod'
])
param environment string
@description('Location for all resources')
param location string = resourceGroup().location
@description('Project name used for naming resources')
param projectName string = 'iaclab'
@description('Admin username for VMs')
param adminUsername string = 'azureadmin'
@description('Admin password for VMs')
@secure()
param adminPassword string
@description('Deploy VMs')
param deployVMs bool = true
@description('Tags to apply to all resources')
param tags object = {
Environment: environment
Project: projectName
ManagedBy: 'Bicep'
}
// ============================================================================
// VARIABLES
// ============================================================================
var resourcePrefix = '${projectName}-${environment}'
var vnetName = 'vnet-${resourcePrefix}'
var nsgName = 'nsg-${resourcePrefix}'
var storageAccountName = replace('st${projectName}${environment}${uniqueString(resourceGroup().id)}', '-', '')
var keyVaultName = 'kv-${resourcePrefix}-${uniqueString(resourceGroup().id)}'
var subnets = [
{
name: 'snet-workloads'
addressPrefix: '10.0.1.0/24'
nsgId: nsgModule.outputs.nsgId
}
{
name: 'AzureBastionSubnet'
addressPrefix: '10.0.2.0/27'
}
]
var nsgRules = [
{
name: 'AllowSSH'
priority: 100
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: '10.0.2.0/27'
destinationPortRange: '22'
}
{
name: 'AllowRDP'
priority: 110
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: '10.0.2.0/27'
destinationPortRange: '3389'
}
{
name: 'DenyAllInbound'
priority: 4096
direction: 'Inbound'
access: 'Deny'
protocol: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
}
]
// ============================================================================
// MODULES
// ============================================================================
module nsgModule 'modules/network/nsg.bicep' = {
name: 'deploy-nsg'
params: {
nsgName: nsgName
location: location
securityRules: nsgRules
tags: tags
}
}
module vnetModule 'modules/network/vnet.bicep' = {
name: 'deploy-vnet'
params: {
vnetName: vnetName
location: location
addressPrefix: '10.0.0.0/16'
subnets: subnets
tags: tags
}
dependsOn: [
nsgModule
]
}
module storageModule 'modules/storage/storage.bicep' = {
name: 'deploy-storage'
params: {
storageAccountName: take(storageAccountName, 24)
location: location
sku: environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
tags: tags
}
}
module keyVaultModule 'modules/security/keyvault.bicep' = {
name: 'deploy-keyvault'
params: {
keyVaultName: take(keyVaultName, 24)
location: location
sku: environment == 'prod' ? 'premium' : 'standard'
tags: tags
}
}
module vmModule 'modules/compute/vm.bicep' = if (deployVMs) {
name: 'deploy-vm'
params: {
vmName: 'vm-${resourcePrefix}'
location: location
vmSize: environment == 'prod' ? 'Standard_D2s_v3' : 'Standard_B2s'
osType: 'Linux'
adminUsername: adminUsername
adminPasswordOrKey: adminPassword
authenticationType: 'password'
subnetId: vnetModule.outputs.subnetIds[0]
tags: tags
}
}
// ============================================================================
// OUTPUTS
// ============================================================================
@description('Virtual Network ID')
output vnetId string = vnetModule.outputs.vnetId
@description('Storage Account Name')
output storageAccountName string = storageModule.outputs.storageAccountName
@description('Key Vault URI')
output keyVaultUri string = keyVaultModule.outputs.keyVaultUri
@description('VM Private IP')
output vmPrivateIp string = deployVMs ? vmModule.outputs.privateIpAddress : 'N/A'
EOFStep 3.2: Create Parameter Files β
bash
# Dev parameters
cat > main.parameters.dev.json << 'EOF'
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": {
"value": "dev"
},
"projectName": {
"value": "iaclab"
},
"adminUsername": {
"value": "azureadmin"
},
"adminPassword": {
"reference": {
"keyVault": {
"id": "/subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.KeyVault/vaults/<kv-name>"
},
"secretName": "vm-admin-password"
}
},
"deployVMs": {
"value": true
}
}
}
EOF
# Prod parameters
cat > main.parameters.prod.json << 'EOF'
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": {
"value": "prod"
},
"projectName": {
"value": "iaclab"
},
"adminUsername": {
"value": "azureadmin"
},
"adminPassword": {
"reference": {
"keyVault": {
"id": "/subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.KeyVault/vaults/<kv-name>"
},
"secretName": "vm-admin-password"
}
},
"deployVMs": {
"value": true
}
}
}
EOFPhase 4: Create CI/CD Pipeline β
Step 4.1: GitHub Actions Workflow β
bash
mkdir -p .github/workflows
cat > .github/workflows/deploy-infrastructure.yml << 'EOF'
name: Deploy Infrastructure
on:
push:
branches:
- main
paths:
- '**.bicep'
- '**.json'
- '.github/workflows/deploy-infrastructure.yml'
pull_request:
branches:
- main
paths:
- '**.bicep'
- '**.json'
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'dev'
type: choice
options:
- dev
- prod
env:
AZURE_RESOURCE_GROUP_DEV: rg-iac-dev-eastus
AZURE_RESOURCE_GROUP_PROD: rg-iac-prod-eastus
permissions:
id-token: write
contents: read
jobs:
# ============================================================================
# BUILD & VALIDATE
# ============================================================================
build:
name: Build & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Bicep CLI
run: |
az bicep install
az bicep version
- name: Lint Bicep files
run: |
az bicep lint --file main.bicep
- name: Build Bicep to ARM
run: |
az bicep build --file main.bicep --outfile main.arm.json
- name: Upload ARM template
uses: actions/upload-artifact@v4
with:
name: arm-templates
path: |
main.arm.json
main.parameters.*.json
# ============================================================================
# DEPLOY TO DEV
# ============================================================================
deploy-dev:
name: Deploy to Dev
needs: build
runs-on: ubuntu-latest
environment: dev
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'dev')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download ARM template
uses: actions/download-artifact@v4
with:
name: arm-templates
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: What-If Deployment
run: |
az deployment group what-if \
--resource-group ${{ env.AZURE_RESOURCE_GROUP_DEV }} \
--template-file main.bicep \
--parameters environment=dev adminPassword=${{ secrets.VM_ADMIN_PASSWORD }}
- name: Deploy to Dev
run: |
az deployment group create \
--resource-group ${{ env.AZURE_RESOURCE_GROUP_DEV }} \
--template-file main.bicep \
--parameters environment=dev adminPassword=${{ secrets.VM_ADMIN_PASSWORD }} \
--name "deploy-$(date +%Y%m%d-%H%M%S)"
# ============================================================================
# DEPLOY TO PROD
# ============================================================================
deploy-prod:
name: Deploy to Prod
needs: deploy-dev
runs-on: ubuntu-latest
environment: prod
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download ARM template
uses: actions/download-artifact@v4
with:
name: arm-templates
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: What-If Deployment
run: |
az deployment group what-if \
--resource-group ${{ env.AZURE_RESOURCE_GROUP_PROD }} \
--template-file main.bicep \
--parameters environment=prod adminPassword=${{ secrets.VM_ADMIN_PASSWORD }}
- name: Deploy to Prod
run: |
az deployment group create \
--resource-group ${{ env.AZURE_RESOURCE_GROUP_PROD }} \
--template-file main.bicep \
--parameters environment=prod adminPassword=${{ secrets.VM_ADMIN_PASSWORD }} \
--name "deploy-$(date +%Y%m%d-%H%M%S)"
EOFStep 4.2: Configure GitHub Secrets β
bash
# Required secrets in GitHub repository:
# Settings β Secrets and variables β Actions β New repository secret
# AZURE_CLIENT_ID: Service Principal App ID
# AZURE_TENANT_ID: Azure AD Tenant ID
# AZURE_SUBSCRIPTION_ID: Azure Subscription ID
# VM_ADMIN_PASSWORD: Password for VMs
# Create service principal for GitHub Actions
az ad sp create-for-rbac \
--name "github-actions-iac" \
--role Contributor \
--scopes /subscriptions/<subscription-id>/resourceGroups/$RG_DEV \
/subscriptions/<subscription-id>/resourceGroups/$RG_PROD \
--sdk-authStep 4.3: Configure GitHub Environments β
- Go to Repository β Settings β Environments
- Create
devenvironment- No protection rules (auto-deploy)
- Create
prodenvironment- Required reviewers: Add approvers
- Wait timer: 5 minutes (optional)
Phase 5: Testing Bicep Templates β
Step 5.1: Create Pester Tests β
bash
cat > tests/main.tests.ps1 << 'EOF'
BeforeAll {
# Build Bicep to ARM JSON for testing
az bicep build --file ../main.bicep --outfile ../main.arm.json
$template = Get-Content -Path ../main.arm.json | ConvertFrom-Json
}
Describe "Main Template Validation" {
Context "Template Structure" {
It "Should have required parameters" {
$template.parameters | Should -Not -BeNullOrEmpty
$template.parameters.environment | Should -Not -BeNullOrEmpty
$template.parameters.adminPassword | Should -Not -BeNullOrEmpty
}
It "Should have resources defined" {
$template.resources | Should -Not -BeNullOrEmpty
$template.resources.Count | Should -BeGreaterThan 0
}
It "Should have outputs defined" {
$template.outputs | Should -Not -BeNullOrEmpty
}
}
Context "Parameter Validation" {
It "Environment parameter should have allowed values" {
$template.parameters.environment.allowedValues | Should -Contain "dev"
$template.parameters.environment.allowedValues | Should -Contain "prod"
}
It "Admin password should be secure" {
$template.parameters.adminPassword.type | Should -Be "securestring"
}
}
Context "Resource Validation" {
It "Should deploy NSG" {
$nsg = $template.resources | Where-Object { $_.type -eq "Microsoft.Resources/deployments" -and $_.name -eq "deploy-nsg" }
$nsg | Should -Not -BeNullOrEmpty
}
It "Should deploy VNet" {
$vnet = $template.resources | Where-Object { $_.type -eq "Microsoft.Resources/deployments" -and $_.name -eq "deploy-vnet" }
$vnet | Should -Not -BeNullOrEmpty
}
It "Should deploy Storage Account" {
$storage = $template.resources | Where-Object { $_.type -eq "Microsoft.Resources/deployments" -and $_.name -eq "deploy-storage" }
$storage | Should -Not -BeNullOrEmpty
}
}
}
Describe "Security Best Practices" {
It "Storage account should enforce HTTPS" {
$storageModule = Get-Content -Path ../modules/storage/storage.bicep -Raw
$storageModule | Should -Match "supportsHttpsTrafficOnly.*true"
}
It "Storage account should use TLS 1.2" {
$storageModule = Get-Content -Path ../modules/storage/storage.bicep -Raw
$storageModule | Should -Match "minimumTlsVersion.*TLS1_2"
}
It "Key Vault should enable soft delete" {
$kvModule = Get-Content -Path ../modules/security/keyvault.bicep -Raw
$kvModule | Should -Match "enableSoftDelete"
}
}
AfterAll {
# Cleanup
Remove-Item -Path ../main.arm.json -Force -ErrorAction SilentlyContinue
}
EOFStep 5.2: Run Tests β
powershell
# Install Pester if needed
Install-Module -Name Pester -Force -SkipPublisherCheck
# Run tests
cd tests
Invoke-Pester -Path ./main.tests.ps1 -Output DetailedPhase 6: Deploy and Verify β
Step 6.1: Manual Deployment (Testing) β
bash
# Deploy to dev with what-if first
az deployment group what-if \
--resource-group $RG_DEV \
--template-file main.bicep \
--parameters environment=dev adminPassword="YourSecurePassword123!"
# Deploy to dev
az deployment group create \
--resource-group $RG_DEV \
--template-file main.bicep \
--parameters environment=dev adminPassword="YourSecurePassword123!" \
--name "manual-deploy-$(date +%Y%m%d-%H%M%S)"
# Verify deployment
az deployment group show \
--resource-group $RG_DEV \
--name "manual-deploy-*" \
--query "properties.provisioningState" -o tsvStep 6.2: View Deployment Outputs β
bash
# Get deployment outputs
az deployment group show \
--resource-group $RG_DEV \
--name "manual-deploy-*" \
--query "properties.outputs"Cleanup β
bash
# Delete resource groups
az group delete --name $RG_DEV --yes --no-wait
az group delete --name $RG_PROD --yes --no-wait
# Delete service principal
az ad sp delete --id <service-principal-id>Key Takeaways β
- Modular Design: Reusable modules for consistency
- Parameter Files: Environment-specific configurations
- What-If Deployments: Preview changes before applying
- CI/CD Automation: Consistent, repeatable deployments
- Testing: Validate templates before deployment
- Approval Gates: Control production deployments
Next Steps β
- Add more modules (App Service, AKS, SQL)
- Implement Bicep linting rules
- Add integration tests with Azure
- Set up Azure Policy for compliance
- Implement drift detection