You ever get that feeling when you're full of ambition, caffeine, and a false sense of DevOps confidence? Like, βYeah, Iβll deploy SCEPman with Terraform, how hard can it be?β Spoiler alert: it's that hard. Let me take you on the tragicomic journey of how I tried to automate SCEPman deployment β and instead deployed pain.
π§ Context Dump Before the Fire
Let me be clear: I wasnβt new to Terraform. Iβd poked at modules, tweaked variables, written main.tf
files with swagger. But hereβs the thing β I had never done a greenfield deployment before.
Thereβs a galaxy of difference between "following a 'Getting Started' tutorial on terraform.io" and building a real-world, integrated SCEPman setup from scratch, complete with app registrations, Key Vaults, and a healthy dose of Azure chaos. It's like practicing on a driving simulator, then being handed the keys to a Formula 1 car mid-race β and the pit crew only speaks YAML.
βοΈ Prologue: CloudCookβs Grand Plan
It started like all bad ideas: with hope.
Cloudcook, your favorite over-caffeinated cloud alchemist, was handed the noble task of deploying SCEPman, the beloved Intune-friendly PKI-in-the-cloud. And I thought: "Letβs do it right. Letβs Terraform this bad boy. Infrastructure as Code! CI/CD pipelines! GitOps glory!"
So I spun up Azure DevOps, created a new Repo, wrote a main.tf
, and mentally prepared myself for that sweet terraform apply
dopamine hit.
Instead, I got terraform cry
.

