5 minute read

Automating Docker image builds with ACR and seamless deployments to ACI

62E98897-FFE5-4FB1-9CC7-18A5C0983419_1_105_c

74DA60FD-1D13-418E-BDCA-3222B082DA2B_1_105_c



I recently picked up the book “Learning DevOps and MLOps by Doing: A Hands-on Guide with Azure, AWS, and GCP”. (Korean title: 따라하며 배우는 DevOps, MLOps). It’s designed as a practical guide where you learn by following along, building real pipelines, and experimenting directly in the cloud. What I like most about this book is that it doesn’t just stay theoretical—it walks through concrete implementations of CI/CD and MLOps across the three major cloud platforms: Azure, AWS, and GCP.

In this blog series, I’ll review the book and share my own takeaways as I follow along with the exercises. For today’s post, I’ll start with Azure—covering how to set up CI/CD pipelines, deploy applications with Azure Container Registry and Azure Container Instances, and connect everything seamlessly with GitHub Actions.

Stay tuned as I dive into the Azure chapter and highlight both what I learned and how it relates to real-world MLOps practices.


Summarized Concept

  • CI: On each push → build Docker image → push to Azure Container Registry (ACR).
  • CD: After build succeeds → deploy that image to Azure Container Instances (ACI).


0) Detailed Explanation

  • CI: Push to ACR
    • ACR acts as a private Docker registry; you’ll need to create one inside your Azure subscription.
    • Authentication is handled using a Service Principal (client ID + secret) or with admin credentials.
      • Service Principal: It is a security credential that enables applications, services, or automation tools, such as GitHub Actions, to manage resources using the Azure API. It is linked to an application in Azure AD, which has specific permissions to Azure resources. This is necessary when you want applications or services to automatically access resources without requiring users to log in directly to perform tasks (for example, when using automation tools like CI/CD pipelines).
    • Use GitHub Actions to build (docker build) and push (docker push) the image.
  • CD: Deploy that image to ACI

    • ACI provides serverless containers: we don’t manage VMs, just run our containers.
    • Make sure the Microsoft.ContainerInstance resource provider is registered in our subscription.
    • Deployment requires passing ACR credentials (registry-login-server, username, password) so ACI can pull the private image.
    • You must explicitly expose the application port (e.g., 8000) in both the Dockerfile (EXPOSE 8000) and the deployment configuration (ports: 8000).
    • By default, ACI assigns a public IP address if you set ip-address: Public; otherwise, it can only be accessed internally.


1) Prerequisites (one-time setup)

1.1 Create Resource Group + Register ACR
  • A Resource Group (RG) is a logical container for Azure resources.
  • Example resource group: devops-RG in eastus
  • Example ACR: devopsimages01 → login server: devopsimages01.azurecr.io
    • What ACR Does
      • Stores container images (Docker images you build locally or in CI/CD).
      • Works like Docker Hub, but private, secure, and integrated with Azure.
      • Pulls images into Azure services such as: Azure Container Instances (ACI), Azure Kubernetes Service (AKS)
1.2 Create a Service Principal (SP) with rights
# Get ACR resource ID
REGISTRY_NAME=devopsimages01
RESOURCE_GROUP=devops-RG
REGISTRY_ID=$(az acr show -n $REGISTRY_NAME -g $RESOURCE_GROUP --query id -o tsv)

# Create SP and grant AcrPush role on this ACR
az ad sp create-for-rbac \
  --name "sp-acr-push-${REGISTRY_NAME}" \
  --role "AcrPush" \
  --scopes "$REGISTRY_ID" \
  -o json

Copy the appId (clientId), password (clientSecret), and tenant.

  • To deploy to ACI, the SP usually also needs Contributor rights on the subscription or resource group.
1.3 Register ACI resource provider (mandatory)
az provider register --namespace Microsoft.ContainerInstance
1.4 Add GitHub repo secrets

