There is an uncomfortable asymmetry in what Contoso Airlines has built so far. The bookings website code is versioned, reviewed by two people, built and tested on every change, packaged with a semantic version and deployed with approvals and a rollback that takes seconds. The platform that code runs on — vnet-contoso-pro with its five subnets, app-contoso-reservas-pro with its slot and its Premium v3 plan, sql-contoso-reservas-pro with its private endpoint, kv-contoso-pro, the WAF, the governance policies — exists only because somebody once typed the right commands. There is no history, no review, no rollback and no way to recreate it in North Europe if West Europe disappears.

This lesson closes that gap and, with it, the module. Bicep is Azure's native infrastructure as code language: you describe the desired state of your resources in text files, version them alongside the rest of the code and let Azure Resource Manager — the same ARM from module 1 — take care of making reality match the description.

Contents

  1. Declarative versus imperative, and idempotency
  2. The tooling landscape and why Bicep
  3. Bicep syntax from scratch
  4. Contoso's template step by step
  5. Modules and the private registry
  6. Loops, conditionals and deployment scopes
  7. Deploying: --what-if, modes and validation
  8. The infrastructure pipeline
  9. State, secrets, drift and decompilation
  10. Common Mistakes and Tips
  11. Exercises
  12. Conclusion

  1. Declarative versus imperative, and idempotency

Imperative (the CLI from modules 1-4) Declarative (Bicep)
What you write The steps: create, then configure, then connect The result: this is what must exist
Running it twice Fails, or duplicates, unless you handle it Nothing happens: it is already as it should be
Unknown state You have to work it out with if statements and queries The platform works it out
Readability You read the story, not the result You read the result

The property that makes declarative useful is idempotency: applying the same template once or a hundred times produces exactly the same final state. That changes the nature of infrastructure deployment. It is no longer "a risky operation that has to be run in the right order and exactly once", but "a check that reality matches what is written", which you can run as often as you need. It is the same property that made that idempotent bash script in 01-06 safe, now guaranteed by the platform instead of by your care.

  1. The tooling landscape and why Bicep

ARM (JSON) Bicep Terraform Pulumi
Language Verbose JSON Its own concise DSL HCL General-purpose languages (C#, Python, TS)
Reach Azure only Azure only Multicloud Multicloud
State Azure stores it Azure stores it Its own state file that you have to look after Its own service or self-managed
Azure services on day 1 Yes Yes Delayed by the provider Delayed
Learning curve Steep Gentle Medium Gentle if you already program
Microsoft support Yes Yes, it is the recommendation Not Microsoft's Not Microsoft's

Bicep is not a different product from ARM: it compiles to ARM JSON. It is a syntax layer, which means it adds no translation layer with a lag and that any Azure resource is available the day it ships.

The honest choice: if your organization uses several clouds, Terraform is the reasonable answer and its ecosystem is enormous. If you are Azure only, Bicep wins on three concrete counts — there is no state file to look after, lock or corrupt; new services are available immediately; and Microsoft's support is first-party. Contoso is Azure only and chooses Bicep. ARM templates in JSON still exist as an intermediate format, but nobody writes them by hand any more.

  1. Bicep syntax from scratch

// PARAMETERS: what changes between environments, with types, decorators and defaults
@description('Target environment, which governs names and sizes')
@allowed(['dev', 'pro'])
param environment string
param location string = resourceGroup().location   // Inherits the resource group's
@secure()                                          // Not recorded in the history
param administratorPassword string

// VARIABLES: computed values, not parameterizable from outside
var commonTags = {
  entorno: environment
  proyecto: 'contoso-reservas'
  'centro-coste': 'CC-1042'
  propietario: '[email protected]'
}
// Interpolation and uniqueString, for a globally unique and DETERMINISTIC name
var storageName = 'stcontoso${environment}${uniqueString(resourceGroup().id)}'

// RESOURCE: type@api-version, local symbolic name, and its properties
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageName
  location: location
  tags: commonTags
  sku: { name: environment == 'pro' ? 'Standard_ZRS' : 'Standard_LRS' }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false      // sin-blobs-publicos policy (04-06)
    supportsHttpsTrafficOnly: true    // solo-https policy
    allowSharedKeyAccess: false       // Contoso's decision: managed identity only
  }
}

output blobEndpoint string = storage.properties.primaryEndpoints.blob

