Day 67 of DevOps, Day 17 of Azure. Both tasks today turned on exactly which word sat in one field. In Kubernetes, redis-master is not a label I chose, it is a hostname compiled into the application. In Azure, the difference between blob and container decides whether a stranger can list your files or merely read the ones they can already name.
One Kubernetes task, one Azure task. Deploy a three-tier Guest Book, then create a storage account and a deliberately public container. The tasks come from the KodeKloud Engineer platform.
Task 1: A Guest Book in three tiers
The task: a redis-master Deployment with one replica behind a ClusterIP Service, a redis-slave Deployment with two replicas behind a ClusterIP Service, and a frontend Deployment with three replicas behind a NodePort Service on 30009. Clicking the app button before any of it existed returned 502.
Step 1: The write tier
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: redis-master
name: redis-master
spec:
replicas: 1
selector:
matchLabels:
app: redis-master
strategy: {}
template:
metadata:
labels:
app: redis-master
spec:
containers:
- image: redis:alpine
name: master-redis-devops
resources:
requests:
cpu: "100m"
memory: "100Mi"
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-master
spec:
selector:
app: redis-master
ports:
- targetPort: 6379
port: 6379
No type on the Service, which makes it ClusterIP: "the default that is used if you don't explicitly specify a type for a Service", and one that "makes the Service only reachable from within the cluster". That is the entire isolation story for the Redis tier, and it is a default rather than a decision.
Step 2: The read tier, two replicas
containers:
- image: gcr.io/google_samples/gb-redisslave:v3
name: slave-redis-devops
resources:
requests:
cpu: "100m"
memory: "100Mi"
ports:
- containerPort: 6379
env:
- name: GET_HOSTS_FROM
value: "dns"
Step 3: The frontend, three replicas, on a node port
containers:
- image: gcr.io/google-samples/gb-frontend@sha256:a908df84...
name: php-redis-devops
env:
- name: "GET_HOSTS_FROM"
value: "dns"
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
type: NodePort
selector:
app: frontend
ports:
- port: 80
targetPort: 80
nodePort: 30009
30009 sits in the static band of the node port range, 30000 to 30085, which is where a hand-picked number belongs. Automatic assignment draws from the dynamic band, 30086 to 32767, first.
Step 4: Apply and check
kubectl apply -f backend-deploy.yaml
kubectl apply -f backend-svc.yaml
kubectl apply -f redis-slave-deploy.yaml
kubectl apply -f redis-slave-svc.yaml
kubectl apply -f frontend-deploy.yaml
kubectl apply -f frontend-svc.yaml
kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
frontend 3/3 3 3 41s
redis-master 1/1 1 1 2m18s
redis-slave 2/2 2 2 90s
Why the Service names are not mine to choose
GET_HOSTS_FROM: dns looks like boilerplate. It is the switch that decides how the frontend finds Redis, and the guestbook PHP shows what the other side of it looks like:
$host = 'redis-master';
if (getenv('GET_HOSTS_FROM') == 'env') {
$host = getenv('REDIS_MASTER_SERVICE_HOST');
}
The hostnames are hardcoded. Setting the variable to anything other than env leaves them as literal strings, and the application relies on cluster DNS to resolve them. A Service gets a DNS record "with a name of the form my-svc.my-namespace.svc.cluster-domain.example", and "DNS queries that don't specify a namespace are limited to the Pod's namespace", so a bare redis-master resolves inside the pod's own namespace.
Which means a Service name here is an address, not a label. Rename the Deployment to anything you like and nothing breaks. Rename the Service and the application stops finding its database, with every object still reporting itself healthy.
The alternative branch is worth noticing too. REDIS_MASTER_SERVICE_HOST is the Docker-link-style variable the kubelet injects, and it only exists for Services created before the pod. That is the ordering trap from Day 62's notes, and dns is how you avoid it.
The 502 I cannot explain
The app returned 502 before the backend existed, and that is where my account stops. Nothing in the Kubernetes documentation describes what a client sees when a Service's selector matches no pods. The EndpointSlice page covers what EndpointSlices contain; the debugging guide goes as far as "If the ENDPOINTS column is <none>, you should check that the spec.selector field".
A 502 is a gateway error, so it came from whatever proxy fronted the lab's app button, not from kube-proxy or the Service. I am recording it as something I saw rather than something Kubernetes does.
What the docs recommend
Know that these images are retired. The current guestbook manifests use us-docker.pkg.dev/google-samples/containers/gke/gb-redis-follower:v2 and gb-frontend:v5. The gcr.io paths above match what the Kubernetes docs shipped around v1.19.
Do not assume the underscore is a typo. google_samples with an underscore and google-samples with a hyphen both appear upstream, in the same manifest set. It is not even a stable per-image rule: at the v1.26 docs branch, the frontend was also gcr.io/google_samples. Both images have used both spellings at different times.
And the tutorial this descends from has moved on. kubernetes.io still publishes a guestbook, and since the v1.21 docs branch, its objects are redis-leader and redis-follower, with the prose reading "Start up a Redis leader." The master and slave naming belongs to the older branch and to this lab.
Redis made the same change, and only halfway. Its SLAVEOF page says the command "is regarded as deprecated" as of 5.0.0 and that "the Redis project no longer uses the word slave", pointing to REPLICAOF. The current replication page still writes "master" for the other side, so replica replaced slave while primary did not replace master.
One resource note. Every container sets requests and no limits, which makes each pod Burstable. The class belongs to the pod, not the container: Burstable means the pod "does not meet the criteria for QoS class Guaranteed" while "At least one Container in the Pod has a memory or CPU request or limit". Day 63 was the mirror image, where limits and no requests produced Guaranteed.
Task 2: A deliberately public container
The task: a storage account and a container with anonymous read access for containers and blobs. Day 16 inverted.
Step 1: Set the account flag at creation
az storage account create -g $RG -n $SA --sku Standard_LRS --kind StorageV2 \
--allow-blob-public-access true
Order matters here. A new account defaults the flag to false, and while it is false, Microsoft says "you can't configure the access setting for a container to permit anonymous access". Doing it the other way round costs a failed command and an az storage account update before retrying.
Step 2: Choose the level deliberately
KEY=$(az storage account keys list -g $RG -n $SA --query "[0].value" -o tsv)
az storage container create --name $CONTAINER --account-name $SA --account-key $KEY \
--public-access container
Three values, and the requirement's wording picked one:
| Value | Read a known blob | List the container |
|---|---|---|
off |
No | No |
blob |
Yes | No |
container |
Yes | Yes |
Microsoft's wording for the two public levels: "Blobs within the container can be read by anonymous request, but container data isn't available anonymously", against "Container and blob data can be read by anonymous request".
The task said "anonymous read access for containers and blobs", naming both, which is container. blob would have satisfied half the requirement while looking correct in the portal. The distinction is not cosmetic: with blob, a caller has to know a blob's exact name, and with container they can enumerate everything and take the lot.
Step 3: Prove it from outside
curl -s -o /dev/null -w "http_code=%{http_code}\n" \
"https://$SA.blob.core.windows.net/$CONTAINER?restype=container&comp=list"
http_code=200
No key, no login, no SAS token. ?restype=container&comp=list is the Blob REST List Blobs operation, so this tests the exact capability the requirement names without uploading anything first. A configuration read tells you the settings are right. This tells you the behaviour is correct.
The failure code I had wrong
I have been repeating that a private container returns 404 rather than 403, and that Azure does this deliberately so as not to confirm the container exists. Checking it, neither half survives.
Microsoft documents three outcomes keyed to the client's service version. For "a service version that supports the bearer challenge", the service "returns error code 401 (Unauthorized)". For an older version where anonymous access is disallowed on the account, 409 Conflict. For an older version where it is allowed, 404 Not Found. And separately: "After you disallow anonymous access for a storage account, clients that use the anonymous bearer challenge find that Azure Storage returns a 403 error (Forbidden) rather than a 401 error."
So 401, 403, 404 and 409 are all documented, depending on which version the client negotiates and what the account permits. The "will not confirm it exists" rationale appears nowhere in Microsoft's documentation at all. It is a tidy story that explains one of the four codes, and I had been treating it as the rule.
What the docs recommend
Understand exactly how far anonymous access reaches, because it is narrower than it sounds and wider than it feels. The documented operation list is all reads, and clients "can enumerate blobs within the container via anonymous request, but can't enumerate containers within the storage account". No writes, no deletes, no visibility into other containers.
Then understand who pays. Egress leaves the owner holding the bill, and Microsoft's bluntest statement of it is in the SAS guidance: "If you've given them read access as well, they may choose to download it 10 times, incurring 2 TB in egress costs for you." Every anonymous download is also a read transaction, and cool or archive tiers add a retrieval fee on top.
Expect to be flagged for it. Microsoft Defender for Cloud carries a recommendation named "Storage account public access should be disallowed", describing anonymous read access as "a convenient way to share data, but might present security risks."
And know that you cannot qualify it. An S3 bucket policy can condition public read on a prefix, a source IP or a referrer, through s3:prefix, aws:SourceIp or aws:Referer. Azure's container access is a single enum with no condition syntax: on for the whole container, or off. The only qualifier that still applies is the storage firewall, since "any firewall rules that you configure for the storage account remain in effect".
Which is why the production answer is a shared access signature rather than a public container. Microsoft notes a SAS "can be issued on a container or a blob regardless of whether container data is available for anonymous read access" and "provides a greater measure of control", and recommends a user delegation SAS "when possible for superior security", because it is signed with Entra credentials rather than the account key.
What I learned
The Kubernetes half was a lesson in which names are addresses. A Deployment name is a label for humans. A Service name is a hostname the application has compiled into it, so the one field that looks most like a free choice is the one that is not.
The Azure half was a lesson about a story I had been repeating. The 404 explanation was plausible, it matched something I had seen, and it turned out to be one of four documented codes with none of the reasoning I had attached to it.
So here is the Day 67 question. Which of your configuration values is a free label, and which one is something else quietly depending on?
Day 67 down. Thirty-three to go.
Top comments (1)
Another thoughtful write-up, Nnamdi! 😊👏
What I appreciate most is how openly you revisit earlier assumptions when the documentation tells a different story. That’s something we can all learn from. The Service naming example is a great reminder that configuration values can become hidden application dependencies. One small nuance I’d add: ClusterIP limits how a Service is exposed, but it doesn’t provide network isolation between pods on its own. That’s where NetworkPolicies become important. And to answer your question, I’d say DNS names, database identifiers, and environment variable names are all good examples of values that look harmless until something quietly depends on them. 😄
Really enjoying the series, my friend. Keep going! 🚀❤️