<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rahimah Sulayman</title>
    <description>The latest articles on DEV Community by Rahimah Sulayman (@rahimah_dev).</description>
    <link>https://dev.to/rahimah_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3673037%2Fd084c1b1-6c80-43de-8b42-c48bd0cad2f8.png</url>
      <title>DEV Community: Rahimah Sulayman</title>
      <link>https://dev.to/rahimah_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rahimah_dev"/>
    <language>en</language>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads - Publishing to GitHub and Cleaning Up Azure Resources (Part 9/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 20:50:19 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-publishing-to-github-and-cleaning-up-azure-2fh8</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-publishing-to-github-and-cleaning-up-azure-2fh8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Building a cloud-native environment is only part of the engineering process. A complete project also needs version-controlled implementation artifacts, a reproducible record of the work, and a deliberate cleanup strategy that prevents unused cloud resources from continuing to incur charges.&lt;/p&gt;

&lt;p&gt;In this final mission, I published the Kubernetes manifests to GitHub, verified that the lab namespace could be removed, deleted the Azure resource group, checked the status of the associated resource groups, and removed the obsolete Kubernetes context from the local kubeconfig.&lt;/p&gt;

&lt;p&gt;The objective was to preserve the implementation as portfolio evidence while ensuring that the temporary Azure environment was decommissioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Committing the Kubernetes manifests to Git.&lt;/li&gt;
&lt;li&gt;Publishing the repository to GitHub.&lt;/li&gt;
&lt;li&gt;Verifying namespace removal.&lt;/li&gt;
&lt;li&gt;Deleting the Azure resource group.&lt;/li&gt;
&lt;li&gt;Checking that the AKS-managed resource group was removed.&lt;/li&gt;
&lt;li&gt;Deleting the obsolete Kubernetes context.&lt;/li&gt;
&lt;li&gt;Confirming what remains available after the cloud environment is deleted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Review the Kubernetes Manifests
&lt;/h2&gt;

&lt;p&gt;The project contains nine Kubernetes manifest files covering the workloads and resources implemented throughout the lab.&lt;/p&gt;

&lt;p&gt;The files are organized under the &lt;code&gt;manifests/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;manifests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;01-pod.yaml
02-deployment.yaml
03-service-clusterip.yaml
04-service-loadbalancer.yaml
05-configmap.yaml
06-statefulset.yaml
07-daemonset.yaml
08-job.yaml
09-cronjob.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifests capture the main resources from the project:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Manifest&lt;/th&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;01-pod.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standalone Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;02-deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;03-service-clusterip.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Internal ClusterIP Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;04-service-loadbalancer.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;LoadBalancer Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;05-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;06-statefulset.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Headless Service and StatefulSet with persistent storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;07-daemonset.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DaemonSet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;08-job.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Batch Job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;09-cronjob.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scheduled CronJob&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keeping these resources in version control preserves the configuration used throughout the lab. The manifests provide a reference for the implementation and can be reviewed independently of the Azure environment in which they were deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Commit the Manifests to Git
&lt;/h2&gt;

&lt;p&gt;I committed the workload manifests with a message describing the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: AKS workloads (pods, deployment, services, config, statefulset, daemonset, job, cronjob)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5zmo9c9n1vja5nzmfv7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5zmo9c9n1vja5nzmfv7.png" alt="commit" width="798" height="151"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Git confirmed the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[main 610883d] feat: AKS workloads (pods, deployment, services, config, statefulset, daemonset, job, cronjob)
9 files changed, 206 insertions(+)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit created the nine manifest files in the repository.&lt;/p&gt;

&lt;p&gt;A descriptive commit message makes the change easier to identify in the project history. It also establishes a clear record of the implementation being preserved for the portfolio.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Configure the GitHub Remote
&lt;/h2&gt;

&lt;p&gt;I configured the local repository to use the GitHub repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/rahimahisah17/aks-kubernetes-lab.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command used was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/rahimahisah17/aks-kubernetes-lab.git 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; git remote set-url origin https://github.com/rahimahisah17/aks-kubernetes-lab.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3jundzlxca9fyjoprjei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3jundzlxca9fyjoprjei.png" alt="git remote add" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This command adds &lt;code&gt;origin&lt;/code&gt; when it does not already exist or updates its URL when the remote has already been configured.&lt;/p&gt;

&lt;p&gt;Using a remote allows the local Git repository to publish commits to GitHub and maintain a shared record of the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Push the Project to GitHub
&lt;/h2&gt;

&lt;p&gt;With the remote configured, I pushed the &lt;code&gt;main&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyj8o21ankwxs4dml9ynp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyj8o21ankwxs4dml9ynp.png" alt="git push" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The command completed successfully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enumerating objects: 19, done.
Counting objects: 100% (19/19), done.
Compressing objects: 100% (18/18), done.
Writing objects: 100% (19/19), 3.70 KiB | 84.00 KiB/s, done.
Total 19 (delta 2), reused 0 (delta 0), pack-reused 0

To https://github.com/rahimahisah17/aks-kubernetes-lab.git
 * [new branch]      main -&amp;gt; main
branch 'main' set up to track 'origin/main'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The push published the branch and configured it to track &lt;code&gt;origin/main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The project repository is available here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/rahimahisah17/aks-kubernetes-lab" rel="noopener noreferrer"&gt;GitHub — aks-kubernetes-lab&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Publishing the manifests ensures that the configuration remains accessible even after the cluster and its cloud resources have been deleted.&lt;/p&gt;

&lt;p&gt;The repository also serves as portfolio evidence of the project implementation, including the manifest structure and version-control history.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Delete the Kubernetes Lab Namespace
&lt;/h2&gt;

&lt;p&gt;After the GitHub push succeeded, I removed the namespace containing the lab workloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete namespace k8s-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace "k8s-lab" deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then inspected the remaining namespaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get namespaces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output listed the remaining namespaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME              STATUS   AGE
default           Active   5h9m
kube-node-lease   Active   5h9m
kube-public       Active   5h9m
kube-system       Active   5h9m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvpy635hl62h5wxxybqq0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvpy635hl62h5wxxybqq0.png" alt="delete ns" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;k8s-lab&lt;/code&gt; namespace was no longer present.&lt;/p&gt;

&lt;p&gt;Deleting a namespace removes the Kubernetes resources contained in it, subject to their deletion and finalization behavior. This removed the dedicated lab namespace and its namespaced workloads from the cluster.&lt;/p&gt;

&lt;p&gt;However, deleting a Kubernetes namespace does not itself decommission the AKS cluster or remove every Azure resource associated with it. The Azure resource group therefore needed to be deleted separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Delete the Azure Resource Group
&lt;/h2&gt;

&lt;p&gt;The Azure environment used the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RESOURCE_GROUP=rg-aks-lab
CLUSTER_NAME=aks-lab
LOCATION=westeurope
NODE_SIZE=Standard_D2s_v3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I first attempted to delete the resource group using the environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returned an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;argument --name/-n/--resource-group/-g: expected one argument
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The variable was not populated in that shell because I switched to another terminal, so Azure CLI did not receive a resource group name.&lt;/p&gt;

&lt;p&gt;I then supplied the resource group name explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="nt"&gt;--name&lt;/span&gt; rg-aks-lab &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8m8i0fa33ncx0lecl48i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8m8i0fa33ncx0lecl48i.png" alt="delete resources" width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This issued the resource group deletion request.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--yes&lt;/code&gt; option skips the interactive confirmation, while &lt;code&gt;--no-wait&lt;/code&gt; returns without waiting for the deletion operation to finish. As a result, the command's return does not, on its own, establish that the resources have already disappeared.&lt;/p&gt;

&lt;p&gt;For cleanup operations, verification is essential because Azure resource deletion can continue asynchronously after the command returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Verify the Azure Resource Group Cleanup
&lt;/h2&gt;

&lt;p&gt;The AKS deployment had a primary resource group and an AKS-managed resource group. I checked for both using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group list &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?starts_with(name, 'rg-aks-lab') || starts_with(name, 'MC_rg-aks-lab')].name"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An initial check still showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rg-aks-lab
MC_rg-aks-lab_aks-lab_westeurope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbr896vpfhdzvzymwel8o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbr896vpfhdzvzymwel8o.png" alt="Initial deletion" width="800" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This indicated that the deletion process had not yet removed both resource groups from the listing.&lt;/p&gt;

&lt;p&gt;I repeated the check after the deletion process had progressed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group list &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?starts_with(name, 'rg-aks-lab') || starts_with(name, 'MC_rg-aks-lab')].name"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subsequent output was empty, confirming that neither matching resource group remained in the returned list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3n8cjo3voa6pb2yug7re.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3n8cjo3voa6pb2yug7re.png" alt="successful deletion" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The verification covered both the main lab resource group and the resource group created by AKS to manage supporting infrastructure.&lt;/p&gt;

&lt;p&gt;This distinction matters because AKS can create Azure resources outside the visible workload definitions in the Kubernetes manifests. A complete cleanup check should account for the managed infrastructure, rather than checking only the primary resource group.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Remove the Obsolete Kubernetes Context
&lt;/h2&gt;

&lt;p&gt;With the AKS environment decommissioned, I removed its local Kubernetes context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config delete-context aks-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warning: this removed your active context, use "kubectl config use-context" to select a different one
deleted context aks-lab from /home/rahimahisah/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyq4nuizvdtun3lzqx9mb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyq4nuizvdtun3lzqx9mb.png" alt="delete context" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The context had stored the connection configuration used by &lt;code&gt;kubectl&lt;/code&gt; to access the &lt;code&gt;aks-lab&lt;/code&gt; cluster.&lt;/p&gt;

&lt;p&gt;Removing it cleans up the local kubeconfig after the cluster has been deleted. The warning also notes that the removed context had been active, so another context would need to be selected before using &lt;code&gt;kubectl&lt;/code&gt; against a different cluster.&lt;/p&gt;

&lt;p&gt;This step removes the local connection entry; it does not delete the local repository, manifests, or GitHub project.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. What Remains After Cleanup?
&lt;/h2&gt;

&lt;p&gt;The cleanup separates the cloud environment from the project artifacts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Final state&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;k8s-lab&lt;/code&gt; namespace&lt;/td&gt;
&lt;td&gt;Deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main Azure resource group &lt;code&gt;rg-aks-lab&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No longer present in the verification listing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AKS-managed resource group&lt;/td&gt;
&lt;td&gt;No longer present in the verification listing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local &lt;code&gt;aks-lab&lt;/code&gt; kubeconfig context&lt;/td&gt;
&lt;td&gt;Deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local project directory&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes YAML manifests&lt;/td&gt;
&lt;td&gt;Preserved locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git commit history&lt;/td&gt;
&lt;td&gt;Preserved in the repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub repository&lt;/td&gt;
&lt;td&gt;Published and available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This separation is important for cloud labs. The resources used to demonstrate the implementation can be removed without discarding the configuration, documentation, and version-control history that make the work reproducible and useful as portfolio evidence.&lt;/p&gt;

&lt;p&gt;Although the Azure resource groups were removed from the verification listing, the final Azure bill remains the authoritative source for confirming any outstanding or previously accrued charges. Deletion stops the continued use of the deleted resources, but it does not erase charges already incurred.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Operational Takeaways
&lt;/h2&gt;

&lt;p&gt;This final mission brought together version control, cloud resource lifecycle management, and verification.&lt;/p&gt;

&lt;p&gt;The main takeaways were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version-controlled infrastructure:&lt;/strong&gt; Kubernetes manifests provide a durable record of the resource configuration used in the lab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository publishing:&lt;/strong&gt; Pushing the project to GitHub preserves the implementation beyond the lifetime of the AKS cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespace cleanup:&lt;/strong&gt; Removing the lab namespace deletes its dedicated Kubernetes workloads and resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure resource cleanup:&lt;/strong&gt; Deleting the primary resource group initiates removal of the cluster and related Azure resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous deletion:&lt;/strong&gt; Commands using &lt;code&gt;--no-wait&lt;/code&gt; require a separate verification step to confirm the resources have disappeared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed-resource awareness:&lt;/strong&gt; AKS can create a separate managed resource group that should be included in cleanup verification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local configuration hygiene:&lt;/strong&gt; Removing an obsolete kubeconfig context prevents local configuration from retaining an unnecessary connection entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost awareness:&lt;/strong&gt; Cloud cleanup should be verified rather than assumed, and actual billed amounts should be checked independently.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion: From Cluster Provisioning to Decommissioning
&lt;/h2&gt;

&lt;p&gt;The nine-part series brought together AKS provisioning, Kubernetes workloads, networking, configuration, persistent storage, batch processing, scheduled automation, and cost optimization.&lt;/p&gt;

&lt;p&gt;In this final mission, I committed the nine workload manifests, pushed the project to GitHub, deleted the &lt;code&gt;k8s-lab&lt;/code&gt; namespace, issued the Azure resource group deletion request, verified that the primary and AKS-managed resource groups were no longer listed, and removed the obsolete Kubernetes context.&lt;/p&gt;

&lt;p&gt;The result is a preserved project repository without the temporary cloud infrastructure used to run it.&lt;/p&gt;

