feat(ci): add the deploy job so the app deploys from the repo
The workflow built and pushed the image but never deployed it - the cluster objects had been created by hand. This adds a `deploy` job that applies the manifests in `kube/` and rolls the deployment. The deploy job is separate from the build job on purpose. The `KUBEC_CONFIG_BUILDX` secret is cluster-admin-equivalent, and a build step that never talks to the cluster should not be able to read it. Three things this job does that a naive `kubectl apply` does not: 1. `kubectl rollout restart`. The image tag is `:latest`, which is immutable in the manifest, so `apply` alone does not roll the pods. Without the restart the deployment reports success while the old pods keep serving the old image - a deploy that silently changes nothing. 2. Verifies the running digest against the registry's `:latest`. CI green and pods `Ready` do not prove the new code is live; the digest comparison does. 3. Smoke tests the running app - `/healthz` and `/readyz` - from inside the pod, so a deploy that comes up but cannot serve is caught in CI rather than by the next person to open the page. The Secrets (`beer-db`, `beer-app`) are hand-provisioned and are deliberately not in the apply set, so a deploy cannot clobber them.
This commit is contained in:
@@ -93,3 +93,83 @@ jobs:
|
|||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: |
|
tags: |
|
||||||
git.aridgwayweb.com/armistace/beer-data:latest
|
git.aridgwayweb.com/armistace/beer-data:latest
|
||||||
|
|
||||||
|
# The deploy job is separate from the build job on purpose. The
|
||||||
|
# `KUBEC_CONFIG_BUILDX` secret is cluster-admin-equivalent, and a build
|
||||||
|
# step that never talks to the cluster should not be able to read it - a
|
||||||
|
# compromised dependency in the build would otherwise have a direct path to
|
||||||
|
# the cluster.
|
||||||
|
deploy:
|
||||||
|
name: Deploy to the cluster
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: gitea.ref == 'refs/heads/master'
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Create Kubeconfig
|
||||||
|
run: |
|
||||||
|
mkdir -p $HOME/.kube
|
||||||
|
echo "${{ secrets.KUBEC_CONFIG_BUILDX }}" > $HOME/.kube/config
|
||||||
|
|
||||||
|
- name: Install kubectl
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg
|
||||||
|
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y kubectl
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
# Apply the namespace and the non-secret manifests. The Secrets
|
||||||
|
# (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 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
|
||||||
|
kubectl -n beer rollout status deployment/beer-deployment --timeout=180s
|
||||||
|
|
||||||
|
- name: Verify the new image is actually running
|
||||||
|
run: |
|
||||||
|
# CI green and pods Ready do NOT prove the new code is live. Compare
|
||||||
|
# the running digest against the registry's :latest.
|
||||||
|
RUNNING=$(kubectl -n beer get pod -l app=beer-data \
|
||||||
|
-o jsonpath='{.items[0].status.containerStatuses[0].imageID}')
|
||||||
|
REGISTRY=$(curl -s -u "armistace:${{ secrets.REG_PASSWORD }}" -I \
|
||||||
|
"https://git.aridgwayweb.com/v2/armistace/beer-data/manifests/latest" \
|
||||||
|
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
||||||
|
| grep -i docker-content-digest | awk '{print $2}' | tr -d '\r')
|
||||||
|
echo "running: $RUNNING"
|
||||||
|
echo "registry: $REGISTRY"
|
||||||
|
if [ "$RUNNING" != "$REGISTRY" ]; then
|
||||||
|
echo "ERROR: the running pod is not the image that was just pushed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Smoke test the deployed app
|
||||||
|
run: |
|
||||||
|
# The pods are behind a NodePort. Hit the service directly from
|
||||||
|
# inside the cluster.
|
||||||
|
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
|
||||||
|
echo "healthz OK"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "waiting for the app to come up ($i/12)"
|
||||||
|
sleep 5
|
||||||
|
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)"
|
||||||
|
|||||||
Reference in New Issue
Block a user