Scripting Our Entire AI Solution with Infrastructure as Code


The Challenge: Manual Configuration Chaos
We had a working solution, but deploying it was a nightmare. Every environment required manual configuration. Every resource needed to be created by hand. Every deployment was different. We’d spend hours clicking through Azure Portal, copying connection strings, setting up storage accounts, configuring App Services, only to realize we’d missed a step or misconfigured something.
We needed to script everything. We needed infrastructure as code.
The Solution: Complete Automation with PowerShell and ARM/Bicep
We transformed our entire deployment process into automated scripts. Every Azure resource, every configuration setting, every CI/CD pipeline, all defined in code. Now we can deploy our entire solution with a single command.
Here’s how we did it.
The Architecture: From Code to Cloud in Minutes
Our infrastructure as code approach covers:
Source Code → ARM/Bicep Templates → PowerShell Scripts → Azure Resources → CI/CD Pipeline
Each component is version-controlled, reproducible, and automated.
Step 1: Infrastructure as Code with ARM/Bicep
We defined all Azure resources using Bicep templates. No more manual resource creation.
Resource Group Template
// main.bicep
targetScope = 'subscription'
@description('Name of the resource group')
param resourceGroupName string = 'rg-acdc-${uniqueString(subscription().id)}'
@description('Location for all resources')
param location string = resourceGroup().location
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: resourceGroupName
location: location
}
module storageAccount 'modules/storage.bicep' = {
name: 'storageDeployment'
scope: rg
params: {
location: location
storageAccountName: 'stracdc${uniqueString(rg.id)}'
}
}
module appService 'modules/appservice.bicep' = {
name: 'appServiceDeployment'
scope: rg
params: {
location: location
appServiceName: 'app-acdc-${uniqueString(rg.id)}'
storageAccountName: storageAccount.outputs.storageAccountName
}
}
Storage Account Module
// modules/storage.bicep
param location string
param storageAccountName string
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
}
}
output storageAccountName string = storageAccount.name
output storageAccountKey string = storageAccount.listKeys().keys[0].value
This template creates:
- Storage accounts with proper security settings
- App Services with correct configurations
- All networking and security rules
- Key Vault for secrets
- Application Insights for monitoring
Everything is defined in code. No manual steps.
Step 2: PowerShell Deployment Scripts
We created PowerShell scripts that orchestrate the entire deployment process.
Main Deployment Script
# deploy-infrastructure.ps1
param(
[string]$ResourceGroupName = "rg-acdc-demo",
[string]$Location = "swedencentral",
[string]$SubscriptionId = ""
)
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "ACDC Infrastructure Deployment" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Set Azure subscription
if ($SubscriptionId) {
Write-Host "Setting subscription to: $SubscriptionId" -ForegroundColor Yellow
az account set --subscription $SubscriptionId
}
# Deploy infrastructure
Write-Host "Deploying infrastructure..." -ForegroundColor Yellow
az deployment sub create `
--location $Location `
--template-file infra/main.bicep `
--parameters resourceGroupName=$ResourceGroupName location=$Location `
--name "acdc-deployment-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
if ($LASTEXITCODE -ne 0) {
Write-Host "Deployment failed!" -ForegroundColor Red
exit 1
}
Write-Host "Infrastructure deployed successfully!" -ForegroundColor Green
Configuration Script
# configure-resources.ps1
param(
[string]$ResourceGroupName = "rg-acdc-demo"
)
Write-Host "Configuring resources..." -ForegroundColor Yellow
# Get resource names from deployment outputs
$deployment = az deployment sub list --query "[?name=='acdc-deployment-*'] | [0]" | ConvertFrom-Json
$outputs = az deployment sub show --name $deployment.name --query "properties.outputs" | ConvertFrom-Json
$storageAccountName = $outputs.storageAccountName.value
$appServiceName = $outputs.appServiceName.value
# Configure storage account containers
Write-Host "Creating storage containers..." -ForegroundColor Cyan
$containers = @("bronze-raw", "silver-cleaned", "gold-analytics", "archives")
foreach ($container in $containers) {
az storage container create `
--account-name $storageAccountName `
--name $container `
--auth-mode login `
--public-access off
Write-Host " Created container: $container" -ForegroundColor Green
}
# Configure App Service settings
Write-Host "Configuring App Service..." -ForegroundColor Cyan
az webapp config appsettings set `
--resource-group $ResourceGroupName `
--name $appServiceName `
--settings `
"StorageAccountName=$storageAccountName" `
"Environment=Production" `
"EnableMonitoring=true"
Write-Host "Configuration complete!" -ForegroundColor Green
These scripts handle:
- Resource group creation
- Infrastructure deployment
- Configuration settings
- Container setup
- Connection string configuration
- Security settings
All automated. All repeatable.
Step 3: CI/CD Pipeline Configuration
We configured Azure DevOps pipelines to automate deployments.
Azure DevOps Pipeline YAML
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Infrastructure
displayName: 'Deploy Infrastructure'
jobs:
- job: DeployInfrastructure
displayName: 'Deploy ARM/Bicep Templates'
steps:
- task: AzureCLI@2
displayName: 'Deploy Infrastructure'
inputs:
azureSubscription: 'Azure-Service-Connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment sub create \
--location swedencentral \
--template-file infra/main.bicep \
--parameters resourceGroupName=$(ResourceGroupName) \
--name "acdc-deployment-$(Build.BuildId)"
- stage: Application
displayName: 'Deploy Application'
dependsOn: Infrastructure
jobs:
- job: DeployApplication
displayName: 'Build and Deploy App'
steps:
- task: AzureWebApp@1
displayName: 'Deploy to App Service'
inputs:
azureSubscription: 'Azure-Service-Connection'
appType: 'webApp'
appName: '$(AppServiceName)'
package: '$(Pipeline.Workspace)/drop'
GitHub Actions Alternative
# .github/workflows/deploy.yml
name: Deploy Infrastructure
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy Infrastructure
run: |
az deployment sub create \
--location swedencentral \
--template-file infra/main.bicep \
--parameters resourceGroupName=${{ secrets.RESOURCE_GROUP_NAME }}
- name: Configure Resources
run: |
pwsh scripts/configure-resources.ps1 \
-ResourceGroupName ${{ secrets.RESOURCE_GROUP_NAME }}
The pipeline:
- Triggers on code changes
- Deploys infrastructure automatically
- Configures all resources
- Deploys application code
- Runs tests and validations
All automated. All consistent.
Step 4: Complete Solution Scripting
We scripted every aspect of our solution:
Database Setup Script
# setup-database.ps1
param(
[string]$ResourceGroupName,
[string]$SqlServerName,
[string]$DatabaseName
)
Write-Host "Setting up database..." -ForegroundColor Yellow
# Create database
az sql db create `
--resource-group $ResourceGroupName `
--server $SqlServerName `
--name $DatabaseName `
--service-objective S0
# Run schema scripts
$schemaFiles = Get-ChildItem -Path "database/schemas" -Filter "*.sql"
foreach ($file in $schemaFiles) {
Write-Host " Executing: $($file.Name)" -ForegroundColor Cyan
az sql db execute `
--resource-group $ResourceGroupName `
--server $SqlServerName `
--database-name $DatabaseName `
--file-path $file.FullName
}
Write-Host "Database setup complete!" -ForegroundColor Green
Monitoring Configuration Script
# setup-monitoring.ps1
param(
[string]$ResourceGroupName,
[string]$AppServiceName
)
Write-Host "Configuring monitoring..." -ForegroundColor Yellow
# Enable Application Insights
$appInsights = az monitor app-insights component create `
--app $AppServiceName `
--location swedencentral `
--resource-group $ResourceGroupName `
--application-type web `
--query "instrumentationKey" -o tsv
# Configure App Service to use Application Insights
az webapp config appsettings set `
--resource-group $ResourceGroupName `
--name $AppServiceName `
--settings "APPINSIGHTS_INSTRUMENTATIONKEY=$appInsights"
Write-Host "Monitoring configured!" -ForegroundColor Green
Security Configuration Script
# setup-security.ps1
param(
[string]$ResourceGroupName,
[string]$KeyVaultName
)
Write-Host "Configuring security..." -ForegroundColor Yellow
# Create Key Vault
az keyvault create `
--name $KeyVaultName `
--resource-group $ResourceGroupName `
--location swedencentral `
--enabled-for-deployment true `
--enabled-for-template-deployment true
# Add secrets
$secrets = @{
"StorageAccountKey" = (az storage account keys list --account-name $storageAccountName --query "[0].value" -o tsv)
"DatabaseConnectionString" = $databaseConnectionString
"ApiKey" = $apiKey
}
foreach ($secret in $secrets.GetEnumerator()) {
az keyvault secret set `
--vault-name $KeyVaultName `
--name $secret.Key `
--value $secret.Value
}
Write-Host "Security configured!" -ForegroundColor Green
Every aspect of our solution is scripted:
- Infrastructure provisioning
- Resource configuration
- Database setup
- Monitoring setup
- Security configuration
- Application deployment
Step 5: One-Command Deployment
With everything scripted, we can deploy the entire solution with a single command:
# deploy-all.ps1
param(
[string]$Environment = "dev",
[string]$SubscriptionId = ""
)
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Complete Solution Deployment" -ForegroundColor Cyan
Write-Host "Environment: $Environment" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Step 1: Deploy Infrastructure
Write-Host "Step 1: Deploying Infrastructure..." -ForegroundColor Yellow
& ".\scripts\deploy-infrastructure.ps1" `
-ResourceGroupName "rg-acdc-$Environment" `
-SubscriptionId $SubscriptionId
# Step 2: Configure Resources
Write-Host "`nStep 2: Configuring Resources..." -ForegroundColor Yellow
& ".\scripts\configure-resources.ps1" `
-ResourceGroupName "rg-acdc-$Environment"
# Step 3: Setup Database
Write-Host "`nStep 3: Setting up Database..." -ForegroundColor Yellow
& ".\scripts\setup-database.ps1" `
-ResourceGroupName "rg-acdc-$Environment" `
-SqlServerName "sql-acdc-$Environment" `
-DatabaseName "db-acdc-$Environment"
# Step 4: Configure Monitoring
Write-Host "`nStep 4: Configuring Monitoring..." -ForegroundColor Yellow
& ".\scripts\setup-monitoring.ps1" `
-ResourceGroupName "rg-acdc-$Environment" `
-AppServiceName "app-acdc-$Environment"
# Step 5: Configure Security
Write-Host "`nStep 5: Configuring Security..." -ForegroundColor Yellow
& ".\scripts\setup-security.ps1" `
-ResourceGroupName "rg-acdc-$Environment" `
-KeyVaultName "kv-acdc-$Environment"
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "Deployment Complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Run this script, and the entire solution is deployed. Infrastructure, configuration, database, monitoring, security, everything.
The Benefits: From Chaos to Control
Before Scripting
- Manual configuration: Hours spent clicking through portals
- Inconsistent deployments: Each environment different
- Error-prone: Easy to miss steps or misconfigure
- Slow: Days to deploy a new environment
- Unreproducible: Can’t recreate exact configurations
After Scripting
- Automated deployment: Single command deploys everything
- Consistent environments: Same configuration every time
- Error-free: Scripts prevent configuration mistakes
- Fast: Minutes to deploy a new environment
- Reproducible: Version-controlled infrastructure
The Technical Win
Our infrastructure as code approach provides:
Version Control: All infrastructure definitions in Git. Track changes, review diffs, rollback if needed.
Reproducibility: Deploy the same infrastructure anywhere, dev, test, production. Identical every time.
Automation: CI/CD pipelines deploy automatically. No manual intervention needed.
Documentation: Infrastructure code documents itself. Bicep templates show exactly what resources exist.
Testing: Test infrastructure changes before deploying. Validate templates, run pre-deployment checks.
Collaboration: Team members can review infrastructure changes through pull requests.
What We Scripted
Our complete solution includes:
- Azure Resources: Storage accounts, App Services, SQL databases, Key Vaults, Application Insights
- Networking: Virtual networks, subnets, network security groups, load balancers
- Security: Key Vault secrets, managed identities, role assignments, firewall rules
- Monitoring: Application Insights, Log Analytics, alerts, dashboards
- CI/CD: Azure DevOps pipelines, GitHub Actions, deployment automation
- Configuration: App settings, connection strings, environment variables
Everything is scripted. Nothing is manual.
The Code Repository
All our infrastructure code is version-controlled:
infra/main.bicep– Main infrastructure templateinfra/modules/– Reusable Bicep modulesscripts/deploy-infrastructure.ps1– Infrastructure deployment scriptscripts/configure-resources.ps1– Resource configuration scriptscripts/setup-database.ps1– Database setup scriptscripts/setup-monitoring.ps1– Monitoring configuration scriptscripts/setup-security.ps1– Security configuration scriptscripts/deploy-all.ps1– Complete deployment orchestrationazure-pipelines.yml– CI/CD pipeline definition
We can deploy our entire solution to any environment with a single command.
The Business Impact
Before infrastructure as code:
- New environment: 2-3 days of manual work
- Configuration errors: Common, hard to debug
- Deployment failures: Frequent, time-consuming to fix
- Environment drift: Configurations diverge over time
After infrastructure as code:
- New environment: 15 minutes automated deployment
- Configuration errors: Caught by validation, prevented by scripts
- Deployment failures: Rare, easy to debug with logs
- Environment consistency: Identical configurations everywhere
We went from manual chaos to automated control.

What’s Next
Infrastructure as code opens new possibilities. The foundation is set, we have complete infrastructure automation. Everything else builds on this.
This infrastructure as code approach demonstrates the power of scripting. By defining all Azure resources in Bicep templates, orchestrating deployments with PowerShell scripts, and automating everything through CI/CD pipelines, we’ve created a solution that can be deployed anywhere, anytime, with complete consistency and reliability.