Files
devops-base-images-gcp/README.md
T
Mukul SharmaandClaude Opus 5 b38af45c31 GKE: mirror base images into this cluster's Harbor over TLS
Same manifest and same script as the homelab repo; what differs is the
registry host and, substantially, TLS.

The homelab's Harbor speaks plain HTTP and its mirror pod passes
--insecure-registry. Harbor here serves a real certificate issued from the
private CA Terraform created, so the pod instead mounts that CA into the
dind container at /etc/docker/certs.d/<registry host>/ca.crt. It goes in
the dind container specifically: dockerd performs the push, while
docker-cli only talks to it over TCP.

The README's project-creation and verification curls now use https and
--cacert, since a laptop has no reason to trust this CA either.

Still no robot credentials in the pod, for the same reason as the
homelab's: the jenkins robot is scoped to the homelab project, and making
base-images public grants anonymous pull but never push. A one-off admin
docker login beats provisioning another robot for something run this
rarely.

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

6.2 KiB

devops-base-images (GKE)

Mirrors minimized base images into Harbor's base-images project, so language builds — devops-lib's buildDocker.groovy fallback templates — never depend on Docker Hub at build time, and the images that ship are the leanest official variant for each language.

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
  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.

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.