Insight Tech APAC Blog Logo

Anti-Pattern 9: Not doing software development lifecycle (SDLC) practices with IaC

trentsteenholdt
December 11, 2024

16 minutes to read

Azure Bicep Anti-Patterns

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.