5.2. Push and Deploy

Push the container image to ACR, write a raw Kubernetes manifest, and deploy the app to the cluster using kubectl — no Pulumi yet, so you can see what raw Kubernetes resources look like.

Step 1 — Push the image to ACR

Log in to the registry and push the image you built in the previous lab. All values come from Pulumi stack outputs — no copy-pasting from the portal.

ACR=$(pulumi stack output registry_name)
ACR_SERVER=$(az acr show --name $ACR --query loginServer -o tsv)

az acr login --name $ACR

docker tag csharp-messages $ACR_SERVER/csharp-messages:latest
docker push $ACR_SERVER/csharp-messages:latest
Apple Silicon (M1/M2/M3)

AKS nodes run on AMD64. If you built the image on an Apple Silicon Mac, rebuild for the correct platform before pushing:

docker build --platform linux/amd64 -t csharp-messages .
docker tag csharp-messages $ACR_SERVER/csharp-messages:latest
docker push $ACR_SERVER/csharp-messages:latest

Confirm the image arrived:

az acr repository list --name $ACR -o table

Explanation

az acr login exchanges your current az CLI identity for a short-lived Docker credential stored in the local Docker credential store — no username or password needed. The AKS node pool can already pull from this registry because the AcrPull role assignment was provisioned by Pulumi in Chapter 3.

CommandPurpose
az acr loginAuthenticates Docker to the registry using your CLI identity
docker tag … :latestCreates a local alias pointing at the ACR login server
docker pushTransfers the image layers to the registry

Step 2 — Write the Kubernetes manifests

Create deployment.yaml inside cloud-native-lab/csharp-app/, replacing YOUR_ACR_LOGIN_SERVER with the value of $ACR_SERVER from the previous step:

apiVersion: v1
kind: Namespace
metadata:
  name: messages-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: messages-app
  namespace: messages-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: messages-app
  template:
    metadata:
      labels:
        app: messages-app
    spec:
      containers:
        - name: messages-app
          image: YOUR_ACR_LOGIN_SERVER/csharp-messages:latest
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: messages-app
  namespace: messages-app
spec:
  selector:
    app: messages-app
  ports:
    - port: 80
      targetPort: 8080

Explanation

A Deployment and a Service are the two objects needed to run and reach any workload in Kubernetes:

ObjectRole
DeploymentDeclares the desired number of pod replicas and the container image to run
ServiceProvides a stable network endpoint that routes traffic to all matching pods

selector.matchLabels on the Deployment and selector on the Service both reference app: messages-app — this label is the glue that connects the three objects (Deployment, Pod, Service) without any explicit reference by name.

The Service maps port 80 → container port 8080. Clients always talk to port 80; the container always listens on 8080 (ASPNETCORE_URLS=http://+:8080 from the Dockerfile).

No DB_CONNECTION_STRING is set, so the app falls back to a local SQLite file inside the pod — enough for smoke-testing the deployment path.


Step 3 — Deploy and verify

Apply the manifest and wait for the pod to become ready.

kubectl apply -f deployment.yaml
kubectl get pods -n messages-app -w

Once STATUS shows Running, check the app is reachable via port-forward:

kubectl port-forward -n messages-app svc/messages-app 8080:80

Open http://localhost:8080, submit a message, and verify it appears in the list. Check the health endpoint:

curl http://localhost:8080/health

Expected response:

{"status":"ok","database":"sqlite","sqliteMode":"default"}

Stop the port-forward with Ctrl-C. Inspect the running pod:

kubectl get pods -n messages-app
kubectl logs -n messages-app deployment/messages-app

Explanation

kubectl apply -f is idempotent — running it again when nothing has changed is safe. The -w flag on get pods streams updates until you interrupt it; use it to watch the pod transition from Pending to Running.

kubectl port-forward -n messages-app svc/messages-app 8080:80 opens a tunnel from your local machine directly to the Service. It does not go through the ingress controller — useful for quick verification without needing DNS or TLS in place.

Data does not survive a pod restart

The SQLite file lives inside the pod’s ephemeral container filesystem. Delete the pod (kubectl delete pod <name>) and the Deployment will schedule a new one — but all submitted messages will be gone. Persistent storage requires an external database, which you will provision in a later step.


Complete files

deployment.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: messages-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: messages-app
  namespace: messages-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: messages-app
  template:
    metadata:
      labels:
        app: messages-app
    spec:
      containers:
        - name: messages-app
          image: YOUR_ACR_LOGIN_SERVER/csharp-messages:latest
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: messages-app
  namespace: messages-app
spec:
  selector:
    app: messages-app
  ports:
    - port: 80
      targetPort: 8080