← Blog

Deploying to CloudHub 2.0 from Azure Pipelines: build once, approve, verify

Reference: templates/jobs/deploy.yml · deployment/ · ADRs 5 and 6

The last part of the flow takes a version that is already in Anypoint Exchange and puts it in front of users: test from develop, then uat and production from each release. Three ideas carry it: build once, approve where the environment is defined, and verify every deployment.

Build once, deploy from Exchange

CloudHub 2.0 deploys applications from Exchange. That makes “build once” natural: the SNAPSHOT from develop and the release from main are published once, and every deployment refers to them by version.

The deployment job never rebuilds. It uses the Mule Maven plugin’s deploy goal with a placeholder artifact:

mvn $(MAVEN_ARGS) -q versions:set -DnewVersion="${APP_VERSION}" -DgenerateBackupPoms=false
mvn $(MAVEN_ARGS) mule:deploy -Dmule.artifact=dummy.jar -DmuleDeploy \
  -Ddeploy.environment="${ANYPOINT_ENV}" \
  -Ddeploy.applicationName="$(y .applicationName)" \
  -Ddeploy.target="$(y .target)" \
  -Ddeploy.runtimeVersion="$(y .runtimeVersion)" \
  -Ddeploy.replicas="$(y .replicas)" \
  -Ddeploy.vCores="$(y .vCores)" \
  -Ddeploy.clientId="${ANYPOINT_CLIENT_ID}" \
  -Ddeploy.clientSecret="${ANYPOINT_CLIENT_SECRET}"

versions:set pins the POM to the version being deployed, and -Dmule.artifact=dummy.jar stops the plugin from packaging: CloudHub 2.0 fetches that version from Exchange. The log confirms it — two minutes, no compilation, Artifact mulesoft-orders-api-uat deployed. uat and production receive exactly the artifact the release produced.

The POM’s cloudhub2Deployment block reads deploy.* properties, and each environment’s values live in the application repository:

# deployment/prod.yaml
applicationName: mulesoft-orders-api-prod
target: Cloudhub-US-East-2
runtimeVersion: 4.9.0
replicas: 1
vCores: 0.1
healthUrl: https://mulesoft-orders-api-prod-aslzjy.5sc6y6-2.usa-e2.cloudhub.io/api/health

Sizing production is now a reviewed change in a pull request, not a click in Runtime Manager. The app also receives mule.env, which selects config-<env>.yaml.

Promotion: test, uat, prod

Each deployment is a deployment job targeting an Azure DevOps environment:

- deployment: deploy
  environment: ${{ parameters.environment }}
  strategy:
    runOnce:
      deploy:
        steps: ...

On main the stages chain: release, then uat, then prod. The version comes from the release stage’s output variable, so prod cannot deploy anything other than what uat received:

- stage: deploy_prod
  dependsOn: [release, deploy_uat]
  variables:
    appVersion: $[ stageDependencies.release.release.outputs['release.appVersion'] ]

Approvals where the environment lives

The prod environment in Azure DevOps has an Approvals check: named approvers, instructions (“check the uat deployment before approving”) and a timeout. When the pipeline reaches prod, it stops. The approver gets an e-mail and approves in the portal; the run records who approved and when.

Putting the check on the environment rather than in YAML means an application cannot opt out of it, and the rule is configured once for every pipeline that deploys to prod.

Approvals can also be given from Slack or Microsoft Teams through the Azure Pipelines apps, which post the pending approval to a channel with Approve and Reject buttons. Both apps require an Azure DevOps organisation backed by Microsoft Entra ID — they reject personal Microsoft accounts, which is why the reference project approves by e-mail and portal. Most company organisations use Entra ID, so in practice this is rarely a blocker, but it is worth knowing before promising it to a team.

Verify every deployment

“Deployed” in the Runtime Manager log means the platform accepted the application, not that it answers correctly. Each deployment ends with a smoke test against the app’s health endpoint:

url=$(yq -r '.healthUrl // ""' "deployment/${ANYPOINT_ENV}.yaml")
for attempt in $(seq 1 20); do
  body=$(curl -fsS -m 10 "$url" || true)
  status=$(echo "$body" | jq -r '.status // empty' 2>/dev/null || true)
  env=$(echo "$body" | jq -r '.environment // empty' 2>/dev/null || true)
  if [[ "$status" == "UP" && "$env" == "${ANYPOINT_ENV}" ]]; then
    echo "Healthy: $body"; exit 0
  fi
  sleep 15
done
exit 1

Checking the reported environment, not just the status, catches a quiet mistake: a deployment that “worked” but points at the wrong environment’s configuration.

What a release looked like

The first release of the reference application, from the pipeline’s timeline:

Stage Result
Build succeeded
Release succeeded — tag v1.0.0, main moved to 1.1.0-SNAPSHOT, two [skip ci] commits
Deploy uat succeeded
Approval approved
Deploy prod succeeded

It took one hotfix to get there — the path filter and the push problem from the earlier articles — which is the most realistic part of the exercise.

Checklist

  • Is every environment deployed from the same published artifact?
  • Is environment sizing in the repository and reviewed in pull requests?
  • Are approvals configured on the environment, so no pipeline can skip them?
  • Does each deployment prove the application answers, from the right environment?