The four most-used decorators: @description documents the parameter and appears in the help; @allowed restricts the accepted values and fails at validation, before anything is touched; @secure() marks a parameter as a secret so it is not recorded in the deployment history; and @minValue/@maxValue bound numeric ones. The usual functions are resourceGroup() and subscription() to read the context, uniqueString() to generate deterministic suffixes — the same resource group always produces the same suffix, which preserves idempotency — and ${} interpolation to compose names.

  1. Contoso's template step by step

We extend the previous file with the plan and the application. What matters is the implicit dependency:

var suffix = environment == 'pro' ? 'pro' : 'dev'

resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: 'plan-contoso-reservas-${suffix}'
  location: location
  tags: commonTags
  sku: {
    name: environment == 'pro' ? 'P1v3' : 'B1'   // Premium v3 in production only
    capacity: environment == 'pro' ? 3 : 1
  }
  properties: {
    reserved: true                               // Linux
    zoneRedundant: environment == 'pro'          // Zone redundancy in production
  }
}

resource app 'Microsoft.Web/sites@2023-12-01' = {
  name: 'app-contoso-reservas-${suffix}'
  location: location
  tags: commonTags
  identity: { type: 'SystemAssigned' }           // Managed identity (04-02)
  properties: {
    // By referencing plan.id, Bicep INFERS that the plan must be created first:
    // that is the implicit dependency, and it makes any dependsOn unnecessary.
    serverFarmId: plan.id
    httpsOnly: true
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|8.0'
      healthCheckPath: '/salud'                  // The health endpoint from 05-04
      appSettings: [
        // The secret's VALUE is not here: only a reference to the vault
        { name: 'PasarelaPagoClave', value: '@Microsoft.KeyVault(SecretUri=${gatewaySecretUri})' }
      ]
    }
  }
}

// The preproduccion slot that 05-04 swaps: it only exists in production
resource slot 'Microsoft.Web/sites/slots@2023-12-01' = if (environment == 'pro') {
  parent: app                                    // Explicit parent-child relationship
  name: 'preproduccion'
  location: location
  properties: { serverFarmId: plan.id }
}

Per-environment values are separated into .bicepparam files, which replace the old JSON parameter files:

// pro.bicepparam
using './main.bicep'          // Bound to the template: errors are caught at validation

param environment = 'pro'
param location = 'westeurope'

One template, as many parameter files as environments. That is the answer to "recreate the platform in another region": change one line of the parameter file.

  1. Modules and the private registry

A single file containing the entire platform would be unmanageable. A module is simply another .bicep file invoked from the main one:

// 'name' identifies the nested deployment in the resource group's history
module network 'modulos/modulo-red.bicep' = {
  name: 'deploy-network'
  params: { environment: environment, location: location, addressSpace: '10.20.0.0/16' }
}

module application 'modulos/modulo-app.bicep' = {
  name: 'deploy-app'
  params: {
    environment: environment
    // Consuming another module's output creates the implicit dependency
    integrationSubnetId: network.outputs.appIntegrationSubnetId
  }
}

When modules are shared between teams, they stop living in the repository and move to a private registry, which in Azure is Azure Container Registry — the same service that hosts container images, the subject of 06-01:

az bicep publish --file modulos/modulo-red.bicep \
  --target br:acrcontosopro.azurecr.io/bicep/modulo-red:v1.2.0
// Consume it by its exact version, just like a package from 05-05
module network 'br:acrcontosopro.azurecr.io/bicep/modulo-red:v1.2.0' = {
  name: 'deploy-network'
  params: { environment: environment, location: location }
}

It is the same idea as pipeline templates pinned to a tag (05-03) and versioned packages (05-05): the shared piece has a version and whoever consumes it decides when to update.

  1. Loops, conditionals and deployment scopes

// LOOP: the five subnets of vnet-contoso-pro, generated from a list
var subnets = [
  { name: 'snet-web',             prefix: '10.20.1.0/24' }
  { name: 'snet-app',             prefix: '10.20.2.0/24' }
  { name: 'snet-datos',           prefix: '10.20.3.0/24' }
  { name: 'snet-gestion',         prefix: '10.20.4.0/24' }
  { name: 'snet-integracion-app', prefix: '10.20.5.0/24' }
]

