---
name: azure-pipeline-docker
description: Extends azure-devops-pipeline for Docker image builds and pushes. Handles buildx with layer caching, Trivy scanning, ECR and ACR login, and a git-SHA/tag tagging strategy. Always load azure-devops-pipeline first.
---

## What I add

Type-specific steps for Docker image pipelines. Merge these into the skeleton from `azure-devops-pipeline`.

## Additional required inputs — ask the user

1. **Registry type** — `ECR` | `ACR`
2. **Registry URL** — e.g. `123456789.dkr.ecr.us-east-1.amazonaws.com` or `myregistry.azurecr.io`
3. **Image repository name** — e.g. `myapp/api`
4. **Dockerfile path** — default `./Dockerfile`
5. **AWS region** — required if ECR
6. **AWS service connection name** — required if ECR
7. **ACR service connection name** — required if ACR

## Lint stage steps

```yaml
- script: |
    docker run --rm -i hadolint/hadolint < <dockerfile-path>
  displayName: "Lint — hadolint Dockerfile"
```

## Security scan stage steps

The security scan builds the image locally and runs Trivy against it **before** pushing. This ensures vulnerabilities are caught pre-push.

```yaml
- script: |
    docker build \
      -t scan-target:$(Build.SourceVersion) \
      -f <dockerfile-path> \
      .
    docker run --rm \
      -v /var/run/docker.sock:/var/run/docker.sock \
      aquasec/trivy:latest image \
      --exit-code 1 \
      --severity HIGH,CRITICAL \
      --format json \
      --output trivy-results.json \
      scan-target:$(Build.SourceVersion)
  displayName: "Security scan — Trivy"
- task: PublishBuildArtifacts@1
  inputs:
    pathToPublish: trivy-results.json
    artifactName: security-scan
  condition: always()
  displayName: "Publish Trivy results"
```

Note: `condition: always()` on the publish step ensures results are available even when Trivy exits 1. The `--exit-code 1` on the scan step itself still fails the pipeline on HIGH/CRITICAL findings.

## Build stage steps

### Step order — always emit in this order

1. Registry login
2. docker buildx build + push

### Registry login — ECR

```yaml
- script: |
    aws ecr get-login-password --region <aws-region> | \
      docker login --username AWS --password-stdin <registry-url>
  displayName: "Login — ECR"
  env:
    AWS_DEFAULT_REGION: <aws-region>
# Wire the OIDC service connection at the job level, not inside the script step.
# In the job or deployment job that contains this step, set:
#
# job: Build
#   pool: EKS-Pool
#   container: {}    # omit if not containerised
#   services:
#     ...
#
# For OIDC federation, the AWSCLI task approach is preferred.
# Alternatively, wrap with AWSShellScript@1:
#
# - task: AWSShellScript@1
#   inputs:
#     awsCredentials: <aws-service-connection-name>
#     regionName: <aws-region>
#     scriptType: inline
#     inlineScript: |
#       aws ecr get-login-password --region <aws-region> | \
#         docker login --username AWS --password-stdin <registry-url>
#   displayName: "Login — ECR (via service connection)"
```

AWS credentials come from the OIDC service connection configured on the job — do not add any `AWS_ACCESS_KEY_ID` or `AWS_SECRET_ACCESS_KEY` env vars.

### Registry login — ACR

```yaml
- task: Docker@2
  inputs:
    command: login
    containerRegistry: <acr-service-connection-name>
  displayName: "Login — ACR"
```

### Build and push — nonprod

```yaml
- script: |
    docker buildx create --use --name pipeline-builder 2>/dev/null || \
      docker buildx use pipeline-builder
    docker buildx build \
      --cache-from type=registry,ref=<registry-url>/<image-repo>:cache \
      --cache-to   type=registry,ref=<registry-url>/<image-repo>:cache,mode=max \
      --tag <registry-url>/<image-repo>:$(Build.SourceVersion) \
      --tag <registry-url>/<image-repo>:latest \
      --file <dockerfile-path> \
      --push \
      .
  displayName: "Build and push — nonprod"
```

### Build and push — prod

```yaml
- script: |
    docker buildx create --use --name pipeline-builder 2>/dev/null || \
      docker buildx use pipeline-builder
    docker buildx build \
      --cache-from type=registry,ref=<registry-url>/<image-repo>:cache \
      --cache-to   type=registry,ref=<registry-url>/<image-repo>:cache,mode=max \
      --tag <registry-url>/<image-repo>:$(Build.SourceBranchName) \
      --tag <registry-url>/<image-repo>:$(Build.SourceVersion) \
      --file <dockerfile-path> \
      --push \
      .
  displayName: "Build and push — prod"
```

## Tagging strategy

| Tier    | Tags applied                                      |
|---------|---------------------------------------------------|
| Nonprod | `<git-sha>`, `latest`                             |
| Prod    | `<git-tag / branch-name>`, `<git-sha>`            |

Never tag prod images as `latest`.

## Hard rules for Docker

- Always use `docker buildx` — never plain `docker build`
- Trivy scan must run before push — the scan in SecurityScan stage uses a locally built image, not a registry pull
- `--exit-code 1` on Trivy is non-negotiable — HIGH and CRITICAL findings must fail the pipeline
- Never tag prod images as `latest` — prod tags use `$(Build.SourceBranchName)` and `$(Build.SourceVersion)` only
- Build args containing secrets must come from ADO variables injected via `env:` — never hardcoded in YAML
- Registry layer cache lives in the registry itself (not ADO pipeline cache) for reproducibility across EKS-Pool agents
- ECR login uses OIDC credentials only — never hardcode `AWS_ACCESS_KEY_ID` or `AWS_SECRET_ACCESS_KEY`
- The `docker buildx create --use ... || docker buildx use ...` pattern is required to handle re-use across runs without error