&lt;p&gt;The central operational principle is straightforward: &lt;strong&gt;infrastructure work is not complete until its implementation has been preserved and its temporary resources have been deliberately decommissioned.&lt;/strong&gt; Version-controlled manifests retain the engineering evidence, while verified cleanup helps prevent unnecessary ongoing resource consumption.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is Part 9 of the nine-part series, “Kubernetes on AKS: From Zero to Production Workloads.”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>azure</category>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads - FinOps: Measure, Price and Cut Cluster Costs (Part 8/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 20:26:27 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-finops-measure-price-and-cut-cluster-costs-4be1</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-finops-measure-price-and-cut-cluster-costs-4be1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Cloud-native infrastructure is not production-ready simply because its workloads are running. It must also be measurable, cost-aware, and configured to avoid paying for unnecessary capacity.&lt;/p&gt;

&lt;p&gt;In this mission, I evaluated the resource consumption of my Azure Kubernetes Service (AKS) cluster, examined Kubernetes resource requests and limits, queried the Azure Retail Prices API to estimate compute costs, and implemented workload and node-pool scaling controls. I also removed an externally exposed LoadBalancer Service, tested the AKS stop/start lifecycle, and documented the findings and optimization actions in &lt;code&gt;COST-REPORT.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The objective was to connect Kubernetes resource telemetry with Azure pricing and practical cost-control measures, while distinguishing observed utilization from requested capacity, configured limits, and estimated charges.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Measuring node and Pod resource usage.&lt;/li&gt;
&lt;li&gt;Inspecting CPU and memory requests and limits.&lt;/li&gt;
&lt;li&gt;Installing &lt;code&gt;jq&lt;/code&gt; to process Azure pricing data.&lt;/li&gt;
&lt;li&gt;Querying the Azure Retail Prices API for the VM size used by the cluster.&lt;/li&gt;
&lt;li&gt;Estimating the monthly compute cost.&lt;/li&gt;
&lt;li&gt;Configuring workload autoscaling with a Horizontal Pod Autoscaler (HPA).&lt;/li&gt;
&lt;li&gt;Enabling cluster autoscaling for the AKS node pool.&lt;/li&gt;
&lt;li&gt;Removing the public LoadBalancer Service.&lt;/li&gt;
&lt;li&gt;Testing AKS stop/start operations.&lt;/li&gt;
&lt;li&gt;Creating and committing a cost report.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Measure Actual Resource Utilization
&lt;/h2&gt;

&lt;p&gt;I started with the node-level metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                                CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
aks-nodepool1-33590657-vmss000000   242m         12%      1479Mi          25%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlxkhjsnj1axlvcuqy8c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlxkhjsnj1axlvcuqy8c.png" alt="top" width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the time of measurement, the node was using approximately 12% of its reported CPU capacity and 25% of its memory capacity.&lt;/p&gt;

&lt;p&gt;These figures provide a snapshot of actual resource consumption. They are useful for identifying whether the node is under pressure or operating with substantial unused capacity, although a single observation does not establish the workload's peak or long-term requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the Pods consuming CPU
&lt;/h3&gt;

&lt;p&gt;I then inspected the Pods across namespaces, sorted by CPU usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top pods &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cpu | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzk89gv2vurbijf4bgddc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzk89gv2vurbijf4bgddc.png" alt="kubectl top pods" width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output included system components in &lt;code&gt;kube-system&lt;/code&gt; and application workloads in &lt;code&gt;k8s-lab&lt;/code&gt;, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;csi-azurefile-node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;azure-cns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;metrics-server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;konnectivity-agent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;coredns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kube-proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;web&lt;/code&gt; Deployment Pods&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node-agent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;store-0&lt;/code&gt; and &lt;code&gt;store-1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The displayed CPU consumption of the application Pods was low at the time of measurement. This helped establish that the cluster's workloads were not consuming a substantial share of the node's available CPU during the observed interval.&lt;/p&gt;

&lt;p&gt;The distinction matters because a Kubernetes cluster can have a relatively low current workload while still reserving significant capacity through its configured resource requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Compare Resource Requests, Limits and Actual Usage
&lt;/h2&gt;

&lt;p&gt;To understand how much capacity the workloads had requested or been permitted to use, I inspected the node's allocated resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe nodes | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 8 &lt;span class="s2"&gt;"Allocated resources"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqpuirc08y6wozoav5ur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqpuirc08y6wozoav5ur.png" alt="kubectl describe nodes" width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests       Limits
  --------           --------       ------
  cpu                1187m (62%)    10692m (562%)
  memory             1330Mi (22%)   13443360Ki (226%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The figures reveal three different aspects of resource management.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Observed node utilization&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocated requests&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocated limits&lt;/td&gt;
&lt;td&gt;562%&lt;/td&gt;
&lt;td&gt;226%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Actual utilization&lt;/strong&gt; represents observed consumption at the time of measurement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requests&lt;/strong&gt; represent the capacity declared by workloads for scheduling purposes. CPU requests of 62% indicate that workloads had reserved a significant proportion of the node's allocatable CPU capacity, even though observed CPU utilization was only 12%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limits&lt;/strong&gt; define upper resource boundaries for individual containers. Their aggregated percentages can exceed 100% because Kubernetes permits resource overcommitment. The reported CPU limit of 562% therefore does not mean that the node was actually using more than five times its CPU capacity.&lt;/p&gt;

&lt;p&gt;These measurements should not be interpreted interchangeably. Low current utilization can indicate an opportunity to review requests, but reducing them requires an understanding of the workload's peak behavior, performance requirements, and reliability objectives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the Deployment's resource settings
&lt;/h3&gt;

&lt;p&gt;I inspected the container configuration for the &lt;code&gt;web&lt;/code&gt; Deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get deployment web &lt;span class="nt"&gt;-n&lt;/span&gt; k8s-lab &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.spec.template.spec.containers[] |
        {name: .name, requests: .resources.requests, limits: .resources.limits}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nginx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"50m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"64Mi"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"200m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"128Mi"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each NGINX container requested &lt;code&gt;50m&lt;/code&gt; CPU and &lt;code&gt;64Mi&lt;/code&gt; memory, with limits of &lt;code&gt;200m&lt;/code&gt; CPU and &lt;code&gt;128Mi&lt;/code&gt; memory.&lt;/p&gt;

&lt;p&gt;With three replicas, those declared requests contribute to the Deployment's aggregate resource reservations. Repeating this comparison across workloads helps identify where requests may be substantially higher than typical consumption or where capacity may be constrained.&lt;/p&gt;

&lt;p&gt;The goal is not to minimize requests indiscriminately. It is to align resource allocation with actual workload requirements while preserving sufficient headroom for bursts and normal operating variation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Install jq for Pricing Data Analysis
&lt;/h2&gt;

&lt;p&gt;To process the JSON returned by Azure's pricing endpoint, I installed &lt;code&gt;jq&lt;/code&gt; in the Linux environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu128umxwf1wcr0l1x9jm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu128umxwf1wcr0l1x9jm.png" alt="install" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The package manager installed &lt;code&gt;jq&lt;/code&gt; and its required dependencies successfully.&lt;/p&gt;

&lt;p&gt;This utility made it easier to filter the Azure Retail Prices API response and extract fields such as the product name, meter, price, unit of measure, region, and price type.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Retrieve VM Pricing from the Azure Retail Prices API
&lt;/h2&gt;

&lt;p&gt;The cluster configuration identified the following environment values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RESOURCE_GROUP=rg-aks-lab
CLUSTER_NAME=aks-lab
LOCATION=westeurope
NODE_SIZE=Standard_D2s_v3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I queried the Azure Retail Prices API for virtual machine pricing matching the region and VM size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sG&lt;/span&gt; &lt;span class="s2"&gt;"https://prices.azure.com/api/retail/prices"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;filter=serviceName eq 'Virtual Machines' and armRegionName eq '&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;' and armSkuName eq '&lt;/span&gt;&lt;span class="nv"&gt;$NODE_SIZE&lt;/span&gt;&lt;span class="s2"&gt;' and priceType eq 'Consumption'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.Items[]
    | select(.meterName | test("Spot|Low Priority"; "i") | not)
    | [.productName, .skuName, .meterName, .retailPrice, .unitOfMeasure, .armRegionName, .priceType]
    | @tsv'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0o3rx0ugtay3toic5qe1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0o3rx0ugtay3toic5qe1.png" alt="cost" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The response contained multiple pricing records for the requested VM SKU. The available records included different operating-system products and pricing types, so the SKU alone was not sufficient to select the correct rate.&lt;/p&gt;

&lt;p&gt;I compared the returned fields to distinguish the applicable consumption rate from Windows-specific, DevTest, Spot, and Low Priority entries.&lt;/p&gt;

&lt;p&gt;For the cost report, I used an observed public retail consumption rate of &lt;strong&gt;$0.12 per hour&lt;/strong&gt; for the &lt;code&gt;Standard_D2s_v3&lt;/code&gt; VM in &lt;code&gt;westeurope&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Estimate the monthly VM compute cost
&lt;/h3&gt;

&lt;p&gt;Using a simplified month of 730 hours, the calculation was:&lt;/p&gt;

&lt;p&gt;[&lt;br&gt;
\text{Estimated monthly cost} =&lt;br&gt;
\text{Hourly rate} \times 730&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;[&lt;br&gt;
\$0.12 \times 730 = \$87.60&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;The resulting estimate was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Estimate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VM size&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Standard_D2s_v3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;&lt;code&gt;westeurope&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hourly rate used&lt;/td&gt;
&lt;td&gt;$0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assumed monthly hours&lt;/td&gt;
&lt;td&gt;730&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated monthly VM compute cost&lt;/td&gt;
&lt;td&gt;$87.60&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a price-based estimate, not a confirmed Azure invoice amount. The calculation covers the VM rate used in the report and excludes additional AKS and Azure resource charges. Actual billing can also depend on the applicable pricing record, subscription or purchasing terms, usage duration, and associated resources.&lt;/p&gt;

&lt;p&gt;For that reason, the estimate provides a useful baseline for cost planning, but it should not be presented as the complete operating cost of the cluster.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Identify and Remove Unnecessary Public Exposure
&lt;/h2&gt;

&lt;p&gt;I reviewed the Services across namespaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get services &lt;span class="nt"&gt;-A&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk4ilpxj4ucfoempg4oxi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk4ilpxj4ucfoempg4oxi.png" alt="get services" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed the internal &lt;code&gt;web-svc&lt;/code&gt; ClusterIP Service, the headless &lt;code&gt;store&lt;/code&gt; Service, and the &lt;code&gt;web-public&lt;/code&gt; LoadBalancer Service.&lt;/p&gt;

&lt;p&gt;The public Service was exposing NGINX using this external IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20.61.154.190
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I inspected its configuration and endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe service web-public &lt;span class="nt"&gt;-n&lt;/span&gt; k8s-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Service used the selector &lt;code&gt;app=web&lt;/code&gt;, exposed port &lt;code&gt;80&lt;/code&gt;, and routed traffic to the NGINX Pods. Its type was &lt;code&gt;LoadBalancer&lt;/code&gt;, and the external IP had been assigned.&lt;/p&gt;

&lt;p&gt;The lab no longer required this public endpoint for its remaining work, so I removed the Service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete service web-public &lt;span class="nt"&gt;-n&lt;/span&gt; k8s-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service "web-public" deleted from k8s-lab namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnpikfqt1m13vo4liosn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnpikfqt1m13vo4liosn.png" alt="service web-public" width="799" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Removing the Service eliminated that Kubernetes-managed public exposure. The internal Service remained available for cluster-internal access.&lt;/p&gt;

&lt;p&gt;This action also removed the LoadBalancer Service configuration that was no longer needed by the lab. Deleting the Service should not be treated as proof that every associated Azure charge disappears immediately; the resulting Azure resources and billing records still need to be considered when validating actual savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Configure Horizontal Pod Autoscaling
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;web&lt;/code&gt; Deployment had been configured with three replicas. Rather than permanently fixing it at that count, I created a Horizontal Pod Autoscaler (HPA) to allow Kubernetes to adjust the number of Pods based on CPU utilization.&lt;/p&gt;

&lt;p&gt;I configured the HPA as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl autoscale deployment web &lt;span class="nt"&gt;-n&lt;/span&gt; k8s-lab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu-percent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;70 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;horizontalpodautoscaler.autoscaling/web autoscaled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command displayed a deprecation notice for &lt;code&gt;--cpu-percent&lt;/code&gt;, recommending the newer &lt;code&gt;--cpu&lt;/code&gt; option with a percentage or resource quantity format.&lt;/p&gt;

&lt;p&gt;I verified the resulting HPA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get hpa web &lt;span class="nt"&gt;-n&lt;/span&gt; k8s-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME   REFERENCE      TARGETS   MINPODS   MAXPODS   REPLICAS
web    Deployment/web cpu: 2%/70%  1      5         3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbog0ml9wk4tfo4ooy2i4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbog0ml9wk4tfo4ooy2i4.png" alt="autoscaling" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the time of the check, the observed CPU utilization was approximately 2% against a 70% target. The HPA was configured to maintain between one and five replicas, with three replicas currently reported.&lt;/p&gt;

&lt;p&gt;This configuration allows the replica count to respond to CPU utilization rather than requiring manual scaling for every workload fluctuation.&lt;/p&gt;

&lt;p&gt;The HPA does not guarantee that replicas immediately scale down whenever usage is low, nor does it remove the need for appropriate resource requests and metrics. It evaluates workload metrics against its configuration and adjusts the desired replica count as conditions warrant.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Enable AKS Node-Pool Autoscaling
&lt;/h2&gt;

&lt;p&gt;Pod autoscaling changes the number of application replicas, but it does not automatically increase the number of virtual machines in the cluster unless cluster-level scaling is configured.&lt;/p&gt;

&lt;p&gt;I enabled the cluster autoscaler for &lt;code&gt;nodepool1&lt;/code&gt;, allowing the node count to vary between one and three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks nodepool update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; nodepool1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-cluster-autoscaler&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-count&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-count&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4rutf3hx7t6x6fgu3la.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4rutf3hx7t6x6fgu3la.png" alt="nodepool update" width="799" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The command updated the node-pool configuration. Azure CLI also displayed a notice that the command behavior had been altered by the &lt;code&gt;aks-preview&lt;/code&gt; extension.&lt;/p&gt;

&lt;p&gt;I then queried the effective settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks nodepool show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; nodepool1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"{Pool:name,Count:count,Autoscaling:enableAutoScaling,Min:minCount,Max:maxCount}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9f8iqn535zuclhc8ueui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9f8iqn535zuclhc8ueui.png" alt="nodepool show" width="799" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pool       Count   Autoscaling   Min   Max
nodepool1  1       True          1     3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This verified that cluster autoscaling was enabled and that the configured minimum and maximum node counts were one and three.&lt;/p&gt;

&lt;p&gt;The node pool was currently at one node. With autoscaling enabled, Kubernetes can request additional node capacity when scheduling demand requires it, subject to the configured limits and Azure's ability to provision the capacity.&lt;/p&gt;

&lt;h3&gt;
  
  
  HPA and cluster autoscaling serve different purposes
&lt;/h3&gt;

&lt;p&gt;These settings control separate layers of the environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Pod Autoscaler:&lt;/strong&gt; adjusts the number of application Pods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster autoscaler:&lt;/strong&gt; adjusts the number of AKS nodes when cluster capacity needs to change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The HPA was configured for one to five &lt;code&gt;web&lt;/code&gt; replicas, while the node pool was configured for one to three nodes. The two mechanisms work at different levels and should be configured together with appropriate resource requests, scheduling requirements, and capacity limits.&lt;/p&gt;

&lt;p&gt;The maximum node count is a limit, not a target. Enabling autoscaling does not mean the cluster will remain at three nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Test the AKS Stop/Start Lifecycle
&lt;/h2&gt;

&lt;p&gt;I also exercised the AKS cluster's stop/start commands.&lt;/p&gt;

&lt;p&gt;To stop the cluster, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks stop &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then started it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks start &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobae8z0i2q0k5twbrjw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobae8z0i2q0k5twbrjw7.png" alt="aks stop and start" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, I checked its reported power state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; powerState.code &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting state was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7q106m0zcm3flef6dod3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7q106m0zcm3flef6dod3.png" alt="az aks show" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that the cluster was running after the stop/start sequence.&lt;/p&gt;

&lt;p&gt;For a lab or development environment that is not needed continuously, stopping the cluster during idle periods can be part of a cost-control strategy. However, the cluster must first be checked for active workloads, scheduled operations, dependencies, and service availability requirements.&lt;/p&gt;

&lt;p&gt;The stop/start exercise demonstrates the control operation; it is not, on its own, a measured comparison of the before-and-after Azure bill. Actual savings should be verified against the applicable billing records and the resources that continue to incur charges.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Document the Findings in COST-REPORT.md
&lt;/h2&gt;

&lt;p&gt;To capture the analysis and decisions, I created &lt;code&gt;COST-REPORT.md&lt;/code&gt; in the project repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; COST-REPORT.md
# AKS FinOps Cost Report

## Cluster Configuration
- Resource Group: rg-aks-lab
- Cluster: aks-lab
- Region: westeurope
- Node Pool: nodepool1
- VM Size: Standard_D2s_v3
- Node Count: 1

## Resource Utilization
- Node CPU usage: 12%
- Node memory usage: 25%
- CPU requests: 62%
- Memory requests: 22%

## Cost Estimate
- Estimated VM rate: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="sh"&gt;.12/hour
- Estimated monthly VM cost (730 hours): &lt;/span&gt;&lt;span class="nv"&gt;$87&lt;/span&gt;&lt;span class="sh"&gt;.60
- Estimate excludes additional AKS and Azure resource charges.

## Cost Optimization Actions
- Configured web deployment autoscaling between 1 and 5 replicas.
- Enabled cluster autoscaling for nodepool1 between 1 and 3 nodes.
- Reduced the node pool to 1 node.
- Removed the web-public LoadBalancer service.
- Stopped the AKS cluster when not in use.

## Notes
The VM price is a public retail-price estimate, not a confirmed invoice amount. Actual costs depend on billing rates and associated Azure resources.
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmele89ywdn36w40xyyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmele89ywdn36w40xyyg.png" alt="cost report" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The report consolidated the cluster configuration, observed utilization, VM price estimate, and optimization actions into a version-controlled artifact.&lt;/p&gt;

&lt;p&gt;A concise cost report is useful because it distinguishes the measurements from the actions taken and documents what remains outside the estimate. In this case, it explicitly records that the $87.60 figure excludes additional AKS and Azure resource charges.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Commit the Cost Report
&lt;/h2&gt;

&lt;p&gt;I checked the repository state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi59vztmh45bzy7tzn3uj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi59vztmh45bzy7tzn3uj.png" alt="git status" width="800" height="190"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The report appeared as an untracked file. I staged it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add COST-REPORT.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I committed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"docs: AKS cost review and optimisation"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa39guq8hs23ynintuer3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa39guq8hs23ynintuer3.png" alt="git status and commit" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Git confirmed the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[main 2725349] docs: AKS cost review and optimisation
1 file changed, 30 insertions(+)
create mode 100644 COST-REPORT.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This committed the cost report to the &lt;code&gt;main&lt;/code&gt; branch, preserving the analysis and optimization decisions alongside the lab's other project documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Summary of Cost Optimization Decisions
&lt;/h2&gt;

&lt;p&gt;The main findings and actions were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Finding or action&lt;/th&gt;
&lt;th&gt;Operational purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node utilization&lt;/td&gt;
&lt;td&gt;12% CPU and 25% memory usage observed&lt;/td&gt;
&lt;td&gt;Establish a snapshot of actual resource consumption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource requests&lt;/td&gt;
&lt;td&gt;CPU requests at 62%; memory requests at 22%&lt;/td&gt;
&lt;td&gt;Identify declared capacity reservations for further review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource limits&lt;/td&gt;
&lt;td&gt;Aggregated CPU and memory limits above 100%&lt;/td&gt;
&lt;td&gt;Understand configured overcommitment rather than actual usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM pricing&lt;/td&gt;
&lt;td&gt;$0.12/hour used for the estimate&lt;/td&gt;
&lt;td&gt;Establish a public retail-price baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly estimate&lt;/td&gt;
&lt;td&gt;$87.60 for 730 hours of VM compute&lt;/td&gt;
&lt;td&gt;Quantify an illustrative compute cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload scaling&lt;/td&gt;
&lt;td&gt;HPA configured for one to five replicas&lt;/td&gt;
&lt;td&gt;Match the &lt;code&gt;web&lt;/code&gt; replica count to CPU demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node scaling&lt;/td&gt;
&lt;td&gt;Cluster autoscaler configured for one to three nodes&lt;/td&gt;
&lt;td&gt;Allow node capacity to adjust to scheduling demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public networking&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;web-public&lt;/code&gt; Service deleted&lt;/td&gt;
&lt;td&gt;Remove an external endpoint no longer required by the lab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster lifecycle&lt;/td&gt;
&lt;td&gt;Stop/start operations tested&lt;/td&gt;
&lt;td&gt;Establish an option for managing idle lab capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;COST-REPORT.md&lt;/code&gt; committed&lt;/td&gt;
&lt;td&gt;Preserve the findings and optimization actions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These actions address different aspects of cost management. Autoscaling controls capacity, removing an unnecessary Service eliminates unneeded exposure, resource measurements inform future tuning, and stopping an idle cluster provides a lifecycle control. The retail-price estimate supports planning but does not replace actual cost analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This FinOps review combined Kubernetes telemetry with Azure pricing data to establish a practical baseline for the AKS lab.&lt;/p&gt;

&lt;p&gt;The observed node utilization was 12% CPU and 25% memory, while resource requests and limits revealed how declared capacity differed from current consumption. A public VM consumption rate of $0.12 per hour was used to estimate $87.60 for 730 hours of VM compute, excluding additional AKS and Azure resource charges.&lt;/p&gt;

&lt;p&gt;I configured the &lt;code&gt;web&lt;/code&gt; HPA to scale between one and five replicas, enabled node-pool autoscaling between one and three nodes, removed the public &lt;code&gt;web-public&lt;/code&gt; Service, tested the AKS stop/start workflow, and committed the findings in &lt;code&gt;COST-REPORT.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The key principle is that cost optimization should be based on measured usage, explicit assumptions, and deliberate trade-offs. Reducing infrastructure cost is valuable only when the resulting capacity and availability remain appropriate for the workload.&lt;/p&gt;

&lt;p&gt;In Part 9, the series will conclude with the final mission, bringing the lab's implementation, documentation, and cleanup together.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is Part 8 of the nine-part series, “Kubernetes on AKS: From Zero to Production Workloads.”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>finops</category>
      <category>azure</category>
      <category>monitoring</category>
      <category>cloudcostoptimization</category>
    </item>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads -DaemonSets, Jobs and CronJobs (Part 7/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:25:12 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-daemonsets-jobs-and-cronjobs-part-79-4d8k</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-daemonsets-jobs-and-cronjobs-part-79-4d8k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Kubernetes supports more than continuously running application workloads. It also provides controllers for node-level agents, one-off batch processing, and scheduled tasks.&lt;/p&gt;

&lt;p&gt;In this mission, I implemented a DaemonSet to run a lightweight agent on each eligible AKS node, scaled the node pool to verify that the agent was scheduled on a newly added node, and then returned the pool to one node. I also configured a batch Job with three completions and a CronJob that runs every minute, verifying their execution through Job status and container logs.&lt;/p&gt;

&lt;p&gt;These workload types address different operational requirements: DaemonSets distribute workloads across nodes, Jobs manage finite tasks, and CronJobs create Jobs on a recurring schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Creating and validating a DaemonSet.&lt;/li&gt;
&lt;li&gt;Injecting the node name into a container through the Downward API.&lt;/li&gt;
&lt;li&gt;Scaling the AKS node pool and verifying DaemonSet placement.&lt;/li&gt;
&lt;li&gt;Defining a batch Job with three successful completions.&lt;/li&gt;
&lt;li&gt;Inspecting Job status and logs.&lt;/li&gt;
&lt;li&gt;Scheduling recurring work with a CronJob.&lt;/li&gt;
&lt;li&gt;Verifying scheduled executions and reviewing the resulting cluster resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Deploy a DaemonSet
&lt;/h2&gt;

&lt;p&gt;A DaemonSet ensures that a copy of a specified Pod runs on each eligible node. This makes it suitable for node-level agents that need to operate wherever workloads are scheduled.&lt;/p&gt;

&lt;p&gt;I defined the DaemonSet in &lt;code&gt;manifests/07-daemonset.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; manifests/07-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-agent
spec:
  selector:
    matchLabels:
      app: node-agent
  template:
    metadata:
      labels:
        app: node-agent
    spec:
      containers:
        - name: agent
          image: busybox:1.36
          command: ["sh", "-c", "while true; do echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt; agent running on node &lt;/span&gt;&lt;span class="nv"&gt;$NODE_NAME&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;; sleep 30; done"]
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
            limits:
              cpu: 50m
              memory: 32Mi
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgdjofi7zj46sih1zl0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgdjofi7zj46sih1zl0a.png" alt="daemonsets" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration details
&lt;/h3&gt;

&lt;p&gt;The manifest defines a DaemonSet named &lt;code&gt;node-agent&lt;/code&gt; with a single container using &lt;code&gt;busybox:1.36&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The container runs a loop that writes a timestamp and the node name to its output every 30 seconds. This provides a lightweight way to confirm which node is hosting each agent.&lt;/p&gt;

&lt;p&gt;The node name is injected through the Kubernetes Downward API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NODE_NAME&lt;/span&gt;
    &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fieldRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;fieldPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spec.nodeName&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids hardcoding a node name into the container command. Each Pod receives the name of the node on which it is running.&lt;/p&gt;

&lt;p&gt;The resource configuration also defines explicit requests and limits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;16Mi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;32Mi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These values keep the agent's resource requirements small while making its resource boundaries explicit.&lt;/p&gt;

&lt;p&gt;I applied the manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/07-daemonset.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;daemonset.apps/node-agent created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then monitored the rollout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout status daemonset/node-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;daemon set "node-agent" successfully rolled out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffiny2190vihz8fauqxo8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffiny2190vihz8fauqxo8.png" alt="daemon set" width="799" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Verify DaemonSet Placement and Logs
&lt;/h2&gt;

&lt;p&gt;To inspect the DaemonSet's status, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get daemonset node-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE
node-agent   1         1         1       1            1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, the cluster had one node, and the DaemonSet had one desired and ready Pod.&lt;/p&gt;

&lt;p&gt;I checked the Pod's placement with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node-agent &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output identified the Pod as &lt;code&gt;node-agent-rdmcq&lt;/code&gt;, running on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aks-nodepool1-33590657-vmss000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pod was in the &lt;code&gt;Running&lt;/code&gt; state and had the internal IP &lt;code&gt;10.244.0.172&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To confirm that the agent was reporting the correct node, I inspected its recent logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node-agent &lt;span class="nt"&gt;--prefix&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[pod/node-agent-rdmcq/agent] Mon Oct 26 10:02:52 UTC 2026 agent running on node aks-nodepool1-33590657-vmss000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7yuf4flruj2193c94ksv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7yuf4flruj2193c94ksv.png" alt="node-agent" width="798" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This verified that the agent was running and that the node name supplied through the Downward API was available inside the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Scale the AKS Node Pool to Verify DaemonSet Behavior
&lt;/h2&gt;

&lt;p&gt;To verify how the DaemonSet responds when the cluster gains another node, I scaled the AKS node pool from one node to two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks scale &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nodepool-name&lt;/span&gt; nodepool1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-count&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyx44xirrsp7gl0pijqrv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyx44xirrsp7gl0pijqrv.png" alt="aks create" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdijcmz2n6stvfmpwu1c9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdijcmz2n6stvfmpwu1c9.png" alt="aks create2" width="800" height="707"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Azure CLI returned the node pool configuration. Its output also displayed a notice that the command's behavior had been altered by the &lt;code&gt;aks-preview&lt;/code&gt; extension.&lt;/p&gt;

&lt;p&gt;Once the second node was available, I checked the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed two nodes in the &lt;code&gt;Ready&lt;/code&gt; state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                                STATUS   ROLES    VERSION
aks-nodepool1-33590657-vmss000000   Ready    &amp;lt;none&amp;gt;   v1.36.4
aks-nodepool1-33590657-vmss000001   Ready    &amp;lt;none&amp;gt;   v1.36.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then checked the DaemonSet Pods again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node-agent &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8iveghhcoybgnqkem4kv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8iveghhcoybgnqkem4kv.png" alt="2 nodes" width="800" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed one agent Pod on each node:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pod&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-agent-rdmcq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Running&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aks-nodepool1-33590657-vmss000000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-agent-mdscg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Running&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aks-nodepool1-33590657-vmss000001&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second agent was running on the newly added node, demonstrating the DaemonSet's node-level placement behavior.&lt;/p&gt;

&lt;p&gt;Unlike a Deployment, where the replica count is explicitly configured, a DaemonSet aims to maintain a Pod on every eligible node. As the eligible node count changes, the number of agent Pods changes accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Return the node pool to one node
&lt;/h3&gt;

&lt;p&gt;After verifying the two-node behavior, I scaled the node pool back to one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks scale &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nodepool-name&lt;/span&gt; nodepool1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-count&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fndmmims5zfmdaucam32m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fndmmims5zfmdaucam32m.png" alt="aks scale" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This returned the lab environment to its previous node count. Scaling a node pool affects the cluster's compute capacity and is also relevant when managing cloud costs for a lab environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Create a Batch Job
&lt;/h2&gt;

&lt;p&gt;A Kubernetes Job manages Pods that perform finite work and are expected to complete successfully. Unlike a continuously running Deployment, a Job tracks successful task completion.&lt;/p&gt;

&lt;p&gt;I defined the Job in &lt;code&gt;manifests/08-job.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; manifests/08-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: report
spec:
  completions: 3
  parallelism: 3
  backoffLimit: 2
  ttlSecondsAfterFinished: 3600
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: report
          image: busybox:1.36
          command: ["sh", "-c", "echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;Report part generated by &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;; sleep 5"]
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgknzucmwguf2d6zu5bgx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgknzucmwguf2d6zu5bgx.png" alt="batch job" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Job configuration
&lt;/h3&gt;

&lt;p&gt;The Job is named &lt;code&gt;report&lt;/code&gt; and uses &lt;code&gt;busybox:1.36&lt;/code&gt; to print a message identifying the Pod that generated each report part.&lt;/p&gt;

&lt;p&gt;The important settings are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;completions: 3&lt;/code&gt;&lt;/strong&gt; specifies that three successful completions are required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;parallelism: 3&lt;/code&gt;&lt;/strong&gt; allows up to three Job Pods to run concurrently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;backoffLimit: 2&lt;/code&gt;&lt;/strong&gt; sets the permitted retry count for failed Pods before the Job is considered failed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ttlSecondsAfterFinished: 3600&lt;/code&gt;&lt;/strong&gt; allows the completed Job to be eligible for automatic cleanup one hour after it finishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;restartPolicy: Never&lt;/code&gt;&lt;/strong&gt; prevents a failed container from being restarted within the same Pod.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This configuration models a small batch operation with three successful executions. Each Pod prints its own hostname, making it possible to distinguish the individual task executions in the logs.&lt;/p&gt;

&lt;p&gt;I applied the Job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/08-job.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initial output confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;job.batch/report created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first verification attempt returned a &lt;code&gt;NotFound&lt;/code&gt; error when waiting for the Job to complete, and the immediate Job listing showed no resources. I reapplied the manifest and checked the resulting Job again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/08-job.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi0jfp5qmx7lrhr3pi7e0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi0jfp5qmx7lrhr3pi7e0.png" alt="job yaml" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The subsequent execution completed successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Verify Job Completion and Inspect the Logs
&lt;/h2&gt;

&lt;p&gt;I checked the Job status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get job report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME     STATUS     COMPLETIONS   DURATION
report   Complete   3/3           10s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;3/3&lt;/code&gt; completion count confirmed that the Job had achieved its three required successful completions.&lt;/p&gt;

&lt;p&gt;To inspect the output from all the Job's Pods, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &lt;span class="nt"&gt;-l&lt;/span&gt; job-name&lt;span class="o"&gt;=&lt;/span&gt;report &lt;span class="nt"&gt;--prefix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj6jlyui74pj9dqzldc6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj6jlyui74pj9dqzldc6v.png" alt="kubectl logs" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The logs included messages such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[pod/report-2v8b5/report] Report part generated by report-2v8b5
[pod/report-bxzps/report] Report part generated by report-bxzps
[pod/report-plnqc/report] Report part generated by report-plnqc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each execution produced a message containing its own Pod hostname. This confirmed that the Job's three task executions ran and completed.&lt;/p&gt;

&lt;p&gt;The logs also demonstrate why Job status and application output are complementary checks. The completion count confirms the controller's recorded outcome, while the logs show what the containers actually executed.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Schedule Recurring Work with a CronJob
&lt;/h2&gt;

&lt;p&gt;A CronJob creates Jobs according to a schedule. It is useful for recurring operations such as periodic reporting, cleanup tasks, and scheduled maintenance.&lt;/p&gt;

&lt;p&gt;I defined the CronJob in &lt;code&gt;manifests/09-cronjob.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; manifests/09-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: heartbeat
spec:
  schedule: "*/1 * * * *"
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: heartbeat
              image: busybox:1.36
              command: ["sh", "-c", "echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;Heartbeat at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;"]
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CronJob is named &lt;code&gt;heartbeat&lt;/code&gt; and uses the schedule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*/1 * * * *
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cron expression requests execution every minute.&lt;/p&gt;

&lt;p&gt;The Job template uses a BusyBox container that prints a timestamp. Unlike the &lt;code&gt;report&lt;/code&gt; Job, which required three successful completions, this CronJob creates a new Job each time its schedule is triggered.&lt;/p&gt;

&lt;p&gt;The history settings control how many finished Jobs are retained by the CronJob controller:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;successfulJobsHistoryLimit: 2&lt;/code&gt; retains up to two successful Jobs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failedJobsHistoryLimit: 1&lt;/code&gt; retains up to one failed Job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Pod template uses &lt;code&gt;restartPolicy: OnFailure&lt;/code&gt;, allowing a failed container to be restarted within its Pod according to Kubernetes' restart behavior.&lt;/p&gt;

&lt;p&gt;I applied the manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/09-cronjob.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cronjob.batch/heartbeat created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2btzp6zgasp61q98mnej.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2btzp6zgasp61q98mnej.png" alt="cronjob" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Verify Scheduled Executions
&lt;/h2&gt;

&lt;p&gt;I inspected the CronJob:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get cronjob heartbeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jyku6ipcbnx5cqn4vkv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jyku6ipcbnx5cqn4vkv.png" alt="cronjob heartbeat" width="799" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed the every-minute schedule and the CronJob's status fields, including its suspension state, active Job count, and last scheduled execution.&lt;/p&gt;

&lt;p&gt;The initial check occurred before the first scheduled run had been recorded, so the &lt;code&gt;LAST SCHEDULE&lt;/code&gt; field was still empty.&lt;/p&gt;

&lt;p&gt;After subsequent checks, I listed Jobs in creation order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get &lt;span class="nb"&gt;jobs&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.metadata.creationTimestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output included the completed &lt;code&gt;report&lt;/code&gt; Job and a completed Job created by the &lt;code&gt;heartbeat&lt;/code&gt; CronJob.&lt;/p&gt;

&lt;p&gt;To inspect the latest scheduled Job's output, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs job/&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get &lt;span class="nb"&gt;jobs&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.metadata.creationTimestamp &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[-1].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Heartbeat at Mon Oct 26 12:34:01 UTC 2026
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3h9m9fmahs6nljf7b4wf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3h9m9fmahs6nljf7b4wf.png" alt="kubectl logs job" width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that the scheduled workload had executed and produced its expected message.&lt;/p&gt;

&lt;p&gt;The naming pattern also makes the controller relationship visible: the CronJob is named &lt;code&gt;heartbeat&lt;/code&gt;, while the Jobs it creates include a generated suffix.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Inspect the Combined Cluster State
&lt;/h2&gt;

&lt;p&gt;After deploying the DaemonSet, Job, and CronJob, I inspected the broader set of resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get all,configmap,secret,pvc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fld6xajdmfmjlo2trdeh2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fld6xajdmfmjlo2trdeh2.png" alt="kubectl get cm,pvc" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flv8mcy8pzstx5m6uztf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flv8mcy8pzstx5m6uztf3.png" alt="kubectl get cm, secret,pvc" width="800" height="265"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls manifests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifests folder contains nine yaml files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimyfhtyj6ixgxtceebt4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimyfhtyj6ixgxtceebt4.png" alt="manifests" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed the different workload types operating in the &lt;code&gt;k8s-lab&lt;/code&gt; namespace.&lt;/p&gt;

&lt;p&gt;The relevant resources included:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Observed state&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;node-agent&lt;/code&gt; DaemonSet&lt;/td&gt;
&lt;td&gt;One desired and ready agent Pod after returning the node pool to one node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;report&lt;/code&gt; Job&lt;/td&gt;
&lt;td&gt;Complete, with &lt;code&gt;3/3&lt;/code&gt; successful completions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;heartbeat&lt;/code&gt; CronJob&lt;/td&gt;
&lt;td&gt;Configured to run every minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;heartbeat&lt;/code&gt; Jobs&lt;/td&gt;
&lt;td&gt;Completed executions recorded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;store&lt;/code&gt; StatefulSet&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2/2&lt;/code&gt; Pods ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;web&lt;/code&gt; Deployment&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;3/3&lt;/code&gt; replicas available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;web-svc&lt;/code&gt; Service&lt;/td&gt;
&lt;td&gt;Internal &lt;code&gt;ClusterIP&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;web-public&lt;/code&gt; Service&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LoadBalancer&lt;/code&gt; with external IP &lt;code&gt;20.61.154.190&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;store&lt;/code&gt; Service&lt;/td&gt;
&lt;td&gt;Headless Service with no cluster IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StatefulSet PVCs&lt;/td&gt;
&lt;td&gt;Two claims bound, each with &lt;code&gt;1Gi&lt;/code&gt; capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  9. DaemonSets, Jobs and CronJobs: Key Differences
&lt;/h2&gt;

&lt;p&gt;Although all three resources can create and manage Pods, they serve distinct purposes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Primary behavior&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DaemonSet&lt;/td&gt;
&lt;td&gt;Maintains a Pod on each eligible node&lt;/td&gt;
&lt;td&gt;Node-level agents, monitoring, logging, and other node-scoped services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;Runs Pods until a finite task reaches its required successful completion count&lt;/td&gt;
&lt;td&gt;Batch processing, report generation, and one-off operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CronJob&lt;/td&gt;
&lt;td&gt;Creates Jobs according to a recurring schedule&lt;/td&gt;
&lt;td&gt;Scheduled reports, periodic maintenance, and recurring automation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The choice depends on the execution model.&lt;/p&gt;

&lt;p&gt;A DaemonSet follows the eligible node count. A Job tracks successful completion of finite work. A CronJob adds a time-based schedule for creating those Jobs.&lt;/p&gt;

&lt;p&gt;These resources are complementary rather than interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Operational Takeaways
&lt;/h2&gt;

&lt;p&gt;This mission demonstrated several useful Kubernetes operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node-level coverage:&lt;/strong&gt; A DaemonSet automatically placed an agent on each eligible node, including the second node added during the test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime visibility:&lt;/strong&gt; The Downward API supplied the node name to the agent without hardcoding infrastructure details into the container command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finite task execution:&lt;/strong&gt; The &lt;code&gt;report&lt;/code&gt; Job completed three successful tasks and exposed their outputs through logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring automation:&lt;/strong&gt; The &lt;code&gt;heartbeat&lt;/code&gt; CronJob created scheduled Jobs that printed timestamps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution verification:&lt;/strong&gt; Job completion counts and container logs provided separate ways to verify task outcomes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute management:&lt;/strong&gt; Scaling the AKS node pool demonstrated the relationship between node count and DaemonSet placement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource awareness:&lt;/strong&gt; The final resource listing showed how node-level, batch, scheduled, stateless, and stateful workloads coexist in the same cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;node-agent&lt;/code&gt; DaemonSet was deployed and verified on the AKS node pool. Scaling the pool to two nodes resulted in one agent Pod on each node, and the pool was subsequently returned to one node.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;report&lt;/code&gt; Job completed all three required executions, with logs confirming that each Pod generated its report message. The &lt;code&gt;heartbeat&lt;/code&gt; CronJob also ran successfully on its configured every-minute schedule, producing timestamped output.&lt;/p&gt;

&lt;p&gt;Together, these implementations demonstrate how Kubernetes handles node-scoped agents, finite batch work, and recurring tasks through dedicated controllers.&lt;/p&gt;

&lt;p&gt;In Part 8, the series will continue with the next mission, building on the workload and operational capabilities established here.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is Part 7 of the nine-part series, “Kubernetes on AKS: From Zero to Production Workloads.”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>daemonsets</category>
      <category>cronjobs</category>
      <category>aks</category>
      <category>containers</category>
    </item>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads - StatefulSets and Persistent Storage (Part 6/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 18:38:17 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-statefulsets-and-persistent-storage-part-27ml</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-statefulsets-and-persistent-storage-part-27ml</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Applications that maintain data need more than a reliable way to run containers. They also need predictable identities, stable network addresses, and storage that remains available when Pods are replaced.&lt;/p&gt;

&lt;p&gt;In this mission, I deployed a StatefulSet on Azure Kubernetes Service (AKS), paired it with a headless Service, and provisioned persistent Azure Disk volumes for its replicas. I then wrote data from inside a Pod, deleted that Pod, and verified that the replacement could still access the data. Finally, I tested the StatefulSet's DNS identity from another Pod in the cluster.&lt;/p&gt;

&lt;p&gt;The objective was to demonstrate how Kubernetes supports stateful workloads whose identity and data must survive individual Pod replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Inspecting the available Kubernetes StorageClasses.&lt;/li&gt;
&lt;li&gt;Creating a headless Service.&lt;/li&gt;
&lt;li&gt;Defining a StatefulSet with two replicas.&lt;/li&gt;
&lt;li&gt;Provisioning persistent storage through &lt;code&gt;managed-csi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verifying Pod identities and PersistentVolumeClaims.&lt;/li&gt;
&lt;li&gt;Writing data to a Pod's persistent volume.&lt;/li&gt;
&lt;li&gt;Deleting and waiting for a StatefulSet Pod to return.&lt;/li&gt;
&lt;li&gt;Verifying that data survives Pod replacement.&lt;/li&gt;
&lt;li&gt;Testing stable DNS access through the headless Service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Inspect the Available StorageClasses
&lt;/h2&gt;

&lt;p&gt;Before defining the StatefulSet, I inspected the StorageClasses available in the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get storageclass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzci9d1uje3svyf8thwc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzci9d1uje3svyf8thwc1.png" alt="storageclass" width="799" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed several StorageClasses for Azure Files and Azure Disk. The &lt;code&gt;managed-csi&lt;/code&gt; StorageClass was available for persistent disk provisioning.&lt;/p&gt;

&lt;p&gt;Its relevant properties included:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;StorageClass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;managed-csi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provisioner&lt;/td&gt;
&lt;td&gt;&lt;code&gt;disk.csi.azure.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reclaim policy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Delete&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume binding mode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WaitForFirstConsumer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume expansion&lt;/td&gt;
&lt;td&gt;Enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;managed-csi&lt;/code&gt; StorageClass uses the Azure Disk Container Storage Interface (CSI) driver. It allows Kubernetes to provision Azure-managed disks for workloads that request persistent storage.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;WaitForFirstConsumer&lt;/code&gt; binding mode delays volume binding and provisioning until a Pod using the claim is scheduled. This allows storage provisioning to account for the Pod's scheduling requirements.&lt;/p&gt;

&lt;p&gt;The reclaim policy is also important. &lt;code&gt;Delete&lt;/code&gt; means that a dynamically provisioned volume can be deleted when its PersistentVolume is released, depending on the lifecycle of the associated storage resources. Deleting an individual StatefulSet Pod does not, by itself, release its PersistentVolumeClaim.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Define a Headless Service and StatefulSet
&lt;/h2&gt;

&lt;p&gt;I created &lt;code&gt;manifests/06-statefulset.yaml&lt;/code&gt; to define both the headless Service and the StatefulSet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; manifests/06-statefulset.yaml
apiVersion: v1
kind: Service
metadata:
  name: store
spec:
  clusterIP: None
  selector:
    app: store
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: store
spec:
  serviceName: store
  replicas: 2
  selector:
    matchLabels:
      app: store
  template:
    metadata:
      labels:
        app: store
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
          volumeMounts:
            - name: data
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: managed-csi
        resources:
          requests:
            storage: 1Gi
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nz2tj36f51b39l0bhbv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nz2tj36f51b39l0bhbv.png" alt="statefulSet" width="799" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why use a headless Service?
&lt;/h3&gt;

&lt;p&gt;The Service is configured with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;clusterIP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it a headless Service. Instead of providing a conventional cluster IP, it enables DNS records to resolve directly to the endpoints of the selected Pods.&lt;/p&gt;

&lt;p&gt;The Service selects Pods labelled &lt;code&gt;app: store&lt;/code&gt; and exposes port &lt;code&gt;80&lt;/code&gt;. It also provides the network identity associated with the StatefulSet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why use a StatefulSet?
&lt;/h3&gt;

&lt;p&gt;The StatefulSet is named &lt;code&gt;store&lt;/code&gt; and declares two replicas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;serviceName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;store&lt;/span&gt;
&lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike ordinary Deployment-managed Pods, StatefulSet Pods have stable ordinal identities. The two replicas use these names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;store-0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;store-1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The StatefulSet also uses the &lt;code&gt;store&lt;/code&gt; headless Service as its governing Service, allowing the Pods to have predictable DNS identities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Persistent storage
&lt;/h3&gt;

&lt;p&gt;The manifest defines a &lt;code&gt;volumeClaimTemplates&lt;/code&gt; entry named &lt;code&gt;data&lt;/code&gt;. Each replica receives its own PersistentVolumeClaim requesting &lt;code&gt;1Gi&lt;/code&gt; of storage from the &lt;code&gt;managed-csi&lt;/code&gt; StorageClass.&lt;/p&gt;

&lt;p&gt;The important settings are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteOnce&lt;/span&gt;
&lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;managed-csi&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container mounts the volume at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/share/nginx/html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the directory NGINX serves as its default document root. Writing a file there allows the test to demonstrate whether the contents remain available after a Pod is replaced.&lt;/p&gt;

&lt;p&gt;Because each StatefulSet replica receives its own claim, &lt;code&gt;store-0&lt;/code&gt; and &lt;code&gt;store-1&lt;/code&gt; do not share the same per-replica volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Deploy the StatefulSet
&lt;/h2&gt;

&lt;p&gt;I applied the manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/06-statefulset.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed that both resources were created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service/store created
statefulset.apps/store created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then monitored the StatefulSet rollout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout status statefulset/store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the rollout, I inspected the StatefulSet, Pods, and PersistentVolumeClaims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get statefulset,pods,pvc &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two Pods were running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME        READY   STATUS    RESTARTS
pod/store-0 1/1     Running   0
pod/store-1 1/1     Running   0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PersistentVolumeClaims were also bound:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                    STATUS   CAPACITY   ACCESS MODES   STORAGECLASS
persistentvolumeclaim/data-store-0   Bound   1Gi   RWO   managed-csi
persistentvolumeclaim/data-store-1   Bound   1Gi   RWO   managed-csi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3exktc0t4f2grm7nhc1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3exktc0t4f2grm7nhc1r.png" alt="verify pvc" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This verified that the StatefulSet had created both replicas and Kubernetes had provisioned a separate &lt;code&gt;1Gi&lt;/code&gt; persistent volume claim for each one.&lt;/p&gt;

&lt;p&gt;The stable Pod names and individual claims are central to the StatefulSet model: a replacement for &lt;code&gt;store-0&lt;/code&gt; is associated with the identity and storage claim belonging to &lt;code&gt;store-0&lt;/code&gt;, rather than being treated as an entirely unrelated replica.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Write Data to the Persistent Volume
&lt;/h2&gt;

&lt;p&gt;To test persistence, I wrote a message to the NGINX document root inside &lt;code&gt;store-0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;store-0 &lt;span class="nt"&gt;--&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo "Hello from $(hostname), written at $(date)" &amp;gt; /usr/share/nginx/html/index.html'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4i45zv8dty5gqq3lbayl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4i45zv8dty5gqq3lbayl.png" alt="kubectl exec" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The command writes a message containing the Pod's hostname and the current date into &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because the document root is backed by the persistent volume mounted at &lt;code&gt;/usr/share/nginx/html&lt;/code&gt;, the test data is stored on the volume rather than only in the container's writable layer.&lt;/p&gt;

&lt;p&gt;This creates a clear verification point: after the Pod is replaced, the file should still contain the original message if the same persistent claim is attached successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Delete the Pod and Wait for Replacement
&lt;/h2&gt;

&lt;p&gt;I deleted &lt;code&gt;store-0&lt;/code&gt; to test the StatefulSet's recovery behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pod store-0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fju5w3fth9v717epu95zz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fju5w3fth9v717epu95zz.png" alt="delete pod" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes confirmed that the Pod had been deleted from the &lt;code&gt;k8s-lab&lt;/code&gt; namespace.&lt;/p&gt;

&lt;p&gt;The StatefulSet controller then worked to restore the desired replica count. Because &lt;code&gt;store-0&lt;/code&gt; is part of a StatefulSet, Kubernetes recreates the Pod with the same ordinal identity and reuses its associated PersistentVolumeClaim.&lt;/p&gt;

&lt;p&gt;I waited for the replacement Pod to become ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready pod/store-0 &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;180s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pod/store-0 condition met
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fm6y312biy4mih68f43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fm6y312biy4mih68f43.png" alt="kubectl wait" width="800" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that &lt;code&gt;store-0&lt;/code&gt; had returned to the &lt;code&gt;Ready&lt;/code&gt; state.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Verify That the Data Survived
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;store-0&lt;/code&gt; ready again, I inspected the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;store-0 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /usr/share/nginx/html/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello from store-0, written at Mon Oct 26 09:56:05 UTC 2026
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdn3kjhuqqvw0p0ukd6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdn3kjhuqqvw0p0ukd6c.png" alt="kubectl exec" width="799" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The message remained available after the original Pod had been deleted and replaced.&lt;/p&gt;

&lt;p&gt;This confirmed the key behavior being tested: the replacement Pod retained access to the data stored on the PersistentVolume associated with &lt;code&gt;store-0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Pod itself is replaceable, but its identity and associated persistent storage allow the application to recover without losing the file created during the test.&lt;/p&gt;

&lt;p&gt;This does not mean that every storage configuration automatically survives every failure scenario. Data persistence depends on the volume lifecycle, reclaim policy, storage backend, and how the claims and volumes are managed. In this test, the StatefulSet's per-replica claim remained available through Pod replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Verify Stable DNS from Another Pod
&lt;/h2&gt;

&lt;p&gt;The headless Service also provides predictable network identities for the StatefulSet Pods.&lt;/p&gt;

&lt;p&gt;To verify DNS access, I launched a temporary BusyBox Pod and requested the content through the hostname &lt;code&gt;store-0.store&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run dns-test &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--&lt;/span&gt; wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; http://store-0.store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello from store-0, written at Mon Oct 26 09:56:05 UTC 2026
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11hy9o8stgky8o6ka0io.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11hy9o8stgky8o6ka0io.png" alt="dns" width="798" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The temporary Pod returned the same content written before &lt;code&gt;store-0&lt;/code&gt; was deleted.&lt;/p&gt;

&lt;p&gt;This verified two aspects of the implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The name &lt;code&gt;store-0.store&lt;/code&gt; resolved and allowed access to the NGINX endpoint.&lt;/li&gt;
&lt;li&gt;The endpoint served the data that persisted across the Pod replacement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;store-0.store&lt;/code&gt; hostname is derived from the StatefulSet Pod identity and its governing headless Service. This predictable naming is particularly useful for applications that need to communicate with specific replicas.&lt;/p&gt;

&lt;p&gt;The temporary &lt;code&gt;dns-test&lt;/code&gt; Pod was configured with &lt;code&gt;--rm&lt;/code&gt;, so Kubernetes removed it when the command completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. StatefulSet vs. Deployment
&lt;/h2&gt;

&lt;p&gt;StatefulSets and Deployments both manage replicated workloads, but they address different operational requirements.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;StatefulSet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pod identity&lt;/td&gt;
&lt;td&gt;Typically interchangeable Pod names&lt;/td&gt;
&lt;td&gt;Stable ordinal names such as &lt;code&gt;store-0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable per-replica storage&lt;/td&gt;
&lt;td&gt;Not provided automatically through a Deployment's usual model&lt;/td&gt;
&lt;td&gt;Supported through &lt;code&gt;volumeClaimTemplates&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common use cases&lt;/td&gt;
&lt;td&gt;Stateless web applications and APIs&lt;/td&gt;
&lt;td&gt;Stateful applications requiring stable identity or storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling and replacement&lt;/td&gt;
&lt;td&gt;Reconciles desired replica count&lt;/td&gt;
&lt;td&gt;Reconciles replicas while preserving ordinal identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Networking&lt;/td&gt;
&lt;td&gt;Commonly accessed through a Service&lt;/td&gt;
&lt;td&gt;Can use a headless Service for per-Pod DNS identities&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A StatefulSet is not automatically the right choice for every application that writes files. The appropriate workload resource depends on whether the application needs stable identity, ordered operations, or persistent storage tied to individual replicas.&lt;/p&gt;

&lt;p&gt;In this mission, those features made the StatefulSet a suitable way to demonstrate data persistence across Pod replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Operational Takeaways
&lt;/h2&gt;

&lt;p&gt;This mission demonstrated how StatefulSets, headless Services, and persistent storage work together on AKS.&lt;/p&gt;

&lt;p&gt;The main takeaways were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable workload identity:&lt;/strong&gt; StatefulSet replicas have predictable names that remain associated with their ordinals when Pods are replaced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent storage:&lt;/strong&gt; Each replica receives its own PersistentVolumeClaim through &lt;code&gt;volumeClaimTemplates&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic provisioning:&lt;/strong&gt; The &lt;code&gt;managed-csi&lt;/code&gt; StorageClass allows Kubernetes to provision Azure Disk storage for the claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pod recovery:&lt;/strong&gt; Deleting &lt;code&gt;store-0&lt;/code&gt; caused the StatefulSet controller to recreate the same logical replica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data continuity:&lt;/strong&gt; The file written before deletion remained available after the replacement Pod became ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable DNS:&lt;/strong&gt; A headless Service enables per-Pod DNS identities, allowing another Pod to access &lt;code&gt;store-0&lt;/code&gt; through &lt;code&gt;store-0.store&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage lifecycle awareness:&lt;/strong&gt; Persistent volume behavior depends on the claim and volume lifecycle, access modes, storage class, and reclaim policy—not only on the Pod resource.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;store&lt;/code&gt; StatefulSet was deployed with two replicas, a headless Service, and separate &lt;code&gt;1Gi&lt;/code&gt; persistent volume claims backed by the &lt;code&gt;managed-csi&lt;/code&gt; StorageClass.&lt;/p&gt;

&lt;p&gt;I wrote a file to &lt;code&gt;store-0&lt;/code&gt;, deleted the Pod, waited for the replacement to become ready, and verified that the original file content remained available. A request to &lt;code&gt;http://store-0.store&lt;/code&gt; from a temporary Pod returned the same content, confirming that the stable DNS identity and persistent storage were working together as intended.&lt;/p&gt;

&lt;p&gt;This is the primary distinction between simply recreating a container and managing a stateful workload: the replacement can recover with its expected identity and retain access to its data.&lt;/p&gt;

&lt;p&gt;In Part 7, the series will continue with the next mission, building on the workload and storage capabilities established here.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is Part 6 of the nine-part series, “Kubernetes on AKS: From Zero to Production Workloads.”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>statefulset</category>
      <category>kubernetes</category>
      <category>aks</category>
      <category>azure</category>
    </item>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads - Services, ConfigMaps and Secrets (Part 5/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 18:12:33 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-services-configmaps-and-secrets-part-59-2lg3</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-services-configmaps-and-secrets-part-59-2lg3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A running application needs more than healthy Pods. It also needs a reliable way to receive traffic and a secure, maintainable approach to handling configuration.&lt;/p&gt;

&lt;p&gt;In this mission, I extended the NGINX Deployment on Azure Kubernetes Service (AKS) by creating internal and external Services, testing application access, and separating configuration values from the container image. I also created a Kubernetes Secret, verified its stored value, and injected both ConfigMap and Secret data into the running Deployment as environment variables.&lt;/p&gt;

&lt;p&gt;The implementation demonstrates how Kubernetes networking and configuration resources work together to make workloads accessible and configurable without rebuilding their images.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Creating a &lt;code&gt;ClusterIP&lt;/code&gt; Service for internal application access.&lt;/li&gt;
&lt;li&gt;Testing the NGINX application inside the cluster.&lt;/li&gt;
&lt;li&gt;Exposing the Deployment through a &lt;code&gt;LoadBalancer&lt;/code&gt; Service.&lt;/li&gt;
&lt;li&gt;Verifying the public endpoint.&lt;/li&gt;
&lt;li&gt;Creating a ConfigMap from literal values.&lt;/li&gt;
&lt;li&gt;Creating a Secret for a sensitive configuration value.&lt;/li&gt;
&lt;li&gt;Injecting ConfigMap and Secret values into the Deployment.&lt;/li&gt;
&lt;li&gt;Verifying the environment variables inside a running container.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Create a ClusterIP Service
&lt;/h2&gt;

&lt;p&gt;I started by defining an internal Service in &lt;code&gt;manifests/03-service-clusterip.yaml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; manifests/03-service-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-svc
spec:
  type: ClusterIP
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcn955b7bopi91kuv2bkf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcn955b7bopi91kuv2bkf.png" alt="service-clusterip" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The manifest defines a Service named &lt;code&gt;web-svc&lt;/code&gt; with the following configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;type: ClusterIP&lt;/code&gt;&lt;/strong&gt; creates an internal Service address that is reachable from within the cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;selector: app: web&lt;/code&gt;&lt;/strong&gt; identifies the Pods that receive traffic through the Service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;port: 80&lt;/code&gt;&lt;/strong&gt; specifies the port exposed by the Service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;targetPort: 80&lt;/code&gt;&lt;/strong&gt; specifies the destination port on the selected Pods.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The selector connects the Service to the &lt;code&gt;web&lt;/code&gt; Deployment's Pods, which carry the &lt;code&gt;app: web&lt;/code&gt; label.&lt;/p&gt;

&lt;p&gt;This is an important distinction: clients communicate with the Service rather than depending on an individual Pod IP address. Kubernetes maintains the Service's routing to the eligible backend Pods as the workload changes.&lt;/p&gt;

&lt;p&gt;I applied the manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/03-service-clusterip.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Service provides a stable internal endpoint for the application. It does not, by itself, expose the application directly to the public internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Test Internal Application Access
&lt;/h2&gt;

&lt;p&gt;After configuring the internal Service, I tested access to the NGINX application from inside the Kubernetes environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run curl-test &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--&lt;/span&gt; wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; http://web-svc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It starts a temporary Pod called &lt;code&gt;curl-test&lt;/code&gt; inside the cluster, using the small busybox image. The Pod runs wget to fetch &lt;a href="http://web-svc" rel="noopener noreferrer"&gt;http://web-svc&lt;/a&gt;, and the Service’s name resolves through CoreDNS. --rm deletes the Pod afterwards, -i shows the output, and --restart=Never stops Kubernetes from restarting it once it finishes. The command returned the default &lt;strong&gt;“Welcome to nginx!”&lt;/strong&gt; HTML, which showed the app was reachable through the ClusterIP Service from inside the cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to nginx!&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19mjltl40p4ev42knq1y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19mjltl40p4ev42knq1y.png" alt="nginx serving" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The response also contained the page's standard message confirming that the NGINX web server was installed and running.&lt;/p&gt;

&lt;p&gt;This verified that the application could respond to an HTTP request from within the cluster.&lt;/p&gt;

&lt;p&gt;Testing internal connectivity before exposing a workload externally is useful because it separates application and internal networking checks from the additional configuration required for public access.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Expose the Application with a LoadBalancer Service
&lt;/h2&gt;

&lt;p&gt;For external access, I created a second Service in &lt;code&gt;manifests/04-service-loadbalancer.yaml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; manifests/04-service-loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-public
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fidx1kq5ceakshsfn68ii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fidx1kq5ceakshsfn68ii.png" alt="landbalancer" width="799" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Service uses the same &lt;code&gt;app: web&lt;/code&gt; selector as the internal Service, so it routes traffic to the Pods managed by the Deployment.&lt;/p&gt;

&lt;p&gt;The key difference is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LoadBalancer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On AKS, this Service type requests an Azure load balancer configuration that can expose the application through an external IP address.&lt;/p&gt;

&lt;p&gt;I applied the manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/04-service-loadbalancer.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed that the Service was created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service/web-public created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then watched the Service status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get service web-public &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME         TYPE           CLUSTER-IP   EXTERNAL-IP      PORT(S)
web-public   LoadBalancer   10.0.73.23   20.61.154.190   80:31236/TCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fov77at5rsy8yxj2kj0sc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fov77at5rsy8yxj2kj0sc.png" alt="clusterip" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The assigned addresses and port information were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster IP:&lt;/strong&gt; &lt;code&gt;10.0.73.23&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External IP:&lt;/strong&gt; &lt;code&gt;20.61.154.190&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service port:&lt;/strong&gt; &lt;code&gt;80&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodePort:&lt;/strong&gt; &lt;code&gt;31236&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The external IP made the NGINX application reachable through the load balancer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the public endpoint
&lt;/h3&gt;

&lt;p&gt;I tested the external endpoint using &lt;code&gt;curl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get service web-public &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.loadBalancer.ingress[0].ip}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;title
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to nginx!&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F821poxmp3lmig0u83v62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F821poxmp3lmig0u83v62.png" alt="verify public ep" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that the request reached the NGINX application through the public Service endpoint.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;LoadBalancer&lt;/code&gt; Service is useful when an application needs an externally reachable endpoint. However, public exposure should be deliberate: production workloads may also require network restrictions, TLS termination, ingress routing, and appropriate access controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Create a ConfigMap
&lt;/h2&gt;

&lt;p&gt;Application configuration should not always be embedded in a container image. Kubernetes ConfigMaps provide a way to store non-sensitive configuration separately from application code and image builds.&lt;/p&gt;

&lt;p&gt;I generated a ConfigMap manifest from literal values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create configmap web-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;WELCOME_MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Hello from AKS"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;client &lt;span class="nt"&gt;-o&lt;/span&gt; yaml &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; manifests/05-configmap.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command generated the YAML locally rather than immediately creating the resource in the cluster. The output was saved to &lt;code&gt;manifests/05-configmap.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The configuration contained two values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APP_ENV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;production&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WELCOME_MSG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Hello from AKS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I then applied the generated manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/05-configmap.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;configmap/web-config created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect the stored configuration, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get configmap web-config &lt;span class="nt"&gt;-o&lt;/span&gt; yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;APP_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
  &lt;span class="na"&gt;WELCOME_MSG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hello from AKS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu5w8z5cwrg0v7mpcysqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu5w8z5cwrg0v7mpcysqv.png" alt="config map" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This verified that the ConfigMap contained the expected configuration values.&lt;/p&gt;

&lt;p&gt;Because the ConfigMap is separate from the container image, these values can be managed independently of the image build. ConfigMaps are intended for non-sensitive data and should not be used to store credentials or other confidential values.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Create a Kubernetes Secret
&lt;/h2&gt;

&lt;p&gt;For the next configuration value, I created a Kubernetes Secret named &lt;code&gt;web-secret&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic web-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;change-me-123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secret/web-secret created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Secret contains the key &lt;code&gt;API_KEY&lt;/code&gt;, with a placeholder value used in this lab.&lt;/p&gt;

&lt;p&gt;Unlike a ConfigMap, a Secret is designed for sensitive configuration data. However, creating a Kubernetes Secret does not automatically make the value safe: access controls, encryption at rest, and appropriate secret-management practices still matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the stored value
&lt;/h3&gt;

&lt;p&gt;I verified the value using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get secret web-secret &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.API_KEY}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;--decode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;change-me-123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8d2sk8qcpmfu5w74h7s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8d2sk8qcpmfu5w74h7s.png" alt="secret" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes stores Secret data in a base64-encoded representation in the API object. Base64 is an encoding mechanism, not encryption. Anyone authorized to retrieve and decode the value can read it.&lt;/p&gt;

&lt;p&gt;The value used here is only a lab placeholder. Real API keys and production credentials should not be committed to a repository or exposed in terminal output, screenshots, or documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Inject the ConfigMap into the Deployment
&lt;/h2&gt;

&lt;p&gt;With the ConfigMap and Secret created, I configured the &lt;code&gt;web&lt;/code&gt; Deployment to consume their values as environment variables.&lt;/p&gt;

&lt;p&gt;First, I added the ConfigMap values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;set env &lt;/span&gt;deployment/web &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;configmap/web-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web env updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This added the ConfigMap's key-value pairs to the container environment defined by the Deployment.&lt;/p&gt;

&lt;p&gt;The values supplied by &lt;code&gt;web-config&lt;/code&gt; were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;APP_ENV=production&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WELCOME_MSG=Hello from AKS&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Inject the Secret into the Deployment
&lt;/h2&gt;

&lt;p&gt;I then added the Secret value to the Deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;set env &lt;/span&gt;deployment/web &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret/web-secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web env updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configured the container environment to include &lt;code&gt;API_KEY&lt;/code&gt; from &lt;code&gt;web-secret&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Changing the Deployment's Pod template triggered a rollout. I verified that the rollout completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout status deployment/web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment "web" successfully rolled out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbbln8k1x9445xl4w5in7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbbln8k1x9445xl4w5in7.png" alt="updates/rollout successful" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new Pods were created using the updated Deployment configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the environment variables
&lt;/h3&gt;

&lt;p&gt;To verify that the values were available inside a running container, I executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/web &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;printenv &lt;/span&gt;APP_ENV WELCOME_MSG API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;production
Hello from AKS
change-me-123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf2481zaq0aeo9fxpra9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf2481zaq0aeo9fxpra9.png" alt="env variables" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that the container received the values from both Kubernetes configuration resources.&lt;/p&gt;

&lt;p&gt;The ConfigMap supplied the non-sensitive application configuration, while the Secret supplied the placeholder API key. Both were injected into the container environment without rebuilding the NGINX image.&lt;/p&gt;

&lt;p&gt;For real workloads, avoid printing secrets to logs or terminal output unnecessarily. When applications support it, mounting secrets as files or retrieving credentials from an external secret manager may be more appropriate, depending on the security requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. ClusterIP vs. LoadBalancer
&lt;/h2&gt;

&lt;p&gt;The two Services created in this mission serve different networking purposes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;code&gt;ClusterIP&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;LoadBalancer&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary purpose&lt;/td&gt;
&lt;td&gt;Internal application access&lt;/td&gt;
&lt;td&gt;External application access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default reachability&lt;/td&gt;
&lt;td&gt;Within the cluster&lt;/td&gt;
&lt;td&gt;Through an external load balancer when provisioned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable Service endpoint&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical use&lt;/td&gt;
&lt;td&gt;Internal APIs and service-to-service traffic&lt;/td&gt;
&lt;td&gt;Applications that require a public or externally reachable endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both Services use label selectors to route traffic to the appropriate Pods. The choice of Service type depends on where the clients are located and how the application should be exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Operational Takeaways
&lt;/h2&gt;

&lt;p&gt;This mission brought together Kubernetes networking and configuration management.&lt;/p&gt;

&lt;p&gt;The main operational takeaways were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable internal networking:&lt;/strong&gt; A ClusterIP Service provides an internal endpoint independent of individual Pod IP addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External access:&lt;/strong&gt; A LoadBalancer Service can expose an application through an Azure-managed external endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of configuration:&lt;/strong&gt; ConfigMaps allow non-sensitive settings to be managed separately from the container image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret handling:&lt;/strong&gt; Kubernetes Secrets provide a dedicated resource for sensitive values, but they still require appropriate security controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment injection:&lt;/strong&gt; ConfigMap and Secret values can be supplied to containers through the Deployment's environment configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollout verification:&lt;/strong&gt; Updating the Deployment's Pod template triggers a rollout, which can be monitored with &lt;code&gt;kubectl rollout status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectivity testing:&lt;/strong&gt; Testing both internal and external access verifies different parts of the application networking path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;web&lt;/code&gt; Deployment was made accessible through an internal ClusterIP Service and an external LoadBalancer Service. The public endpoint returned the NGINX welcome page, confirming that external access was working.&lt;/p&gt;

&lt;p&gt;I also created &lt;code&gt;web-config&lt;/code&gt; and &lt;code&gt;web-secret&lt;/code&gt;, injected their values into the Deployment, and verified the resulting environment variables inside a running container.&lt;/p&gt;

&lt;p&gt;The result is a workload with a stable networking interface and configuration managed independently of its container image. These patterns form an important foundation for applications that need controlled connectivity and maintainable configuration on AKS.&lt;/p&gt;

&lt;p&gt;In Part 6, the series will continue with the next mission, building on the networking and configuration resources established here.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is Part 5 of the nine-part series, “Kubernetes on AKS: From Zero to Production Workloads.”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>containers</category>
      <category>security</category>
      <category>aks</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads -Deployments, Self-Healing, Scaling and Rolling Updates (Part 4/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:05:48 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-deployments-self-healing-scaling-and-47po</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-deployments-self-healing-scaling-and-47po</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Running a container in Kubernetes is only the starting point. Production workloads also need mechanisms for maintaining the desired number of replicas, recovering from Pod failures, scaling with demand, and updating application versions without unnecessary disruption.&lt;/p&gt;

&lt;p&gt;In this mission, I used a Kubernetes Deployment to manage an NGINX workload on Azure Kubernetes Service (AKS). I configured three replicas, added health probes and resource constraints, verified the rollout, deleted a Pod to observe self-healing, scaled the Deployment, updated the container image, inspected rollout history, and rolled back to the previous version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Defining a Deployment with three NGINX replicas.&lt;/li&gt;
&lt;li&gt;Configuring resource requests, limits, and health probes.&lt;/li&gt;
&lt;li&gt;Verifying the rollout and associated ReplicaSet.&lt;/li&gt;
&lt;li&gt;Observing automatic Pod replacement after deletion.&lt;/li&gt;
&lt;li&gt;Scaling the Deployment from three replicas to five and back.&lt;/li&gt;
&lt;li&gt;Updating the NGINX image.&lt;/li&gt;
&lt;li&gt;Inspecting rollout history and recording the change cause.&lt;/li&gt;
&lt;li&gt;Rolling back to the previous Deployment revision.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Define the Deployment Manifest
&lt;/h2&gt;

&lt;p&gt;I created the Deployment manifest at &lt;code&gt;manifests/02-deployment.yaml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; manifests/02-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 10
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8q8i3xzpth5st91gzfkn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8q8i3xzpth5st91gzfkn.png" alt="manifest" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The manifest defines a Deployment named &lt;code&gt;web&lt;/code&gt;, using the &lt;code&gt;apps/v1&lt;/code&gt; API and the &lt;code&gt;app: web&lt;/code&gt; label to identify its Pods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replica management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Deployment declares three desired replicas. Its selector matches Pods labelled &lt;code&gt;app: web&lt;/code&gt;, while the Pod template applies that label to each replica it creates.&lt;/p&gt;

&lt;p&gt;This relationship allows the Deployment controller to manage the workload through a ReplicaSet, which maintains the required number of Pods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rolling update strategy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RollingUpdate&lt;/span&gt;
  &lt;span class="na"&gt;rollingUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;maxSurge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;maxUnavailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The update strategy controls how Kubernetes replaces old Pods during an update.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;maxSurge: 1&lt;/code&gt; allows one additional Pod above the desired replica count during a rollout.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxUnavailable: 0&lt;/code&gt; prevents the Deployment from intentionally reducing the number of available replicas below the desired count during the rolling update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These settings prioritize availability during updates, provided the cluster has sufficient capacity and the replacement Pods can become ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container resources and health checks
&lt;/h3&gt;

&lt;p&gt;The NGINX container uses the &lt;code&gt;nginx:1.27&lt;/code&gt; image and exposes container port &lt;code&gt;80&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its resource settings are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;200m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;64Mi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;128Mi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The manifest also defines a readiness probe that sends an HTTP request to &lt;code&gt;/&lt;/code&gt; on port &lt;code&gt;80&lt;/code&gt;. It begins checking after three seconds and repeats every ten seconds.&lt;/p&gt;

&lt;p&gt;A readiness probe helps Kubernetes determine whether a container is ready to receive traffic. The configuration also establishes resource boundaries for each replica, making resource expectations explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Apply the Deployment and Verify the Rollout
&lt;/h2&gt;

&lt;p&gt;I applied the manifest to the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/02-deployment.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then monitored the rollout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout status deployment/web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment "web" successfully rolled out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect the Deployment, ReplicaSet, and Pods together, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get deployment,replicaset,pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                 READY   UP-TO-DATE   AVAILABLE
deployment.apps/web  3/3     3            3

NAME                     DESIRED   CURRENT   READY
replicaset.apps/web-5b77b547f8  3   3         3

NAME                          READY   STATUS
pod/web-5b77b547f8-5jwm5       1/1     Running
pod/web-5b77b547f8-5tzd h      1/1     Running
pod/web-5b77b547f8-qpzqh       1/1     Running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh79xtu5o878874zykhxb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh79xtu5o878874zykhxb.png" alt="pod deployment" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important result was that the Deployment had three available replicas, the ReplicaSet maintained three Pods, and all three Pods were running.&lt;/p&gt;

&lt;p&gt;The Deployment and ReplicaSet provide different levels of workload management. The Deployment manages rollout and revision history, while the ReplicaSet maintains the required number of matching Pods.&lt;/p&gt;

&lt;p&gt;Together, they provide more control over application availability than a standalone Pod.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Verify Self-Healing by Deleting a Pod
&lt;/h2&gt;

&lt;p&gt;To test the Deployment controller's reconciliation behavior, I deleted one of the Pods selected by the &lt;code&gt;app=web&lt;/code&gt; label.&lt;/p&gt;

&lt;p&gt;I used the following command to identify a Pod and pass its name to &lt;code&gt;kubectl delete&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pod &lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed that the selected Pod had been deleted from the &lt;code&gt;k8s-lab&lt;/code&gt; namespace.&lt;/p&gt;

&lt;p&gt;I then checked the Pods again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv1bly6ytj6lh4t8clc7g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv1bly6ytj6lh4t8clc7g.png" alt="self healing" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed three running Pods. A replacement had been created to maintain the Deployment's desired replica count.&lt;/p&gt;

&lt;p&gt;This is the Deployment controller's reconciliation behavior in action. The desired state remained three replicas, so Kubernetes worked to restore that state after one Pod was removed.&lt;/p&gt;

&lt;p&gt;The replacement Pod did not need to retain the deleted Pod's name. Kubernetes created a new Pod from the Deployment's template through its ReplicaSet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational takeaway:&lt;/strong&gt; Deleting a Pod managed by a Deployment does not normally reduce the workload's desired replica count. The controller works to maintain that count.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Scale the Deployment
&lt;/h2&gt;

&lt;p&gt;The next step was to change the desired replica count.&lt;/p&gt;

&lt;p&gt;I scaled the Deployment from three replicas to five:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl scale deployment web &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web scaled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To verify the result and inspect the Pod placement, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed five running Pods, each with a Pod IP and placement on the AKS node.&lt;/p&gt;

&lt;p&gt;I then scaled the Deployment back to three replicas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl scale deployment web &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web scaled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7l2blg3eoc73flqumunq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7l2blg3eoc73flqumunq.png" alt="scaliing replicas" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scaling changes the number of Pod replicas managed by the Deployment. Scaling out from three to five increased the number of running instances, while scaling back to three reduced the desired replica count.&lt;/p&gt;

&lt;p&gt;This operation was performed manually with &lt;code&gt;kubectl scale&lt;/code&gt;. It was not an automatic response to CPU utilization or incoming traffic; automatic scaling requires additional configuration, such as a Horizontal Pod Autoscaler.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Update the NGINX Container Image
&lt;/h2&gt;

&lt;p&gt;After verifying replica management, I updated the Deployment to use the &lt;code&gt;nginx:1.27-alpine&lt;/code&gt; image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;set &lt;/span&gt;image deployment/web &lt;span class="nv"&gt;nginx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx:1.27-alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web image updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new image uses the Alpine-based NGINX image variant. Changing the image in a Deployment triggers a rollout, allowing Kubernetes to replace Pods according to the configured update strategy.&lt;/p&gt;

&lt;p&gt;I then annotated the Deployment with the reason for the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl annotate deployment/web kubernetes.io/change-cause&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"switch to nginx 1.27-alpine"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web annotated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Filtqu7k4j607zd5l44be.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Filtqu7k4j607zd5l44be.png" alt="annotated" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recording the change cause makes the intention behind a revision easier to identify when reviewing rollout history.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Verify the Rollout and Inspect Revision History
&lt;/h2&gt;

&lt;p&gt;I checked the status of the updated Deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout status deployment/web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rollout completed successfully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment "web" successfully rolled out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then inspected the revision history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout &lt;span class="nb"&gt;history &lt;/span&gt;deployment/web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed two revisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web

REVISION  CHANGE-CAUSE
1         &amp;lt;none&amp;gt;
2         switch to nginx 1.27-alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvvu6gdybvpws45mxp1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvvu6gdybvpws45mxp1x.png" alt="rollback" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Revision 1 represented the original Deployment configuration, while revision 2 recorded the change to the Alpine image.&lt;/p&gt;

&lt;p&gt;The history provides an operational record of Deployment revisions and their associated change causes. This can help identify which version is currently deployed and which revision to restore if an update needs to be reversed.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Roll Back to the Previous Revision
&lt;/h2&gt;

&lt;p&gt;To demonstrate rollback, I reverted the Deployment to its previous revision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout undo deployment/web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deployment.apps/web rolled back
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command also displayed a warning explaining that the Deployment had previously been managed with &lt;code&gt;kubectl apply&lt;/code&gt;. A rollback does not update the &lt;code&gt;kubectl.kubernetes.io/last-applied-configuration&lt;/code&gt; annotation, which can affect the behavior of future &lt;code&gt;kubectl apply&lt;/code&gt; operations. Kubernetes recommended applying the previous configuration file when appropriate.&lt;/p&gt;

&lt;p&gt;This warning is worth noting when mixing imperative changes, such as &lt;code&gt;kubectl set image&lt;/code&gt;, with declarative manifest management. The live Deployment, its rollout history, and the last-applied configuration annotation are related, but they are not interchangeable.&lt;/p&gt;

&lt;p&gt;To verify the image configured in the Deployment after the rollback, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get deployment web &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.template.spec.containers[0].image}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx:1.27
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu8yax4mb2x0fjakns9wy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu8yax4mb2x0fjakns9wy.png" alt="verify rollback" width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that the Deployment's Pod template had returned to the original image.&lt;/p&gt;

&lt;p&gt;The rollback demonstrated how Deployment revision history can be used to reverse a change without manually recreating the entire workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Operational Takeaways
&lt;/h2&gt;

&lt;p&gt;This mission demonstrated several core capabilities used to manage Kubernetes applications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desired-state management:&lt;/strong&gt; A Deployment maintains the declared number of replicas through its controller and ReplicaSet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-healing:&lt;/strong&gt; When a managed Pod is deleted, Kubernetes creates a replacement to restore the desired replica count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling:&lt;/strong&gt; The replica count can be increased or reduced to change the number of running application instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checks:&lt;/strong&gt; Readiness probes help Kubernetes determine whether a container is ready to receive traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling updates:&lt;/strong&gt; A Deployment can replace Pods gradually according to its update strategy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revision history:&lt;/strong&gt; Rollout history provides a record of Deployment revisions and their recorded change causes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback:&lt;/strong&gt; A previous Deployment revision can be restored when an update needs to be reversed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration management:&lt;/strong&gt; Mixing imperative updates with declarative &lt;code&gt;kubectl apply&lt;/code&gt; requires attention to the last-applied configuration and future changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These capabilities provide a stronger operational foundation than managing individual Pods directly. They make application changes more controlled and help maintain the intended state of a workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;web&lt;/code&gt; Deployment was successfully created with three NGINX replicas, and its rollout was verified. I confirmed that Kubernetes replaced a deleted Pod, scaled the Deployment to five replicas and back to three, updated the image to &lt;code&gt;nginx:1.27-alpine&lt;/code&gt;, reviewed the rollout history, and restored the original &lt;code&gt;nginx:1.27&lt;/code&gt; image through rollback.&lt;/p&gt;

&lt;p&gt;The key distinction is that a Deployment manages the desired state of an application rather than relying on individual Pods to remain unchanged. Replica management, health checks, rolling updates, and rollback together provide essential tools for operating workloads reliably on AKS.&lt;/p&gt;

&lt;p&gt;In Part 5, the series will continue with the next mission, building on the workload-management capabilities established here.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is Part 4 of the nine-part series, “Kubernetes on AKS: From Zero to Production Workloads.”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aks</category>
      <category>containers</category>
      <category>scaling</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads - Pods, the Smallest Unit (Part 3/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 16:42:24 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-pods-the-smallest-unit-part-39-5d9i</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-pods-the-smallest-unit-part-39-5d9i</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A Kubernetes Pod is the smallest deployable unit in Kubernetes. It provides the execution environment for one or more containers, along with shared networking and storage resources.&lt;/p&gt;

&lt;p&gt;In this mission, I deployed a standalone NGINX Pod to my Azure Kubernetes Service (AKS) cluster, defined its resource requirements, inspected its runtime state, reviewed its logs, accessed the application locally, and examined what happens when a standalone Pod is deleted.&lt;/p&gt;

&lt;p&gt;The focus was on the Pod lifecycle and the operational details that matter when running containerized workloads on Kubernetes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Defining a Pod using a Kubernetes YAML manifest.&lt;/li&gt;
&lt;li&gt;Deploying the Pod to the &lt;code&gt;k8s-lab&lt;/code&gt; namespace.&lt;/li&gt;
&lt;li&gt;Verifying its status, IP address, and assigned node.&lt;/li&gt;
&lt;li&gt;Inspecting container configuration and resource constraints.&lt;/li&gt;
&lt;li&gt;Reviewing NGINX logs and verifying its version.&lt;/li&gt;
&lt;li&gt;Accessing the application through port forwarding.&lt;/li&gt;
&lt;li&gt;Observing the behavior of a standalone Pod after deletion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Define the Pod Manifest
&lt;/h2&gt;

&lt;p&gt;I created the manifest at &lt;code&gt;manifests/01-pod.yaml&lt;/code&gt; in the project repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; manifests/01-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: web-pod
  labels:
    app: web-pod
spec:
  containers:
    - name: nginx
      image: nginx:1.27
      ports:
        - containerPort: 80
      resources:
        requests:
          cpu: 50m
          memory: 64Mi
        limits:
          cpu: 200m
          memory: 128Mi
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0tfgm1fbsixrbfxlg21l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0tfgm1fbsixrbfxlg21l.png" alt="pod creation" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The manifest defines a Pod named &lt;code&gt;web-pod&lt;/code&gt; containing one NGINX container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key configuration details
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apiVersion: v1&lt;/code&gt;&lt;/strong&gt; specifies the Kubernetes API version for the Pod resource.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kind: Pod&lt;/code&gt;&lt;/strong&gt; identifies the resource being created.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;metadata.name&lt;/code&gt;&lt;/strong&gt; assigns the name &lt;code&gt;web-pod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;metadata.labels&lt;/code&gt;&lt;/strong&gt; attaches the &lt;code&gt;app: web-pod&lt;/code&gt; label, which can be used to identify and select the Pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;image: nginx:1.27&lt;/code&gt;&lt;/strong&gt; specifies the NGINX container image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;containerPort: 80&lt;/code&gt;&lt;/strong&gt; documents the port on which the container is expected to receive HTTP traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU request: &lt;code&gt;50m&lt;/code&gt;&lt;/strong&gt; declares 50 millicores of CPU as the container's requested compute capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory request: &lt;code&gt;64Mi&lt;/code&gt;&lt;/strong&gt; declares 64 mebibytes of requested memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU limit: &lt;code&gt;200m&lt;/code&gt;&lt;/strong&gt; sets the container's CPU usage limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory limit: &lt;code&gt;128Mi&lt;/code&gt;&lt;/strong&gt; sets the container's memory limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resource requests and limits serve different purposes. Requests influence scheduling decisions, while limits constrain the resources a container can consume. CPU limits can result in throttling, whereas exceeding a container's memory limit can lead to termination.&lt;/p&gt;

&lt;p&gt;Defining these values makes the workload's resource expectations explicit rather than leaving them entirely unspecified.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Deploy the Pod
&lt;/h2&gt;

&lt;p&gt;With the manifest in place, I applied it to the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/01-pod.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed that the resource had been created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pod/web-pod created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then checked the Pod's status and placement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME      READY   STATUS    RESTARTS   AGE   IP           NODE
web-pod   1/1     Running   0          25s   10.244.0.139 aks-nodepool1-33590657-vmss000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bp59ac7ri5t7cias20k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bp59ac7ri5t7cias20k.png" alt="pods" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important observations were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ready: &lt;code&gt;1/1&lt;/code&gt;&lt;/strong&gt; — the Pod's single container was ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status: &lt;code&gt;Running&lt;/code&gt;&lt;/strong&gt; — the Pod had started successfully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restarts: &lt;code&gt;0&lt;/code&gt;&lt;/strong&gt; — no container restarts had occurred at the time of inspection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pod IP: &lt;code&gt;10.244.0.139&lt;/code&gt;&lt;/strong&gt; — Kubernetes assigned the Pod an internal IP address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node:&lt;/strong&gt; &lt;code&gt;aks-nodepool1-33590657-vmss000000&lt;/code&gt; — the scheduler placed the Pod on this AKS worker node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;-o wide&lt;/code&gt; option exposes additional placement and networking information beyond the default Pod listing.&lt;/p&gt;

&lt;p&gt;This verified that the manifest had been accepted and the NGINX workload was running in the &lt;code&gt;k8s-lab&lt;/code&gt; namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Inspect the Pod Configuration and Runtime State
&lt;/h2&gt;

&lt;p&gt;To examine the Pod in more detail, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod web-pod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftawmopyr1biru2xh8qh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftawmopyr1biru2xh8qh4.png" alt="decribe pod" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output confirmed that &lt;code&gt;web-pod&lt;/code&gt; was running in the &lt;code&gt;k8s-lab&lt;/code&gt; namespace, with the &lt;code&gt;nginx&lt;/code&gt; container using the &lt;code&gt;nginx:1.27&lt;/code&gt; image.&lt;/p&gt;

&lt;p&gt;The inspection also exposed the container's runtime information, resource configuration, readiness conditions, assigned node, and recent events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container readiness
&lt;/h3&gt;

&lt;p&gt;The Pod reported the following conditions as true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Initialized&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ready&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ContainersReady&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PodScheduled&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3j9mbm3nh6p993wnl8lt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3j9mbm3nh6p993wnl8lt.png" alt="pod status" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These conditions provide useful operational information. A Pod's existence alone does not guarantee that its application is ready to serve traffic. Checking readiness and container state helps distinguish a successfully scheduled workload from one that is actually running and ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource configuration
&lt;/h3&gt;

&lt;p&gt;The container's reported resource settings matched the manifest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;200m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;64Mi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;128Mi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The events also recorded the scheduling, image-pulling, container-creation, and container-start operations.&lt;/p&gt;

&lt;p&gt;The image was pulled successfully, and Kubernetes started the container without reporting a restart.&lt;/p&gt;

&lt;p&gt;This inspection connected the desired configuration in the YAML manifest with the state Kubernetes was reporting for the running workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Review the NGINX Logs
&lt;/h2&gt;

&lt;p&gt;I retrieved the container logs with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs web-pod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuk35bh7cclsj75xykzet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuk35bh7cclsj75xykzet.png" alt="kubectl logs" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output showed the NGINX container entrypoint initializing its configuration and launching the NGINX process.&lt;/p&gt;

&lt;p&gt;The logs included messages indicating that configuration was complete and that NGINX was ready to start up. They also showed the NGINX worker processes starting.&lt;/p&gt;

&lt;p&gt;Reviewing logs is an important part of troubleshooting containerized applications. They can reveal startup failures, configuration problems, and application-level errors that may not be obvious from the Pod status alone.&lt;/p&gt;

&lt;p&gt;In this case, the startup output was consistent with a successfully initialized NGINX container.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Verify the NGINX Version
&lt;/h2&gt;

&lt;p&gt;To inspect the NGINX version from inside the running container, I executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;web-pod &lt;span class="nt"&gt;--&lt;/span&gt; nginx &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx version: nginx/1.27.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvhjoduush3j110led2ll.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvhjoduush3j110led2ll.png" alt="kubectl exec" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This verified the version of NGINX running inside the container.&lt;/p&gt;

&lt;p&gt;The manifest specified the &lt;code&gt;nginx:1.27&lt;/code&gt; image tag, while the running binary reported version &lt;code&gt;1.27.5&lt;/code&gt;. This illustrates why inspecting the runtime can provide useful information beyond the image tag declared in a manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Access the Application with Port Forwarding
&lt;/h2&gt;

&lt;p&gt;The Pod was running NGINX on container port 80. To access it from my local machine without creating a Kubernetes Service or exposing the application publicly, I used port forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward pod/web-pod 8080:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forwarding from 127.0.0.1:8080 -&amp;gt; 80
Forwarding from [::1]:8080 -&amp;gt; 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddofm8463j25l4ak07f5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddofm8463j25l4ak07f5.png" alt="port forward" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This forwarded local port &lt;code&gt;8080&lt;/code&gt; to port &lt;code&gt;80&lt;/code&gt; on the Pod.&lt;/p&gt;

&lt;p&gt;With the port-forwarding process running, I opened the following address in my browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmp6hm2oz9meufcot60tl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmp6hm2oz9meufcot60tl.png" alt="nginx displayed" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser displayed the default &lt;strong&gt;Welcome to nginx!&lt;/strong&gt; page, confirming that the NGINX web server was accessible through the local port-forwarding connection.&lt;/p&gt;

&lt;p&gt;Port forwarding provided a convenient way to test the application without creating a public endpoint. The connection remained dependent on the &lt;code&gt;kubectl port-forward&lt;/code&gt; process, so it was useful for this verification rather than as a production traffic-routing solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The port-forwarding command occupies the terminal while it is running. I kept that process active while accessing the page in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Delete the Standalone Pod
&lt;/h2&gt;

&lt;p&gt;After verifying the application, I deleted the Pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pod web-pod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pod "web-pod" deleted from k8s-lab namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then checked the namespace for remaining Pods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No resources found in k8s-lab namespace.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglduqgrgl3csw61ad1s4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglduqgrgl3csw61ad1s4.png" alt="pod deleted" width="800" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that the standalone Pod had been removed and that no Pods remained in the namespace at the time of the check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the Pod was not recreated
&lt;/h3&gt;

&lt;p&gt;A standalone Pod does not have a higher-level workload controller responsible for maintaining a desired number of replicas.&lt;/p&gt;

&lt;p&gt;Deleting it therefore removed the workload without automatically creating a replacement.&lt;/p&gt;

&lt;p&gt;This behavior is an important distinction between running a Pod directly and managing workloads through resources such as Deployments or StatefulSets. Controllers can reconcile the actual state with the declared desired state, including replacing Pods when necessary.&lt;/p&gt;

&lt;p&gt;For this mission, deleting the standalone Pod demonstrated its lifecycle and the absence of automatic replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Operational Takeaways
&lt;/h2&gt;

&lt;p&gt;This mission established a complete workflow for deploying and inspecting a basic Kubernetes workload.&lt;/p&gt;

&lt;p&gt;The key operational points were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declarative configuration:&lt;/strong&gt; A YAML manifest defines the desired Pod configuration and can be stored alongside the rest of the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource management:&lt;/strong&gt; CPU and memory requests and limits establish explicit resource expectations for the container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime inspection:&lt;/strong&gt; &lt;code&gt;kubectl get pods -o wide&lt;/code&gt; and &lt;code&gt;kubectl describe pod&lt;/code&gt; expose different levels of information about scheduling, readiness, networking, and container state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log inspection:&lt;/strong&gt; &lt;code&gt;kubectl logs&lt;/code&gt; helps reveal what happens during application startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container-level verification:&lt;/strong&gt; &lt;code&gt;kubectl exec&lt;/code&gt; can be used to inspect the running container, including its software version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local application access:&lt;/strong&gt; Port forwarding provides a practical way to test a Pod without creating a public endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pod lifecycle:&lt;/strong&gt; A standalone Pod is not automatically recreated after deletion because no higher-level controller manages its desired state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;web-pod&lt;/code&gt; workload was successfully deployed to AKS, inspected at both the Kubernetes and container levels, accessed through local port forwarding, and subsequently deleted.&lt;/p&gt;

&lt;p&gt;The exercise demonstrated how Kubernetes manifests translate into running workloads and how the Kubernetes CLI supports verification throughout the Pod lifecycle.&lt;/p&gt;

&lt;p&gt;A standalone Pod is useful for understanding the fundamentals and performing targeted tests. For workloads that require resilience, scaling, and automatic replacement, higher-level controllers provide the management capabilities needed to maintain the desired state.&lt;/p&gt;

&lt;p&gt;In the next part of this series, I will continue building on the AKS environment and explore the next workload-management concept in the lab.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is Part 3 of the nine-part series, “Kubernetes on AKS: From Zero to Production Workloads.”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>containers</category>
      <category>aks</category>
      <category>nginx</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads -Creating the AKS Cluster (Part 2/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 16:13:41 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-creating-the-aks-cluster-part-29-1kf1</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-creating-the-aks-cluster-part-29-1kf1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Provisioning a Kubernetes cluster is the next step after preparing the development environment. In this part of the series, I moved from local tooling setup to creating an Azure Kubernetes Service (AKS) cluster, connecting &lt;code&gt;kubectl&lt;/code&gt; to it, verifying the cluster, and preparing a dedicated namespace for workloads.&lt;/p&gt;

&lt;p&gt;The objective was to establish a working Kubernetes environment on Azure that could support the deployments and experiments in the missions that follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Part Covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Loading the lab configuration.&lt;/li&gt;
&lt;li&gt;Creating an Azure resource group.&lt;/li&gt;
&lt;li&gt;Provisioning an AKS cluster.&lt;/li&gt;
&lt;li&gt;Retrieving cluster credentials and configuring &lt;code&gt;kubectl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verifying the cluster and its system components.&lt;/li&gt;
&lt;li&gt;Creating and selecting a Kubernetes namespace.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Load the Lab Configuration
&lt;/h2&gt;

&lt;p&gt;I started from the project directory created in Part 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /mnt/c/Users/Admin/Desktop/labs/aks-kubernetes-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before provisioning any Azure resources, I loaded the environment variables defined in &lt;code&gt;lab.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;lab.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The environment file provides the configuration used by the commands in this mission, including the resource group name, cluster name, Azure region, and node size.&lt;/p&gt;

&lt;p&gt;Using variables instead of repeatedly hardcoding resource names makes the commands easier to maintain and reuse throughout the lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check the Resource Group and Create It
&lt;/h2&gt;

&lt;p&gt;Before creating the resource group, I checked whether it already existed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group show &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; location &lt;span class="nt"&gt;--output&lt;/span&gt; tsv 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command queries the resource group's location. Redirecting standard error to &lt;code&gt;/dev/null&lt;/code&gt; suppresses error messages if the resource group does not exist.&lt;/p&gt;

&lt;p&gt;I then created the resource group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returned the following result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Location    Name
----------  ----------
westeurope  rg-aks-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2xhtr1irn78i0vhx0o2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2xhtr1irn78i0vhx0o2.png" alt="azure cli" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that the resource group &lt;code&gt;rg-aks-lab&lt;/code&gt; was available in &lt;code&gt;westeurope&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An Azure resource group provides a logical container for related resources. In this lab, it serves as the main resource group for the AKS environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Provision the AKS Cluster
&lt;/h2&gt;

&lt;p&gt;With the resource group created, I provisioned the AKS cluster using the Azure CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tier&lt;/span&gt; free &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-count&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-vm-size&lt;/span&gt; &lt;span class="nv"&gt;$NODE_SIZE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--generate-ssh-keys&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what the main options do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--resource-group&lt;/code&gt; specifies the Azure resource group that will contain the cluster.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--name&lt;/code&gt; sets the AKS cluster name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--location&lt;/code&gt; specifies the Azure region.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--tier free&lt;/code&gt; selects the Free tier for AKS cluster management.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--node-count 1&lt;/code&gt; configures one initial worker node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--node-vm-size&lt;/code&gt; specifies the virtual machine size for the node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--generate-ssh-keys&lt;/code&gt; generates SSH keys if required.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--output table&lt;/code&gt; presents the returned information in a table format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command displayed a notice that the &lt;code&gt;aks-preview&lt;/code&gt; extension had altered its behavior. It also warned that the new node pool would enable SSH access and recommended using &lt;code&gt;--ssh-access disabled&lt;/code&gt; to disable SSH access for better security.&lt;/p&gt;

&lt;p&gt;The command subsequently returned cluster information, including the Kubernetes version, provisioning state, resource group, and cluster name.&lt;/p&gt;

&lt;p&gt;The key result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProvisioningState: Succeeded
ResourceGroup:     rg-aks-lab
Name:              aks-lab
Location:          westeurope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provisioning state of &lt;code&gt;Succeeded&lt;/code&gt; confirmed that Azure had completed the cluster creation operation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8051u4xginf1j8c4twhe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8051u4xginf1j8c4twhe.png" alt="AKS creation" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Connect kubectl to the AKS Cluster
&lt;/h2&gt;

&lt;p&gt;Creating the cluster is only part of the process. I also needed to configure my local Kubernetes client to communicate with it.&lt;/p&gt;

&lt;p&gt;I retrieved the cluster credentials with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks get-credentials &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--overwrite-existing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merged "aks-lab" as current context in /home/rahimahisah/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This merged the AKS cluster credentials into the local kubeconfig file and selected the &lt;code&gt;aks-lab&lt;/code&gt; context.&lt;/p&gt;

&lt;p&gt;To confirm the active context, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config current-context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aks-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirmed that &lt;code&gt;kubectl&lt;/code&gt; was configured to use the AKS cluster context.&lt;/p&gt;

&lt;p&gt;Kubernetes contexts allow a single kubeconfig file to hold connection details for multiple clusters. Checking the active context before running commands is a useful habit because it helps prevent accidentally targeting the wrong environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetjowp9onvuzx2deiwc8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetjowp9onvuzx2deiwc8.png" alt="Retrieving AKS credentials and verifying the current Kubernetes context" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Verify the Kubernetes Cluster
&lt;/h2&gt;

&lt;p&gt;After connecting &lt;code&gt;kubectl&lt;/code&gt;, I checked the cluster's node status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                                STATUS   ROLES    AGE     VERSION
aks-nodepool1-33590657-vmss000000   Ready    &amp;lt;none&amp;gt;   3m18s   v1.36.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node was in the &lt;code&gt;Ready&lt;/code&gt; state, indicating that Kubernetes considered it ready to accept workloads.&lt;/p&gt;

&lt;p&gt;The wider output also included information about the node's internal IP address, operating system image, kernel version, and container runtime.&lt;/p&gt;

&lt;p&gt;The node was running Ubuntu 24.04.5 LTS, and the Kubernetes version shown was &lt;code&gt;v1.36.4&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the cluster components
&lt;/h3&gt;

&lt;p&gt;Next, I checked the cluster information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl cluster-info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output confirmed that the Kubernetes control plane was running and displayed the endpoints for CoreDNS and the metrics server.&lt;/p&gt;

&lt;p&gt;I then inspected the system pods in the &lt;code&gt;kube-system&lt;/code&gt; namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;--namespace&lt;/span&gt; kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed system components in the &lt;code&gt;Running&lt;/code&gt; state, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;azure-cns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;azure-ip-masq-agent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloud-node-manager&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;coredns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;coredns-autoscaler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;csi-azuredisk-node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;csi-azurefile-node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kube-proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;metrics-server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;konnectivity-agent&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These components support the operation of the cluster. For example, CoreDNS provides cluster DNS services, the CSI node components support storage integration, and the metrics server provides resource metrics used by Kubernetes tools and autoscaling components.&lt;/p&gt;

&lt;p&gt;Checking the node, cluster information, and system pods provided an initial verification that the cluster was accessible and its core components were running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot 4:&lt;/strong&gt; Node status, cluster information, and system pods.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr446uilrdhlr052k1msc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr446uilrdhlr052k1msc.png" alt="Node status, cluster information, and system pods" width="799" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Create a Dedicated Kubernetes Namespace
&lt;/h2&gt;

&lt;p&gt;With the cluster verified, I created a namespace for the lab workloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace k8s-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes confirmed the operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace/k8s-lab created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then configured the current context to use the new namespace by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config set-context &lt;span class="nt"&gt;--current&lt;/span&gt; &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;k8s-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output confirmed that the &lt;code&gt;aks-lab&lt;/code&gt; context had been modified.&lt;/p&gt;

&lt;p&gt;To verify the namespace configured in the current context, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config view &lt;span class="nt"&gt;--minify&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={..namespace}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k8s-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that subsequent &lt;code&gt;kubectl&lt;/code&gt; commands that do not explicitly specify a namespace will use &lt;code&gt;k8s-lab&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;Namespaces provide a way to organize Kubernetes resources within a cluster. Creating a dedicated namespace keeps the lab workloads separate from system components running in &lt;code&gt;kube-system&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5iqt54m5rxuka7qeotw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5iqt54m5rxuka7qeotw.png" alt="Namespace creation" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What Learned
&lt;/h2&gt;

&lt;p&gt;This mission connected the local tooling setup from Part 1 to a working Kubernetes environment hosted on Azure.&lt;/p&gt;

&lt;p&gt;The key takeaways were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure resource organization:&lt;/strong&gt; Resource groups provide a logical boundary for related Azure resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster provisioning:&lt;/strong&gt; The Azure CLI can provision an AKS cluster using a repeatable set of configuration options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes authentication and context:&lt;/strong&gt; Retrieving credentials allows &lt;code&gt;kubectl&lt;/code&gt; to communicate with the selected cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster verification:&lt;/strong&gt; Checking node readiness, cluster information, and system pods helps establish whether the environment is operational.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespace management:&lt;/strong&gt; A dedicated namespace helps organize lab workloads and avoid mixing them with system resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important habit I reinforced was verifying the active Kubernetes context before working with cluster resources. It is a small check that can help prevent mistakes when managing multiple environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The AKS cluster was provisioned successfully, the local Kubernetes client was connected to it, and the &lt;code&gt;k8s-lab&lt;/code&gt; namespace was created and selected as the default namespace for the current context.&lt;/p&gt;

&lt;p&gt;The environment is now ready for the next stage of the series, where I will begin working with Kubernetes resources and workloads.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 3&lt;/strong&gt;, the series will continue with the next mission in the lab, building on the cluster and namespace established here.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of the nine-part series, “Kubernetes on AKS: From Zero to Production Workloads.”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aks</category>
      <category>cloud</category>
      <category>linux</category>
    </item>
    <item>
      <title>Kubernetes on AKS: From Zero to Production Workloads -Setting Up the Foundation (Part 1/9)</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:31:25 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-setting-up-the-foundation-part-19-1gpl</link>
      <guid>https://dev.to/rahimah_dev/kubernetes-on-aks-from-zero-to-production-workloads-setting-up-the-foundation-part-19-1gpl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; Kubernetes on AKS: From Zero to Production Workloads&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Part 1:&lt;/strong&gt; Workspace, Tools and Azure Sign-In&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Environment:&lt;/strong&gt; Ubuntu on WSL, VS Code terminal, Azure CLI, &lt;code&gt;kubectl&lt;/code&gt; and Git&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before deploying workloads to Kubernetes, there is some groundwork to do. A reliable environment needs more than a cluster: it needs a structured project directory, version control, the right command-line tools, authenticated access to Azure, and the necessary resource providers.&lt;/p&gt;

&lt;p&gt;In this first part of my nine-part series, I'll walk through how I prepared the environment for a hands-on Azure Kubernetes Service (AKS) lab. I'll cover the project workspace, Git initialization, Azure CLI installation, Azure authentication, Kubernetes command-line tools, and Azure resource provider registration. I'll also share an installation error I encountered, how I investigated it, and the workaround that allowed me to continue.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Creating the project workspace
&lt;/h2&gt;

&lt;p&gt;I started by creating a dedicated directory for the lab and navigating into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/c/Users/Admin/Desktop/labs/aks-kubernetes-lab
&lt;span class="nb"&gt;cd&lt;/span&gt; /mnt/c/Users/Admin/Desktop/labs/aks-kubernetes-lab
&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what each command does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mkdir -p&lt;/code&gt; creates the project directory, including any missing parent directories.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cd&lt;/code&gt; moves into the directory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pwd&lt;/code&gt; prints the current working directory so we can verify the location.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The terminal returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/mnt/c/Users/Admin/Desktop/labs/aks-kubernetes-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirming the working directory matters because subsequent commands create files and perform Git operations relative to that location.&lt;/p&gt;

&lt;p&gt;A dedicated workspace keeps the project's documentation, Kubernetes manifests, and configuration organized as the lab grows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa379lghsccgeawm124p4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa379lghsccgeawm124p4.png" alt="part-01-workspace-creation.png" width="800" height="95"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Initializing Git
&lt;/h2&gt;

&lt;p&gt;With the workspace ready, I initialized a Git repository using &lt;code&gt;main&lt;/code&gt; as the initial branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init &lt;span class="nt"&gt;-b&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initialized empty Git repository in /mnt/c/Users/Admin/Desktop/labs/aks-kubernetes-lab/.git/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff032g314t7hx4slqhtvh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff032g314t7hx4slqhtvh.png" alt="git init" width="800" height="95"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Git provides version control, allowing us to track changes, review previous versions, and maintain a history of the files created throughout the project.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-b main&lt;/code&gt; option establishes &lt;code&gt;main&lt;/code&gt; as the initial branch.&lt;/p&gt;

&lt;p&gt;At this point, the repository existed locally. It had not yet been connected to a remote GitHub repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Creating the initial project structure
&lt;/h2&gt;

&lt;p&gt;Next, I created a directory to hold the Kubernetes manifests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; manifests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then created an initial &lt;code&gt;README.md&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Kubernetes on AKS\n\nProduction Kubernetes workloads on Azure Kubernetes Service, built as a hands-on lab.\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect the file, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Kubernetes on AKS&lt;/span&gt;

Production Kubernetes workloads on Azure Kubernetes Service, built as a hands-on lab.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This README was intentionally minimal. It established the repository's entry point, while the &lt;code&gt;manifests/&lt;/code&gt; directory provided a dedicated location for Kubernetes YAML files.&lt;/p&gt;

&lt;p&gt;The initial project structure looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aks-kubernetes-lab/
├── manifests/
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This represents the initial structure, not the final repository layout.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpebh5d4nkezi4bvxssoy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpebh5d4nkezi4bvxssoy.png" alt="part-01-initial-project-structure" width="800" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Staging the README
&lt;/h2&gt;

&lt;p&gt;Before creating the first commit, I staged the README and checked the repository status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add README.md
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached &amp;lt;file&amp;gt;..." to unstage)
        new file:   README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvyqcoxdqf52n9jb9wiz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvyqcoxdqf52n9jb9wiz0.png" alt="git status" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output confirmed that &lt;code&gt;README.md&lt;/code&gt; was staged and ready to be included in a commit.&lt;/p&gt;

&lt;p&gt;Git's staging area lets us choose which changes belong in a commit. This is useful when a project contains multiple modifications but we want to record only a specific set of changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Adding &lt;code&gt;.gitignore&lt;/code&gt; and creating the first commit
&lt;/h2&gt;

&lt;p&gt;Before committing the initial files, I created a &lt;code&gt;.gitignore&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'lab.env\n*.log\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its contents were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lab.env
*.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;lab.env&lt;/code&gt; rule tells Git to ignore a file named &lt;code&gt;lab.env&lt;/code&gt;, while &lt;code&gt;*.log&lt;/code&gt; ignores files ending in &lt;code&gt;.log&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a useful precaution when working with cloud infrastructure. Environment files may contain configuration values that should not be published, and log files can add unnecessary clutter to a repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; &lt;code&gt;.gitignore&lt;/code&gt; does not remove files that have already been committed or staged. It is a preventive measure, not a replacement for proper secret management.&lt;/p&gt;

&lt;p&gt;I staged the files and created the initial commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add README.md .gitignore
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: initialise AKS lab workspace"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git confirmed the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[main (root-commit) 7bdb9aa] chore: initialise AKS lab workspace
2 files changed, 5 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This established the first checkpoint in the repository's history. Subsequent changes could now be recorded as separate commits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqg7u9yyfl1j7g4kh6lu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqg7u9yyfl1j7g4kh6lu7.png" alt="initial-git-commit" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Installing Azure CLI
&lt;/h2&gt;

&lt;p&gt;The next step was to install the Azure command-line interface (Azure CLI).&lt;/p&gt;

&lt;p&gt;Azure CLI allows us to manage Azure resources from a terminal. It is particularly useful for repeatable infrastructure workflows, where commands can be documented and reused instead of relying exclusively on the Azure portal.&lt;/p&gt;

&lt;p&gt;I ran the installation command provided by the lab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://aka.ms/InstallAzureCLIDeb | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjifu05xni07fzqdoa99i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjifu05xni07fzqdoa99i.png" alt="part-01-azure-cli-installation" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The command downloads and executes Microsoft's installation script for Debian-based Linux environments. Because it runs with elevated privileges, it is important to use a trusted installation source and understand what the command does.&lt;/p&gt;

&lt;p&gt;The terminal showed the package repositories being updated and Azure CLI being installed.&lt;/p&gt;

&lt;p&gt;I then verified the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"azure-cli"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.91.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"azure-cli-core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.91.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"azure-cli-telemetry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirmed that Azure CLI was installed and available from the terminal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8qqtdcling14kuwyjbrk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8qqtdcling14kuwyjbrk.png" alt="az version" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Signing in to Azure
&lt;/h2&gt;

&lt;p&gt;With Azure CLI installed, I authenticated using device-code sign-in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az login &lt;span class="nt"&gt;--use-device-code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fecarl3543e4gfods24j6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fecarl3543e4gfods24j6.png" alt="azure-device-login" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The CLI instructed me to open Microsoft's device-login page in a browser and enter the displayed code. After completing the browser authentication, I selected the available subscription.&lt;/p&gt;

&lt;p&gt;The terminal showed the selected subscription as &lt;code&gt;Azure subscription 1&lt;/code&gt; under &lt;code&gt;Default Directory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Device-code authentication is useful when the terminal cannot conveniently open a browser or when authentication needs to be completed in a separate browser session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the active account
&lt;/h3&gt;

&lt;p&gt;I checked the active Azure account using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az account show &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmxsfbhu5ivlwfm82c3gi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmxsfbhu5ivlwfm82c3gi.png" alt="azure-account-verification" width="797" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output displayed account information, including the subscription name, state, and tenant details.&lt;/p&gt;

&lt;p&gt;This verification is important because the subscription selected in Azure CLI determines where subsequent Azure commands will operate. Accidentally using the wrong subscription can result in resources being created in the wrong environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Before publishing terminal screenshots, redact device codes, access tokens, credentials, and any identifiers you don't want to expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Installing Kubernetes command-line tools
&lt;/h2&gt;

&lt;p&gt;Azure CLI manages Azure resources, but we also need &lt;code&gt;kubectl&lt;/code&gt; to interact with Kubernetes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl&lt;/code&gt; is the Kubernetes command-line client. It allows us to inspect resources, apply manifests, examine workload status, and communicate with the Kubernetes API when a cluster is available and the appropriate credentials and permissions are configured.&lt;/p&gt;

&lt;p&gt;The lab's initial installation command was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;az aks install-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Troubleshooting: The default installation path failed
&lt;/h3&gt;

&lt;p&gt;The command encountered an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connection error while attempting to download client
([Errno 2] No such file or directory: '/usr/local/bin/kubectl')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal also showed that &lt;code&gt;/usr/local/bin&lt;/code&gt; existed.&lt;/p&gt;

&lt;p&gt;This was a useful reminder that an installation command can fail at its target path even when Azure CLI itself is working. Rather than repeatedly retrying the same command, I inspected the directory and switched to a user-local installation location.&lt;/p&gt;

&lt;p&gt;First, I checked the directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /usr/local/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I created a directory under my home folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.local/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reran the installation with explicit paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks install-cli &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--install-location&lt;/span&gt; ~/.local/bin/kubectl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kubelogin-install-location&lt;/span&gt; ~/.local/bin/kubelogin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This instructed the installer to place &lt;code&gt;kubectl&lt;/code&gt; and &lt;code&gt;kubelogin&lt;/code&gt; in my user-local binary directory.&lt;/p&gt;

&lt;p&gt;The installer completed the downloads and advised that &lt;code&gt;~/.local/bin&lt;/code&gt; needed to be included in &lt;code&gt;PATH&lt;/code&gt; so the shell could find the commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding the installation directory to &lt;code&gt;PATH&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I added the directory to my Bash configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export PATH="$HOME/.local/bin:$PATH"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I reloaded the configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, I verified the Kubernetes client installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl version &lt;span class="nt"&gt;--client&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client Version: v1.37.1
Kustomize Version: v5.8.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirmed that the &lt;code&gt;kubectl&lt;/code&gt; client was installed and available from the shell.&lt;/p&gt;

&lt;h3&gt;
  
  
  What resolved the problem?
&lt;/h3&gt;

&lt;p&gt;The workaround used a directory within my home folder rather than the original default installation location. Updating &lt;code&gt;PATH&lt;/code&gt; then allowed the shell to locate the downloaded binaries.&lt;/p&gt;

&lt;p&gt;The failure occurred during local tooling setup, before a Kubernetes cluster was involved. Distinguishing local installation problems from cluster or workload problems makes troubleshooting more systematic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fja30oqlks108oqviabio.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fja30oqlks108oqviabio.png" alt="kubectl-install" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Registering Azure resource providers
&lt;/h2&gt;

&lt;p&gt;Before creating Azure resources, the subscription may need the relevant resource providers registered.&lt;/p&gt;

&lt;p&gt;A resource provider exposes the management operations for a category of Azure resources. For this lab, I registered four providers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.ContainerService
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.Compute
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.Network
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.Storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These providers correspond to the services used throughout the lab:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource provider&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Microsoft.ContainerService&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AKS and related container-service management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Microsoft.Compute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Virtual machines and compute resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Microsoft.Network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Networking resources, including virtual networks and load balancers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Microsoft.Storage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Azure storage resources&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Registering a provider does not create an AKS cluster or provision the associated resources. It enables the subscription to use the relevant provider's operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying registration
&lt;/h3&gt;

&lt;p&gt;I checked the registration state of the AKS provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az provider show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.ContainerService &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; registrationState &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Registered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa24hy71mtf33puvpwa2p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa24hy71mtf33puvpwa2p.png" alt="registered" width="797" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirmed that &lt;code&gt;Microsoft.ContainerService&lt;/code&gt; was registered. The screenshot documents the verification of this provider; it does not independently confirm the registration state of all four providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Preparing the lab environment
&lt;/h2&gt;

&lt;p&gt;The lab used an environment file to keep its configuration values consistent across subsequent commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; lab.env
export RESOURCE_GROUP=rg-aks-lab
export LOCATION=westeurope
export CLUSTER_NAME=aks-lab
export NODE_SIZE=Standard_D2s_v3
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I loaded the variables into the current shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;lab.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I checked their values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$NODE_SIZE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rg-aks-lab westeurope aks-lab Standard_D2s_v3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqklrbs90j2ls1r4dgi2k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqklrbs90j2ls1r4dgi2k.png" alt="list" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using variables avoids repeatedly typing the same resource names and configuration values throughout a lab. It also reduces the chance of accidentally using inconsistent names in later commands.&lt;/p&gt;

&lt;p&gt;The values describe the intended lab configuration; they do not mean the AKS cluster has already been created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security reminder:&lt;/strong&gt; &lt;code&gt;lab.env&lt;/code&gt; is included in &lt;code&gt;.gitignore&lt;/code&gt;. Keep credentials and secrets out of source control, and check the repository's status before committing changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A dedicated workspace and Git history provide a maintainable starting point for infrastructure projects.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.gitignore&lt;/code&gt; helps prevent selected local files from being added to the repository, but it does not replace proper secret management.&lt;/li&gt;
&lt;li&gt;Azure CLI and &lt;code&gt;kubectl&lt;/code&gt; have different roles: one manages Azure resources, while the other interacts with Kubernetes.&lt;/li&gt;
&lt;li&gt;When an installer fails at its default path, inspecting the environment and selecting a suitable installation location can provide a practical workaround.&lt;/li&gt;
&lt;li&gt;Always verify the active Azure account and subscription before creating or modifying resources.&lt;/li&gt;
&lt;li&gt;Registering a resource provider enables its operations; it does not create resources by itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, I'll continue with the next stage of the lab: provisioning and configuring the Azure Kubernetes Service infrastructure, following the lab guide.&lt;/p&gt;

&lt;p&gt;We'll move from preparing the local environment to establishing the Azure infrastructure on which the Kubernetes workloads will run.&lt;/p&gt;

&lt;p&gt;This is the foundation for the remaining parts of the series, covering Kubernetes workloads, networking, configuration, storage, automation, scaling, cost optimization, and cleanup.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article documents the commands and outcomes captured during the lab. Installation results and version numbers reflect the recorded environment and may differ on other systems. Review commands, permissions, and potential Azure charges before running them in your own subscription.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>azure</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>S3 Has No Folders. Here's What You're Really Using.</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Thu, 22 Oct 2026 10:41:57 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/s3-has-no-folders-heres-what-youre-really-using-2p7h</link>
      <guid>https://dev.to/rahimah_dev/s3-has-no-folders-heres-what-youre-really-using-2p7h</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/rahimah_dev/getting-started-with-aws-s3-creating-a-bucket-uploading-an-object-and-understanding-why-the-1f2l"&gt;previous article&lt;/a&gt;, I created an S3 bucket and uploaded a single object. This time I'm organizing that bucket: creating folders, uploading multiple files at once, moving an object between folders, and finally deleting the bucket.&lt;/p&gt;

&lt;p&gt;Along the way I'll explain a fact that trips up many people: &lt;strong&gt;Amazon S3 doesn't have folders.&lt;/strong&gt; What the console shows you is something else, and once you understand it, a lot of S3 behavior starts to make sense.&lt;/p&gt;

&lt;p&gt;By the end of this article, you'll be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create folders (and nested folders) in a bucket&lt;/li&gt;
&lt;li&gt;Upload several files in one go&lt;/li&gt;
&lt;li&gt;Explain what a &lt;strong&gt;prefix&lt;/strong&gt; is and how it differs from a real folder&lt;/li&gt;
&lt;li&gt;Move an object from one folder to another, and understand what S3 does behind the scenes&lt;/li&gt;
&lt;li&gt;Empty and delete a bucket correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where we're starting
&lt;/h2&gt;

&lt;p&gt;I'm using the same bucket from the first article, &lt;strong&gt;&lt;code&gt;aws-lab1-rahimah&lt;/code&gt;&lt;/strong&gt;, which currently holds one object, &lt;code&gt;datacenter.png&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a folder
&lt;/h2&gt;

&lt;p&gt;Inside the bucket, I clicked &lt;strong&gt;Create folder&lt;/strong&gt;. The form asks for a &lt;strong&gt;folder name&lt;/strong&gt;, so I entered &lt;code&gt;food&lt;/code&gt;. Notice the hint under the field: folder names can't contain a &lt;code&gt;/&lt;/code&gt;, because S3 uses that character itself to mark folder boundaries (more on that shortly).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F575o889okd3gpxs5w3uu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F575o889okd3gpxs5w3uu.png" alt="Create folder page with the name food" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page also has a &lt;strong&gt;Server-side encryption&lt;/strong&gt; section with two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't specify an encryption key:&lt;/strong&gt; the folder uses the bucket's default encryption setting. I kept this one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specify an encryption key:&lt;/strong&gt; choose a key just for this folder object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two warnings on that page are worth reading. The encryption settings apply only to the folder object itself, not to the files you later put inside it. And if your bucket policy requires a specific encryption key, you must specify that same key here, or folder creation will fail.&lt;/p&gt;

&lt;p&gt;After clicking &lt;strong&gt;Create folder&lt;/strong&gt;, S3 confirmed with a green banner, and the bucket now lists &lt;strong&gt;Objects (2)&lt;/strong&gt;: &lt;code&gt;datacenter.png&lt;/code&gt; and &lt;code&gt;food/&lt;/code&gt;. The &lt;strong&gt;Type&lt;/strong&gt; column shows &lt;strong&gt;Folder&lt;/strong&gt; for &lt;code&gt;food/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9s80t7p7oocwn3d9l94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9s80t7p7oocwn3d9l94.png" alt="create folder" width="799" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6zqbdn316wqdqheqndu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6zqbdn316wqdqheqndu.png" alt="Successfully created folder food" width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opening &lt;code&gt;food/&lt;/code&gt; shows an empty folder: &lt;strong&gt;Objects (0)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8pxj3052mlyooyfgbv8o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8pxj3052mlyooyfgbv8o.png" alt="The empty food/ folder" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is a "folder" in S3?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Here's the important part. S3 uses a &lt;strong&gt;flat structure&lt;/strong&gt;. There is a bucket, and inside it there are objects. That's it. There are no real directories nested inside each other.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What the console calls a folder is built from three ideas:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key:&lt;/strong&gt; the full name of an object. A file I upload into &lt;code&gt;food/&lt;/code&gt; has the key &lt;code&gt;food/burger.png&lt;/code&gt;, not just &lt;code&gt;burger.png&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefix:&lt;/strong&gt; the beginning part of a key. For &lt;code&gt;food/burger.png&lt;/code&gt;, the prefix is &lt;code&gt;food/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delimiter:&lt;/strong&gt; the character that separates the parts of a key. In S3, it's the forward slash &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you click &lt;strong&gt;Create folder&lt;/strong&gt;, S3 creates a tiny, empty object whose key ends in &lt;code&gt;/&lt;/code&gt; (here, &lt;code&gt;food/&lt;/code&gt;). The console then uses the &lt;code&gt;/&lt;/code&gt; delimiter to &lt;em&gt;display&lt;/em&gt; objects with the same prefix as if they lived in a folder. It's a presentation layer that makes a flat store feel familiar.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Coming from Azure?&lt;/strong&gt; It works much the same way in Blob Storage. A container holds blobs in a flat namespace, and "folders" are just &lt;code&gt;/&lt;/code&gt; characters in blob names (virtual directories). Real directories only appear when you enable the hierarchical namespace on a storage account (Data Lake Storage Gen2).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: Upload multiple files into the folder
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;food/&lt;/code&gt; open, I clicked &lt;strong&gt;Upload&lt;/strong&gt;, then &lt;strong&gt;Add files&lt;/strong&gt;. In the file picker, I selected three images at once: &lt;code&gt;burger.png&lt;/code&gt;, &lt;code&gt;cake.png&lt;/code&gt; and &lt;code&gt;pizza.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgp68q18g6e2wxhmqsi4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgp68q18g6e2wxhmqsi4v.png" alt="Selecting three files in the file picker" width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Upload page lists all three (3 total, 8.2 MB), and the &lt;strong&gt;Destination&lt;/strong&gt; is now &lt;code&gt;s3://aws-lab1-rahimah/food/&lt;/code&gt;. That path is the S3 URI of the folder, which is exactly the prefix the files will receive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F97kqxg0ulmy1t7fi8ub0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F97kqxg0ulmy1t7fi8ub0.png" alt="Three files ready to upload to the food/ prefix" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked &lt;strong&gt;Upload&lt;/strong&gt;, and the status page reported &lt;strong&gt;Upload succeeded&lt;/strong&gt;: 3 files, 8.2 MB, 100%, with 0 failures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwg06l7x4zoru8il0e29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwg06l7x4zoru8il0e29.png" alt="Upload succeeded for all three files" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back at the bucket's root, there are still only two entries, &lt;code&gt;datacenter.png&lt;/code&gt; and &lt;code&gt;food/&lt;/code&gt;. The three new files live &lt;em&gt;inside&lt;/em&gt; the folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsu0uk1h4ake87xrye2s8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsu0uk1h4ake87xrye2s8.png" alt="The bucket root still shows two entries" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opening &lt;code&gt;food/&lt;/code&gt; shows &lt;strong&gt;Objects (3)&lt;/strong&gt;: &lt;code&gt;burger.png&lt;/code&gt;, &lt;code&gt;cake.png&lt;/code&gt; and &lt;code&gt;pizza.png&lt;/code&gt;, each with the &lt;strong&gt;Standard&lt;/strong&gt; storage class and its own last-modified time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdccgl0nltxy8n3epwaj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdccgl0nltxy8n3epwaj.png" alt="Three objects inside food/" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Open a file and see the prefix in the URL
&lt;/h2&gt;

&lt;p&gt;To prove the prefix idea, I selected &lt;code&gt;burger.png&lt;/code&gt; and clicked &lt;strong&gt;Open&lt;/strong&gt;. The image loaded in a new tab, and the address bar shows the key right in the path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://aws-lab1-rahimah.s3.eu-north-1.amazonaws.com/food/burger.png?X-Amz-Algorithm=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhzb4x1k2rotxg30dy8hm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhzb4x1k2rotxg30dy8hm.png" alt="burger.png opened in the browser, with food/ visible in the URL" width="799" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See &lt;code&gt;food/burger.png&lt;/code&gt;? That's the full key. There's no directory lookup involved. The long &lt;code&gt;X-Amz-...&lt;/code&gt; parameters after it are the presigned URL signature I covered in the first article, which is why the file opens even though the bucket is private.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create a nested folder
&lt;/h2&gt;

&lt;p&gt;Next, I wanted a sub-folder. Inside &lt;code&gt;food/&lt;/code&gt;, I clicked &lt;strong&gt;Create folder&lt;/strong&gt; again and named it &lt;code&gt;menu&lt;/code&gt;. S3 confirmed with &lt;strong&gt;Successfully created folder "menu"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now &lt;code&gt;food/&lt;/code&gt; contains &lt;code&gt;burger.png&lt;/code&gt;, &lt;code&gt;cake.png&lt;/code&gt;, &lt;code&gt;pizza.png&lt;/code&gt; and the new &lt;code&gt;menu/&lt;/code&gt; folder. The key of that new folder is &lt;code&gt;food/menu/&lt;/code&gt;, which is just a longer prefix. Nesting folders only means chaining more prefixes with &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I deleted the datacenter.png and created an empty folder called menu/.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Move a file into the new folder
&lt;/h2&gt;

&lt;p&gt;I wanted &lt;code&gt;cake.png&lt;/code&gt; to live in &lt;code&gt;menu/&lt;/code&gt;. I ticked the checkbox next to &lt;code&gt;cake.png&lt;/code&gt; and opened the &lt;strong&gt;Actions&lt;/strong&gt; menu. It offers a lot of options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download as&lt;/strong&gt; and &lt;strong&gt;Share with a presigned URL&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calculate total size&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy&lt;/strong&gt; and &lt;strong&gt;Move&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query with S3 Select&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Edit actions&lt;/strong&gt;: &lt;strong&gt;Rename object&lt;/strong&gt;, &lt;strong&gt;Edit storage class&lt;/strong&gt;, &lt;strong&gt;Edit server-side encryption&lt;/strong&gt;, &lt;strong&gt;Edit tags&lt;/strong&gt;, and &lt;strong&gt;Make public using ACL&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fni5jt1su6d28iv167nou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fni5jt1su6d28iv167nou.png" alt="The Actions menu with cake.png selected" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I chose &lt;strong&gt;Move&lt;/strong&gt;. The page opens with an important note at the top: &lt;em&gt;this action creates a copy of the object with a new last-modified date, and depending on the bucket's versioning status, a delete marker will be inserted or the original object will be permanently deleted.&lt;/em&gt; Keep that sentence in mind, because it explains what move really does (more below).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7w06f81if51ey22vkbrn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7w06f81if51ey22vkbrn.png" alt="the move page" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Destination&lt;/strong&gt; field expects an S3 path in the format &lt;code&gt;s3://bucket-name/optional-prefix/&lt;/code&gt;. You can type it, or click &lt;strong&gt;Browse S3&lt;/strong&gt; and navigate to the folder. I used Browse S3, went into &lt;code&gt;aws-lab1-rahimah&lt;/code&gt; &amp;gt; &lt;code&gt;food/&lt;/code&gt;, and selected the &lt;code&gt;menu/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz5q1q6mtcjswyvfdcr0e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz5q1q6mtcjswyvfdcr0e.png" alt="Browsing S3 to pick the menu/ folder" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser also lets you open &lt;code&gt;menu/&lt;/code&gt; to check what's inside. At this point, it says there are no objects under this path, which is expected for a brand-new folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo64xfxgckfstrb2caf1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo64xfxgckfstrb2caf1.png" alt="Inside menu/ in the destination browser" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After clicking &lt;strong&gt;Choose destination&lt;/strong&gt;, the form filled itself in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Destination:&lt;/strong&gt; &lt;code&gt;s3://aws-lab1-rahimah/food/menu/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destination bucket name:&lt;/strong&gt; &lt;code&gt;aws-lab1-rahimah&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destination prefix:&lt;/strong&gt; &lt;code&gt;food/menu/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below that, &lt;strong&gt;Destination details&lt;/strong&gt; shows the settings of the target bucket: &lt;strong&gt;Bucket Versioning&lt;/strong&gt; is &lt;strong&gt;Disabled&lt;/strong&gt;, &lt;strong&gt;Default encryption&lt;/strong&gt; is &lt;strong&gt;SSE-S3&lt;/strong&gt;, and &lt;strong&gt;Object Lock&lt;/strong&gt; is &lt;strong&gt;Disabled&lt;/strong&gt;. Because versioning is off, the console shows a recommendation to enable it. The &lt;strong&gt;Specified objects&lt;/strong&gt; table confirms the one file being moved, &lt;code&gt;cake.png&lt;/code&gt; (2.7 MB).&lt;/p&gt;

&lt;p&gt;Last, &lt;strong&gt;Additional copy settings&lt;/strong&gt; offers three choices. I kept the default, &lt;strong&gt;Copy source settings&lt;/strong&gt;, which carries over the source object's storage class, tags, metadata and encryption. The other two let you skip those settings or specify new ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqn6z8rlt3fle6vqrb5ls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqn6z8rlt3fle6vqrb5ls.png" alt="Move configuration before clicking Move" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked &lt;strong&gt;Move&lt;/strong&gt;. The status page reported &lt;strong&gt;Successfully moved objects&lt;/strong&gt;: 1 object, 2.7 MB, with 0 failures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6mb1yzeaxcjmdxun6lt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6mb1yzeaxcjmdxun6lt.png" alt="Successfully moved objects" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checking the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;food/&lt;/code&gt; now shows &lt;strong&gt;Objects (3)&lt;/strong&gt;: &lt;code&gt;burger.png&lt;/code&gt;, &lt;code&gt;menu/&lt;/code&gt; and &lt;code&gt;pizza.png&lt;/code&gt;. &lt;code&gt;cake.png&lt;/code&gt; is gone from here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhutk5d3gnzy0fax4vqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhutk5d3gnzy0fax4vqp.png" alt="food/ after the move" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;menu/&lt;/code&gt; shows &lt;strong&gt;Objects (1)&lt;/strong&gt;: &lt;code&gt;cake.png&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29nl7i2urhly9tn2b3j2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29nl7i2urhly9tn2b3j2.png" alt="cake.png now lives in menu/" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What "move" really does
&lt;/h3&gt;

&lt;p&gt;Look at the &lt;strong&gt;Last modified&lt;/strong&gt; time on the moved file. It changed to a newer timestamp than when I originally uploaded it. That's the giveaway: S3 has no true "move" or "rename" operation. The console does two steps for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Copy&lt;/strong&gt; the object to the new key (&lt;code&gt;food/cake.png&lt;/code&gt; becomes &lt;code&gt;food/menu/cake.png&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete&lt;/strong&gt; the original.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This matters for two reasons. First, because versioning was &lt;strong&gt;disabled&lt;/strong&gt; on this bucket, the original was &lt;strong&gt;permanently deleted&lt;/strong&gt;. If versioning were enabled, S3 would insert a &lt;strong&gt;delete marker&lt;/strong&gt; instead, and the old version could be recovered. Second, the same logic applies to &lt;strong&gt;Rename object&lt;/strong&gt;: it's also a copy followed by a delete, since a rename just means giving the object a new key.&lt;/p&gt;

&lt;p&gt;For anything you can't afford to lose, enable versioning before you start moving things around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Delete the bucket
&lt;/h2&gt;

&lt;p&gt;When the lab is finished, clean up so nothing lingers in your account. On the &lt;strong&gt;General purpose buckets&lt;/strong&gt; page, I selected the radio button next to &lt;code&gt;aws-lab1-rahimah&lt;/code&gt;. The toolbar offers &lt;strong&gt;Copy ARN&lt;/strong&gt;, &lt;strong&gt;Empty&lt;/strong&gt;, &lt;strong&gt;Delete&lt;/strong&gt; and &lt;strong&gt;Create bucket&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8g6umlvr9yr7whjvxht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8g6umlvr9yr7whjvxht.png" alt="The bucket list with the bucket selected" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked &lt;strong&gt;Delete&lt;/strong&gt; first, and S3 stopped me with this error: &lt;strong&gt;"This bucket is not empty. Buckets must be empty before they can be deleted."&lt;/strong&gt; The page also asks you to type the bucket's name to confirm deletion, but the &lt;strong&gt;Delete bucket&lt;/strong&gt; button stays disabled until the bucket is empty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fys0phknctgtf4sfl8zhn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fys0phknctgtf4sfl8zhn.png" alt="Delete bucket error: This bucket is not empty" width="799" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The solution is to empty the bucket first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Empty bucket&lt;/strong&gt; (on that error banner, or &lt;strong&gt;Empty&lt;/strong&gt; on the bucket list).
Or go back to the bucket, select the objects and click &lt;strong&gt;delete&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Confirm by typing &lt;code&gt;permanently delete&lt;/code&gt; when prompted, then click &lt;strong&gt;Empty&lt;/strong&gt;. This can never be retrieved because versioning is not enabled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd4lxkynqqmciqupldgcz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd4lxkynqqmciqupldgcz.png" alt="permanently delete" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Return to &lt;strong&gt;Delete bucket&lt;/strong&gt;, type &lt;code&gt;aws-lab1-rahimah&lt;/code&gt; in the confirmation field, and click &lt;strong&gt;Delete bucket&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi58jhdxzvqn7oaurssnl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi58jhdxzvqn7oaurssnl.png" alt="delete bucket" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Emptying removes every object, including the folder objects, and it cannot be undone. That's why S3 makes you confirm twice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feu55mdcpgqd9on0wk433.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feu55mdcpgqd9on0wk433.png" alt="successful deletion" width="799" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 has no real folders.&lt;/strong&gt; A "folder" is a &lt;strong&gt;prefix&lt;/strong&gt; that the console displays using the &lt;code&gt;/&lt;/code&gt; &lt;strong&gt;delimiter&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;An object's &lt;strong&gt;key&lt;/strong&gt; is its full name, including the prefix. &lt;code&gt;burger.png&lt;/code&gt; inside &lt;code&gt;food/&lt;/code&gt; has the key &lt;code&gt;food/burger.png&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Creating a folder just creates a small, empty object whose key ends in &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can &lt;strong&gt;upload several files at once&lt;/strong&gt;, and they all receive the destination folder's prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move&lt;/strong&gt; and &lt;strong&gt;Rename&lt;/strong&gt; are really &lt;strong&gt;copy + delete&lt;/strong&gt;, which is why the last-modified date changes.&lt;/li&gt;
&lt;li&gt;With &lt;strong&gt;versioning disabled&lt;/strong&gt;, a move permanently removes the original. With versioning enabled, a delete marker is added instead.&lt;/li&gt;
&lt;li&gt;A bucket must be &lt;strong&gt;emptied before it can be deleted&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In Azure terms, this is the same flat-namespace model as Blob Storage virtual directories.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Folders in S3 are a convenience, not a structure. Once you think in terms of keys and prefixes, things like moving files, setting lifecycle rules and writing IAM policies for &lt;code&gt;food/*&lt;/code&gt; all make sense, because they all work on prefixes.&lt;/p&gt;

&lt;p&gt;Next up in this series, I'll look at S3 permissions and what it takes to make an object public safely.&lt;/p&gt;

&lt;p&gt;Which S3 feature should I cover after that: versioning, lifecycle rules or static website hosting? Let me know in the comments.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloudstorage</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Getting Started with AWS S3: Creating a Bucket, Uploading an Object, and Understanding Why the Object URL Says Access Denied</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Tue, 20 Oct 2026 23:11:02 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/getting-started-with-aws-s3-creating-a-bucket-uploading-an-object-and-understanding-why-the-1f2l</link>
      <guid>https://dev.to/rahimah_dev/getting-started-with-aws-s3-creating-a-bucket-uploading-an-object-and-understanding-why-the-1f2l</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I'm starting a new series on AWS, and this first article covers Amazon S3 (Simple Storage Service), one of the most widely used services on AWS. I did this lab in my own personal AWS free-tier account, and I'll explain every term as it comes up.&lt;/p&gt;

&lt;p&gt;By the end, you'll know how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an S3 bucket with a consistent naming convention&lt;/li&gt;
&lt;li&gt;Read the bucket's ARN and properties&lt;/li&gt;
&lt;li&gt;Upload a file to the bucket&lt;/li&gt;
&lt;li&gt;Find an object's URL, and understand why opening the image works while opening the Object URL doesn't&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Amazon S3?
&lt;/h2&gt;

&lt;p&gt;Amazon S3 is an &lt;strong&gt;object storage&lt;/strong&gt; service. Instead of organizing data in a traditional disk with a file system, S3 stores data as &lt;strong&gt;objects&lt;/strong&gt; inside &lt;strong&gt;buckets&lt;/strong&gt;. You can store and retrieve any amount of data from anywhere, and you pay only for what you use.&lt;/p&gt;

&lt;p&gt;Three terms to know first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bucket:&lt;/strong&gt; a container for your data. Every object in S3 lives inside a bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object:&lt;/strong&gt; the thing you store. It's the file itself plus its metadata (size, type, last modified date and so on).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key:&lt;/strong&gt; the object's unique name inside the bucket. For my file, the key is &lt;code&gt;datacenter.png&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Open the S3 service
&lt;/h3&gt;

&lt;p&gt;In the AWS Management Console, I typed &lt;strong&gt;S3&lt;/strong&gt; into the search bar and selected &lt;strong&gt;S3 (Scalable Storage in the Cloud)&lt;/strong&gt; from the results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa49x69z6485s27aljqgw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa49x69z6485s27aljqgw.png" alt="Searching for S3 in the AWS console" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This opens the Amazon S3 landing page, where &lt;strong&gt;Create bucket&lt;/strong&gt; is the first thing you see.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3tlx0ygatm4un5z5fct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3tlx0ygatm4un5z5fct.png" alt="Amazon S3 landing page with the Create bucket button" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a bucket
&lt;/h2&gt;

&lt;p&gt;I clicked &lt;strong&gt;Create bucket&lt;/strong&gt;, and the only thing I had to provide was the &lt;strong&gt;bucket name&lt;/strong&gt;. The &lt;strong&gt;region&lt;/strong&gt; was already set for me (more on that below), and I left everything else at the defaults to keep things simple.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnphsnb91a34wnybssgzw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnphsnb91a34wnybssgzw.png" alt="Create bucket page, General configuration section" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Region
&lt;/h3&gt;

&lt;p&gt;I didn't pick the region on this page. The Create bucket page shows it as plain text, not a dropdown: &lt;strong&gt;Europe (Stockholm) &lt;code&gt;eu-north-1&lt;/code&gt;&lt;/strong&gt;. That's because S3 creates a bucket in whichever region your console is currently set to, and mine was already set to Stockholm. &lt;em&gt;I can't change it because this is a free plan&lt;/em&gt;. If you want a different region, switch it with the region selector in the top bar of the console &lt;em&gt;before&lt;/em&gt; you click Create bucket.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;region&lt;/strong&gt; is a physical geographic location where AWS runs data centers. Your bucket's data stays in the region it was created in, and the region affects latency (how close the data is to your users) and pricing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket type
&lt;/h3&gt;

&lt;p&gt;I kept &lt;strong&gt;General purpose&lt;/strong&gt;, which AWS recommends for most use cases. The other option, &lt;strong&gt;Directory&lt;/strong&gt;, is for low-latency workloads that use a single Availability Zone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket namespace
&lt;/h3&gt;

&lt;p&gt;I kept &lt;strong&gt;Global namespace&lt;/strong&gt;. A &lt;strong&gt;namespace&lt;/strong&gt; is the set of names in which yours has to be unique. In the global namespace, a bucket name must be unique across all AWS accounts, in every region. If someone else already has &lt;code&gt;my-bucket&lt;/code&gt;, you can't use it. The console also offers an &lt;strong&gt;Account Regional namespace&lt;/strong&gt; (labelled "recommended"). Buckets created there are unique only within your own account, so no other AWS account can ever claim the same name. I stayed with the global namespace for this lab.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket name and the prefix naming convention
&lt;/h3&gt;

&lt;p&gt;I named my bucket &lt;strong&gt;&lt;code&gt;aws-lab1-rahimah&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;aws-lab1-&lt;/code&gt; part is a &lt;strong&gt;prefix&lt;/strong&gt; I chose as a naming convention. A consistent prefix gives you three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It keeps names unique, since adding your own name or project helps avoid clashes in the global namespace.&lt;/li&gt;
&lt;li&gt;It groups related resources together (&lt;code&gt;aws-lab1-...&lt;/code&gt;, &lt;code&gt;aws-lab2-...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;It makes resources easy to find and to clean up later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;S3 bucket names must be 3 to 63 characters long, can contain only lowercase letters, numbers, periods and hyphens, and must begin and end with a letter or number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Default encryption
&lt;/h3&gt;

&lt;p&gt;Further down the page, &lt;strong&gt;Default encryption&lt;/strong&gt; was already set to &lt;strong&gt;Server-side encryption with Amazon S3 managed keys (SSE-S3)&lt;/strong&gt;. This means S3 automatically encrypts every new object at rest, using keys that AWS manages for you. You don't have to do anything. I left it as it was and clicked &lt;strong&gt;Create bucket&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfiizxzcmlfm9ykir2pz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfiizxzcmlfm9ykir2pz.png" alt="create bucket" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;S3 confirmed with a green banner: &lt;strong&gt;Successfully created bucket "aws-lab1-rahimah"&lt;/strong&gt;. The Objects tab shows &lt;strong&gt;Objects (0)&lt;/strong&gt; because the bucket is empty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmcmg2vlthx6angtr1e5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmcmg2vlthx6angtr1e5d.png" alt="Bucket created successfully with Objects (0)" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Look at the bucket's properties and ARN
&lt;/h2&gt;

&lt;p&gt;I opened the &lt;strong&gt;Properties&lt;/strong&gt; tab. The &lt;strong&gt;Bucket overview&lt;/strong&gt; shows three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Region:&lt;/strong&gt; Europe (Stockholm) &lt;code&gt;eu-north-1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creation date:&lt;/strong&gt; October 20, 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Resource Name (ARN):&lt;/strong&gt; &lt;code&gt;arn:aws:s3:::aws-lab1-rahimah&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F32mksngtunnnxnusyr4x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F32mksngtunnnxnusyr4x.png" alt="Bucket overview on the Properties tab" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What is an ARN?
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;Amazon Resource Name (ARN)&lt;/strong&gt; is the unique identifier AWS gives every resource. It's how you point at a specific resource in IAM policies, logs and other services. The general shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arn:partition:service:region:account-id:resource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For S3 buckets, the region and account sections are empty, which is why you see three colons in a row (&lt;code&gt;:::&lt;/code&gt;). The bucket name is the final part.&lt;/p&gt;

&lt;p&gt;The Properties tab also shows two settings that were off by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bucket Versioning: Disabled.&lt;/strong&gt; Versioning keeps multiple versions of an object, so you can recover from accidental overwrites or deletions. With it disabled, uploading a file with the same key replaces the old one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-factor authentication (MFA) delete: Disabled.&lt;/strong&gt; This extra layer of security requires MFA before you can permanently delete object versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Upload a file to the bucket
&lt;/h2&gt;

&lt;p&gt;Back on the &lt;strong&gt;Objects&lt;/strong&gt; tab, I clicked &lt;strong&gt;Upload&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fighcpq4jy0tqupu885bj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fighcpq4jy0tqupu885bj.png" alt="Empty bucket with the Upload button" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the Upload page, I clicked &lt;strong&gt;Add files&lt;/strong&gt; and picked an image called &lt;code&gt;datacenter.png&lt;/code&gt; from my Downloads folder. You can also drag and drop files, or use &lt;strong&gt;Add folder&lt;/strong&gt; to upload a whole folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw9exbsh5e5rxsozcvayr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw9exbsh5e5rxsozcvayr.png" alt="File picker window" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page then listed the file as &lt;strong&gt;1 total, 2.4 MB&lt;/strong&gt;, type &lt;code&gt;image/png&lt;/code&gt;, with the destination shown as &lt;code&gt;s3://aws-lab1-rahimah&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5x1gglvmnh4atpice5rw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5x1gglvmnh4atpice5rw.png" alt="File listed under Files and folders" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scrolling down, you'll see two collapsed sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permissions:&lt;/strong&gt; where you can grant public access and access to other AWS accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Properties:&lt;/strong&gt; where you can set the storage class, encryption settings and tags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I left both alone and clicked &lt;strong&gt;Upload&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd5rn0et9q5pgdhxyr1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd5rn0et9q5pgdhxyr1k.png" alt="Permissions and Properties sections with the Upload button" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next page showed &lt;strong&gt;Upload succeeded&lt;/strong&gt;, with a summary of 1 file, 2.4 MB, 100% succeeded and 0 failed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsg4scom3fxgadfyincd6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsg4scom3fxgadfyincd6.png" alt="Upload status page showing success" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back in the bucket, &lt;strong&gt;Objects (1)&lt;/strong&gt; now lists &lt;code&gt;datacenter.png&lt;/code&gt;, size 2.4 MB, and storage class &lt;strong&gt;Standard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3r2oi64m1cz6taj37rs5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3r2oi64m1cz6taj37rs5.png" alt="Bucket listing the uploaded object" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage class&lt;/strong&gt; controls how your data is stored and what it costs. &lt;strong&gt;Standard&lt;/strong&gt; is the default and is meant for frequently accessed data. Other classes, such as Glacier, are cheaper for data you rarely need to retrieve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Explore the object's details
&lt;/h2&gt;

&lt;p&gt;I clicked &lt;code&gt;datacenter.png&lt;/code&gt; to open its details. The &lt;strong&gt;Object overview&lt;/strong&gt; shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Owner:&lt;/strong&gt; the canonical ID of the AWS account that owns the object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Region:&lt;/strong&gt; &lt;code&gt;eu-north-1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Last modified, Size and Type&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;datacenter.png&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 URI:&lt;/strong&gt; &lt;code&gt;s3://aws-lab1-rahimah/datacenter.png&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARN:&lt;/strong&gt; &lt;code&gt;arn:aws:s3:::aws-lab1-rahimah/datacenter.png&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity tag (ETag):&lt;/strong&gt; an identifier for this specific version of the object's content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object URL:&lt;/strong&gt; &lt;code&gt;https://aws-lab1-rahimah.s3.eu-north-1.amazonaws.com/datacenter.png&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft1g1b1zaqpn7pcmmrmu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft1g1b1zaqpn7pcmmrmu7.png" alt="Object overview page" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two of these are easy to confuse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;S3 URI&lt;/strong&gt; (&lt;code&gt;s3://bucket/key&lt;/code&gt;) is what AWS tools like the AWS CLI and SDKs use to refer to an object.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Object URL&lt;/strong&gt; is the HTTPS address of the object on the internet, in the form &lt;code&gt;https://bucket-name.s3.region.amazonaws.com/key&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;ARN&lt;/strong&gt; here is the bucket's ARN with the object key added on the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Open the image vs. use the Object URL
&lt;/h2&gt;

&lt;p&gt;This is the part that surprised me, and it taught me the most. There are two ways to view the uploaded image, and they behave differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Click "Open"
&lt;/h3&gt;

&lt;p&gt;Clicking the &lt;strong&gt;Open&lt;/strong&gt; button displays the image in a new browser tab. The address bar shows a very long URL that carries extra parameters such as &lt;code&gt;X-Amz-Algorithm=AWS4-HMAC-SHA256&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8hbfez8988ujai060tt1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8hbfez8988ujai060tt1.png" alt="The image displayed in the browser after clicking Open" width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a &lt;strong&gt;presigned URL&lt;/strong&gt;. When you click Open, the console uses your own credentials to generate a temporary, signed link that grants access to the object for a short time. S3 sees the signature, confirms that a permitted user created it, and serves the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Paste the Object URL
&lt;/h3&gt;

&lt;p&gt;When I copied the plain &lt;strong&gt;Object URL&lt;/strong&gt; into the browser, I got this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Error&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Code&amp;gt;&lt;/span&gt;AccessDenied&lt;span class="nt"&gt;&amp;lt;/Code&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Message&amp;gt;&lt;/span&gt;Access Denied&lt;span class="nt"&gt;&amp;lt;/Message&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/Error&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1tv21x2grgk974fz3gn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1tv21x2grgk974fz3gn.png" alt="AccessDenied error when opening the Object URL" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the difference?
&lt;/h3&gt;

&lt;p&gt;The Object URL is an &lt;em&gt;unauthenticated&lt;/em&gt; request. When you paste it into a browser, there's no signature and nothing that tells S3 who you are, so S3 treats you as an anonymous visitor. S3 buckets and objects are &lt;strong&gt;private by default&lt;/strong&gt;, and only the owner can access them unless permissions are explicitly granted. So the anonymous request is refused with &lt;code&gt;AccessDenied&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The error is not a bug. It shows S3's security model working as designed: nothing is public until you deliberately make it so.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making objects public
&lt;/h3&gt;

&lt;p&gt;If you need public access, for example to host images on a website, you'd have to change two things: turn off the relevant &lt;strong&gt;Block Public Access&lt;/strong&gt; settings on the bucket, and then grant read access through a bucket policy or object permissions. Only do this for data that's genuinely meant to be public. For private files, presigned URLs are a safer way to share something temporarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;bucket&lt;/strong&gt; is a container and an &lt;strong&gt;object&lt;/strong&gt; is a file inside it, identified by its &lt;strong&gt;key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Bucket names must be &lt;strong&gt;globally unique&lt;/strong&gt;, so a naming prefix like &lt;code&gt;aws-lab1-&lt;/code&gt; helps keep names unique and organized.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;region&lt;/strong&gt; determines where your data physically lives.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;ARN&lt;/strong&gt; uniquely identifies any AWS resource, and an object's ARN is its bucket's ARN plus the key.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;S3 URI&lt;/strong&gt; is for AWS tools, while the &lt;strong&gt;Object URL&lt;/strong&gt; is the HTTPS address.&lt;/li&gt;
&lt;li&gt;S3 is &lt;strong&gt;private by default&lt;/strong&gt;. Opening an object from the console works because it uses a presigned URL, while the plain Object URL returns &lt;code&gt;AccessDenied&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Clean up
&lt;/h2&gt;

&lt;p&gt;To avoid unexpected charges, delete what you created. You must empty a bucket before you can delete it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;code&gt;datacenter.png&lt;/code&gt; and click the box, make sure you tick it, so that the Delete button is not greyed out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfpqlrhwfhhe0vb569m3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfpqlrhwfhhe0vb569m3.png" alt="delete datacenter" width="800" height="302"&gt;&lt;/a&gt;&lt;br&gt;
Then confirm deletion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrmkc4u1afewtjny0kbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrmkc4u1afewtjny0kbi.png" alt="confirm deletion" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go back to the bucket list, select &lt;code&gt;aws-lab1-rahimah&lt;/code&gt; and click &lt;strong&gt;Delete object&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this first lab, I created an S3 bucket, uploaded an object, and learned why access to it is blocked by default. Next in the series, I'll build on this with more S3 features and other AWS services.&lt;/p&gt;

&lt;p&gt;If you're learning AWS, let me know in the comments which service I should cover next.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloud</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Configure a project and repository structure to support secure pipelines.</title>
      <dc:creator>Rahimah Sulayman</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:37:18 +0000</pubDate>
      <link>https://dev.to/rahimah_dev/configure-a-project-and-repository-structure-to-support-secure-pipelines-6o4</link>
      <guid>https://dev.to/rahimah_dev/configure-a-project-and-repository-structure-to-support-secure-pipelines-6o4</guid>
      <description>&lt;p&gt;In this lab, you will learn how to configure a project and repository structure in Azure DevOps to support secure pipelines. This lab covers best practices for organizing projects and repositories, assigning permissions, and managing secure files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You’ll need an Azure subscription, Azure DevOps organization, and the eShopOnWeb application to follow the labs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow the steps to validate your lab environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Validate your lab environment
&lt;/h3&gt;

&lt;p&gt;In preparation for the labs, it is crucial to have your environment correctly set up. This section will guide you through the setup process, ensuring all prerequisites are met.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The labs require &lt;strong&gt;Microsoft Edge&lt;/strong&gt; or an &lt;a href="https://learn.microsoft.com/en-us/azure/devops/server/compatibility?view=azure-devops#web-portal-supported-browsers" rel="noopener noreferrer"&gt;Azure DevOps-supported browser&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up an Azure Subscription&lt;/strong&gt;: If you don’t already have an Azure subscription, create one by following the instructions on this page or visit &lt;a href="https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account?icid=azurefreeaccount" rel="noopener noreferrer"&gt;https://azure.microsoft.com/free&lt;/a&gt; to sign up for a free.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up an Azure DevOps organization&lt;/strong&gt;: If you don’t already have an Azure DevOps organization that you can use for the labs, create one by following the instructions on this page, or at &lt;a href="https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account?icid=azurefreeaccount" rel="noopener noreferrer"&gt;Create an organization or project collection&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://gitforwindows.org/" rel="noopener noreferrer"&gt;Git for Windows download page&lt;/a&gt;. This will be installed as part of prerequisites for this lab.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://code.visualstudio.com/" rel="noopener noreferrer"&gt;Visual Studio Code&lt;/a&gt;. This will be installed as part of prerequisites for this lab.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest" rel="noopener noreferrer"&gt;Azure CLI&lt;/a&gt;. Install the Azure CLI on the self-hosted agent machines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dotnet.microsoft.com/en-us/download/visual-studio-sdks" rel="noopener noreferrer"&gt;.NET SDK - Latest version&lt;/a&gt;. Install the .NET SDK on the self-hosted agent machines.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instructions to create an Azure DevOps Organization (you only have to do this once)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Start at step 3, if you do already have a &lt;strong&gt;personal Microsoft Account&lt;/strong&gt; setup and an active Azure Subscription linked to that account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;1.Use a private browser session to get a new &lt;strong&gt;personal Microsoft Account (MSA)&lt;/strong&gt; at &lt;code&gt;https://account.microsoft.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;2.Using the same browser session, sign up for a free Azure subscription at &lt;code&gt;https://azure.microsoft.com/free&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;3.Open a browser and navigate to Azure portal at &lt;code&gt;https://portal.azure.com&lt;/code&gt;, then search at the top of the Azure portal screen for &lt;strong&gt;Azure DevOps&lt;/strong&gt;. In the resulting page, click &lt;strong&gt;Azure DevOps organizations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;4.Next, click on the link labelled &lt;strong&gt;My Azure DevOps Organizations&lt;/strong&gt; or navigate directly to &lt;code&gt;https://aex.dev.azure.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;5.On the &lt;strong&gt;We need a few more details&lt;/strong&gt; page, select &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;6.In the drop-down box on the left, choose &lt;strong&gt;Default Directory&lt;/strong&gt;, instead of &lt;strong&gt;Microsoft Account&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;7.If prompted (&lt;em&gt;“We need a few more details”&lt;/em&gt;), provide your name, e-mail address, and location and click &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;8.Back at &lt;code&gt;https://aex.dev.azure.com&lt;/code&gt; with &lt;strong&gt;Default Directory&lt;/strong&gt; selected click the blue button &lt;strong&gt;Create new organization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;9.Accept the &lt;em&gt;Terms of Service&lt;/em&gt; by clicking &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;10.If prompted (&lt;em&gt;“Almost done”&lt;/em&gt;), leave the name for the Azure DevOps organization at default (it needs to be a globally unique name) and pick a hosting location close to you from the list.&lt;/p&gt;

&lt;p&gt;11.Once the newly created organization opens in &lt;strong&gt;Azure DevOps&lt;/strong&gt;, select &lt;strong&gt;Organization settings&lt;/strong&gt; in the bottom left corner.&lt;/p&gt;

&lt;p&gt;12.At the &lt;strong&gt;Organization settings&lt;/strong&gt; screen select &lt;strong&gt;Billing&lt;/strong&gt; (opening this screen takes a few seconds).&lt;/p&gt;

&lt;p&gt;13.Select &lt;strong&gt;Setup billing&lt;/strong&gt; and on the right-hand side of the screen, select your &lt;strong&gt;Azure Subscription&lt;/strong&gt; and then select &lt;strong&gt;Save&lt;/strong&gt; to link the subscription with the organization.&lt;/p&gt;

&lt;p&gt;14.Once the screen shows the linked Azure Subscription ID at the top, change the number of &lt;strong&gt;Paid parallel jobs&lt;/strong&gt; for &lt;strong&gt;MS Hosted CI/CD&lt;/strong&gt; from 0 to &lt;strong&gt;1&lt;/strong&gt;. Then select &lt;strong&gt;SAVE&lt;/strong&gt; button at the bottom.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxrqnerg77rmcft0fo0y7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxrqnerg77rmcft0fo0y7.png" alt="Paid parallel jobs" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You may &lt;strong&gt;wait a couple of minutes before using the CI/CD capabilities&lt;/strong&gt; so that the new settings are reflected in the backend. Otherwise, you will still see the message “No hosted parallelism has been purchased or granted”.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;15.In &lt;strong&gt;Organization Settings&lt;/strong&gt;, go to section &lt;strong&gt;Pipelines&lt;/strong&gt; and click &lt;strong&gt;Settings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;16.Toggle the switch to &lt;strong&gt;Off&lt;/strong&gt; for &lt;strong&gt;Disable creation of classic build pipelines&lt;/strong&gt; and &lt;strong&gt;Disable creation of classic release pipelines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flx2myih6imoufyjejp8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flx2myih6imoufyjejp8b.png" alt="Disable creation of classic release pipelines" width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;strong&gt;Disable creation of classic release pipelines&lt;/strong&gt; switch sets to &lt;strong&gt;On&lt;/strong&gt; hides classic release pipeline creation options such as the &lt;strong&gt;Release&lt;/strong&gt; menu in the &lt;strong&gt;Pipeline&lt;/strong&gt; section of DevOps projects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;17.In &lt;strong&gt;Organization Settings&lt;/strong&gt;, go to section &lt;strong&gt;Security&lt;/strong&gt; and click &lt;strong&gt;Policies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;18.Toggle the switch to &lt;strong&gt;On&lt;/strong&gt; for &lt;strong&gt;Allow public projects&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Filn7fmshz4b85oxqyyse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Filn7fmshz4b85oxqyyse.png" alt="policies" width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Extensions used in some labs might require a public project to allow using the free version.&lt;br&gt;
That option no longer exists.&lt;br&gt;
Instructions to create and configure the Azure DevOps project (you only have to do this once)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: make sure you completed the steps to create your Azure DevOps Organization before continuing with these steps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To follow all lab instructions, you’ll need set up a new Azure DevOps project, create a repository that’s based on the eShopOnWeb application, and create a service connection to your Azure subscription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the team project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, you’ll create an &lt;strong&gt;eShopOnWeb&lt;/strong&gt; Azure DevOps project to be used by several labs.&lt;/p&gt;

&lt;p&gt;1.Open your browser and navigate to your Azure DevOps organization.&lt;/p&gt;

&lt;p&gt;2.Select the &lt;strong&gt;New Project&lt;/strong&gt; option and use the following settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: &lt;strong&gt;eShopOnWeb&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;visibility: &lt;strong&gt;Private&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Advanced: Version Control: &lt;strong&gt;Git&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Advanced: Work Item Process: &lt;strong&gt;Scrum&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3.Select &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv1yvueowec5e67wge2i5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv1yvueowec5e67wge2i5.png" alt="new project" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68icwunu5m4pyozsld2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68icwunu5m4pyozsld2v.png" alt="create" width="743" height="908"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Import eShopOnWeb git repository&lt;/strong&gt;&lt;br&gt;
Now, you’ll import the eShopOnWeb into your git repository.&lt;/p&gt;

&lt;p&gt;1.Open your browser and navigate to your Azure DevOps organization.&lt;/p&gt;

&lt;p&gt;2.Open the previously created &lt;strong&gt;eShopOnWeb&lt;/strong&gt; project.&lt;/p&gt;

&lt;p&gt;3.Select the &lt;strong&gt;Repos &amp;gt; Files, Import a Repository&lt;/strong&gt; and then select &lt;strong&gt;Import&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyq7a80v7h5z209x9rq27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyq7a80v7h5z209x9rq27.png" alt="Import Repository" width="799" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4.On the &lt;strong&gt;Import a Git Repository&lt;/strong&gt; window, paste the following URL &lt;code&gt;https://github.com/MicrosoftLearning/eShopOnWeb&lt;/code&gt; and select &lt;strong&gt;Import&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6rm7g3hxyr8erpl4fco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6rm7g3hxyr8erpl4fco.png" alt="Import" width="578" height="921"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5.The repository is organized the following way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.ado&lt;/strong&gt; folder contains Azure DevOps YAML pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.devcontainer&lt;/strong&gt; folder container setup to develop using containers (either locally in VS Code or GitHub Codespaces).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.azure&lt;/strong&gt; folder contains Bicep &amp;amp; ARM infrastructure as code templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.github&lt;/strong&gt; folder container YAML GitHub workflow definitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;src&lt;/strong&gt; folder contains the .NET 8 website used on the lab scenarios.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;6.Leave the web browser window open.&lt;/p&gt;

&lt;p&gt;7.Go to &lt;strong&gt;Repos &amp;gt; Branches&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;8.Hover on the &lt;strong&gt;main&lt;/strong&gt; branch then click the ellipsis on the right of the column.&lt;/p&gt;

&lt;p&gt;9.Click on &lt;strong&gt;Set as default branch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flod9tg0d91lbez4fd19k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flod9tg0d91lbez4fd19k.png" alt="Set as default branch" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmstu3bdy7xi4xz9spwn3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmstu3bdy7xi4xz9spwn3.png" alt="main now default" width="799" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a service connection to access Azure resources&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, you will create a service connection in Azure DevOps which will allow you to deploy and access resources in your Azure subscription.&lt;/p&gt;

&lt;p&gt;1.Start a web browser, navigate to the Azure DevOps portal with the &lt;strong&gt;eShopOnWeb&lt;/strong&gt; project open and select &lt;strong&gt;Project settings&lt;/strong&gt; in the bottom left corner of the portal.&lt;/p&gt;

&lt;p&gt;2.Select the &lt;strong&gt;Service connections&lt;/strong&gt; under Pipelines, and then select &lt;strong&gt;Create service connection&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx1xt5w4qsx786efftqe5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx1xt5w4qsx786efftqe5.png" alt="Service connections" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3.On the &lt;strong&gt;New service connection&lt;/strong&gt; blade, select &lt;strong&gt;Azure Resource Manager&lt;/strong&gt; and &lt;strong&gt;Next&lt;/strong&gt; (may need to scroll down).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxftd1708sk5xj1i7s6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxftd1708sk5xj1i7s6c.png" alt="New service connection" width="610" height="914"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4.Select &lt;strong&gt;Workload Identity federation (automatic)&lt;/strong&gt; and &lt;strong&gt;Next&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You can also use &lt;strong&gt;Workload identity federation (manual)&lt;/strong&gt; if you prefer to manually configure the service connection. Follow the steps in the &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/library/connect-to-azure?view=azure-devops" rel="noopener noreferrer"&gt;Azure DevOps documentation&lt;/a&gt; to create the service connection manually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;5.Fill in the empty fields using the information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subscription&lt;/strong&gt;: Select your Azure subscription. Then sign in to your azure account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff77tccaj8l1bug8qekn9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff77tccaj8l1bug8qekn9.png" alt="Subscription" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource group&lt;/strong&gt;: Select the resource group where you want to deploy resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service connection name&lt;/strong&gt;: Type &lt;code&gt;azure subs&lt;/code&gt;. This name will be referenced in YAML pipelines to access your Azure subscription.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;6.Make sure the &lt;strong&gt;Grant access permission to all pipelines&lt;/strong&gt; option is unchecked and select &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjge07d59rz6yvqrxc9rt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjge07d59rz6yvqrxc9rt.png" alt="save" width="566" height="914"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;strong&gt;Grant access permission to all pipelines&lt;/strong&gt; option is not recommended for production environments. It is only used in this lab to simplify the configuration of the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you see an error message indicating you don’t have the necessary permissions to create a service connection, try again, or configure the service connection manually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You have now completed the necessary prerequisite steps to continue with the labs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Instructions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Exercise 1: Configure a secure project structure
&lt;/h3&gt;

&lt;p&gt;In this exercise, you will configure a secure project structure by creating a new project and assigning it project permissions. Separating responsibilities and resources into different projects or repositories with specific permissions supports security.&lt;/p&gt;
&lt;h3&gt;
  
  
  Task 1: Create a new team project
&lt;/h3&gt;

&lt;p&gt;1.Navigate to the Azure DevOps portal at &lt;code&gt;https://aex.dev.azure.com&lt;/code&gt; and open your organization.&lt;/p&gt;

&lt;p&gt;2.Open your &lt;strong&gt;organization settings&lt;/strong&gt; at the bottom left corner of the portal and then &lt;strong&gt;Projects&lt;/strong&gt; under the General section.&lt;/p&gt;

&lt;p&gt;3.Select the &lt;strong&gt;New Project&lt;/strong&gt; option and use the following settings:&lt;br&gt;
        - name: &lt;strong&gt;eShopSecurity&lt;/strong&gt;&lt;br&gt;
        - visibility: &lt;strong&gt;Private&lt;/strong&gt;&lt;br&gt;
        - Advanced: Version Control: &lt;strong&gt;Git&lt;/strong&gt;&lt;br&gt;
        - Advanced: Work Item Process: &lt;strong&gt;Scrum&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;4.Select &lt;strong&gt;Create&lt;/strong&gt; to create the new project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqg52qo8mo6ujmmk96bbp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqg52qo8mo6ujmmk96bbp.png" alt="eShopSecurity Project" width="761" height="914"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5.You can now switch between the different projects by clicking on the Azure DevOps icon in the upper left corner of the Azure DevOps portal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffpjulxjv9dsqrymbvtu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffpjulxjv9dsqrymbvtu5.png" alt="both projects" width="799" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can manage permissions and settings for each project separately by going to the Project settings menu and selecting the appropriate team project. If you have multiple users or teams working on different projects, you can also assign permissions to each project separately.&lt;/p&gt;
&lt;h3&gt;
  
  
  Task 2: Create a new repository and assign project permissions
&lt;/h3&gt;

&lt;p&gt;1.Select the organization name in the upper left corner of the Azure DevOps portal and select the new &lt;strong&gt;eShopSecurity&lt;/strong&gt; project.&lt;/p&gt;

&lt;p&gt;2.Select the &lt;strong&gt;Repos&lt;/strong&gt; menu.&lt;/p&gt;

&lt;p&gt;3.Select the &lt;strong&gt;Initialize&lt;/strong&gt; button to initialize the new repository by adding the README.md file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcw0b65javtudm3xyotcr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcw0b65javtudm3xyotcr.png" alt="Initialize" width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4.Open the &lt;strong&gt;Project settings&lt;/strong&gt; menu in the lower left corner of the portal and select &lt;strong&gt;Repositories&lt;/strong&gt; under the Repos section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5e1pj4z37laiq17fbwi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5e1pj4z37laiq17fbwi.png" alt="Repositories" width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5.Select the new &lt;strong&gt;eShopSecurity&lt;/strong&gt; repository and select the &lt;strong&gt;Security&lt;/strong&gt; tab.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Ensure that you select the Security tab in the specific repository only, and not for all repositories in the project. If you select all repositories, you may lose access to other repositories in the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;6.Remove the Inherit permissions from parent by unchecking the &lt;strong&gt;Inheritance&lt;/strong&gt; toggle button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdgb12aqne9rc9yq77is.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdgb12aqne9rc9yq77is.png" alt="Inheritance" width="799" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;7.Select the &lt;strong&gt;Contributors&lt;/strong&gt; group and select the &lt;strong&gt;Deny&lt;/strong&gt; dropdown for all permissions except &lt;strong&gt;Manage permissions&lt;/strong&gt; and &lt;strong&gt;Read&lt;/strong&gt;. This will prevent all users from the Contributors group from accessing the repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fazmbo0zhvshtkdg2sc8j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fazmbo0zhvshtkdg2sc8j.png" alt="Contributors" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: In a real world scenario, you will deny the manage permissions to the Contributors group as well. For this lab, we are allowing the Contributors group to manage permissions to allow you to complete the lab.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;8.Select your user under Users and select the &lt;strong&gt;Allow&lt;/strong&gt; button to allow all permissions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you don’t see your name in the &lt;strong&gt;Users&lt;/strong&gt; section, enter your name in the &lt;strong&gt;Search for users or groups&lt;/strong&gt; text box and select it in the list of results.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flht7uvp8m30f7os2fq8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flht7uvp8m30f7os2fq8x.png" alt="Permissions" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;9.Your changes will be saved automatically.&lt;/p&gt;

&lt;p&gt;Now only the user you assigned permissions and the administrators can access the repository. This is useful when you want to allow specific users to access the repository and run pipelines from the eShopOnWeb project.&lt;/p&gt;
&lt;h3&gt;
  
  
  Exercise 2: Configure a pipeline and template structure to support secure pipelines
&lt;/h3&gt;
&lt;h3&gt;
  
  
  Task 1: Import and run the CI pipeline
&lt;/h3&gt;

&lt;p&gt;Let’s start by importing the CI pipeline named &lt;a href="https://github.com/MicrosoftLearning/eShopOnWeb/blob/main/.ado/eshoponweb-ci.yml" rel="noopener noreferrer"&gt;eshoponweb-ci.yml&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;1.Navigate to the Azure DevOps portal at &lt;code&gt;https://aex.dev.azure.com&lt;/code&gt; and open your organization.&lt;/p&gt;

&lt;p&gt;2.Open the &lt;strong&gt;eShopOnWeb&lt;/strong&gt; project in Azure DevOps.&lt;/p&gt;

&lt;p&gt;3.Go to &lt;strong&gt;Pipelines &amp;gt; Pipelines&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;4.Select the &lt;strong&gt;Create Pipeline&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fojw3lv2behycjtcrl0s1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fojw3lv2behycjtcrl0s1.png" alt="Create Pipeline" width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5.Select &lt;strong&gt;Azure Repos Git (Yaml)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmh6kqgl34zhwk8lp9ft0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmh6kqgl34zhwk8lp9ft0.png" alt="Azure Repos Git" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;6.Select the &lt;strong&gt;eShopOnWeb&lt;/strong&gt; repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmxg98yln6b4qyuec8s76.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmxg98yln6b4qyuec8s76.png" alt="eShopOnWeb" width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;7.Select &lt;strong&gt;Existing Azure Pipelines YAML File&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1i5bqwdnz404echc2mow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1i5bqwdnz404echc2mow.png" alt="Existing Azure Pipelines YAML File" width="800" height="672"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;8.Select the &lt;strong&gt;/.ado/eshoponweb-ci.yml&lt;/strong&gt; file then click on &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ho0zsyfcob1erc2bp82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ho0zsyfcob1erc2bp82.png" alt="select the file" width="561" height="916"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;9.Select the &lt;strong&gt;Run&lt;/strong&gt; button to run the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1u6lu3jnevjn18qtvznd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1u6lu3jnevjn18qtvznd.png" alt="Run" width="799" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Your pipeline will take a name based on the project name. You will rename it to easier identify the pipeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;10.Go to &lt;strong&gt;Pipelines &amp;gt; Pipelines&lt;/strong&gt; and select the recently created pipeline. Select the ellipsis and then select &lt;strong&gt;Rename/move&lt;/strong&gt; option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ogt389kmlr251squ1hc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ogt389kmlr251squ1hc.png" alt="Rename" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;11.Name it &lt;strong&gt;eshoponweb-ci&lt;/strong&gt; and select &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ktgqeytsv66v6szso4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ktgqeytsv66v6szso4a.png" alt="save" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Task 2: Import and run the CD pipeline
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: In this task, you will import and run the CD pipeline named &lt;a href="https://github.com/MicrosoftLearning/eShopOnWeb/blob/main/.ado/eshoponweb-cd-webapp-code.yml" rel="noopener noreferrer"&gt;eshoponweb-cd-webapp-code.yml&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;1.Go to &lt;strong&gt;Pipelines &amp;gt; Pipelines&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;2.Select &lt;strong&gt;New pipeline&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcz46zffh7la4s7pndmo6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcz46zffh7la4s7pndmo6.png" alt="New pipeline" width="799" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3.Select &lt;strong&gt;Azure Repos Git (Yaml)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fewki7aqde82gbz1d8s38.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fewki7aqde82gbz1d8s38.png" alt="Azure Repos Git" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4.Select the &lt;strong&gt;eShopOnWeb&lt;/strong&gt; repository.&lt;/p&gt;

&lt;p&gt;5.Select &lt;strong&gt;Existing Azure Pipelines YAML File&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;6.Select the &lt;strong&gt;/.ado/eshoponweb-cd-webapp-code.yml&lt;/strong&gt; file then select &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gjcneubatsptri0uwjc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gjcneubatsptri0uwjc.png" alt="Continue" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;7.In the YAML pipeline definition, set the variables section to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;resource-group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUR-RESOURCE-GROUP-NAME'&lt;/span&gt;
 &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;centralus'&lt;/span&gt;
 &lt;span class="na"&gt;templateFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;infra/webapp.bicep'&lt;/span&gt;
 &lt;span class="na"&gt;subscriptionid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUR-SUBSCRIPTION-ID'&lt;/span&gt;
 &lt;span class="na"&gt;azureserviceconnection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUR-AZURE-SERVICE-CONNECTION-NAME'&lt;/span&gt;
 &lt;span class="na"&gt;webappname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUR-WEB-APP-NAME'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8.Replace the values of the variables with the values of your environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;strong&gt;YOUR-RESOURCE-GROUP-NAME&lt;/strong&gt; with the name of the resource group you want to use in this lab, for example, &lt;strong&gt;rg-eshoponweb-secure&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the value of the &lt;strong&gt;location&lt;/strong&gt; variable to the name of the Azure region you want to deploy your resources, for example, &lt;strong&gt;centralus&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Replace &lt;strong&gt;YOUR-SUBSCRIPTION-ID&lt;/strong&gt; with your Azure subscription id.&lt;/li&gt;
&lt;li&gt;Replace &lt;strong&gt;YOUR-AZURE-SERVICE-CONNECTION-NAME&lt;/strong&gt; with &lt;strong&gt;azure subs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Replace &lt;strong&gt;YOUR-WEB-APP-NAME&lt;/strong&gt; with a globally unique name of the web app to be deployed, for example, the string &lt;strong&gt;eshoponweb-lab-multi-123456&lt;/strong&gt; followed by a random six-digit number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;9.Select &lt;strong&gt;Save and Run&lt;/strong&gt; and choose to commit directly to the main branch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhbqwwj9fvt3jfw16zv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhbqwwj9fvt3jfw16zv2.png" alt="Save and Run" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;10.Select &lt;strong&gt;Save and Run&lt;/strong&gt; again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy1849d67tv7xy3ovnigi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy1849d67tv7xy3ovnigi.png" alt="Save and Run" width="676" height="909"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;11.Open the pipeline run. If you receive the message “This pipeline needs permission to access a resource before this run can continue to Deploy to WebApp”, select &lt;strong&gt;View, Permit&lt;/strong&gt; and &lt;strong&gt;Permit&lt;/strong&gt; again. This is needed to allow the pipeline to create the Azure App Service resource.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fixde7lggka6feiy0c6rc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fixde7lggka6feiy0c6rc.png" alt="Permission needed" width="799" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7nfuvzta70zg3fajv8jy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7nfuvzta70zg3fajv8jy.png" alt="Permit" width="561" height="907"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;12.The deployment may take a few minutes to complete, wait for the pipeline to execute. The pipeline is triggered following the completion of the CI pipeline and it includes the following tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AzureResourceManagerTemplateDeployment&lt;/strong&gt;: Deploys the Azure App Service web app using bicep template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AzureRmWebAppDeployment&lt;/strong&gt;: Publishes the Web site to the Azure App Service web app.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: In case the deployment fails, navigate to the pipeline run page and select &lt;strong&gt;Rerun failed jobs&lt;/strong&gt; to invoke another pipeline run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Your pipeline will take a name based on the project name. Let’s &lt;strong&gt;rename&lt;/strong&gt; it for identifying the pipeline better.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;13.Go to &lt;strong&gt;Pipelines &amp;gt; Pipelines&lt;/strong&gt; and select the recently created pipeline. Select the ellipsis and then select &lt;strong&gt;Rename/move&lt;/strong&gt; option.&lt;/p&gt;

&lt;p&gt;14.Name it &lt;strong&gt;eshoponweb-cd-webapp-code&lt;/strong&gt; and click on &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now you should have two pipelines running in your eShopOnWeb project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8zl36otc5yggz9pizae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8zl36otc5yggz9pizae.png" alt="cd pipeline" width="799" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7dqj3beykyacsv0u1kx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7dqj3beykyacsv0u1kx.png" alt="failed pipeline" width="799" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After troubleshooting, the pipeline finally succeeded. &lt;br&gt;
&lt;strong&gt;NOTE: Check the Troubleshooting section below to see how I resolved the errors.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbatj2o5xbcqdwxly3wd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbatj2o5xbcqdwxly3wd.png" alt="failed pipelines" width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fykp0w2fl69ly5lzk94uz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fykp0w2fl69ly5lzk94uz.png" alt="succeeded pipelines" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Task 3: Move the CD pipeline variables to a YAML template
&lt;/h3&gt;

&lt;p&gt;In this task, you will create a YAML template to store the variables used in the CD pipeline. This will allow you to reuse the template in other pipelines.&lt;/p&gt;

&lt;p&gt;1.Go to &lt;strong&gt;Repos&lt;/strong&gt; and then &lt;strong&gt;Files&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;2.Expand the &lt;strong&gt;.ado&lt;/strong&gt; folder and select &lt;strong&gt;New file&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk3cn2izihy0ljsrx3hb5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk3cn2izihy0ljsrx3hb5.png" alt="new file" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3.Name the file &lt;strong&gt;eshoponweb-secure-variables.yml&lt;/strong&gt; and select &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F32ctq71vjo5e8afdajeg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F32ctq71vjo5e8afdajeg.png" alt="create" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4.Add the variables section used in the CD pipeline to the new file. The file should look like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;resource-group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rg-eshoponweb-secure'&lt;/span&gt;
  &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;southcentralus'&lt;/span&gt; &lt;span class="c1"&gt;#the name of the Azure region you want to deploy your resources&lt;/span&gt;
  &lt;span class="na"&gt;templateFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;infra/webapp.bicep'&lt;/span&gt;
  &lt;span class="na"&gt;subscriptionid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUR-SUBSCRIPTION-ID'&lt;/span&gt;
  &lt;span class="na"&gt;azureserviceconnection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;azure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;subs'&lt;/span&gt; &lt;span class="c1"&gt;#the name of the service connection to your Azure subscription&lt;/span&gt;
  &lt;span class="na"&gt;webappname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eshoponweb-lab-secure-XXXXXX'&lt;/span&gt; &lt;span class="c1"&gt;#the globally unique name of the web app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Replace the values of the variables with the values of your environment (resource group, location, subscription ID, Azure service connection, and web app name).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1yu6p1y94wsddr0prki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1yu6p1y94wsddr0prki.png" alt="Replace the values of the variables" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5.Select &lt;strong&gt;Commit&lt;/strong&gt;, in the commit comment text box, enter &lt;code&gt;[skip ci]&lt;/code&gt;, and then select &lt;strong&gt;Commit&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: By adding the &lt;code&gt;[skip ci]&lt;/code&gt; comment to the commit, you will prevent automatic pipeline execution, which, at this point, runs by default following every change to the repo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4ag19ym3qdjamqlgof6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4ag19ym3qdjamqlgof6.png" alt="skip ci" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;6.From the list of files in the repo, open the &lt;strong&gt;eshoponweb-cd-webapp-code.yml&lt;/strong&gt; pipeline definition, and replace the variables section with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eshoponweb-secure-variables.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypixiba3541dxhg8l51v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypixiba3541dxhg8l51v.png" alt="replace variables" width="799" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;7.Select &lt;strong&gt;Commit&lt;/strong&gt;, accept the default comment, and then select &lt;strong&gt;Commit&lt;/strong&gt; to run the pipeline again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcshm3550sl2gasbrk658.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcshm3550sl2gasbrk658.png" alt="commit" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;8.Verify that the pipeline run completed successfully.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flnbwysg7stxqctayyzcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flnbwysg7stxqctayyzcc.png" alt="succeeded pipeline" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you have a YAML template with the variables used in the CD pipeline. You can reuse this template in other pipelines in scenarios where you need to deploy the same resources. Also, your operations team can control the resource group and location where the resources are deployed and other information in your template values and you don’t need to make any changes to your pipeline definition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Task 4: Move the YAML templates to a separate repository and project
&lt;/h3&gt;

&lt;p&gt;In this task, you will move the YAML templates to a separate repository and project.&lt;/p&gt;

&lt;p&gt;1.In your eShopSecurity project, go to &lt;strong&gt;Repos &amp;gt; Files.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;2.Create a new file named &lt;strong&gt;eshoponweb-secure-variables.yml&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i5a47ejzsjf24nn7cb9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i5a47ejzsjf24nn7cb9.png" alt="create a new file" width="799" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6121pk16h01lhsvcbqtd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6121pk16h01lhsvcbqtd.png" alt="create" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3.Copy the content of the file &lt;strong&gt;.ado/eshoponweb-secure-variables.yml&lt;/strong&gt; from the eShopOnWeb repository to the new file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72u2eoobwpd4bvuhdfly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72u2eoobwpd4bvuhdfly.png" alt="copy contents of the other file" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4.Commit the changes.&lt;/p&gt;

&lt;p&gt;5.Open the &lt;strong&gt;eshoponweb-cd-webapp-code.yml&lt;/strong&gt; pipeline definition in the eShopOnWeb repo.&lt;/p&gt;

&lt;p&gt;6.Add the following to the resources section before the variables section in the pipeline definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="na"&gt;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eShopSecurity&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eShopSecurity/eShopSecurity&lt;/span&gt; &lt;span class="c1"&gt;#name of the project and repository&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7.Replace the variables section with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eshoponweb-secure-variables.yml@eShopSecurity&lt;/span&gt; &lt;span class="c1"&gt;#name of the template and repository&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgitzxgf0mn2n2kzdlddr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgitzxgf0mn2n2kzdlddr.png" alt="pipeline definition with the new variables and resource sections" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;8.Select &lt;strong&gt;Commit&lt;/strong&gt;, accept the default comment, and then select &lt;strong&gt;Commit&lt;/strong&gt; to run the pipeline again.&lt;/p&gt;

&lt;p&gt;9.Navigate to the pipeline run and verify that the pipeline is using the YAML file from the eShopSecurity repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2pegdefilhq03qyzv7bk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2pegdefilhq03qyzv7bk.png" alt="pipeline execution using the YAML template from the eShopSecurity repository" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3608rurdfroo9plfjya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3608rurdfroo9plfjya.png" alt="repositories" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you have the YAML file in a separate repository and project. You can reuse this file in other pipelines in scenarios where you need to deploy the same resources. Also, your operations team can control the resource group, location, security and where the resources are deployed and other information by modifying values in the YAML file and you don’t need to make any changes to your pipeline definition.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT] Remember to delete the resources created in the Azure portal to avoid unnecessary charges.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3xowwu5onrp1n35s59rz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3xowwu5onrp1n35s59rz.png" alt="azure portal" width="799" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Review
&lt;/h2&gt;

&lt;p&gt;In this lab, you learned how to configure and organize a secure project and repository structure in Azure DevOps. By managing permissions effectively, you can ensure that the right users have access to the resources they need while maintaining the security and integrity of your DevOps pipelines and processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting: Problems I Hit and How I Fixed Them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. “Could not find any file matching the template file pattern”
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Symptom&lt;/em&gt;: The “Deploy App Service Plan Bicep” step failed before reaching Azure.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cause&lt;/em&gt;: I set &lt;code&gt;templateFile: 'infra/webapp.bicep'&lt;/code&gt;. The task already builds the path as &lt;code&gt;$(Pipeline.Workspace)/eshoponweb-ci/Bicep/$(templateFile)&lt;/code&gt;, so it looked for &lt;code&gt;.../Bicep/infra/webapp.bicep&lt;/code&gt;, which doesn’t exist. The CI pipeline publishes the file in a Bicep folder in the artifact, not in infra.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fix&lt;/em&gt;: Set &lt;code&gt;templateFile: 'webapp.bicep'&lt;/code&gt;, with no folder prefix.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Quota error: &lt;code&gt;SubscriptionIsOverQuotaForSku&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Symptom&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Current Limit (S1 VMs): 0&lt;br&gt;
Amount required for this deployment (S1 VMs): 1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cause&lt;/em&gt;: The Bicep template defaults to the &lt;code&gt;S1&lt;/code&gt; SKU, and my subscription had a quota of 0 for it. This can happen even on pay-as-you-go subscriptions, because new subscriptions often start with no App Service quota for some tiers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fix attempt&lt;/em&gt;: I changed the SKU in &lt;code&gt;infra/webapp.bicep&lt;/code&gt; to the Free tier, &lt;code&gt;F1&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The change didn’t take effect, and F1 failed too
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Symptom&lt;/em&gt;: The next run still reported &lt;code&gt;S1 VMs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cause&lt;/em&gt;: The CD pipeline deploys the copy of the Bicep file inside the &lt;code&gt;eshoponweb-ci&lt;/code&gt; build artifact, not the file in the repo. My edit only took effect after I re-ran eshoponweb-ci. After that, the error changed to &lt;code&gt;Current Limit (F1 VMs): 0&lt;/code&gt;, so the subscription had no Free-tier quota in that region either.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fix&lt;/em&gt;: Quota is per region, so I created a new resource group in West Europe and updated the &lt;code&gt;resource-group&lt;/code&gt; and &lt;code&gt;location&lt;/code&gt; variables.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. 403 when checking the new resource group
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Symptom&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Checking if the following resource group exists: RG.&lt;br&gt;
Failed to check the resource group status. Error: {"statusCode":403}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cause&lt;/em&gt;: The Azure DevOps service connection signs in as a service principal whose role assignment only covered my original resource group.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fix&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;In Azure DevOps, &lt;strong&gt;Project settings → Service connections → Manage App registration&lt;/strong&gt; gave me the service principal’s display name.&lt;br&gt;
In the Azure portal, on the new resource group: &lt;strong&gt;Access control (IAM) → Add → Add role assignment&lt;/strong&gt;.&lt;br&gt;
I picked the plain &lt;strong&gt;Contributor&lt;/strong&gt; role (not Web Plan Contributor or Website Contributor) and selected the service principal as the member. Check that the Role tab doesn’t say “None” before clicking Review + assign.&lt;br&gt;
After waiting a few minutes, the deployment succeeded.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. TF401027: You need the Git 'CreateBranch' permission
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Symptom&lt;/em&gt;: Clicking Initialize on the empty &lt;code&gt;eShopSecurity&lt;/code&gt; repo failed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cause&lt;/em&gt;: My account lacked the Create branch permission on that repository, even though I was the organization owner and in the Project Collection Administrators group. An explicit Deny or a repo-level permission issue overrides group membership.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fix&lt;/em&gt;: I checked the repo’s Security tab for a Deny, then created a fresh repo instead, because the creator of a repo gets full rights on it. I reused the name &lt;code&gt;eShopSecurity&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. Pipeline run “Informational run” with a YAML error
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Symptom&lt;/em&gt;: A run appeared with an error about YAML preventing Azure Pipelines from deciding whether to run, and clicking into it showed &lt;code&gt;No plan found for identifier&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cause&lt;/em&gt;: The YAML couldn’t be parsed, so the pipeline never started. There’s no execution plan to open or rerun.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fix&lt;/em&gt;: Don’t rerun that entry. Use &lt;strong&gt;Edit → ⋮ → Validate&lt;/strong&gt; on the pipeline to find the broken line, fix it, and start a new run.&lt;/p&gt;
&lt;h3&gt;
  
  
  7. The shared variables template
&lt;/h3&gt;

&lt;p&gt;The lab moves variables into a template in a separate repo and references it from the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eShopSecurity&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eShopSecurity/eShopSecurity&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eshoponweb-secure-variables.yml@eShopSecurity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Things to check&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;The alias after &lt;code&gt;@&lt;/code&gt; must match the &lt;code&gt;repository:&lt;/code&gt; name in &lt;code&gt;resources&lt;/code&gt;.&lt;br&gt;
Put a space before any &lt;code&gt;#&lt;/code&gt; comment, or YAML treats it as part of the value.&lt;br&gt;
The template file holds the variable definitions (&lt;code&gt;- name: / value:&lt;/code&gt;). It must not contain a &lt;code&gt;template:&lt;/code&gt; line pointing to itself.&lt;br&gt;
Because the pipeline no longer defines variables, the template must include every one it uses (&lt;code&gt;resource-group&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;templateFile&lt;/code&gt;, &lt;code&gt;subscriptionid&lt;/code&gt;, &lt;code&gt;azureserviceconnection&lt;/code&gt;, &lt;code&gt;webappname&lt;/code&gt;).&lt;br&gt;
On the first run, Azure DevOps may ask you to permit access to the other repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors resolved while trying to view the App in a browser
&lt;/h2&gt;

&lt;h3&gt;
  
  
  8. Git: “Authentication failed” and PATs
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Symptom&lt;/em&gt;: &lt;code&gt;git pull&lt;/code&gt; and &lt;code&gt;git push&lt;/code&gt; over HTTPS asked for a password and rejected it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cause&lt;/em&gt;: Azure Repos doesn’t accept your Microsoft account password. It needs a &lt;strong&gt;Personal Access Token&lt;/strong&gt; with &lt;strong&gt;Code → Read &amp;amp; write&lt;/strong&gt;, created for the right organization.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fix&lt;/em&gt;: I created a new PAT (&lt;strong&gt;User settings → Personal access tokens&lt;/strong&gt;), pasted it at the prompt (nothing shows as you type, and Shift+Insert or right-click pastes in WSL), and the push worked. I revoked the unused token afterwards.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8ik73dgmi32ab3wpeyb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8ik73dgmi32ab3wpeyb.png" alt="successful git push" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the push, I waited for the pipeline to run successfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Build warning NU1903 (known high severity vulnerability)
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Symptom&lt;/em&gt;: The CI run and &lt;code&gt;dotnet build&lt;/code&gt; both warned that &lt;code&gt;Microsoft.AspNetCore.Components.WebAssembly.Server&lt;/code&gt; 8.0.15 had a known high severity vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fix&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet list src/Web/Web.csproj package --outdated --highest-patch&lt;/code&gt; showed the newest 8.0.x patch (8.0.32).&lt;br&gt;
Because the repo uses central package management, I changed the version in &lt;code&gt;Directory.Packages.props&lt;/code&gt;.&lt;br&gt;
Rebuilding with &lt;code&gt;dotnet build eShopOnWeb.sln&lt;/code&gt; reported 0 warnings.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Running the app locally in WSL
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MSB1011: more than one project or solution file&lt;/code&gt;: the repo root has both a &lt;code&gt;.sln&lt;/code&gt; and a &lt;code&gt;.dcproj&lt;/code&gt;. Name the target: &lt;code&gt;dotnet build eShopOnWeb.sln&lt;/code&gt; and &lt;code&gt;dotnet run --project src/Web&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;LocalDB is not supported on this platform&lt;/code&gt;: LocalDB is Windows-only. Run with the in-memory database: &lt;code&gt;UseOnlyInMemoryDatabase=true dotnet run --project src/Web&lt;/code&gt;, then open &lt;code&gt;http://localhost:5001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqddkce8uva6carhcxv6y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqddkce8uva6carhcxv6y.png" alt="eShopOnWeb in a browser" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;p&gt;A quota error is per SKU and per region, so changing either can clear it.&lt;br&gt;
The CD pipeline deploys the &lt;strong&gt;CI artifact&lt;/strong&gt;, so Bicep changes only apply after CI runs again.&lt;br&gt;
A new resource group needs its own role assignment for the service connection’s service principal.&lt;br&gt;
A failed YAML parse produces an informational run, so use &lt;strong&gt;Validate&lt;/strong&gt; rather than rerunning it.&lt;br&gt;
Azure Repos over HTTPS needs a PAT, not your account password.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>azuredevops</category>
      <category>passwordsecurity</category>
      <category>identitymanagement</category>
    </item>
  </channel>
</rss>