resource network 'Microsoft.Network/virtualNetworks@2023-11-01' = {
  name: 'vnet-contoso-${suffix}'
  location: location
  properties: {
    addressSpace: { addressPrefixes: ['10.20.0.0/16'] }
    subnets: [for s in subnets: {
      name: s.name
      properties: { addressPrefix: s.prefix }
    }]
  }
}

// CONDITIONAL: the SQL private endpoint only exists in production
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (environment == 'pro') {
  name: 'pe-sql-reservas'
  location: location
  properties: { /* snet-datos subnet and connection to the server */ }
}

Not everything is deployed into a resource group. The scope is declared at the top of the file, and this is what lets you take module 4's policies to code:

targetScope = 'subscription'    // Allows creating resource groups and assigning policies

resource networkGroup 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: 'rg-contoso-red-pro'
  location: 'westeurope'
  tags: commonTags
}

// The governance initiative from 04-06, now as reviewable code
resource assignment 'Microsoft.Authorization/policyAssignments@2024-04-01' = {
  name: 'base-gobernanza-contoso'
  properties: {
    displayName: 'Base de gobernanza de Contoso'
    policyDefinitionId: governanceInitiativeId
    enforcementMode: 'Default'
  }
}

The four possible scopes are resourceGroup (the default), subscription, managementGroup — to govern mg-contoso and its children — and tenant. With this, the "Base de gobernanza de Contoso" initiative with its regiones-permitidas, requiere-etiqueta-*, hereda-centro-coste, sin-blobs-publicos, solo-https, tamanos-vm-dev and diagnostico-app-service assignments stops being something somebody configured one day and becomes reviewable code.

  1. Deploying: --what-if, modes and validation

COMMON="--resource-group rg-contoso-reservas-pro --template-file main.bicep \
        --parameters pro.bicepparam"

# 1. Validate: checks syntax, types and permissions WITHOUT changing anything
az deployment group validate $COMMON

# 2. Preview: exactly WHAT would change. The step that prevents disasters.
az deployment group create $COMMON --what-if

# 3. Deploy for real, in incremental mode (the default)
az deployment group create $COMMON --mode Incremental \
  --name deployment-$(date +%Y%m%d-%H%M)

The --what-if output marks each resource with + Create, ~ Modify, - Delete, = NoChange or ! Deploy. Reading it is mandatory: it is the difference between knowing what is going to happen and hoping it goes well.

And now the most serious warning in the lesson. The two deployment modes are not equivalent:

Mode What it does with what is in the group but not in the template
Incremental (default) Leaves it untouched. It only creates or modifies what is declared
Complete DELETES IT. The resource group ends up exactly as the template says

Warning: a Complete mode deployment against rg-contoso-reservas-pro with a template that forgot to declare the database deletes the database, along with its dependent backups. It is the fastest way to cause an irreversible disaster in Azure. Use Incremental unless you know exactly what you are doing, always run --what-if first, and protect critical resources with CanNotDelete locks (01-05), which prevent deletion even in complete mode.

On top of that there is the built-in linter, which warns about bad practices — unused parameters, old API versions, secrets in outputs — and which can be configured in bicepconfig.json so that certain warnings become errors:

az bicep lint --file main.bicep    # Should run in the pipeline and block

  1. The infrastructure pipeline

This is where the whole module converges: the contoso-infra repository gets its own pipeline, with the same guarantees as the code one.

name: infra-$(Date:yyyyMMdd)$(Rev:.r)

trigger:
  branches: { include: [ main ] }
  paths:   { include: [ 'infra/*' ] }
pr:
  branches: { include: [ main ] }

pool: { vmImage: ubuntu-latest }

variables:
  - name: common
    value: '-g rg-contoso-reservas-pro -f infra/main.bicep -p infra/pro.bicepparam'

stages:
  # STAGE 1: also runs on pull requests
  - stage: validate
    jobs:
      - job: analyze
        steps:
          - script: az bicep lint --file infra/main.bicep
            displayName: Bicep linting
          - task: AzureCLI@2
            displayName: Validate and publish the what-if
            inputs:
              azureSubscription: sc-contoso-infra-pro   # Connection with MORE permissions
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                az deployment group validate $(common)
                # The what-if is saved as an artifact so that the reviewer READS it
                az deployment group create --what-if --no-pretty-print $(common) \
                  > $(Build.ArtifactStagingDirectory)/whatif.txt
          - publish: $(Build.ArtifactStagingDirectory)
            artifact: whatif

  # STAGE 2: only from main, and after the approval configured on the environment
  - stage: deploy
    dependsOn: validate
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: apply
        environment: produccion-infra    # This is where Marta Ríos' approval waits
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
                - task: AzureCLI@2
                  inputs:
                    azureSubscription: sc-contoso-infra-pro
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: az deployment group create $(common) --mode Incremental