Go to Repo → Settings → Secrets and variables → Actions → New repository secret:

  • AZURE_CREDENTIALS: JSON object like:
{
  "clientId": "<appId>",
  "clientSecret": "<password>",
  "subscriptionId": "<subscriptionId>",
  "tenantId": "<tenant>",
  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
  "resourceManagerEndpointUrl": "https://management.azure.com/",
  "activeDirectoryGraphResourceId": "https://graph.windows.net/",
  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
  "galleryEndpointUrl": "https://gallery.azure.com/",
  "managementEndpointUrl": "https://management.core.windows.net/"
}
  • REGISTRY_LOGIN_SERVER → e.g. devopsimages01.azurecr.io
  • REGISTRY_USERNAME → usually the appId
  • REGISTRY_PASSWORD → the password
  • RESOURCE_GROUPdevops-RG


2) Application + Dockerfile

Repo structure:

repo/
├─ Dockerfile
├─ src/
│  └─ main.py   # app = FastAPI(...)
└─ .github/
   └─ workflows/
      └─ cd-azure.yaml
  • Dockerfile placement: Keep it at the repository root so that docker build . works without extra flags. If it’s in a subfolder, you’ll need -f path/to/dockerfile.

Dockerfile:

FROM python:3.11-slim
WORKDIR /mlops
COPY . .
RUN pip install --no-cache-dir --upgrade pip \
 && pip install --no-cache-dir fastapi "uvicorn[standard]"
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
  • COPY . .: Copies the entire repository into the container image. Use a .dockerignore file to exclude unnecessary files (.git, __pycache__, etc.).
  • For larger projects, it’s best to maintain a requirements.txt file and install from it (this improves caching).


3) GitHub Actions CI Workflows

.github/workflows/ci.yaml

name: Python application CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Set up Python 3.10
      uses: actions/setup-python@v3
      with:
        python-version: "3.10"

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest
        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

    - name: Lint with flake8
      run: |
        # Check only for basic syntax errors
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
        # Run a full style check
        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

    - name: Test with pytest
      env:
        PYTHONPATH: .
      run: |
        python -m pytest tests
  • on: Triggers run on two events.
    • push to the main branch.
    • pull_request into the main branch.
  • It uses flake8 for linter and pytest for testing framework.
    • Runs flake8 twice:
      • First, strict mode: checks only for fatal syntax/logic errors (E9, F63, etc.).
      • Second, full style check: reports warnings and style issues but doesn’t fail the workflow (--exit-zero).
    • Runs pytest:
      • Runs all tests in the tests folder with pytest.
      • If any test fails, the workflow fails.

4) CD Workflows

name: fastAPI app development to ACI

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout GitHub Action'
        uses: actions/checkout@v4

      - name: 'Login via Azure CLI'
        uses: azure/login@v1
        with:
          creds: $

      - name: 'ACR Login'
        uses: azure/docker-login@v1
        with:
          login-server: $
          username: $
          password: $

      - name: 'Build and Push Image'
        run: |
          docker build . -t $/fastapiapp:$
          docker push $/fastapiapp:$

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: 'Login via Azure CLI'
        uses: azure/login@v1
        with:
          creds: $

      - name: 'Deploy to Azure Container Instances'
        uses: azure/aci-deploy@v1
        with:
          resource-group: $
          dns-name-label: $$
          image: $/fastapiapp:$
          registry-login-server: $
          registry-username: $
          registry-password: $
          name: aci-fastapiapp
          location: eastus


4) Usage

  • Direct deploy on push to main
git add .
git commit -m "ci/cd: build to ACR & deploy to ACI"
git push origin main

→ Workflow runs automatically.

  • PR test first
git checkout -b chore/update-pipeline
# edit files...
git add .
git commit -m "ci: fix pipeline"
git push -u origin chore/update-pipeline
# open PR → Actions runs → merge into main → deploy



Leave a comment