Introduction
Ensuring high availability of container images in multi-region deployments using Kubernetes can be tricky. Redundancy and high availability usually get attention only at the K8s cluster and persistent stores level. However, it is frequently forgotten that Kubernetes systems have a strong dependency on container registries’ availability to be able to instantiate pods. It is common these days that, to facilitate regular updates and propagate changes quickly, Kubernetes deployments and pods use imagePullPolicy: Always to force the kubelet to pull an updated image every time it launches a container. What happens then if the precise container registry used to host the image becomes unavailable due to, perhaps, the registry going down, connectivity issues, or other types of outages?
The first problem that needs to be addressed is to create a mirror of your container registry in an alternative location. Containers can then failover their image request to this registry replica in an outage scenario. CI/CD pipelines or products like Rackware, FSDR, etc., provide tools to automate this replication. The second part of the problem is making pods and deployments use this alternative container registry’s address. Some frameworks replace the image’s address based on the region hosting the K8s cluster (i.e., they update the image: location tag in pods depending on what region the pods are running in). However, it is incorrect to attach the failover of the registry to the failover of the K8s cluster itself. In other words, registries may experience outages NOT affecting the K8s cluster region itself. In fact, registries may reside in different OCI regions or even in locations completely external to OCI or in a customer Data Center. Kubernetes clusters act like clients to container registries and should “virtualize” the container registry access so that they don’t need to be updated when an outage occurs in their primary registry system.
The Challenge
In OCI, for example, Container Registries are region-specific and, while you can configure registry replication in diverse ways, you will want to use a stable image address in your deployments that can provide transparent “failover” to other registries. For example, imagine you have deployed the same image in two different registries:
- fra.ocir.io
- ams.ocir.io
You want to provide seamless access to the image with some requisites:
–> Kubernetes pods should pull images seamlessly even if one region is unavailable.
–> TLS certificate validation must remain secure (no insecure=true should be used to retrieve images).
–> Podman CLI and kubelet/CRI-O runtime should behave consistently.
What Doesn’t Work
DNS or /etc/hosts overrides that map a virtual hostname to multiple backend registry addresses will not work properly with Cloud registries. This is because using a hostname alias to point to a region’s registry IP breaks TLS: the certificate from OCI or other production-ready registries only covers *.ocir.io. Podman’s CLI might ignore it with –tls-verify=false, but Kubernetes CRI-O fails with X509 errors because we are requesting an SSL connection with a site name that doesn’t match our request (Updating certificates on backend OCI registries is obviously not possible since OCI manages registries’ certificates.) This approach is valid, though, for custom local registries where there is control over the certificates and SANs used in the backends. On top of this, DNS steering does not allow using authenticated requests in HTTP health checks, and all accesses to Container Registry paths require authentication. This precludes running Health Checks on the status of the precise repository and limits the HC to a simple TCP verification of the registry’s address being reachable.
The Working Solution: Registry Mirrors in the Repository’s Configuration
It is frequently recommended to use local registry mirrors to improve performance and offload large central registries with tons of images from the burden of continuous access and image retrievals. However, registry mirrors are useful beyond this. Configuring the repository’s access with registry mirrors can also provide the failover capabilities we are looking for. To use registry mirrors transparently in your K8s cluster and container ,follow these steps:
NOTE: The sample commands provided use OCI container registries, podman, and cri-o as container manager and container runtime, respectively. Refer to your precise implementation for specific steps in other cases.
- Step 1 — Deploy your images using the same secret to your primary registry and at least one mirror:
- Build your image
podman build -t helidon-pod -f Dockerfile- Verify the image locally
podmand images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/helidon-pod latest 0042018ed34c 26 hours ago 681 MB- Log in to your container registry in region 1 and push the image
podman login fra.ocir.io -u tenancy/oracleidentitycloudservice/user@oracle.com -p "XYZWQ"podman push helidon-pod:latest fra.ocir.io/tenancy/myrepo- Log in to your container registry in region 2 and push the image
podman login ams.ocir.io -u tenancy/oracleidentitycloudservice/user@oracle.com -p "XYZWQ"podman push helidon-pod:latest ams.ocir.io/tenancy/myrepo
- Step 2 — Use one of your registries as the Top-Level Registry and add a secondary mirror
- Configure your /etc/containers/registries.conf (refer to your precise container configuration if using other than cri-0) with entries for your main registry and the mirror (Apply this change in all your worker nodes)
[[registry]]location = "fra.ocir.io"
[[registry.mirror]] location = "ams.ocir.io"- Restart CRI-O on each node:
sudo systemctl restart crio
- Step 3 — Deploy to Kubernetes
Create a secret in your K8s cluster to access the Container Registries (same secret or auth token needs to be used in all CRs for seamless access)
kubectl create secret docker-registry fraregcred --docker-server=fra.ocir.io --docker-username=tenancy/oracleidentitycloudservice/user@oracle.com --docker-password="XYZWQ" --docker-email=user@oracle.comReference your image and secret in your corresponding pod/deployments and trigger their creation or restart. Kubernetes pods should be pulling images and will now automatically use the first mirror. If an image is missing in
fra.ocir.io, it falls back toams.ocir.io.
kind: Service
apiVersion: v1
metadata:
name: helidon-pod
labels:
app: pod
spec:
type: ClusterIP
selector:
app: helidon-pod
ports:
- name: tcp
port: 8080
protocol: TCP
targetPort: 8080
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: helidon-pod
spec:
replicas: 1
selector:
matchLabels:
app: helidon-pod
template:
metadata:
labels:
app: helidon-pod
version: v1
spec:
containers:
- name: helidon-quickstart-mp
image: fra.ocir.io/tenancy/myrepo:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
imagePullSecrets:
- name: fraregcred
...
- Step 4 — Validate Failover
You can check that failover is working properly using iptables rules (k8s worker nodes do not use firewalld; instead, they rely on complex iptables routing, hence, you can use it instead). Assuming fra.ocir.io is set first in the list of mirrors, obtain its IP and disable traffic to it. The container should “jump” to the next mirror registry address and obtain the image. If you disable traffic to all the registries’ IPs, you should see that the pull of images fails.
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ nslookup fra.ocir.io
Server: 169.254.169.254
Address: 169.254.169.254#53
Non-authoritative answer:
Name: fra.ocir.io
Address: 147.154.141.197
Name: fra.ocir.io
Address: 147.154.178.110
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ nslookup ams.ocir.io
Server: 169.254.169.254
Address: 169.254.169.254#53
Non-authoritative answer:
Name: ams.ocir.io
Address: 192.29.192.138
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ sudo iptables -A OUTPUT -d 192.29.192.138 -j DROP
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ podman pull fra.ocir.io/tenancy/helidon-pod:latest
Trying to fra.ocir.io/tenancy/helidon-pod:latest...
Getting image source signatures
Copying blob cca2af61522d skipped: already exists
Copying blob 180f7e7c1d47 skipped: already exists
Copying blob 0cac2d858a1f skipped: already exists
Copying blob dd337f169991 skipped: already exists
Copying blob 2fca19049556 skipped: already exists
Copying blob cc3d1a8fa2ac skipped: already exists
Copying config df3b1ca66c done |
Writing manifest to image destination
df3b1ca66c9818e022a59f14df47d4b743a477c55d7eb3eef95279326ac90749
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ sudo iptables -A OUTPUT -d 192.29.192.138 -j DROP
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ sudo iptables -A OUTPUT -d 147.154.141.197 -j DROP
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ sudo iptables -A OUTPUT -d 147.154.178.110 -j DROP
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ podman pull fra.ocir.io/tenancy/helidon-pod:latest
Trying to pull fra.ocir.io/tenancy/helidon-pod:latest...
Error: initializing source docker://fra.ocir.io/tenancy/helidon-pod:latest: (Mirrors also failed: [ams.ocir.io/tenancy/helidon-pod:latest: pinging container registry ams.ocir.io: Get "https://ams.ocir.io/v2/": dial tcp 192.29.192.138:443: i/o timeout]
[fra.ocir.io/tenancy/helidon-pod:latest: pinging container registry fra.ocir.io: Get "https://fra.ocir.io/v2/": dial tcp 147.154.141.197:443: i/o timeout]): fra.ocir.io/tenancy/helidon-pod:latest: pinging container registry fra.ocir.io: ...
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ sudo iptables -D OUTPUT -d 192.29.192.138 -j DROP
[opc@oke-c6nieuduvlq-nrmiw4rws7a-stvzoukvq5q-3 ~]$ podman pull fra.ocir.io/tenancy/helidon-pod:latest
Trying to pull fra.ocir.io/tenancy/helidon-pod:latest...
Getting image source signatures
Copying blob 180f7e7c1d47 skipped: already exists
Copying blob 2fca19049556 skipped: already exists
Copying blob dd337f169991 skipped: already exists
Copying blob 0cac2d858a1f skipped: already exists
Copying blob cca2af61522d skipped: already exists
Copying blob cc3d1a8fa2ac skipped: already exists
Copying config df3b1ca66c done |
Writing manifest to image destination
df3b1ca66c9818e022a59f14df47d4b743a477c55d7eb3eef95279326ac90749
Automation
For large clusters, it’s practical to automate the update /etc/containers/registries.conf across all worker nodes. You can extend this script with additional mirrors and customizations:
#!/bin/bash
# -------------------------------
# Configuration
# -------------------------------
#Customize your current primary and mirror registry's addresses
MAIN_REGISTRY="fra.ocir.io"
MIRROR1="ams.ocir.io"
# Replace with your SSH username and key
SSH_USER="opc"
SSH_KEY=/home/opc/my_keys/sshkey.priv
#Provide your containers configuration file
REGISTRIES_CONF="/etc/containers/registries.conf"
# -------------------------------
# Function to generate registry entry
# -------------------------------
generate_registry_entry() {
cat <<EOF
[[registry]]
location = "$MAIN_REGISTRY"
[[registry.mirror]]
location = "$MIRROR1"
EOF
}
# -------------------------------
# Get all worker nodes
# -------------------------------
#WORKERS=$(kubectl get nodes -o jsonpath='{range .items[?(@.spec.taints==null || !(@.spec.taints[*].effect=="NoSchedule"))]}{.metadata.name}{" "}{end}')
WORKERS=$(kubectl --kubeconfig=$kcfg get nodes | grep -v 'master\|control' | grep -v NAME | awk '{print $1}')
if [[ -z "$WORKERS" ]]; then
echo "No worker nodes found"
exit 1
fi
# -------------------------------
# Traverse and update each worker
# -------------------------------
for NODE in $WORKERS; do
echo "Updating registries.conf on node: $NODE"
# Backup existing registries.conf
ssh -i $SSH_KEY "$SSH_USER@$NODE" "sudo cp $REGISTRIES_CONF ${REGISTRIES_CONF}.bak"
# Append or replace registry entry
ssh -i $SSH_KEY "$SSH_USER@$NODE" "sudo bash -c 'grep -q \"$MAIN_REGISTRY\" $REGISTRIES_CONF && sed -i \"/$MAIN_REGISTRY/,+3d\" $REGISTRIES_CONF; cat >> $REGISTRIES_CONF'" <<< "$(generate_registry_entry)"
# Restart CRI-O
echo "Restarting CRI-O on node $NODE"
ssh -i $SSH_KEY "$SSH_USER@$NODE" "sudo systemctl restart crio"
echo "Done on node: $NODE"
done
echo "All worker nodes updated successfully."
Conclusion
Container registry access may fail for several reasons (connectivity, storage downtime or other outages), and this can preclude a Kubernetes microservice or application from working during deployment updates or pod restarts. To avoid CRs becoming a SPOF for a K8s cluster, it is required to configure redundant registries. To provide transparent failover in kubelet‘s image instantiations, a mirror configuration can be used in the repository configuration. This allows using a list of registries that are traversed in failure scenarios WITHOUT requiring the manipulation of the registry address used by pods and deployments. This pattern is now the recommended MAA approach to implement multi-region OCI registry redundancy in Kubernetes clusters.
