diff --git a/.gitea/workflows/build_push.yml b/.gitea/workflows/build_push.yml index 3480056..edbd23e 100644 --- a/.gitea/workflows/build_push.yml +++ b/.gitea/workflows/build_push.yml @@ -93,6 +93,7 @@ jobs: platforms: linux/amd64 tags: | git.aridgwayweb.com/armistace/beer-data:latest + git.aridgwayweb.com/armistace/beer-data:${{ github.sha }} # The deploy job is separate from the build job on purpose. The # `KUBEC_CONFIG_BUILDX` secret is cluster-admin-equivalent, and a build @@ -117,6 +118,11 @@ jobs: - name: Install kubectl run: | + # Skip the whole apt dance if the runner already has a kubectl. + if command -v kubectl >/dev/null 2>&1; then + kubectl version --client + exit 0 + fi apt-get update apt-get install -y apt-transport-https ca-certificates curl gnupg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg @@ -131,14 +137,21 @@ jobs: # (beer-db, beer-app) are hand-provisioned and are NOT in the repo, # so they are deliberately not in the apply set. kubectl apply -f kube/beer_namespace.yaml - kubectl apply -f kube/beer-deployment.yaml kubectl apply -f kube/beer-service.yaml + # The deployment manifest carries a sentinel image tag; the real + # SHA-pinned image is set immediately below. Applying it here + # keeps the rest of the spec (probes, resources, security context) + # in the repo and under review. + kubectl apply -f kube/beer-deployment.yaml - # The image tag is `:latest`, which is immutable in the manifest, so - # `kubectl apply` alone does NOT roll the pods. Without the restart - # the deployment reports success while the old pods keep serving - # the old image. - kubectl -n beer rollout restart deployment/beer-deployment + # The image is pinned to the commit SHA rather than `:latest`. + # `kubectl set image` writes the SHA into the pod template, which + # makes the change a real manifest change - the Deployment rolls + # because its pod template changed, not because it was told to. + # That also makes `kubectl rollout undo` meaningful: it can go back + # to the previous SHA. + kubectl -n beer set image deployment/beer-deployment \ + beer-data=git.aridgwayweb.com/armistace/beer-data:${{ github.sha }} kubectl -n beer rollout status deployment/beer-deployment --timeout=180s - name: Verify the new image is actually running @@ -160,8 +173,10 @@ jobs: - name: Smoke test the deployed app run: | - # The pods are behind a NodePort. Hit the service directly from - # inside the cluster. + # Check the app's own port from inside the pod, AND the Service in + # front of it. The pod check alone would pass even if the Service + # selector or the port mapping were wrong, which is exactly the + # kind of breakage a deploy can introduce silently. for i in $(seq 1 12); do if kubectl -n beer exec deploy/beer-deployment -- \ python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:5000/healthz').status==200 else 1)"; then @@ -173,3 +188,7 @@ jobs: done kubectl -n beer exec deploy/beer-deployment -- \ python -c "import urllib.request,sys; r=urllib.request.urlopen('http://127.0.0.1:5000/readyz'); print('readyz:', r.read().decode()); sys.exit(0 if r.status==200 else 1)" + + # Through the Service - the path a real client takes. + kubectl -n beer run smoke --rm -i --restart=Never --image=python:3.14-slim -- \ + python -c "import urllib.request,sys; r=urllib.request.urlopen('http://beer-service.beer.svc.cluster.local/healthz'); print('service healthz:', r.read().decode()); sys.exit(0 if r.status==200 else 1)" diff --git a/kube/beer-deployment.yaml b/kube/beer-deployment.yaml index dc67fb2..3fa07ba 100644 --- a/kube/beer-deployment.yaml +++ b/kube/beer-deployment.yaml @@ -22,7 +22,13 @@ spec: spec: containers: - name: beer-data - image: git.aridgwayweb.com/armistace/beer-data:latest + # The image tag is pinned to the commit SHA by CI (`kubectl set + # image`). The value here is a sentinel: applying the manifest + # as-is would leave the deployment on that sentinel, so CI always + # replaces it before rolling. It is deliberately NOT `:latest` — + # a mutable tag makes the rollout a no-op (the pod template does not + # change) and makes `rollout undo` meaningless. + image: git.aridgwayweb.com/armistace/beer-data:SET_BY_CI imagePullPolicy: Always ports: - containerPort: 5000 @@ -56,7 +62,10 @@ spec: capabilities: drop: - ALL - readOnlyRootFilesystem: false + # The app reads its own source and talks to Mongo over the + # network. It writes no files, so the root filesystem can be + # read-only. /tmp is still writable via the emptyDir below. + readOnlyRootFilesystem: true livenessProbe: httpGet: path: /healthz