Notice what has happened. The infrastructure has acquired exactly the same guarantees as the code:

graph LR
    A["Change to<br/>infra/main.bicep"] --> B["Pull request"]
    B --> C["Lint + validate<br/>+ published what-if"]
    C --> D{"Review by<br/>Contoso-Infraestructura<br/>reading the what-if"}
    D -->|approved| E["Squash onto main"]
    E --> F{"Marta's approval<br/>on produccion-infra"}
    F -->|approved| G["az deployment group create<br/>--mode Incremental"]

Note also that the service connection is a different one with more permissions (sc-contoso-infra-pro), rather than widening those of sc-contoso-pro: the one that deploys the application still cannot create networks.

  1. State, secrets, drift and decompilation

State management. Terraform maintains a state file recording which resources it manages, and that file has to be stored, locked to avoid simultaneous writes and protected, because it contains sensitive data; if it gets corrupted or lost, recovering it is painful. Bicep has no state of its own: the state is Azure itself, queried through ARM on every deployment. It is Bicep's most practical operational advantage, and the trade-off is that Bicep only knows about Azure.

What not to put in Bicep. Secrets, never. Not even with @secure(), which only stops the value being recorded but still requires somebody to pass it in. The correct approach is for the template to read the secret from the vault at deployment time:

// Reference to an EXISTING Key Vault, without creating it
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: 'kv-contoso-pro'
  scope: resourceGroup('rg-contoso-seguridad-pro')
}

module database 'modulos/modulo-sql.bicep' = {
  name: 'deploy-sql'
  params: {
    // getSecret can only be used when passing a @secure() parameter to a module:
    // the value never appears in the deployment history or in the logs
    administratorPassword: keyVault.getSecret('sql-admin-clave')
  }
}

Although, as you know from module 4, the best password is the one that does not exist: the application authenticates with a managed identity and the vault only guards what is irreducible.

Configuration drift. This is what happens when somebody changes something from the portal to resolve an emergency and does not carry it back into the template: reality and code stop matching, and the next deployment will revert that change without warning — or worse, will not revert it and nobody will know which is the truth. You detect it by running --what-if on a schedule against production: if the preview shows modifications when nobody has touched the repository, there is drift. Adding that nightly run to the pipeline is the infrastructure equivalent of the nightly build from 05-03. Prevention is the usual: resource locks, read-only permissions in production and the discipline that every change goes through the repository.

Decompiling what already exists. Contoso's entire platform has already been created by hand. The starting point is generated automatically:

# Export the resource group's ARM template and convert it to Bicep
az group export --name rg-contoso-reservas-pro > exported.json
az bicep decompile --file exported.json

The result always has to be cleaned up, and it is worth knowing why: the export produces unreadable symbolic names (resource resource0 ...), embeds literal values where parameters belong, drags in read-only properties that ARM rejects on deployment, pins old API versions and contains hard-coded full resource identifiers. It works as a draft so you do not start from zero, never as a final result. The practical rule: decompile, clean up, run --what-if and do not consider the template good until the what-if returns = NoChange on everything. That is the moment the template faithfully describes what exists.

Common Mistakes and Tips

  • Deploying in Complete mode without reading the what-if. It deletes everything not in the template. It is the most expensive mistake in this lesson.
  • Putting secrets in the template or in the .bicepparam. Both are in the repository. Use getSecret from kv-contoso-pro, or better still, a managed identity and no secret at all.
  • A single giant file, or its opposite, explicit dependsOn everywhere. Use modules by domain — network, application, data, security — and let Bicep infer dependencies from the references: manual dependsOn entries are usually redundant and hide real design errors.
  • Accepting the decompile output as is. It is a draft. Without cleaning it up, it is less maintainable than the original JSON.
  • Living with drift. As soon as an urgent portal change does not make it back into the repository, the template stops being the truth and the whole edifice loses its point.
  • Non-deterministic names. A uniqueString(utcNow()) generates a different name on every run and breaks idempotency: it creates new resources instead of updating the existing ones.
  • Tip: run --what-if on a schedule against production; it is your drift detector and it costs a few agent minutes.
  • Tip: protect resources holding data — databases, key vaults, storage accounts — with CanNotDelete locks. It is your safety net against a template error.