π§ The Dream Setup
Hereβs what I wanted:
- SCEPman deployed in a clean, declarative Terraform way.
- Automated App Registrations.
- Key Vault magic.
- Custom domain & TLS certs.
- CI/CD pipeline to keep things nice and tidy.
I even found the official docs and figured: "Hey, they made a module! Itβs gonna be easy!"
Ha. Ha. Ha.
π¦ Step 1: βUse the TerraformInstaller Task,β They Said
So, I used the Azure DevOps Terraform Installer task. But surprise! There are two identically named tasks:
TerraformInstaller is ambiguous.
Specify one of the following identifiers:
- JasonBJohnson.azure-pipelines-tasks-terraform.azure-pipelines-tasks-terraform-installer.TerraformInstaller
- ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller
Which one installs Terraform and which one installs pain? Both were installed as Extensions in Azure DevOps.I picked one at random because YOLO, and moved on. Mistake #1.
Spoiler: It only worked with the Azure DevLabs one.
πͺ¦ Step 2: Storage Account Requiredβ¦ by Whom Exactly?
My pipeline died with:
Error: Input required: backendAzureRmStorageAccountName
Classic Terraform: βWeβre stateless! Except when we need state. Then you better pray to the storage gods.β So I added a storage account. Manually. Because what's Infrastructure-as-Code if not followed by Infrastructure-by-Handβ’?
Still broken.
π§Ώ Step 3: AZ Login β But I Am Logged In?!
Terraform doesnβt trust your login unless it personally watched you type az login
in front of it, while singing the Azure anthem. So it complained:
waiting for the Azure CLI: exit status 1: ERROR: Please run 'az login' to setup account.
Okay cool, except this was running in a pipeline. Not exactly keyboard-friendly.
Eventually, I remembered: service principal.
So I made one. Gave it enough permissions to rewrite our entire tenant. You know, just in case.
Spoiler: The Servicer Prinicpal needs to have Contributor Right on the whole Subscription, Owner on the ResourceGroup and Key Vault Admin on the Resource Group. Trust me - I had to learn it the hard way.
π§± Step 3.5: Oh Right, the Resource Group
One tiny, undocumented landmine I stumbled into: the resource group for SCEPman must already exist.
Yes, Terraform is supposed to create resources, but the module just assumes the resource group is out there, chilling in Azure, waiting to embrace its new contents like a neglected parent.
If you forget this and just hit apply, everything goes kaboom with a cryptic error about nonexistent locations or resource IDs. Ask me how I know. π€‘
π» Step 4: Letβs Talk App Reg Permissions, Shall We?
SCEPman loves a good app reg. But Terraforming an app reg with all the right permissions feels like trying to write a symphony with a kazoo.
You need:
- Microsoft Graph API permissions
- Key Vault access policies
- The patience of a Buddhist monk
One wrong permission? Boom. Deployment fails silently or weirdly. Youβll stare at the logs like you're reading ancient Sumerian.
π¨ Final Boss: Certificates and Key Vault Mayhem
SCEPman wants a certificate in your Key Vault. Fair.
Terraform wants to create the cert, reference the secret, inject it into App Service, and then sacrifice a goat.
Azure meanwhile is like:
βSorry, the certificate you just created isnβt usable because reasons. Please contact your spiritual advisor.β
π΅ The Aftermath
I got SCEPman deployed eventually, but I rage-clicked so many times I think I summoned Clippy from the dead. He popped up like:
βIt looks like you're trying to deploy PKI with Terraform. Would you like to give up?β
The Pipeline ran approx. 80 Times with an Error before it worked - FUN FUN FUN FUN FUN.
π§ Lessons Learned (aka Things Iβll Forget by Next Week)
- Azure DevOps pipelines are allergic to ambiguity.
- Always declare your
azurerm_backend
values or get ghosted. - Don't trust Terraform with your Key Vault unless you've babysat it yourself.
- App registrations via Terraform are a war crime in slow motion.
- SCEPman works great β once it's up.
π₯² TL;DR
If you're trying to deploy SCEPman with Terraform, prepare for:
β
Spiritual growth
β
Painful debugging
β
Massive respect for infra engineers
β
One glorious moment when it actually works
Until then, you're just Terraforming your sanity into oblivion.
βΉοΈThe only thing that matters
The Pipeline:
# .azure-pipelines.yml β deploy SCEPman with Terraform + OIDC
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
trigger:
branches: # run when you push to the scep branch
include: [ scep ]
pool:
vmImage: ubuntu-latest
variables:
# 1) name of your Azure service connection (OIDC-enabled)
azureServiceConnection: "Service Connection Name"
# 2) Terraform CLI version β 1.7.x is the first that bundles Azurerm 3.75+
tfVersion: '1.7.5'
steps:
# βββ 0. Install the exact Terraform version on the agent βββββββββββββββββββββ
- task: TerraformInstaller@1
inputs:
terraformVersion: $(tfVersion)
# OPTIONAL: one-off debug β prove the creds really arrived
- script: |
echo 'ARM_* variables now visible to this job:'
env | grep ^ARM_ || true
displayName: Show ARM_ env vars
# βββ 1. terraform init β inject creds via backendServiceArm βββββββββββββββββββ
- task: TerraformCLI@1
displayName: Terraform init
inputs:
command: init
provider: azurerm # must be present for the task to inject
backendServiceArm: $(azureServiceConnection)
environmentServiceNameAzureRM: $(azureServiceConnection)
# βββ 2. terraform plan β creds injected via environmentServiceNameAzureRM βββββ
- task: TerraformCLI@1
displayName: Terraform plan
inputs:
command: plan
provider: azurerm
environmentServiceNameAzureRM: $(azureServiceConnection)
commandOptions: -out=tfplan
# βββ 3. terraform apply β run the plan file we just created βββββββββββββββββββ
- task: TerraformCLI@1
displayName: Terraform apply
inputs:
command: apply
provider: azurerm
environmentServiceNameAzureRM: $(azureServiceConnection)
commandOptions: tfplan # no -auto-approve β safe, interactive
The Main.tf:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "4.34.0"
}
}
}
provider "azurerm" {
features {}
}
module "scepman" {
source = "scepman/scepman/azurerm"
version = "0.5.0" # pin to a tested version
resource_group_name = var.resource_group
location = var.location
storage_account_name = var.storage_account_name
key_vault_name = var.key_vault_name
law_name = var.law_name
app_service_name_primary = var.app_service_name_primary
app_service_name_certificate_master = var.app_service_name_certificate_maser
service_plan_name = var.service_plan_name
# You can override defaults for Key Vault, App Service SKU, custom domain, etc.
# See all inputs: https://registry.terraform.io/modules/scepman/scepman/azurerm/latest :contentReference[oaicite:2]{index=2}
}
The versions.tf:
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.34.0" # 4.34+ understands OIDC
}
}
}
provider "azurerm" {
features {}
}