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

184 lines
6.2 KiB
Markdown

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