Azure and DevOps: from a commit to production

How a change travels from a commit to production safely: CI and CD, Azure DevOps pipelines, supply-chain scans and SBOMs, Docker images, Azure building blocks, Cloudflare tunnels, releases with migrations first and automatic rollback, backups and OpenTelemetry - with pictures, generic code and interview answers.

In short

The craft that gets code safely into production is DevOps. CI builds and tests every change. CD keeps every good build ready to release. The servers, databases and networks are run so that a bad release can be undone. {{brand}} uses Azure DevOps pipelines, Docker images, a managed PostgreSQL database and a Cloudflare tunnel.

After this lesson you can 1. Read a YAML pipeline. 2. Explain how scans and an SBOM protect the supply chain. 3. Build a small, safe Docker image. 4. Describe a release with migrations first and automatic rollback.

Words you need

CI and CD: what each one promises

Teams mix up three terms. They are three levels of trust in automation: integration, delivery and deployment, and each one adds a promise. Knowing which one you have tells you how risky a Friday release is.

CI works only when branches are short-lived. In trunk-based development everyone merges small changes to the main branch at least daily, and branch policies (a required green build, a required reviewer) protect it. A long-lived branch turns every merge into a small project of its own.

Ship what you tested The thing you tested must be the thing you ship. The safest way is to build once and promote that same artifact through every environment. If you rebuild, build from the exact same commit and run the checks again, because a rebuild can pick up new package versions and fresh base-image fixes.

Azure DevOps pipelines, from the inside

Azure DevOps is a hosted suite: repositories, work boards, test plans and Pipelines. A pipeline is a YAML file that lives in the repository next to the code. Because it is code, it is reviewed, versioned and rolled back like code. The service reads the file; it does not run your steps itself. Agents run them.

Three kinds of expression look alike and run at different moments. A template expression is filled in when the YAML is compiled, a macro just before a step runs, and a runtime expression while the run executes. Mixing them up is the commonest YAML surprise.

Agents dial out A self-hosted agent opens an outgoing HTTPS connection to Azure DevOps and asks for work, so the target needs no open inbound port for deployments. This is safer than letting a cloud service log in to it.

Secrets in pipelines Secret variables are masked in logs, but masking only matches the exact text. Never print them, never put them in a script argument that is echoed, and never pass them as Docker build arguments. Prefer a service connection or a managed identity, which hold no secret you could leak.

Docker images and multi-stage builds

An image is a read-only stack of layers: an operating-system base, then your files, each step of the Dockerfile adding a layer. A container is a process started from an image, with its own file system and network, sharing the host's kernel. That is why it starts in a second and why the same image behaves the same everywhere.

A multi-stage build uses several FROM lines in one Dockerfile. The first stage has everything needed to build: compilers, package managers, test tools. The last stage starts again from a small base and copies in only the finished output. The build tools never reach production.

A tag moves; a digest does not node:22-alpine today and node:22-alpine next month are different images. When an image is pinned by digest, the build is repeatable and a poisoned re-tag cannot slip in. When you pin, put the update in your routine: a weekly report and a reviewed change that moves the pin.

Build arguments are not secret Values passed with --build-arg are recorded in the image history. Values compiled into a web bundle, such as an API address, end up in files every visitor downloads. Treat both as public. For a real secret at build time use BuildKit secret mounts.

Compose describes a group of containers on one machine. Two networks work like firewall rules: only the container that meets visitors sits on both sides.

The options in the market

{{brand}} supports both through a setting: Application Insights where Azure is the home, or any OpenTelemetry collector where it is not.

What {{brand}} chose, and why

{{brand}} chose the simplest setup that is still safe and repeatable. Azure DevOps holds the code and runs the pipelines. Docker images run on Azure. The database is a managed PostgreSQL service, files live in object storage, and a tunnel fronts the site. Each of these is explained in the sections below; come back here after reading them to see how they fit together. The reasons:

The trade-off: you own more of the operating work, in return for a setup you understand end to end and a bill that is easy to predict.

When another option is right Choose Kubernetes when many services and teams need standard scheduling. Choose managed containers when traffic is spiky and you want scaling without machines. Choose serverless for event-driven glue. And once you have more than one environment, describe your infrastructure as code (Bicep or Terraform, see the section on infrastructure as code) instead of clicking in a portal.

Environments, approvals and the release pipeline

A release is a different job from a build. A build runs on every change and may fail often. A release changes production and should happen only on purpose. So the two are separate pipelines, or at least separate stages, joined by an artifact and a gate.

The YAML also has strategy: settings for a deployment job: runOnce does it once, rolling updates a few targets at a time, and canary sends a small share first. Which one fits depends on how many copies of the app you run.

Approval is a control, not a ceremony An approval only means something if the approver sees what will change: the version, the commit, the scan results. Put those in the run's title and tags. Remember, too, that the person who pushed the code should not be the only person who can approve it.

Supply-chain security: scans, SBOMs and trusting your tools

