Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

name: API CI

on:
push:
branches:
- main
- feature/production-ready-api
pull_request:
branches:
- main

permissions:
contents: read

jobs:
build:
name: Validate and build API
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: src/requirements.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r src/requirements.txt

- name: Check Python syntax
run: |
python -m compileall -q src/app src/migrations

- name: Run backend tests
working-directory: ./src
run: python -m pytest -v

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build API Docker image
uses: docker/build-push-action@v6
with:
context: ./src
push: false
tags: album-api:ci
88 changes: 88 additions & 0 deletions .github/workflows/publish-azure.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Publish API to Azure ACR

on:
push:
branches:
- main

permissions:
contents: read
id-token: write

jobs:
publish:
name: Build and publish API image
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Login to Azure using OIDC
uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

- name: Login to Azure Container Registry
run: az acr login --name ca7e04894508acr

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push API image
uses: docker/build-push-action@v6
with:
context: ./src
push: true
tags: |
ca7e04894508acr.azurecr.io/album-api:latest
ca7e04894508acr.azurecr.io/album-api:${{ github.sha }}

deploy:
name: Deploy API to Azure Container Apps
runs-on: ubuntu-latest
needs: publish

steps:
# The template lives in the deployment repo, which owns all IaC.
- name: Checkout deployment repository
uses: actions/checkout@v4
with:
repository: JacobusTheOne/album-app-deployment
token: ${{ secrets.DEPLOY_REPO_TOKEN }}
path: deployment

- name: Login to Azure using deployment identity
uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_DEPLOY_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

# Deploying the template rather than `az containerapp update --image`
# keeps Bicep the single source of truth: an imperative image update
# would be rolled back by the next deployment, and would never apply
# the probes or identity configuration the template declares.
- name: Deploy API with Bicep
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
az deployment group create \
--resource-group rg-azure-container-apps-dev \
--template-file deployment/infra/api.bicep \
--parameters apiImageTag='${{ github.sha }}' \
databaseUrl="$DATABASE_URL"

# az deployment needs no extension, but az containerapp does.
- name: Install Container Apps CLI extension
run: az extension add --name containerapp --upgrade

- name: Verify deployment
run: |
az containerapp show \
--name album-api \
--resource-group rg-azure-container-apps-dev \
--query "{image:properties.template.containers[0].image,latestRevision:properties.latestRevisionName}" \
--output table
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
.LSOverride

# Icon must end with two \r
Icon
Icon


# Thumbnails
._*
Expand Down Expand Up @@ -226,3 +227,12 @@ cython_debug/

# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)


# Local SQLite databases
*.db
*.db-shm
*.db-wal

.venv/
__pycache__/
.pytest_cache/
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,62 @@ This backend Album API sample is available in other languages:

| [C#](https://github.com/azure-samples/containerapps-albumapi-csharp) | [Go](https://github.com/azure-samples/containerapps-albumapi-go) | [JavaScript](https://github.com/azure-samples/containerapps-albumapi-javascript) | [Java](https://github.com/azure-samples/containerapps-albumapi-java) |
| -------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- |

---

## API Endpoints

The API exposes health and readiness endpoints for monitoring and deployment orchestration.

### `GET /health`

Checks whether the API application is running and responding.

Successful response:

HTTP `200 OK`

```json
{
"status": "healthy"
}
```

This endpoint does not check database connectivity. It is intended for liveness checks.

### `GET /ready`

Checks whether the API is ready to serve requests by executing a simple database query (`SELECT 1`).

When the database is available:

HTTP `200 OK`

```json
{
"status": "ready"
}
```

When the database is unavailable:

HTTP `503 Service Unavailable`

```json
{
"detail": "database unavailable"
}
```

This endpoint is intended for readiness checks. A replica should not receive application traffic when its database dependency is unavailable.

### Why separate health and readiness checks?

An API process can be running while its database connection is unavailable.

Separating these checks allows the platform to distinguish between:

- **Liveness:** Is the application process responding?
- **Readiness:** Can the application currently serve requests?

This is particularly useful when Azure Container Apps starts new replicas, restarts containers, or handles temporary database connectivity issues.
1 change: 1 addition & 0 deletions src/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
**/*.db
README.md
42 changes: 39 additions & 3 deletions src/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,43 @@
FROM python:3.11

# Use a lightweight Python 3.11 image.
FROM python:3.11-slim

WORKDIR /code

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Install Microsoft ODBC Driver 18 for SQL Server.
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates curl; \
. /etc/os-release; \
DEBIAN_VERSION="${VERSION_ID%%.*}"; \
curl -fsSLo /tmp/packages-microsoft-prod.deb \
"https://packages.microsoft.com/config/debian/${DEBIAN_VERSION}/packages-microsoft-prod.deb"; \
dpkg -i /tmp/packages-microsoft-prod.deb; \
rm /tmp/packages-microsoft-prod.deb; \
apt-get update; \
ACCEPT_EULA=Y apt-get install -y --no-install-recommends \
msodbcsql18 \
libgssapi-krb5-2; \
rm -rf /var/lib/apt/lists/*

# Install Python dependencies.
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy application source.
COPY . .

# Create a non-root user and prepare the database directory.
RUN useradd --create-home appuser \
&& mkdir -p /data \
&& chown -R appuser:appuser /code /data

USER appuser

EXPOSE 8080
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

# Initialize the database, seed it, then start FastAPI.
CMD ["sh", "-c", "alembic upgrade head && python -m app.init_db && exec uvicorn app.main:app --host 0.0.0.0 --port 8080"]
Loading