Anti-Pattern 9: Not doing software development lifecycle (SDLC) practices with IaC
Trent Steenholdt
December 11, 2024
16 minutes to read
Here is the ninth and final instalment of the series on Azure Bicep anti-patterns!
In this post, we’re focusing on the critical mistake of not applying Software Development Lifecycle (SDLC) practices to your Infrastructure as Code (IaC). While IaC is “just code”, many teams and engineers neglect essential disciplines—such as CI/CD, testing, version control, code reviews, and promoting changes through multiple environments—that are standard in application development.
Infrastructure code should never be treated any different to application code!
By failing to incorporate these SDLC practices, you risk creating unmaintainable infrastructure and encountering unnecessary deployment failures. This generally always ends up with accrual of technical debt that is never reduced.
Let’s explore some key anti-patterns and illustrate how to avoid them.
1. Skipping CI/CD Pipelines
Bad Practice: Deploying IaC manually from your local machine or via ad hoc scripts. Yep, you’d be surprised just how often this still happens even in large organisations and enterprises!
Impact: Inconsistent environments and a higher likelihood of human errors creep in. Without a consistent pipeline, it’s difficult to maintain predictable outcomes across Dev, Test, and Prod.
Solution:
- Always implement CI/CD pipelines that automatically, at a minimum, build, and deploy your IaC.
Tip: If this is a steep learning curve for you, start small and build Rome later. E.g. Your linting and tests can come in once you have experience with CI/CD tooling but always still build and deploy your IaC using CI/CD tooling; you’ll thank me later. - Use tools like Azure DevOps, GitHub Actions, or Octopus Deploy to ensure every commit triggers a predictable and auditable deployment process.
2. Avoiding Automated Testing
Bad Practice: Treating IaC as if it doesn’t need testing beyond “apply and see what breaks”.
Impact: Without testing, you discover misconfigurations way too late, likely impacting production and end-users. Issues like incorrect resource properties or missing dependencies can go unnoticed until runtime.
Solution:
- Integrate testing frameworks like Terratest (Go-based), PSRule (PowerShell-based) or Pester (PowerShell-based) to run unit and integration tests on your IaC.
- Validate that resources are configured correctly, policies are enforced (not to be confused with Azure Policy), and dependencies are satisfied before production deployments.
3. Neglecting Version Control or simply misunderstanding “Git”
Bad Practice: Cloud Engineers without software development experience are notorious in the industtry for pushing commits directly to main
, skipping branching and pull requests.
Impact: No audit trail, difficulty in rolling back changes, and a higher risk of introducing unreviewed or misconfigured infrastructure.
Solution:
- Adopt Git best practices: feature branches, pull requests (PRs), and protected
main
branches.
Tip: Don’t know how to do this? Start here and play the Git game here. - Require that all changes are peer-reviewed before merging.
- Use tags for marking key releases or deployments, ensuring traceability. Your IaC has release history, generally coupled to when you add a new service or resource
4. Skipping quality Code Reviews
Bad Practice: Making infrastructure changes without a second set of trained eyes.
Impact: Misconfigurations and security oversights slip into production. You miss opportunities to improve naming conventions, parameter usage, and general code quality.
Too often Cloud Engineers will peer review code in nanoseconds with the typical ‘LGTM’ comment… Don’t be that person in your organisation.
Solution:
- Require peer reviews for all changes. Always ensure that person understands the code you have written; if they don’t, they cannot approve it. Don’t let them!
- Automate linting and checks (e.g., Superlinter) in your PR pipeline so reviewers focus on higher-level concerns.
- Make your code format be consistent between Cloud Engineers integrated development environments (IDE) by ensuring they open code using features like Code Workspaces in VSCode.
Example 1: Code Workspace File in VSCode.
Using this file in VSCode will ensure defaultFormatter
are the same for files types in each user that makes change to the code. Complimented with .vscode/extensions.json
, the Code Workspace file will ensure no format is differnt between engineers ever again!
{
"folders": [
{
"path": "./",
"name": "EXP Analytics Platform"
},
{
"name": "As-Built Documentation",
"path": "./docs/wiki/AsBuilt"
},
{
"name": "High Level Design Documentation",
"path": "./docs/wiki/highleveldesign"
},
{
"name": "Wiki",
"path": "./docs/wiki/"
}
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
".idea": true
},
"powershell.codeFormatting.autoCorrectAliases": true,
"powershell.codeFormatting.newLineAfterCloseBrace": false,
"powershell.codeFormatting.preset": "Stroustrup",
"powershell.cwd": "EXP Analytics Platform",
"dotnet.defaultSolution": "disable",
"[powershell]": {
"editor.defaultFormatter": "ms-vscode.powershell"
},
"[bicep]": {
"editor.defaultFormatter": "ms-azuretools.vscode-bicep"
},
"[bicep-params]": {
"editor.defaultFormatter": "ms-azuretools.vscode-bicep"
}
}
}
Example 2: Example GitHub Action PR pipeline
Below is a GitHub Actions workflow snippet that runs on every pull request to main
. It demonstrates good SDLC practices by running lint checks, validating markdown links, performing IaC policy checks (via PSRule), and updating documentation. This ensures that all changes introduced through PRs are fully validated before merging, aligning perfectly with the goals of code reviews and CI/CD integration:
---
name: PR
on:
pull_request:
branches:
- main
concurrency:
group: pr
cancel-in-progress: false
env:
PSRULE_DIRECTORY: ./
INFRA_WORKING_DIRECTORY: ./
jobs:
linting:
name: Lint Testing
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
statuses: write
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Lint Testing
uses: super-linter/super-linter@v7
env:
DEFAULT_BRANCH: 'main'
GITHUB_TOKEN: $
VALIDATE_JSON: true
VALIDATE_MARKDOWN: true
VALIDATE_POWERSHELL: true
VALIDATE_YAML: true
FILTER_REGEX_EXCLUDE: .*/(src/modules/[^/]+|docs/wiki/(Bicep|PS-Rule|Scripts|Pricing|Policy|Firewall)).*\.md$
markdown-links:
name: Validate Markdown Links
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Check Links in Markdown Files
uses: gaurav-nelson/github-action-markdown-link-check@1.0.15
with:
config-file: '.github/linters/markdown-link-check.json'
check-modified-files-only: 'yes'
use-verbose-mode: 'yes'
use-quiet-mode: 'yes'
base-branch: main
run-psrule-tests-modules:
name: Run PSRule Tests [Modules]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: PSRule >> Modules
continue-on-error: true
uses: ./.github/actions/ps-rule
with:
option: 'ps-rule.yaml'
bicepPath: 'src/modules'
path: $
run-psrule-tests-orchestration:
name: Run PSRule Tests [Orchestration]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: PSRule >> Orchestration
continue-on-error: true
uses: ./.github/actions/ps-rule
with:
option: 'ps-rule.yaml'
bicepPath: 'src/orchestration'
path: $
PS-Docs:
name: Generate Documentation
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout Pull Request
run: |
echo "==> Check out Pull Request"
gh pr checkout $
env:
GITHUB_TOKEN: $
- name: Configure Git
run: |
git config user.name github-actions
git config user.email action@github.com
- name: Run ./scripts/Set-Documentation.ps1
uses: azure/powershell@v2
with:
inlineScript: |
Write-Information "==> Installing needed modules..." -InformationAction Continue
Install-Module -Name powershell-yaml -Force -SkipPublisherCheck
Install-Module -Name PSDocs -Force -SkipPublisherCheck
Install-Module -Name PSDocs.Azure -Force -SkipPublisherCheck
Write-Information "==> Running script..." -InformationAction Continue
./scripts/Set-Documentation.ps1 -PricingCSVFilePaths @()
azPSVersion: 'latest'
- name: Run ./scripts/Set-DocumentationforVending.ps1
uses: azure/powershell@v2
with:
inlineScript: |
Write-Information "==> Running script..." -InformationAction Continue
./scripts/Set-DocumentationforVending.ps1 -GitHub
azPSVersion: 'latest'
- name: Check for changes
id: git_status
run: |
CHECK_GIT_STATUS=($(git status -s))
git status -s
echo "changes=${#CHECK_GIT_STATUS[@]}" >> $GITHUB_OUTPUT
- name: Git Commit & Push ($)
if: steps.git_status.outputs.changes > 0
run: |
git config core.autocrlf false
git add --all
git commit -m 'Generation of documentation - $ (automated)'
git push
update_release_draft:
name: Update Release Draft
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
with:
config-name: actions/release-drafter/config.yaml
env:
GITHUB_TOKEN: $
5. Deploying without proper Environment Flow
Bad Practice: Directly deploying infrastructure changes to higher environments like Production without validating them in lower environments like Development and Test.
I’m not sure why this one is not followed, but I have seen time and time again in the industry, again at even larger organisations that should know better, pipelines that allow deployment straight in Production without going through lower environments.
Impact: Undiscovered issues emerge in Production, risking downtime and chaotic emergency fixes. Dirty clickops is usually how it’s fixed too.
Solution:
- Adopt an environment promotion strategy. Deploy changes to Development first, run tests, then promote to Test, and finally to Production.
- This staged approach ensures you catch issues early and gain confidence with each step.
Example: CI/CD pipeline for Dev
, Test
and Prod
Below is a GitHub Actions workflow snippet to demonstrate a flow from Development (dev_exp
) to Test (tst_exp
), and finally to Production (prd_exp
). Notice how each step depends on the previous environment’s successful deployment, ensuring a proper flow:
---
name: Release EXP Subscription Vending
on:
push:
branches:
- main
workflow_dispatch: {}
env:
PSRULE_DIRECTORY: ./
INFRA_WORKING_DIRECTORY: ./
concurrency:
group: release_EXP
jobs:
linting:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Lint Testing
uses: super-linter/super-linter@v7
env:
VALIDATE_JSON: true
VALIDATE_MARKDOWN: true
VALIDATE_POWERSHELL: true
VALIDATE_YAML: true
build:
runs-on: ubuntu-latest
needs: [linting]
steps:
- name: Check Out Repository
uses: actions/checkout@v4
- name: Bicep Build
uses: Azure/cli@v2.1.0
with:
azcliversion: 2.63.0
inlineScript: |
az bicep build -f ./src/orchestration/main.bicep --stdout
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: infrastructure
path: ./
deploy_dev:
name: EXP Development Deployment
uses: ./.github/workflows/deploy.yml
needs: build
with:
artifactName: infrastructure
environmentName: 'dev_exp'
location: $
managementGroupId: $
templateFile: ./src/orchestration/main.bicep
templateParameterFile: ./src/configuration/sub-exp-dev-01.bicepparam
moduleName: 'sub-exp-dev-01'
secrets: inherit
deploy_tst:
name: EXP Test Deployment
uses: ./.github/workflows/deploy.yml
needs: deploy_dev
with:
artifactName: infrastructure
environmentName: 'tst_exp'
location: $
managementGroupId: $
templateFile: ./src/orchestration/main.bicep
templateParameterFile: ./src/configuration/sub-exp-tst-01.bicepparam
moduleName: 'sub-exp-tst-01'
secrets: inherit
deploy_prd:
name: EXP Production Deployment
uses: ./.github/workflows/deploy.yml
needs: deploy_tst
with:
artifactName: infrastructure
environmentName: 'prd_exp'
location: $
managementGroupId: $
templateFile: ./src/orchestration/main.bicep
templateParameterFile: ./src/configuration/sub-exp-prd-01.bicepparam
moduleName: 'sub-exp-prd-01'
secrets: inherit
In this example, code changes are built and packaged after linting and testing. Then they move across the various environments with simple Apporval Gates to ensure the code must be approved to run by the right people.
This structured flow ensures that by the time you reach Production, the code has been thoroughly validated in real, controlled environments.
Conclusion
By integrating SDLC best practices into your IaC workflows—such as automated CI/CD pipelines, testing, strict version control, code reviews, and environment promotion strategies—you treat IaC with the same rigour as application code. This reduces the risk of outages, improves maintainability, and builds confidence in your infrastructure deployments.
Wrap up of the Anti-Pattern Series
Thank you for checking out my series as this is the last post. I hope you were able to take some learnings from these anti-patterns and ensure that your next Azure Bicep IaC project doesn’t fall for the same traps.