DEV Community

Cover image for Day 57: Env Vars and a Public IP
Nnamdi Felix Ibe
Nnamdi Felix Ibe

Posted on AI-assisted

Day 57: Env Vars and a Public IP

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Welcome to xFusionCorp Group
Enter fullscreen mode Exit fullscreen mode

Step 3: Check how the pod ended

kubectl get pods
kubectl describe pod print-envars-greeting
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (6)

Collapse
 
merbayerp profile image
Mustafa ERBAY •

The defaults that scare me most are the ones that quietly become part of the architecture. πŸ˜„

A default port changing is annoying. A default affecting security, networking, storage, or persistence can ruin your afternoon.

That’s why I increasingly treat defaults as borrowed decisions: perfectly fine for a lab, but if a value matters in production, I’d rather make it explicit even when I’m choosing the default value anyway.

The Azure public IP example is a great illustration of that. Same command, but the meaning of something you never specified can evolve underneath you.

Also, great catch on $(GREETING). Correcting your own notes and explaining why they were wrong makes this series much more useful than simply showing commands.

Day 57 already. You’re moving suspiciously fast, man. πŸ˜‚

Collapse
 
ndcodes profile image
Nnamdi Felix Ibe •

"Borrowed decisions" I like the phrase, and it explains why the Azure one bothered me more than a plain bad default would. I never made that decision at all. Microsoft made it, then changed it, and my command stayed identical through both.

Writing the default out explicitly even when you're picking the default anyway sounds like busywork until exactly that happens.

And haha, the pace is less impressive than it looks. I've got a free stretch at the moment for personal things, so I'm doing a lab a day and writing it up the same evening. That's the whole trick. No clever system, just time I won't have in a few months.

The note corrections are becoming my favourite part, honestly. Three posts running now where the lab was easy and the thing I believed going in was wrong. πŸ‘Š

Collapse
 
merbayerp profile image
Mustafa ERBAY •

Haha, β€œno clever system, just time” is exactly what someone with a suspiciously effective system would say. πŸ˜‚

But seriously, I think the note corrections are becoming one of the strongest parts of the series.

A successful lab tells me the commands worked. Finding out that something you believed beforehand was wrong tells me why it worked β€” and that’s usually the part that sticks.

And enjoy that free stretch while you have it. A lab a day + writing it up the same evening is a pretty damn good way to spend it. By Day 100 you won’t just have finished 100 tasks; you’ll have built a searchable record of 100 things you actually investigated.

Keep going, man. πŸ‘Š

Thread Thread
 
ndcodes profile image
Nnamdi Felix Ibe •

Hahaha, you've caught me. The system is a calendar with nothing in it. πŸ˜‚

And the searchable record is exactly what I'm after. Not the hundred tasks, the hundred corrections. Those are the ones I'd otherwise have carried around for years, confident and wrong, because being over-cautious or slightly off never throws an error at you.

Forty-three to go. πŸ‘Š

Thread Thread
 
merbayerp profile image
Mustafa ERBAY •

Exactly. The dangerous mistakes aren’t always the ones that break things β€” they’re the ones that work perfectly while reinforcing the wrong mental model. πŸ˜‚

Those can survive for years because nothing ever forces you to question them.

So β€œ100 tasks” is nice, but β€œ100 things I was confident about and actually verified” is a much better series.

43 more chances to discover we’ve all been confidently lying to ourselves. πŸ˜‚πŸ‘Š

Collapse
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

❀️