Most of the code in a product is not yours. It comes from packages, base images and build tools. A supply-chain attack goes after those: a poisoned package, a name that looks like a popular one, a stolen publisher account, a tampered base image, a compromised build tool. You cannot read every line you depend on, so you automate the checking and keep a record.

Scan again later An SBOM turns a security alert into a question you can answer in seconds: does any of our images contain the broken package? A scanner can read an old SBOM against today's advisories. Without the list, you must rebuild and hope.

Your scanner is software too Security tools run with wide access. Tags of a popular scanner have been replaced with malicious builds in the past. The lesson is general: pin tools by digest or checksum, look the digest up from a trusted source, and move the pin through a reviewed change.

A short policy keeps this honest. A fixable high or critical finding fails the build. An unfixable one is written down with a reason and a date to look again. A red scheduled run is treated as a security finding, not as noise.

Azure building blocks

Azure is a catalogue of services. A small production setup needs only a few. Knowing what each one is for - and which parts are yours to look after - is what interviewers test.

Encrypted is not the same as verified In many database drivers the require TLS mode encrypts the connection but does not check who answered. Use verify-full (or your driver's equivalent) so the certificate and the server name are both checked.

The same idea removes passwords between Azure parts. A managed identity lets a machine prove who it is by asking Azure's metadata service, a link-local address reachable only from that machine, for a short-lived token. You then grant that identity a role on the target, for example *Storage Blob Data Contributor*. This is role-based access control.

Key Vault is for what cannot be avoided: third-party API keys, signing keys, certificates. It keeps secrets, keys and certificates apart, logs every access, and supports soft delete. A common pattern is to load the vault into the application's configuration at start, so the code only reads settings. Secret names allow letters, digits and dashes, so double dashes stand in for the colons of nested settings.

Infrastructure as code

Infrastructure as code means the machines, networks and databases are described in text files kept in the repository, not created by clicking in a portal. A tool reads the files and makes the cloud match them. The result is reviewable, repeatable and recoverable: you can rebuild an environment from the repository instead of from memory.

State and secrets A Terraform state file can contain secrets in clear text. Store it in a protected remote location, and never commit it to the repository.

From visitor to server: Cloudflare, tunnels and private networks

The classic way to publish a site is to open ports 80 and 443 on a server and point DNS at it. Then the whole internet can reach your machine directly, including scanners and floods. A tunnel turns the connection around.

Lock the origin A tunnel moves trust to the edge. Make the origin refuse anything that did not come through the tunnel, and trust a forwarded visitor address only from a known source. Otherwise the origin can be fooled about who is calling.

The rule that connects this section to the last one: expose as little as possible. Aim for this shape: the app reached through a tunnel, the database on a private address, storage reached by identity, and secrets never in the repository.

A release without drama: migrations first, health checks, rollback

A release is risky for two reasons: the new code might be wrong, and the database has to change at the same time. The calm way is to make both reversible, in small steps, with a check at the end.

Step 5 only works if the old code runs on the new database. That is the rule behind expand and contract: change a schema in a way that two neighbouring versions can both live with.

Databases do not roll back like code Running a migration backwards often loses data, so the safe rollback is old code on the new, additive schema. If a bad release damaged data, the way back is a restore to a time before the release (see backups below). A good habit is to write down the time just before a release. A test that scans migrations and fails on a drop or a rename turns the rule from a promise into a gate.

Use a startup probe for apps that take a minute to warm up, so liveness does not kill them too early.

Why must liveness not look at the database? Imagine the database is down for a minute. If liveness checked it, the platform would restart every copy of the app at once, and the app would then fail to start: a restart storm. Readiness may check the database: not ready means "send no traffic here", which is exactly right.

A health gate with automatic rollback works with every strategy above; it is the part that makes any of them safe.

Backups and restore drills

Managed PostgreSQL on Azure keeps this change log for a retention period that can be as long as 35 days, and it always restores to a new server. That makes a practice restore safe: nothing existing is overwritten.

A backup you never restored is a hope Restore drills find the broken script, the missing key and the expired password while it is cheap to fix them. Run one before launch, after changes to the setup, and on a calendar.

Logs, traces and OpenTelemetry

When something is slow or broken at 3 a.m. you need to answer three questions. What happened? How often? Where did the time go? Each has its own kind of data, and together they are called observability.

Do not log what you should not keep Log the route template (/orders/{id}), not the raw address, because tokens in links end up in logs. Log a user id, never an email. Cut query strings from exported spans, and scrub errors reported by browsers, which cannot be trusted to clean them. Log levels follow the status class: 2xx as information, 4xx as warnings, 5xx as errors, and probes quietly at debug.

Application Insights is Azure's way to receive all this. A library in the app collects requests, dependencies and exceptions with almost no code. Adaptive sampling thins busy periods. A daily cap on the workspace stops a surprise bill. {{brand}} lets an operator choose: nothing, Application Insights, or any OpenTelemetry collector.

Try it yourself: a small image, a scan and a health-gated rollback

You need Docker, and optionally Trivy. Nothing here costs money and it all runs on a laptop. By the end you will have seen the whole release loop in miniature.

Common mistakes

Interview questions

Related