Files
devops-base-images-gcp/README.md
T
Mukul SharmaandClaude Opus 5 2f80cfe288 Fold build-tools into this repo
build-tools is the image the pipeline's build pod runs its shell steps in.
It was owned by devops-lib, which is an odd home: it is not library code,
it is an artefact built by hand and pushed to Harbor, exactly like the
mirrors here. devops-lib only ever referenced the result by tag, and it
still will.

Three deliberate changes from the homelab's version:

- Pushed to base-images/build-tools:1, not homelab/. That project is
  public, so build pods pull it with no credentials — the same reason the
  language images live there.

- FROM the mirrored docker:27-cli rather than Docker Hub, with that tag
  added to images.txt. Otherwise building the image that exists to remove
  a Docker Hub dependency would itself depend on Docker Hub.

- yq is pinned instead of "releases/latest". An image that resolves a
  different yq on every build is not reproducible, and that is the kind
  of drift that surfaces months later as an unexplained pipeline failure.

The README gains the build-and-push procedure: the same pod shape as the
mirror, with the registry CA mounted into dind so the push is trusted, and
DOCKER_BUILDKIT=0, since BuildKit wants to write state under /root/.docker
where the push credentials get mounted read-only.

Verified the three places that must agree do: images.txt, the README's
inlined ConfigMap copy of it, and the Dockerfile's FROM tag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEsTefWWifp4ikvhHF5s6N
2026-09-13 01:12:24 +05:30

9.5 KiB

devops-base-images (GKE)

Owns every image the build pipeline pulls, in Harbor's base-images project:

  • Mirrored language base images (images.txt), so builds — devops-lib's buildDocker.groovy fallback templates — never depend on Docker Hub at build time, and what ships is the leanest official variant per language.
  • build-tools (build-tools.Dockerfile), the image the build pod runs its own shell steps in.

Both are built or mirrored by hand from here, and devops-lib only ever references the results by tag. build-tools lives here rather than in devops-lib because it is the same kind of artefact as the mirrors, and because a Jenkins job cannot build the image its own build pod needs.

GKE counterpart of the homelab repo of the same name. The manifest and the script are identical; what differs is the registry host and, more substantially, TLS. The homelab's Harbor speaks plain HTTP and its dind pod passes --insecure-registry. Here Harbor has a real certificate issued from a private CA, so instead the CA must be installed into the Docker daemon's trust store.

Not wired into any pipeline. This is deliberately manual and occasional — re-run when images.txt changes (a new language, a version bump, or picking up an upstream base-image update), using a throwaway kubectl DinD pod, since neither the nodes nor your laptop necessarily has Docker.

One-time setup

1. Create the Harbor project. Public: these are re-hosted public images, and public means no pull credentials need wiring into any build.

Harbor UI → New Project → name base-images → check Public → Create.

Or via the API. --cacert is needed because the certificate is signed by the private CA, which your laptop has no reason to trust:

TF=~/Documents/localgit/gcp/toolshed-gke-infra/envs/prod/10-infra
curl --cacert <(terraform -chdir=$TF output -raw registry_ca_cert_pem) \
  -X POST -u admin:'<harbor admin password>' -H "Content-Type: application/json" \
  -d '{"project_name":"base-images","metadata":{"public":"true"}}' \
  "https://harbor.35.238.248.203.nip.io/api/v2.0/projects"

2. Make sure the CA is available in the cluster. The registry-ca ConfigMap in the jenkins namespace holds the CA's public certificate and is managed by devops-infra-argo-config-gcp (extra-manifests/). Check:

kubectl -n jenkins get configmap registry-ca

If app-of-extra-manifests has not synced yet, create it directly — it is a public certificate, not a secret:

kubectl -n jenkins create configmap registry-ca \
  --from-file=ca.crt=<(terraform -chdir=$TF output -raw registry_ca_cert_pem)

3. Run the mirror. Note where the CA is mounted: dockerd looks for /etc/docker/certs.d/<registry host>/ca.crt, and the path must contain the registry hostname exactly. It goes in the dind container, not docker-cli — dockerd performs the push, the CLI only talks to it over TCP.

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: base-images-mirror
  namespace: jenkins
data:
  images.txt: |
    golang:1.22-alpine golang:1.22-alpine
    golang:1.24-alpine golang:1.24-alpine
    alpine:3.20 alpine:3.20
    node:20-alpine node:20-alpine
    python:3.12-alpine python:3.12-alpine
    maven:3-eclipse-temurin-21-alpine maven:3-eclipse-temurin-21-alpine
    eclipse-temurin:21-jre-alpine eclipse-temurin:21-jre-alpine
    php:8.3-cli-alpine php:8.3-cli-alpine
    docker:27-cli docker:27-cli
  mirror.sh: |
    #!/bin/sh
    set -eu
    REGISTRY="harbor.35.238.248.203.nip.io"
    PROJECT="base-images"
    while read -r src target; do
        [ -z "$src" ] && continue
        case "$src" in \#*) continue ;; esac
        dest="${REGISTRY}/${PROJECT}/${target}"
        echo "=== ${src} -> ${dest} ==="
        docker pull "$src"
        docker tag "$src" "$dest"
        docker push "$dest"
    done < images.txt
    echo "=== done ==="
---
apiVersion: v1
kind: Pod
metadata:
  name: base-images-mirror
  namespace: jenkins
