Skip to content

Commit 1be7294

Browse files
committed
feat(automation): add Azure Automation module with scheduled runbook for AKS and PostgreSQL startup
1 parent 3b15665 commit 1be7294

File tree

10 files changed

+534
-0
lines changed

10 files changed

+534
-0
lines changed

deploy/001-iac/automation/main.tf

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* # Azure Automation Standalone Configuration
3+
*
4+
* Deploys Azure Automation Account with scheduled runbook to start
5+
* AKS cluster and PostgreSQL server every morning.
6+
* Uses data sources to reference existing platform infrastructure.
7+
*/
8+
locals {
9+
resource_group_name = coalesce(var.resource_group_name, "rg-${var.resource_prefix}-${var.environment}-${var.instance}")
10+
aks_cluster_name = coalesce(var.aks_cluster_name, "aks-${var.resource_prefix}-${var.environment}-${var.instance}")
11+
postgresql_name = coalesce(var.postgresql_name, "psql-${var.resource_prefix}-${var.environment}-${var.instance}")
12+
}
13+
14+
data "azurerm_resource_group" "this" {
15+
name = local.resource_group_name
16+
}
17+
18+
data "azurerm_kubernetes_cluster" "this" {
19+
name = local.aks_cluster_name
20+
resource_group_name = local.resource_group_name
21+
}
22+
23+
data "azurerm_postgresql_flexible_server" "this" {
24+
count = var.should_start_postgresql ? 1 : 0
25+
name = local.postgresql_name
26+
resource_group_name = local.resource_group_name
27+
}
28+
29+
// ============================================================
30+
// Automation Module
31+
// ============================================================
32+
33+
module "automation" {
34+
source = "../modules/automation"
35+
36+
// Core variables
37+
environment = var.environment
38+
resource_prefix = var.resource_prefix
39+
location = var.location
40+
instance = var.instance
41+
tags = {}
42+
43+
resource_group = data.azurerm_resource_group.this
44+
45+
// Dependencies from data sources
46+
aks_cluster = data.azurerm_kubernetes_cluster.this
47+
48+
postgresql_server = var.should_start_postgresql ? data.azurerm_postgresql_flexible_server.this[0] : null
49+
50+
// Automation configuration
51+
schedule_config = var.schedule_config
52+
runbook_script_path = "${path.module}/scripts/Start-AzureResources.ps1"
53+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* # Automation Deployment Outputs
3+
*
4+
* Outputs from standalone automation deployment.
5+
*/
6+
7+
/*
8+
* Automation Account Outputs
9+
*/
10+
11+
output "automation_account" {
12+
description = "Automation account resource details including id, name, and principal_id"
13+
value = module.automation.automation_account
14+
}
15+
16+
/*
17+
* Runbook Outputs
18+
*/
19+
20+
output "runbook" {
21+
description = "Runbook resource details"
22+
value = module.automation.runbook
23+
}
24+
25+
/*
26+
* Schedule Outputs
27+
*/
28+
29+
output "schedule" {
30+
description = "Schedule resource details including name, week_days, and timezone"
31+
value = module.automation.schedule
32+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<#
2+
.SYNOPSIS
3+
Starts Azure resources (PostgreSQL and AKS) for daily operations.
4+
.DESCRIPTION
5+
This runbook starts a PostgreSQL Flexible Server first, waits for it
6+
to become available, then starts an AKS cluster.
7+
Uses system-assigned managed identity for authentication.
8+
.PARAMETER ResourceGroupName
9+
The resource group containing the resources.
10+
.PARAMETER PostgresServerName
11+
The PostgreSQL Flexible Server name (optional, empty string to skip).
12+
.PARAMETER AksClusterName
13+
The AKS cluster name.
14+
#>
15+
16+
param(
17+
[Parameter(Mandatory = $true)]
18+
[string]$ResourceGroupName,
19+
20+
[Parameter(Mandatory = $false)]
21+
[string]$PostgresServerName,
22+
23+
[Parameter(Mandatory = $true)]
24+
[string]$AksClusterName
25+
)
26+
27+
$ErrorActionPreference = 'Stop'
28+
Disable-AzContextAutosave -Scope Process | Out-Null
29+
30+
try {
31+
Write-Output "=========================================="
32+
Write-Output "Start Azure Resources Runbook"
33+
Write-Output "=========================================="
34+
Write-Output "Resource Group: $ResourceGroupName"
35+
Write-Output "PostgreSQL: $($PostgresServerName ? $PostgresServerName : '(not configured)')"
36+
Write-Output "AKS Cluster: $AksClusterName"
37+
Write-Output ""
38+
39+
Write-Output "Connecting to Azure using managed identity..."
40+
$AzureContext = (Connect-AzAccount -Identity).Context
41+
Set-AzContext -SubscriptionId $AzureContext.Subscription.Id | Out-Null
42+
Write-Output "Connected to subscription: $($AzureContext.Subscription.Name)"
43+
44+
# Start PostgreSQL first (dependency for AKS workloads)
45+
if ($PostgresServerName -and $PostgresServerName -ne "") {
46+
Write-Output ""
47+
Write-Output "Checking PostgreSQL Flexible Server '$PostgresServerName' status..."
48+
$pgServer = Get-AzPostgreSqlFlexibleServer -ResourceGroupName $ResourceGroupName -Name $PostgresServerName
49+
50+
if ($pgServer.State -eq "Stopped") {
51+
Write-Output "Starting PostgreSQL Flexible Server..."
52+
Start-AzPostgreSqlFlexibleServer -ResourceGroupName $ResourceGroupName -Name $PostgresServerName
53+
54+
# Poll for Ready state with timeout
55+
$maxWaitSeconds = 300
56+
$pollInterval = 15
57+
$elapsed = 0
58+
while ($elapsed -lt $maxWaitSeconds) {
59+
$pgServer = Get-AzPostgreSqlFlexibleServer -ResourceGroupName $ResourceGroupName -Name $PostgresServerName
60+
if ($pgServer.State -eq "Ready") {
61+
Write-Output "PostgreSQL server is now Ready."
62+
break
63+
}
64+
Write-Output "PostgreSQL state: $($pgServer.State). Waiting..."
65+
Start-Sleep -Seconds $pollInterval
66+
$elapsed += $pollInterval
67+
}
68+
if ($pgServer.State -ne "Ready") {
69+
Write-Warning "PostgreSQL did not reach Ready state within $maxWaitSeconds seconds. Current state: $($pgServer.State)"
70+
}
71+
}
72+
elseif ($pgServer.State -eq "Ready") {
73+
Write-Output "PostgreSQL server is already running."
74+
}
75+
else {
76+
Write-Warning "PostgreSQL server is in unexpected state: $($pgServer.State)"
77+
}
78+
}
79+
80+
# Start AKS cluster
81+
Write-Output ""
82+
Write-Output "Starting AKS cluster '$AksClusterName'..."
83+
Start-AzAksCluster -Name $AksClusterName -ResourceGroupName $ResourceGroupName
84+
Write-Output "AKS cluster start initiated."
85+
86+
Write-Output ""
87+
Write-Output "=========================================="
88+
Write-Output "Resource startup completed successfully!"
89+
Write-Output "=========================================="
90+
}
91+
catch {
92+
Write-Error "Runbook failed: $_"
93+
throw
94+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* # Automation Deployment Variables
3+
*
4+
* Input variables for standalone automation deployment.
5+
*/
6+
7+
/*
8+
* Core Variables - Required
9+
*/
10+
11+
variable "environment" {
12+
type = string
13+
description = "Environment for all resources in this module: dev, test, or prod"
14+
}
15+
16+
variable "location" {
17+
type = string
18+
description = "Location for all resources in this module"
19+
}
20+
21+
variable "resource_prefix" {
22+
type = string
23+
description = "Prefix for all resources in this module"
24+
}
25+
26+
/*
27+
* Core Variables - Optional
28+
*/
29+
30+
variable "instance" {
31+
type = string
32+
description = "Instance identifier for naming resources: 001, 002, etc"
33+
default = "001"
34+
}
35+
36+
variable "resource_group_name" {
37+
type = string
38+
description = "Existing resource group name (Otherwise 'rg-{resource_prefix}-{environment}-{instance}')"
39+
default = null
40+
}
41+
42+
/*
43+
* Resource Override Variables - Optional
44+
*/
45+
46+
variable "aks_cluster_name" {
47+
type = string
48+
description = "Override AKS cluster name (Otherwise 'aks-{resource_prefix}-{environment}-{instance}')"
49+
default = null
50+
}
51+
52+
variable "postgresql_name" {
53+
type = string
54+
description = "Override PostgreSQL server name (Otherwise 'psql-{resource_prefix}-{environment}-{instance}')"
55+
default = null
56+
}
57+
58+
/*
59+
* Automation Configuration - Optional
60+
*/
61+
62+
variable "schedule_config" {
63+
type = object({
64+
start_time = string
65+
week_days = list(string)
66+
timezone = string
67+
})
68+
description = "Schedule configuration for startup runbook including start time (HH:MM), week days, and timezone"
69+
default = {
70+
start_time = "08:00"
71+
week_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
72+
timezone = "UTC"
73+
}
74+
}
75+
76+
variable "should_start_postgresql" {
77+
type = bool
78+
description = "Whether to include PostgreSQL in the startup sequence"
79+
default = true
80+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
terraform {
2+
required_providers {
3+
azurerm = {
4+
source = "hashicorp/azurerm"
5+
version = ">= 4.51.0"
6+
}
7+
}
8+
required_version = ">= 1.9.8, < 2.0"
9+
}
10+
11+
provider "azurerm" {
12+
storage_use_azuread = true
13+
partner_id = "acce1e78-0375-4637-a593-86aa36dcfeac"
14+
features {}
15+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* # Azure Automation Module
3+
*
4+
* Creates an Azure Automation Account with a scheduled PowerShell runbook
5+
* for automated startup of AKS clusters and PostgreSQL servers.
6+
*/
7+
locals {
8+
automation_account_name = "aa-${var.resource_prefix}-${var.environment}-${var.instance}"
9+
runbook_name = "Start-AzureResources"
10+
schedule_name = "morning-startup"
11+
location = coalesce(var.location, var.resource_group.location)
12+
}
13+
14+
// ============================================================
15+
// Automation Account
16+
// ============================================================
17+
18+
resource "azurerm_automation_account" "this" {
19+
name = local.automation_account_name
20+
location = local.location
21+
resource_group_name = var.resource_group.name
22+
sku_name = "Basic"
23+
24+
identity {
25+
type = "SystemAssigned"
26+
}
27+
28+
tags = var.tags
29+
}
30+
31+
// ============================================================
32+
// Runbook
33+
// ============================================================
34+
35+
resource "azurerm_automation_runbook" "start_resources" {
36+
name = local.runbook_name
37+
location = local.location
38+
resource_group_name = var.resource_group.name
39+
automation_account_name = azurerm_automation_account.this.name
40+
runbook_type = "PowerShell72"
41+
log_verbose = true
42+
log_progress = true
43+
description = "Starts PostgreSQL and AKS cluster for morning operations"
44+
45+
content = file(var.runbook_script_path)
46+
47+
tags = var.tags
48+
}
49+
50+
// ============================================================
51+
// Schedule
52+
// ============================================================
53+
54+
resource "azurerm_automation_schedule" "morning_startup" {
55+
name = local.schedule_name
56+
resource_group_name = var.resource_group.name
57+
automation_account_name = azurerm_automation_account.this.name
58+
frequency = "Week"
59+
interval = 1
60+
timezone = var.schedule_config.timezone
61+
start_time = timeadd(timestamp(), "24h")
62+
week_days = var.schedule_config.week_days
63+
description = "Start AKS and PostgreSQL every morning at ${var.schedule_config.start_time} ${var.schedule_config.timezone}"
64+
65+
lifecycle {
66+
ignore_changes = [start_time]
67+
}
68+
}
69+
70+
// ============================================================
71+
// Job Schedule
72+
// ============================================================
73+
74+
resource "azurerm_automation_job_schedule" "start_resources" {
75+
resource_group_name = var.resource_group.name
76+
automation_account_name = azurerm_automation_account.this.name
77+
schedule_name = azurerm_automation_schedule.morning_startup.name
78+
runbook_name = azurerm_automation_runbook.start_resources.name
79+
80+
// Parameter keys MUST be lowercase (Azure API normalization)
81+
parameters = {
82+
resourcegroupname = var.resource_group.name
83+
postgresservername = var.postgresql_server != null ? var.postgresql_server.name : ""
84+
aksclustername = var.aks_cluster.name
85+
}
86+
}
87+
88+
// ============================================================
89+
// RBAC Assignments
90+
// ============================================================
91+
92+
resource "azurerm_role_assignment" "aks_contributor" {
93+
scope = var.aks_cluster.id
94+
role_definition_name = "Azure Kubernetes Service Contributor Role"
95+
principal_id = azurerm_automation_account.this.identity[0].principal_id
96+
principal_type = "ServicePrincipal"
97+
skip_service_principal_aad_check = true
98+
}
99+
100+
resource "azurerm_role_assignment" "postgresql_contributor" {
101+
count = var.postgresql_server != null ? 1 : 0
102+
103+
scope = var.postgresql_server.id
104+
role_definition_name = "Contributor"
105+
principal_id = azurerm_automation_account.this.identity[0].principal_id
106+
principal_type = "ServicePrincipal"
107+
skip_service_principal_aad_check = true
108+
}

0 commit comments

Comments
 (0)