Exercises

Exercise 1: parameterizing for two environments

Write the skeleton of a template for the Contoso Miles exercise project (centro-coste=CC-2077) that deploys an App Service plan and an application, knowing that: in dev the plan is B1 with one instance and in pro it is P1v3 with two and zone redundancy; the four mandatory tags are the ones from module 1; only westeurope and northeurope are allowed; and in pro there must be a preproduccion slot that does not exist in dev.

  1. State which parameters, decorators, variables and conditionals you would use.
  2. What goes in the template and what goes in the .bicepparam files?
  3. How do you guarantee that nobody deploys into a region that is not allowed, and which module 4 mechanism reinforces it?

Exercise 2: reading a what-if and deciding

The infrastructure pipeline shows this preview before deploying to rg-contoso-reservas-pro:

~ Microsoft.Web/serverfarms/plan-contoso-reservas-pro
    ~ sku.capacity: 3 => 1
= Microsoft.Web/sites/app-contoso-reservas-pro
- Microsoft.Sql/servers/sql-contoso-reservas-pro/databases/db-reservas
+ Microsoft.Storage/storageAccounts/stcontosopro7f2a
  1. Interpret each of the four lines.
  2. Which one would make you reject the pull request immediately, and what has most likely caused it?
  3. Which two mechanisms would have stopped that change from ever running?

Exercise 3: taking what was created by hand to code

Contoso wants to turn the rg-contoso-seguridad-pro group into Bicep; it was created by hand in module 4 and contains kv-contoso-pro and log-contoso-pro.

  1. Describe the full procedure, from the first command to the approved template.
  2. How do you know the template faithfully describes what already exists?
  3. Name three specific defects you expect to find in the decompile output.

Solutions

Solution 1:

  1. Parameters: environment with @allowed(['dev','pro']), location with @allowed(['westeurope','northeurope']) and a default of resourceGroup().location, and owner as a string with @description. Variables: commonTags with the entorno, proyecto: 'contoso-millas', centro-coste: 'CC-2077' and propietario tag keys; and suffix derived from environment. Conditionals: the ternary operator for sku.name (environment == 'pro' ? 'P1v3' : 'B1'), for capacity (2 or 1) and for zoneRedundant (environment == 'pro'); and if (environment == 'pro') on the slot resource, so that it does not exist in development.
  2. Everything structural goes in the template: which resources exist, how they relate and the logic that translates the environment into sizes. Only the values that distinguish one environment from another go in the .bicepparam files: environment, location and owner. The proof that the separation is right is that deploying into another region only requires changing one line of the parameter file.
  3. In the template, with @allowed, which fails at the validation phase before anything is touched. But that only protects whoever uses the template: the real reinforcement is the regiones-permitidas policy from the "Base de gobernanza de Contoso" initiative (04-06), with Deny effect, which rejects creation wherever it comes from, including the portal or a stray az command. It is the two-layer principle: the template makes the right thing easy, the policy makes the wrong thing impossible.

Solution 2:

  1. ~ on the plan: it is modified, dropping capacity from 3 instances to 1. = on the application: no changes. - on db-reservas: the database is deleted. +: a new storage account is created.
  2. The - line for db-reservas, without a shadow of doubt: it is the production bookings database. The most likely cause is a Complete mode deployment with a template that does not declare the database, or somebody having removed it from the file while refactoring the modules. Dropping capacity from 3 instances to 1 in production also deserves a question, because it compromises zone redundancy and peak-hour capacity.
  3. First, a CanNotDelete lock on the database, which prevents deletion even if the deployment attempts it. Second, the combination of mandatory review by Contoso-Infraestructura through the path policy from 05-02 and the publication of the what-if in the pull request, which exists precisely so that a human reads this output before approving. As reinforcement, set the mode to Incremental explicitly in the pipeline.

