Day 57 of DevOps, Day 7 of Azure. Both tasks today had values in them that I did not put there myself. In the pod, Kubernetes fills in $(GREETING) before the shell ever sees it. In Azure, a public IP created with nothing but a name still arrives with a SKU, an allocation method and a zone setting.

One Kubernetes task, one Azure task. Print three environment variables from a pod, then allocate a public IP address. The tasks come from the KodeKloud Engineer platform.

Task 1: Print environment variables from a pod

The task: a pod called print-envars-greeting, with one container, print-env-container, on the bash image. Three environment variables, a fixed echo command that prints them, and a restart policy of Never.

Step 1: Write the manifest

apiVersion: v1 kind: Pod metadata: name: print-envars-greeting spec: restartPolicy: Never containers: - name: print-env-container image: bash command: ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"'] env: - name: GREETING value: "Welcome to" - name: COMPANY value: "xFusionCorp" - name: GROUP value: "Group" 

Each entry under env is a name and a value. The command runs through /bin/sh -c, and restartPolicy: Never tells Kubernetes to leave the container alone once the echo has finished.

Step 2: Apply it and read the output

kubectl apply -f k3s-pod.yml kubectl logs -f print-envars-greeting 
Welcome to xFusionCorp Group 

Step 3: Check how the pod ended

kubectl get pods kubectl describe pod print-envars-greeting 

get pods shows Completed. describe prints the pod's spec, so it lists the three variables under Environment, next to the command exactly as I wrote it.

Completed is what kubectl displays. The pod's phase is Succeeded, which the docs define as: "All containers in the Pod have terminated in success, and will not be restarted." The Kubernetes docs warn against mixing the two up, calling Status "a kubectl display field for user intuition".

That ending depends on restartPolicy: Never. The default is Always, which restarts a container "after any termination", a successful exit included. Without the setting, this echo would be restarted over and over, with a delay that starts at 10 seconds and is capped at 5 minutes.

Who expands $(GREETING)

This is the part my notes had wrong. I had written that $(VARNAME) is shell variable syntax, processed by the shell at runtime.

It is the other way round. In a shell, $(name) is command substitution, which the Bash manual says "allows the output of a command to replace the command itself". A shell would try to run a command called GREETING.

Kubernetes gets there first. The API reference says of a container's command: "Variable references $(VAR_NAME) are expanded using the container's environment." So the three references are replaced with the values from env before the container starts, and the shell is handed a finished line:

echo "Welcome to xFusionCorp Group" 

The shell's own forms are $GREETING and ${GREETING}. They would have worked here too, because the variables are in the container's environment. The difference is who does the substitution, and it matters when something is misspelt. The API reference again: "If a variable cannot be resolved, the reference in the input string will be unchanged." A typo such as $(GRETING) is passed through untouched, and the shell then treats it as a command to run.

To print the literal text, double the dollar sign: $$(GREETING).

One command that cannot work here

My notes included this as a way to see the variables:

kubectl exec print-envars-greeting -- env 

It cannot work on this pod. kubectl checks the pod's phase first and refuses a pod that has finished. Going by the message in kubectl's source code, the error for this pod reads:

cannot exec into a container in a completed pod; current phase is Succeeded 

kubectl describe pod is the way to see the variables afterwards. To inspect the live environment, the pod has to still be running.

What the docs recommend

A pod that runs once and exits is really a Job. The Jobs page recommends "a Job rather than a bare Pod, even if your application requires only a single Pod", because a bare pod is not restarted if its node reboots or fails.

Real configuration should not be hardcoded in the pod spec. A ConfigMap holds non-confidential values, a Secret holds confidential ones, and env points at them:

env: - name: DATABASE_HOST valueFrom: configMapKeyRef: name: app-config key: db_host - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: db-secret key: password 