spec:
  restartPolicy: Never
  containers:
    - name: docker
      image: docker:27-dind
      securityContext:
        privileged: true
      # No --insecure-registry, unlike the homelab: Harbor here has a real
      # certificate. The CA below is what makes dockerd accept it.
      env:
        - name: DOCKER_TLS_CERTDIR
          value: ""
      volumeMounts:
        - name: docker-graph-storage
          mountPath: /var/lib/docker
        - name: registry-ca
          mountPath: /etc/docker/certs.d/harbor.35.238.248.203.nip.io
          readOnly: true
    - name: docker-cli
      image: docker:27-cli
      command: ["cat"]
      tty: true
      env:
        - name: DOCKER_HOST
          value: tcp://localhost:2375
      volumeMounts:
        - name: mirror
          mountPath: /mirror
  volumes:
    - name: docker-graph-storage
      emptyDir: {}
    - name: mirror
      configMap:
        name: base-images-mirror
        defaultMode: 0755
    - name: registry-ca
      configMap:
        name: registry-ca
EOF

Deliberately no harbor-robot-dockerconfig mount. That robot is scoped to the homelab project only, and making base-images public grants anonymous pull, never push — push always needs credentials scoped to the project. For something run this rarely, logging in with the Harbor admin account inside the pod beats provisioning another robot:

kubectl wait --for=condition=Ready pod/base-images-mirror -n jenkins --timeout=180s
kubectl exec -it -n jenkins base-images-mirror -c docker-cli -- sh -c '
  for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break; sleep 2; done
  docker login harbor.35.238.248.203.nip.io
'

(enter the Harbor admin username and password when prompted)

kubectl exec -n jenkins base-images-mirror -c docker-cli -- sh -c '
  cd /mirror && sh mirror.sh
'
kubectl delete pod base-images-mirror -n jenkins
kubectl delete configmap base-images-mirror -n jenkins

Pulls come from Docker Hub, which the nodes reach through Cloud NAT — the nodes have no public IPs of their own.

4. Verify:

curl --cacert <(terraform -chdir=$TF output -raw registry_ca_cert_pem) \
  -s https://harbor.35.238.248.203.nip.io/v2/_catalog

Should list base-images/golang, base-images/alpine, base-images/node, base-images/python, base-images/maven, base-images/eclipse-temurin and base-images/php.

If the push fails with a certificate error, the mount path is the usual cause: it must be /etc/docker/certs.d/harbor.35.238.248.203.nip.io/ca.crt exactly, hostname included, in the dind container.

Building build-tools

Do this after the mirror, since it builds FROM a mirrored image.

The pod is the same shape as the mirror pod — privileged dind, the CA mounted so pushes are trusted, a docker-cli sidecar — with the Dockerfile supplied through a ConfigMap. Create it from the file in this repo so the two cannot drift:

kubectl -n jenkins create configmap build-tools-src \
  --from-file=Dockerfile=build-tools.Dockerfile

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: build-tools-build
  namespace: jenkins
spec:
  restartPolicy: Never
  containers:
    - name: docker
      image: docker:27-dind
      securityContext:
        privileged: true
      env:
        - name: DOCKER_TLS_CERTDIR
          value: ""
      volumeMounts:
        - name: docker-graph-storage
          mountPath: /var/lib/docker
        - name: registry-ca
          mountPath: /etc/docker/certs.d/harbor.35.238.248.203.nip.io
          readOnly: true
    - name: docker-cli
      image: docker:27-cli
      command: ["cat"]
      tty: true
      env:
        - name: DOCKER_HOST
          value: tcp://localhost:2375
        # The classic builder. BuildKit wants to write its own state under
        # /root/.docker, which is read-only wherever the push credentials are
        # mounted as a file — the homelab hit exactly this.
        - name: DOCKER_BUILDKIT
          value: "0"
      volumeMounts:
        - name: src
          mountPath: /src
  volumes:
    - name: docker-graph-storage
      emptyDir: {}
    - name: src
      configMap:
        name: build-tools-src
    - name: registry-ca
      configMap:
        name: registry-ca
EOF

Then log in and build. The tag is deliberate: bump it rather than overwriting, because devops-lib-gcp's dind-pod.yaml pins this tag and an overwritten tag rolls out silently on the next pod start.

kubectl wait --for=condition=Ready pod/build-tools-build -n jenkins --timeout=180s
kubectl exec -it -n jenkins build-tools-build -c docker-cli -- sh -c '
  for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break; sleep 2; done
  docker login harbor.35.238.248.203.nip.io'

kubectl exec -n jenkins build-tools-build -c docker-cli -- sh -c '
  cd /src &&
  docker build -t harbor.35.238.248.203.nip.io/base-images/build-tools:1 . &&
  docker push harbor.35.238.248.203.nip.io/base-images/build-tools:1'

kubectl delete pod build-tools-build -n jenkins
kubectl delete configmap build-tools-src -n jenkins

Verify it has what the stages expect:

kubectl run bt --rm -it --restart=Never \
  --image=harbor.35.238.248.203.nip.io/base-images/build-tools:1 \
  -- sh -c 'git --version && yq --version && python3 -V && bash --version | head -1'

That pull needs no credentials — base-images is public — and the nodes already trust the CA, which is what makes an ordinary pod able to run it.

Adding a new image or updating a version

Add a line to images.txt, commit, push, then re-run step 3 — the ConfigMap's copy of images.txt has to be updated to match, since the pod reads that rather than this repo.