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
286 lines
9.5 KiB
Markdown
286 lines
9.5 KiB
Markdown
# 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.
|