Solution 3:

  1. (a) az group export --name rg-contoso-seguridad-pro > exported.json and az bicep decompile --file exported.json. (b) Clean up: rename the symbols, extract repeated values into parameters and variables, remove read-only properties, update API versions and split into modules. (c) az bicep lint and az deployment group validate. (d) --what-if until it proposes no changes at all. (e) Commit to contoso-infra with a pull request reviewed by Contoso-Infraestructura. (f) Run the infrastructure pipeline with its approval.
  2. Because --what-if returns = NoChange on every resource. As long as any ~ or - appears that nobody asked for, the template does not match reality. That is the objective criterion that the adoption is complete, and it is the same command that will later detect drift.
  3. (a) Unreadable symbolic names of the resource0, resource1 variety. (b) Full resource identifiers hard-coded as literals instead of references and functions, which makes it impossible to reuse the template in another environment or region. (c) Exported read-only properties that ARM rejects on deployment, along with old API versions and values that should be parameters — the location, the names — embedded as literals.

Conclusion

This lesson closes the module, and it is worth seeing how much has changed. You understand the difference between imperative and declarative, and why idempotency turns infrastructure deployment into a repeatable check rather than a risky operation. You have placed Bicep in the landscape honestly — Terraform if there are several clouds, Bicep if you are Azure only, thanks to the state you do not have to look after, services available on day one and first-party support — knowing that Bicep compiles to ARM and that is why it is never late to anything. You have mastered its syntax: param with @allowed, @secure and @description, var, resource, output, resourceGroup(), uniqueString() and interpolation; and you have built Contoso's template step by step, from the lone storage account to the plan, the application and its slot, leaning on the implicit dependencies Bicep infers by itself and separating per-environment values into .bicepparam files — the concrete answer to "recreate everything in North Europe".

You know how to compose with modules and publish them versioned in a private Azure Container Registry, iterate over lists with for, make resources conditional with if and deploy at subscription and management group scopes to take module 4's policies to code. You know how to deploy with a safety net: validate, the --what-if you must always read, the linter, and the warning you must not forget — Complete mode deletes everything not in the template, and CanNotDelete locks are what stand against that. You have built the infrastructure pipeline that validates, publishes the what-if into the pull request so the review is informed, requests approval and applies, with its own service connection separate from the one that deploys the application. And you have covered the remaining flanks: Bicep needs no state file because the state is Azure; secrets do not go in the template but in kv-contoso-pro, read with getSecret, or better still they do not exist because there is a managed identity; configuration drift is caught with a scheduled --what-if; and what was created by hand is decompiled with az bicep decompile, knowing that the result is a draft that is only finished when the what-if says NoChange on everything.

To recap the whole module: Contoso Airlines came in with Diego emailing a ZIP and Marta pasting commands on a Friday night, and leaves with a complete, automated cycle. Work is planned and tracked in Boards, with every commit tied to its work item. Code lives in Repos with short branches, mandatory review and policies that nobody — not even an administrator — can skip. Every change is built, tested and analyzed in Pipelines, producing a single artifact that is built once and deployed many times. That artifact travels through the desarrollo, preproduccion and produccion environments with approvals, deployment windows and a slot swap that makes rollback a matter of seconds. Shared pieces are distributed with versions from Artifacts, with the supply chain protected. And the entire platform — networks, applications, databases, policies included — is now reviewable, versioned and repeatable code. The DORA metrics the module started with finally have a real path to improvement.

What comes next is no longer about how software is delivered, but about what is delivered. Contoso's platform is well built, but it still rests on module 2's pieces, and one of them stands out: the availability engine still lives on vm-motor-disponibilidad-dev, a virtual machine somebody has to patch, size and watch over, and which in production was solved by multiplying instances in a scale set. In module 6, Advanced Azure Services, you will start exactly there: you will package that legacy engine into a container, publish it to Azure Container Registry and take it to Container Apps, and from that starting point the platform takes a leap — orchestration with Azure Kubernetes Service, serverless functions with Azure Functions, integration automation with Logic Apps, message- and event-based architectures with Service Bus, Event Grid and Event Hubs, and finally the Azure AI services that will let Contoso offer things it cannot today. Delivery is solved; now it is time to modernize what gets delivered.

Azure Course

Module 1: Introduction to Azure

Module 2: Core Azure Services

Module 3: Azure Databases

Module 4: Security in Azure

Module 5: Azure DevOps

Module 6: Advanced Azure Services

Module 7: Monitoring and Management

Module 8: Cost Management and Optimization

Module 9: Case Studies and Best Practices

© Copyright 2026. All rights reserved