Two cautions from the docs go with that. ConfigMaps consumed as environment variables "are not updated automatically and require a pod restart". And Secrets are "by default, stored unencrypted in the API server's underlying data store (etcd)", so a Secret is only as protected as the cluster makes it.

Last, image: bash has no tag, and "If you don't specify a tag, Kubernetes assumes you mean the tag latest." The same page advises against :latest in production, because it is harder to track which version is running and harder to roll back.

Task 2: Allocate a public IP

The task: one public IP address named nautilus-pip. Nothing else was specified.

Step 1: Find the resource group

RG=$(az group list --query "[0].name" -o tsv) 

The lab provides one resource group with a generated name, so I read it from the API instead of pasting it.

Step 2: Create the address

az network public-ip create -g "$RG" -n nautilus-pip 

One thing to know before querying the response: create wraps its result in an object called publicIp, while show returns the same resource flat. A query written for one returns null for every field on the other.

Step 3: Check what was created

az network public-ip show -g "$RG" -n nautilus-pip --query "{IP:ipAddress,Sku:sku.name,State:provisioningState}" 

The address was 20.84.50.139, the SKU was Standard, and the provisioning state was Succeeded. The create response had also shown the allocation method as Static. I had asked for none of it except the name.

What the defaults chose for me

The address is static, and it is already assigned. Nothing is attached to this IP, and it still has a real routable address. That follows from the SKU. Microsoft lists the allocation method for Standard as Static, and describes static assignment as: "The resource is assigned an IP address at the time it's created." Dynamic allocation only existed on the Basic SKU, and "On September 30, 2025, Basic SKU public IPs were retired."

It is closed to inbound traffic. Microsoft describes Standard as a "Secure by default model", closed to inbound traffic until a network security group allows it. Attach this address to a VM with no NSG rule, and the VM is unreachable, in a way that looks like a network fault and is really a policy.

It has a zone setting I did not choose. The CLI printed a warning on create:

[Coming breaking change] In the coming release, the default behavior will be changed as follows when sku is Standard and zone is not provided: For zonal regions, you will get a zone-redundant IP indicated by zones:["1","2","3"]; For non-zonal regions, you will get a non zone-redundant IP indicated by zones:null. 

My notes read that as a change still to come. Microsoft's documentation says it has already happened on the platform: "All formerly Standard non-zonal IPs are now zone-redundant in all regions that support availability zones". An address that shows no zones and one that shows zones 1, 2 and 3 "are equivalently zone-redundant". The meaning of a value I never set changed underneath an unchanged command.

It probably has a cost. Microsoft prices a Standard static IPv4 address by the hour, and its docs say "Public IPv4 addresses have a nominal charge". My notes said the charge applies whether or not the address is attached. I could not find a Microsoft page that says so in those words, so I would treat an idle address as billable until the bill says otherwise. AWS does say it outright: since 1 February 2024, it charges "for all public IPv4 addresses, whether attached to a service or not".

What the docs recommend

Pass what matters instead of inheriting it:

az network public-ip create -g "$RG" -n nautilus-pip --sku Standard --zone 1 2 3 

The zone deserves the care, because Microsoft says a public IP cannot change its availability zone once created, and a zonal IP "shares fate with the health of the zone".

Find the addresses nothing is using:

az network public-ip list --query "[?ipConfiguration==null].{Name:name,IP:ipAddress,RG:resourceGroup}" -o table 

And know the order for removing one. A public IP can only be deleted when it "isn't associated to any IP configuration or virtual machine network interface", so dissociate first, then delete. A static address that is deleted "can't be recovered", so anything holding the old address in DNS or a firewall allowlist breaks.

What I learned

Neither task had a hard step. The work was in noticing who supplied each value. Kubernetes substituted three variables before the shell started. Azure chose a SKU, an allocation method and a zone behaviour, and one of those has changed its meaning since the warning about it was written.

So here is the Day 57 question. Which default in your last deployment would surprise you if it changed tomorrow?

Day 57 down. Forty-three to go.