<?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: Great-Victor Anjorin</title>
    <description>The latest articles on DEV Community by Great-Victor Anjorin (@victorthegreat7).</description>
    <link>https://dev.to/victorthegreat7</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1710703%2F6c487d95-32b5-41c5-b6a9-4d39abcedc4b.jpeg</url>
      <title>DEV Community: Great-Victor Anjorin</title>
      <link>https://dev.to/victorthegreat7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/victorthegreat7"/>
    <language>en</language>
    <item>
      <title>How Blocking Port 80 Made My Time API Project More Secure (And More Annoying)</title>
      <dc:creator>Great-Victor Anjorin</dc:creator>
      <pubDate>Tue, 30 Sep 2025 16:39:12 +0000</pubDate>
      <link>https://dev.to/victorthegreat7/how-blocking-port-80-made-my-time-api-project-more-secure-and-more-annoying-4a8e</link>
      <guid>https://dev.to/victorthegreat7/how-blocking-port-80-made-my-time-api-project-more-secure-and-more-annoying-4a8e</guid>
      <description>&lt;p&gt;At some point, when the deadline for the assessment passed, it turned into a project for me. Trying to accomplish each of the tasks helped me to learn how to implement ideas and practices I only knew in theory.&lt;/p&gt;

&lt;p&gt;For this article, we'll talk about using a domain for my Time API project. My implementation can be found in the &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/tree/namecom_domain" rel="noopener noreferrer"&gt;namecom_domain&lt;/a&gt; branch of the project's repository. It was actually the main branch before the domain I was using expired. I wanted my API to be accessible from a simple URL.&lt;/p&gt;

&lt;p&gt;Before my access to the domain expired, I was able to get a working implementation. To grasp how I achieved that, you'll have to understand a couple of the decisions I made for the project first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ingress Routing and Network Security Decisions Shaping Version of the Project
&lt;/h2&gt;

&lt;p&gt;The first decision was to deploy an ingress controller to help with exposing the API, using a &lt;a href="https://registry.terraform.io/modules/terraform-iaac/nginx-controller/helm/latest" rel="noopener noreferrer"&gt;Terraform module&lt;/a&gt; that deploys an NGINX Ingress Controller Helm chart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"nginx-controller"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-iaac/nginx-controller/helm"&lt;/span&gt;
 &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;=2.3.0"&lt;/span&gt;

 &lt;span class="nx"&gt;create_namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
 &lt;span class="nx"&gt;namespace&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nginx-ingress"&lt;/span&gt;

 &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;azurerm_kubernetes_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_cluster&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What is an Ingress Controller?&lt;/strong&gt; It acts as a manager for all external traffic to microservices intended to be exposed outside the cluster. Its core function is to implement the routing rules defined in Ingress resources. &lt;/p&gt;

&lt;p&gt;It handles advanced networking tasks, such as distributing user requests across instances of an application (Layer 7 load balancing), securing and decrypting data connections between users and servers (SSL/TLS termination), and directing traffic to different services using separate unique URL paths on the same website domain (host/path-based routing).&lt;/p&gt;

&lt;p&gt;For my specific setup, I created an ingress Resource that defined the following routing rule: when external traffic arrives at the domain &lt;code&gt;api.mywonder.works&lt;/code&gt; and the path &lt;code&gt;/time&lt;/code&gt;, it should be routed to the API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes_ingress_v1"&lt;/span&gt; &lt;span class="s2"&gt;"time_api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"time-api-ingress"&lt;/span&gt;
   &lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"time-api"&lt;/span&gt;
   &lt;span class="nx"&gt;annotations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="s2"&gt;"cert-manager.io/cluster-issuer"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"certmanager"&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;ingress_class_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt;

   &lt;span class="nx"&gt;tls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;hosts&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api.mywonder.works"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
     &lt;span class="nx"&gt;secret_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"time-api-tls"&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.mywonder.works"&lt;/span&gt;
     &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/time"&lt;/span&gt;
         &lt;span class="nx"&gt;path_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Prefix"&lt;/span&gt;
         &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;kubernetes_service_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
             &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;kubernetes_service_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;
             &lt;span class="p"&gt;}&lt;/span&gt;
           &lt;span class="p"&gt;}&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="c1"&gt;# Added a Default rule (no host) because my domain expired and I need to use the public IP for now&lt;/span&gt;

   &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

       &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/time"&lt;/span&gt;
         &lt;span class="nx"&gt;path_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Prefix"&lt;/span&gt;
         &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;kubernetes_service_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
             &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;kubernetes_service_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;
             &lt;span class="p"&gt;}&lt;/span&gt;

           &lt;span class="p"&gt;}&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;

     &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kubernetes_service_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;time_sleep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wait_for_nginx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a domain, the default access method would have been directly through the IP address assigned by the Cloud Service Provider as an endpoint for ingress.&lt;/p&gt;

&lt;p&gt;My second decision was to restrict access to the cluster through Port 80 at the Virtual Private Cloud Network level by denying all inbound traffic except on Port 443 and from within the Virtual Network itself, using Network Security Group (NSG) security rules. This prevented unencrypted traffic to the API and minimized the overall attack surface of my setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"time_api_nsg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nsg-${azurerm_resource_group.time_api_rg.name}"&lt;/span&gt;
 &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
 &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;

 &lt;span class="nx"&gt;security_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow-https-access"&lt;/span&gt;
   &lt;span class="nx"&gt;priority&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
   &lt;span class="nx"&gt;direction&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
   &lt;span class="nx"&gt;access&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
   &lt;span class="nx"&gt;protocol&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Tcp"&lt;/span&gt;
   &lt;span class="nx"&gt;source_port_range&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"443"&lt;/span&gt;
   &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;security_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow-vnet-inbound"&lt;/span&gt;
   &lt;span class="nx"&gt;priority&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;102&lt;/span&gt;
   &lt;span class="nx"&gt;direction&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
   &lt;span class="nx"&gt;access&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
   &lt;span class="nx"&gt;protocol&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;source_port_range&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"VirtualNetwork"&lt;/span&gt;
   &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;

 &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;security_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"deny-all-inbound"&lt;/span&gt;
   &lt;span class="nx"&gt;priority&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;
   &lt;span class="nx"&gt;direction&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
   &lt;span class="nx"&gt;access&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deny"&lt;/span&gt;
   &lt;span class="nx"&gt;protocol&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;source_port_range&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
   &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decision alone made the project a little more complex—in other words, I made it much harder for myself.&lt;br&gt;
&lt;strong&gt;How hard?&lt;/strong&gt; Well, now I couldn't use the http-01 validation method to get an SSL/TLS Certificate from Let's Encrypt.&lt;br&gt;
For clarity, let me explain what an SSL certificate and a certificate authority is.&lt;/p&gt;

&lt;p&gt;An SSL/TLS Certificate authenticates that a user's browser is truly connecting to your API and not a malicious third party, and it enables the use of the HTTPS protocol — which encrypts all data transmitted between the user's browser and my API's host.&lt;/p&gt;

&lt;p&gt;It also reassures users that the connection is secure, since major browsers display a padlock icon in the address bar for websites that have a certificate from a trusted Certificate Authority like Let's Encrypt.&lt;/p&gt;

&lt;p&gt;A Certificate Authority is a trusted third party organisation that issues SSL/TLS Certificates. The different ways to get a certificate issued are called validation methods.&lt;/p&gt;
&lt;h2&gt;
  
  
  Validation Challenge
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;So why couldn’t I use the http-01 validation method?&lt;/strong&gt; Http-01 requires access to port 80 on the host server. Since I restricted that, I had to use a different validation method. I opted for dns-01 and that required a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A DNS Provider:&lt;/strong&gt; The DNS-01 method works by automatically adding a specific TXT record to a DNS Provider's records. To do this programmatically, I needed a DNS provider that Cert-Manager could interact with. I used Name.com since they also had an API that I could use to interact with their services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions to Modify DNS Records:&lt;/strong&gt; This is where the Name.com API came in. Cert-Manager needed the authority to create and delete the temporary DNS records required for the challenge. I had to securely store and provide these credentials for accessing the Name.com API to the cluster for the validation to work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Cert-Manager Webhook:&lt;/strong&gt; The dns-01 challenge is not natively supported for all DNS providers. To enable this functionality for my Name.com domain, I had to deploy a custom webhook—an external component that extends Cert-Manager to support my specific DNS provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thankfully, I didn't have to create the &lt;a href="https://github.com/imgrant/cert-manager-webhook-namecom" rel="noopener noreferrer"&gt;webhook&lt;/a&gt; by myself. The credit for that goes to someone named Ian Grant. There was a Helm chart available in a GitHub repository of theirs. There is always a risk of it not being maintained, and I intend to learn how to create my own, but it was a huge help.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation with Terraform
&lt;/h2&gt;

&lt;p&gt;Now that you understand my decisions a little, I can better explain my implementation.&lt;/p&gt;

&lt;p&gt;Since I am relying on Infrastructure as Code (IaC) with Terraform, I was able to define and automate the entire setup in a reproducible way. In the namecom_domain branch, the core infrastructure provisioning is separated from the application deployment — there's a dedicated &lt;code&gt;microservices/&lt;/code&gt; folder holding the deployment-specific configs like &lt;code&gt;deploy.tf&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This let me provision the AKS cluster, networking, and base add-ons first, then layer on the API deployment and cert management in a second Terraform apply step. I structured it this way to ensure the infrastructure was provisioned correctly before the application deployed—minimizing errors in the first workflow run.&lt;/p&gt;

&lt;p&gt;Helm, as a package manager for Kubernetes, made deploying Cert-Manager Controller straightforward. The Cert-Manager Controller manages the issuance and renewal of SSL/TLS certificates in the cluster. I used Helm to install the Cert-Manager Controller and a ClusterIssuer resource, as defined in the &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/namecom_domain/terraform/provision.tf" rel="noopener noreferrer"&gt;provision.tf&lt;/a&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"helm_release"&lt;/span&gt; &lt;span class="s2"&gt;"cert_manager"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager"&lt;/span&gt;
 &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://charts.jetstack.io"&lt;/span&gt;
 &lt;span class="nx"&gt;chart&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager"&lt;/span&gt;
 &lt;span class="nx"&gt;version&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"v1.5.4"&lt;/span&gt;
 &lt;span class="nx"&gt;create_namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
 &lt;span class="nx"&gt;namespace&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager"&lt;/span&gt;

 &lt;span class="nx"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"installCRDs"&lt;/span&gt;
   &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;

 &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nginx-controller&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"helm_release"&lt;/span&gt; &lt;span class="s2"&gt;"cert_manager_issuers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;chart&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager-issuers"&lt;/span&gt;
 &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager-issuers"&lt;/span&gt;
 &lt;span class="nx"&gt;version&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.3.0"&lt;/span&gt;
 &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://charts.adfinis.com"&lt;/span&gt;
 &lt;span class="nx"&gt;namespace&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager"&lt;/span&gt;

 &lt;span class="c1"&gt;# https://acme-staging-v02.api.letsencrypt.org/directory&lt;/span&gt;
 &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;

 clusterIssuers:
  - name: certmanager
    spec:
      acme:
        email: "greatvictor.anjorin@gmail.com"
        server: "https://acme-v02.api.letsencrypt.org/directory"
        privateKeySecretRef:
          name: certmanager
        solvers:
          - dns01:
              webhook:
                groupName: acme.name.com
                solverName: namedotcom
                config:
                  username: "${var.namecom_username}"
                  apitokensecret:
                    name: namedotcom-credentials
                    key: api-token               
&lt;/span&gt;&lt;span class="no"&gt;
EOT

&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

 &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;helm_release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cert_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kubernetes_secret_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namecom_api_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I configured the ClusterIssuer to use Let's Encrypt's ACME server. The issuer is cluster-wide—meaning it can issue certificates for any namespace. Key configs include setting the ACME server URL to the production endpoint (&lt;code&gt;https://acme-v02.api.letsencrypt.org/directory&lt;/code&gt;) and defining the dns-01 solver. &lt;/p&gt;

&lt;p&gt;For the solver, I pointed it to the custom Name.com webhook, which handles the actual Name.com API calls to add and remove TXT records during validation. I installed both the controller and the issuer via Helm release resources in the &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/namecom_domain/terraform/provision.tf" rel="noopener noreferrer"&gt;provision.tf&lt;/a&gt; file.&lt;/p&gt;

&lt;p&gt;As for the webhook, I deployed it as another Helm release in the same &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/namecom_domain/terraform/provision.tf" rel="noopener noreferrer"&gt;provision.tf&lt;/a&gt; file. In the GitHub Actions workflow, I used the &lt;code&gt;actions/checkout@v4&lt;/code&gt; action to clone &lt;a href="https://github.com/imgrant/cert-manager-webhook-namecom" rel="noopener noreferrer"&gt;Ian Grant's Name.com webhook GitHub repository&lt;/a&gt; into a local &lt;code&gt;webhook/&lt;/code&gt; directory. The Helm release then references the chart using a relative path (../webhook/deploy) to the locally cloned repository during the workflow run.&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="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;Checkout Name.com webhook Github repository&lt;/span&gt;
     &lt;span class="s"&gt;uses&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

     &lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="err"&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;imgrant/cert-manager-webhook-namecom&lt;/span&gt;
       &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webhook&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"helm_release"&lt;/span&gt; &lt;span class="s2"&gt;"namecom_webhook"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"namecom-webhook"&lt;/span&gt;
 &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../webhook/deploy"&lt;/span&gt;
 &lt;span class="nx"&gt;chart&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager-webhook-namecom"&lt;/span&gt;
 &lt;span class="nx"&gt;namespace&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager"&lt;/span&gt;

 &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;helm_release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cert_manager&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook runs in the cert-manager namespace and needs access to Name.com API credentials. I provided these securely by creating a Kubernetes Secret for the sensitive API token, populated with a Terraform variable that gets its value from &lt;code&gt;DOMAIN_API_TOKEN&lt;/code&gt; in GitHub Secrets during the Actions workflow.&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="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;Apply Terraform configuration&lt;/span&gt;
     &lt;span class="s"&gt;env&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;ARM_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_CLIENT_ID }}&lt;/span&gt;
       &lt;span class="na"&gt;ARM_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_CLIENT_SECRET }}&lt;/span&gt;
       &lt;span class="na"&gt;ARM_SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_SUBSCRIPTION_ID }}&lt;/span&gt;
       &lt;span class="na"&gt;ARM_TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_TENANT_ID }}&lt;/span&gt;

       &lt;span class="na"&gt;USER_OBJECT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.MY_USER_OBJECT_ID }}&lt;/span&gt;
       &lt;span class="na"&gt;USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOMAIN_API_USERNAME }}&lt;/span&gt;
       &lt;span class="na"&gt;TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOMAIN_API_TOKEN }}&lt;/span&gt;
     &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
       &lt;span class="s"&gt;cat &amp;lt;&amp;lt;EOF &amp;gt; terraform.tfvars.json&lt;/span&gt;
       &lt;span class="s"&gt;{&lt;/span&gt;
         &lt;span class="s"&gt;"my_user_object_id": "$USER_OBJECT_ID",&lt;/span&gt;
         &lt;span class="s"&gt;"namecom_username": "$USERNAME",&lt;/span&gt;
         &lt;span class="s"&gt;"namecom_token": "$TOKEN"&lt;/span&gt;
       &lt;span class="s"&gt;}&lt;/span&gt;

       &lt;span class="s"&gt;EOF&lt;/span&gt;
       &lt;span class="s"&gt;terraform apply --auto-approve&lt;/span&gt;
     &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes_secret_v1"&lt;/span&gt; &lt;span class="s2"&gt;"namecom_api_token"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

 &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"namedotcom-credentials"&lt;/span&gt;
   &lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cert-manager"&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;api-token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namecom_token&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Opaque"&lt;/span&gt;

&lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;helm_release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namecom_webhook&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the username, I took a slightly different approach. It is still stored in GitHub Secrets and passed to Terraform as a variable, but instead of creating a Kubernetes Secret, I injected it directly into the values block of the ClusterIssuer Helm release.&lt;/p&gt;

&lt;p&gt;This approach ensures the Cert-Manager Controller can authenticate with Name.com via the webhook, without exposing credentials in code.&lt;/p&gt;

&lt;p&gt;Once Cert-Manager and the webhook are in place, the magic happens in the Ingress resource defined in &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/namecom_domain/terraform/microservices/deploy.tf" rel="noopener noreferrer"&gt;microservices/deploy.tf&lt;/a&gt;. This file deploys the actual Time API as a Kubernetes Deployment and Service, then creates an Ingress to route traffic.&lt;/p&gt;

&lt;p&gt;The Ingress spec includes a host rule for &lt;code&gt;api.mywonder.works&lt;/code&gt;, routing the &lt;code&gt;/time&lt;/code&gt; path to the backend service on port 5000. To trigger automatic certificate issuance, the annotation &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/a23a59aaad87b209513f5961e0584c908991765f/terraform/microservices/deploy.tf#L124" rel="noopener noreferrer"&gt;cert-manager.io/cluster-issuer: certmanager&lt;/a&gt; ensures that Cert-Manager:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;watches the &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/a23a59aaad87b209513f5961e0584c908991765f/terraform/microservices/deploy.tf#L138" rel="noopener noreferrer"&gt;time-api Ingress resource&lt;/a&gt; for changes,&lt;/li&gt;
&lt;li&gt;requests certificates for hosts listed in the tls block (in our case, &lt;code&gt;api.mywonder.works&lt;/code&gt;) via the &lt;code&gt;certmanager&lt;/code&gt; ClusterIssuer using the challenge type specified in the ClusterIssuer resource (&lt;code&gt;dns-01&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;stores the issued certificate in the specified secret (&lt;code&gt;time-api-tls&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;and enables TLS termination at the ingress controller level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, when you apply this, Cert-Manager creates a Certificate resource, initiates the ACME challenge with Let's Encrypt, and uses the webhook to temporarily add a TXT record to your Name.com DNS zone (something like, in my case, &lt;code&gt;_acme-challenge.api.mywonder.works&lt;/code&gt;). Let's Encrypt verifies the record to confirm domain ownership, issues the certificate, and Cert-Manager stores it as a Secret mounted to the Ingress. Renewals happen automatically before the 90-day expiration, with the same process repeating seamlessly.&lt;/p&gt;

&lt;p&gt;This implementation is carefully scripted out in the GitHub Actions workflow defined in &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/namecom_domain/.github/workflows/build.yaml" rel="noopener noreferrer"&gt;build.yaml&lt;/a&gt;. The workflow first builds and pushes the Docker image of the Flask app to Docker Hub, then uses Terraform to provision the infrastructure. It populates a &lt;code&gt;terraform.tfvars.json&lt;/code&gt; file with GitHub Secrets (domain and Name.com credentials) that I created before triggering the workflow, to avoid hardcoding sensitive info.&lt;/p&gt;

&lt;p&gt;After the base infrastructure is up, it moves the &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/namecom_domain/terraform/microservices/deploy.tf" rel="noopener noreferrer"&gt;microservices/deploy.tf&lt;/a&gt; into the root and runs another &lt;code&gt;terraform apply&lt;/code&gt; to deploy the app and Ingress.&lt;/p&gt;

&lt;p&gt;Like I explained earlier, this staged approach ensures dependencies like the cluster and Cert-Manager are ready before certificate issuance kicks in.&lt;/p&gt;

&lt;p&gt;If you want to try this with your own Name.com domain, once deployed, you can hit &lt;code&gt;https://api.YOUR_DOMAIN_HERE/time&lt;/code&gt; (note the HTTPS) in your browser or via curl, and you'll get the current UTC time securely. If things go wrong—say, DNS propagation delays or webhook issues—you can troubleshoot with &lt;code&gt;kubectl get certificates&lt;/code&gt; to check certificate status, or dive into Cert-Manager logs with &lt;code&gt;kubectl logs -n cert-manager&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;This setup not only made my API accessible via a clean, branded URL but also enforced HTTPS-only access without manual cert management. It was a bit of extra work upfront due to the Port 80 restriction, but the automation pays off for scalability and security.&lt;/p&gt;

&lt;p&gt;If you're adapting this, remember to update details like the domain in the &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/a23a59aaad87b209513f5961e0584c908991765f/terraform/microservices/deploy.tf#L138" rel="noopener noreferrer"&gt;time-api Ingress resource&lt;/a&gt; and email in the &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/a23a59aaad87b209513f5961e0584c908991765f/terraform/provision.tf#L99" rel="noopener noreferrer"&gt;cluster issuer resource&lt;/a&gt;, and ensure your Name.com API token has the right permissions.&lt;/p&gt;

&lt;p&gt;The branch's &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/namecom_domain/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; has more on &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/a23a59aaad87b209513f5961e0584c908991765f/README.md?plain=1#L27" rel="noopener noreferrer"&gt;prerequisites&lt;/a&gt; and &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment/blob/a23a59aaad87b209513f5961e0584c908991765f/README.md?plain=1#L208" rel="noopener noreferrer"&gt;manual deployment&lt;/a&gt; if you want to tinker locally before going full CI/CD.&lt;/p&gt;

&lt;p&gt;Overall, I was forced to delve a little deeper into some cloud-native practices and how to make them happen, and I'm glad I pushed through the complexities—domain expirations aside!&lt;/p&gt;

</description>
      <category>networking</category>
      <category>cloud</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>Confessions of a DevOps Noob Who Tried to pick a K8S Service CIDR from an Azure Subnet 🤦‍♂️</title>
      <dc:creator>Great-Victor Anjorin</dc:creator>
      <pubDate>Sun, 10 Aug 2025 15:16:19 +0000</pubDate>
      <link>https://dev.to/victorthegreat7/confessions-of-a-devops-noob-who-tried-to-pick-a-k8s-service-cidr-from-an-azure-subnet-43i0</link>
      <guid>https://dev.to/victorthegreat7/confessions-of-a-devops-noob-who-tried-to-pick-a-k8s-service-cidr-from-an-azure-subnet-43i0</guid>
      <description>&lt;p&gt;Let's kickoff with the first issue I remember that made me feel very dumb - configuring VPC networking for my cluster.&lt;/p&gt;

&lt;p&gt;The assessment required that I setup "a NAT gateway to manage egress traffic from the cluster, VPC networking, subnets, and firewall rules for secure communication.&lt;/p&gt;

&lt;p&gt;Like I said in my little introduction to this article series, I didn't have to think about or do any of those for my final project at &lt;a href="https://altschoolafrica.com/" rel="noopener noreferrer"&gt;AltSchool&lt;/a&gt;. I let Azure Kubernetess Service (AKS) handle it by default. My knowledge on setting up address spaces was weak and I had no experience with NAT gateways, so I got to googling.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Rookie Mistake
&lt;/h2&gt;

&lt;p&gt;I came across this article on provisioning AKS and a NAT gateway with Terraform. Luckily, they also detailed their Vnet and Subnet configurations, so I tried to replicate it - which is a fancy way of saying I copied and pasted. But I am not that much of a degenerate; I tried to make it my own. Ironically, that where the problems started. I must have thought "why is bro not picking an address space for &lt;code&gt;service_cidr&lt;/code&gt; from the VNet/Subnet he provisioned?". As you may have already guessed, I f***ed around and found out. I kept getting errors at terraform apply that I didn't have the patience to decipher.&lt;/p&gt;

&lt;p&gt;Since I was basically vibe-coding with knowledge of VPC and cluster networking that was next to zero, it took me quite a while to get out of this hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a &lt;code&gt;service_cidr&lt;/code&gt; is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;service_cidr&lt;/code&gt; isn’t part of your physical network. It's a virtual IP space that Kubernetes manages on its own. It’s meant for internal cluster service discovery, separate from your VNet and subnets.&lt;/p&gt;

&lt;p&gt;The subnet (attached to your node pool) on the other hand is meant for the nodes (virtual machines) and pods (containers or groups of containers).&lt;/p&gt;

&lt;p&gt;To help drive the point home, let's say you have a VNet with an address space of &lt;code&gt;10.240.0.0/16&lt;/code&gt;, a subnet carved out for your cluster node pool at &lt;code&gt;10.240.0.0/22&lt;/code&gt; and a &lt;code&gt;service_cidr&lt;/code&gt; of &lt;code&gt;172.16.0.0/16&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
[VNet: 10.240.0.0/16]
└── [Cluster Subnet: 10.240.1.0/22]
├── Node 1 (10.240.1.10)
│   └── Deployment (3 Replicas) ──────────────────┐
│       ├── Pod (Replica 1) (10.240.1.11) ←┐    Kubernetes Service (svc) ←─ Other deployment/pods or ingress
│       ├── Pod (Replica 2) (10.240.1.12) ←┤←────┘
│       └── Pod (Replica 3) (10.240.1.13) ←┘
└── Node 2 (10.240.1.20)
└── Pod (10.240.1.21)  # Unrelated to the service/deployment above

[service_cidr: 172.16.0.0/16] (Virtual)
└── Service (svc) IP: 172.16.0.50   # Virtual IP for load balancing. Distributes traffic to pods with selector: app=my-app
    └── Load Balancer (kube-proxy)  # Traffic to the Virtual IP is dynamically routed to one available pod endpoint based on kube-proxy rules
        │                           # (iptables/nftables/IPVS) and pod readiness (e.g., round-robin, random, or least connections)
        └──→ Pod IPs: 10.240.1.11, 10.240.1.12, 10.240.1.13

Notes:

- Kubernetes Service (svc): A Kubernetes abstraction that allows pods to communicate with each other using a stable virtual IP address.
- service_cidr: A range of IP addresses used by Kubernetes to assign IPs to services.
- Kube-proxy: A component that manages network rules on nodes to route traffic to the correct pod IPs.
- Pod IP: The IP address assigned to a pod within the provided Cluster Subnet.

- Service (svc) IPs (e.g. 172.16.0.50) are virtual IPs that do not correspond to a physical device.
- The svc uses a selector (e.g., app=my-app) to dynamically target all pods matching the label.
- kube-proxy (running on each node) handles load balancing (e.g., round-robin, random) to distribute traffic to available pods.
- Pods can be on any node (Node 1 or Node 2) and are dynamically added/removed based on deployment scaling or failures.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a pod calls a service, kube-proxy steps in, maps the service IP (say, 172.16.0.50) to a pod IP (like &lt;code&gt;10.240.1.13&lt;/code&gt;), and routes the traffic over the real network. The &lt;code&gt;service_cidr&lt;/code&gt; is just an abstraction — it doesn’t “live” on the Azure infrastructure.&lt;/p&gt;

&lt;p&gt;If you're relatively new to Kubernetes, I know what you are thinking. Why not connect directly to a pod’s IP? Well, here are are few more things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pod IPs are ephemeral (can change at any time); Services provide a stable VIP/DNS.&lt;/li&gt;
&lt;li&gt;Services load balance across pod replicas and only route to the ones that are available.&lt;/li&gt;
&lt;li&gt;Zero-downtime rollouts are made possible by the abstraction; Upgrades and scaling can change backends without affecting access.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Code That Saved the Day
&lt;/h2&gt;

&lt;p&gt;After almost pulling my hair out, I finally got it right. Here’s the Terraform config that worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_network"&lt;/span&gt; &lt;span class="s2"&gt;"time_api_vnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vnet-${azurerm_resource_group.time_api_rg.name}"&lt;/span&gt;
  &lt;span class="nx"&gt;address_space&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.240.0.0/16"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"time_api_subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"subnet-${azurerm_resource_group.time_api_rg.name}"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;virtual_network_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;address_prefixes&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.240.0.0/22"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# .....NSG rules and association configuration would go here, but is omitted for brevity.&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_nat_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"time_api_nat_gateway"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"natgw-${azurerm_resource_group.time_api_rg.name}"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;sku_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard"&lt;/span&gt;
  &lt;span class="nx"&gt;idle_timeout_in_minutes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_public_ip"&lt;/span&gt; &lt;span class="s2"&gt;"time_api_public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public-ip-${azurerm_resource_group.time_api_rg.name}"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;allocation_method&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Static"&lt;/span&gt;
  &lt;span class="nx"&gt;sku&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subnet_nat_gateway_association"&lt;/span&gt; &lt;span class="s2"&gt;"time_api_natgw_subnet_association"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nat_gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_nat_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_nat_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_nat_gateway_public_ip_association"&lt;/span&gt; &lt;span class="s2"&gt;"time_api_natgw_public_ip_association"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nat_gateway_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_nat_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_nat_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;public_ip_address_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_public_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_public_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_kubernetes_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"time_api_cluster"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aks-${azurerm_resource_group.time_api_rg.name}-cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;dns_prefix&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dns-${azurerm_resource_group.time_api_rg.name}-cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;kubernetes_version&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azurerm_kubernetes_service_versions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_version&lt;/span&gt;
  &lt;span class="nx"&gt;node_resource_group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nrg-aks-${azurerm_resource_group.time_api_rg.name}-cluster"&lt;/span&gt;

  &lt;span class="nx"&gt;default_node_pool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;
    &lt;span class="nx"&gt;vm_size&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard_D2_v2"&lt;/span&gt;
    &lt;span class="nx"&gt;auto_scaling_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;max_count&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;min_count&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="nx"&gt;os_disk_size_gb&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"VirtualMachineScaleSets"&lt;/span&gt;
    &lt;span class="nx"&gt;vnet_subnet_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

    &lt;span class="c1"&gt;# ...other node pool configurations&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SystemAssigned"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;azure_active_directory_role_based_access_control&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azure_rbac_enabled&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;admin_group_object_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;azuread_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time_api_admins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;network_profile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;network_plugin&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"azure"&lt;/span&gt;
    &lt;span class="nx"&gt;network_policy&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"azure"&lt;/span&gt;
    &lt;span class="nx"&gt;load_balancer_sku&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"standard"&lt;/span&gt;
    &lt;span class="nx"&gt;dns_service_ip&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"172.16.0.10"&lt;/span&gt;
    &lt;span class="nx"&gt;service_cidr&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"172.16.0.0/16"&lt;/span&gt;
    &lt;span class="nx"&gt;outbound_type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"userAssignedNATGateway"&lt;/span&gt;
    &lt;span class="nx"&gt;nat_gateway_profile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;idle_timeout_in_minutes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# ....rest of the AKS cluster configuration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that &lt;code&gt;service_cidr&lt;/code&gt; set to &lt;code&gt;172.16.0.0/16&lt;/code&gt;? It’s safely outside the VNet’s &lt;code&gt;10.240.0.0/16&lt;/code&gt; range — no overlap, no drama.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Relevant VPC Networking Points
&lt;/h2&gt;

&lt;p&gt;Alright, so I survived the &lt;code&gt;service_cidr&lt;/code&gt; fiasco, but there were a few more networking nuggets that made me go, “Oh, that’s how it works!” Here’s the stuff I wish I knew before crashing and burning my way through AKS networking. These are the bits that tie the VNet, subnets, and that fancy NAT Gateway together, so you don’t end up in a similar hole like mine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Subnet Is the Real Deal for Nodes and Pods
&lt;/h3&gt;

&lt;p&gt;The subnet you define (like &lt;code&gt;10.240.0.0/22&lt;/code&gt; in my setup) is where the real network lives. It’s carved out of your VNet (e.g. &lt;code&gt;10.240.0.0/16&lt;/code&gt;) and assigned to your AKS node pool via vnet_subnet_id in the Terraform config. Nodes (VMs) and pods (containers or individual groups of containers) grab IPs from this range—like &lt;code&gt;10.240.1.10&lt;/code&gt; for a node or &lt;code&gt;10.240.1.11&lt;/code&gt; for a pod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not so Pro Tip&lt;/strong&gt;: make sure your subnet is big enough (e.g., /22 gives ~1,000 IPs) to handle all your nodes and pods. So that AKS doesn't start throwing “no IPs left” errors when scaling.&lt;br&gt;
Also, some Network Security Group (NSG) rules on that subnet can help to control traffic on the cloud level. Isolation and least privilege access is the name of the game.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;service_cidr&lt;/code&gt; Is Just Kubernetes Playing Pretend
&lt;/h3&gt;

&lt;p&gt;That &lt;code&gt;172.16.0.0/16&lt;/code&gt; &lt;code&gt;service_cidr&lt;/code&gt;? It’s not touching your Azure network — it’s like a figment of Kubernetes' imagination (you know, make-believe). It’s where service IPs (like &lt;code&gt;172.16.0.50&lt;/code&gt; in the diagram) and the DNS IP (&lt;code&gt;172.16.0.10&lt;/code&gt; for CoreDNS) live. When a pod or ingress pings a service IP, kube-proxy (the traffic cop) translates it to a real pod IP (like &lt;code&gt;10.240.1.11&lt;/code&gt;, &lt;code&gt;10.240.1.12&lt;/code&gt;, or &lt;code&gt;10.240.1.13&lt;/code&gt;) and sends it over the subnet. The diagram shows this load balancing in action — traffic hits the service IP, and kube-proxy picks a healthy pod to route to, using tricks like round-robin. No physical device has that &lt;code&gt;172.16.0.50&lt;/code&gt; IP; it’s all software magic.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pick Your &lt;code&gt;service_cidr&lt;/code&gt; Carefully (or Pay the Price)
&lt;/h3&gt;

&lt;p&gt;You can let AKS pick a &lt;code&gt;service_cidr&lt;/code&gt; (it defaults to &lt;code&gt;10.0.0.0/16&lt;/code&gt;), but if it overlaps with your VNet or peered networks, you’re in for a bad time. Trust me, I got my fair share of Terraform errors before I learned this. Stick to RFC 1918 private ranges (&lt;code&gt;10.0.0.0/8&lt;/code&gt;, &lt;code&gt;172.16.0.0/12&lt;/code&gt;, or &lt;code&gt;192.168.0.0/16&lt;/code&gt;) — Public IP ranges? Forget it. And whatever you choose, make sure it doesn't clash with your VNet, peered networks, or anything else in your setup. Here's a quick cheat sheet I wish I'd had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Your VNet Range   | Good `service_cidr` Options         |
|-------------------|-------------------------------------|
| `10.x.x.x/16`     | `172.16.0.0/16` or `192.168.0.0/16` |
| `172.16.x.x/12`   | `10.96.0.0/12` or `192.168.0.0/16`  |
| `192.168.x.x/16`  | `10.96.0.0/12` or `172.16.0.0/16`   |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh, and that &lt;code&gt;dns_service_ip&lt;/code&gt;? Avoid the first IP in your range (like &lt;code&gt;172.16.0.1&lt;/code&gt;) — Kubernetes reserves it for internals. As you must have noticed, I went with &lt;code&gt;.10&lt;/code&gt; myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Balancing Ties It All Together
&lt;/h3&gt;

&lt;p&gt;Back to that diagram: the Kubernetes Service (&lt;code&gt;172.16.0.50&lt;/code&gt;) is your VIP (Very Important IP) that load-balances across pods. Kube-proxy decides which pod gets the traffic based on readiness and rules (e.g., round-robin). This is why you don’t hardcode pod IPs — they’re temporary! The &lt;code&gt;service_cidr&lt;/code&gt; and kube-proxy make sure your app stays reachable, even if pods move or die. It’s like a traffic director who doesn’t care which car (pod) gets you there, as long as you arrive.&lt;/p&gt;

&lt;h3&gt;
  
  
  NAT Gateway: The Unsung Hero for Outbound Traffic
&lt;/h3&gt;

&lt;p&gt;Since I used &lt;code&gt;outbound_type = "userAssignedNATGateway"&lt;/code&gt;, my cluster’s outbound traffic (like pods hitting external APIs) goes through the NAT Gateway. The &lt;code&gt;idle_timeout_in_minutes = 4&lt;/code&gt; in my AKS config (and 10 in the &lt;code&gt;azurerm_nat_gateway&lt;/code&gt; resource — oops, my bad for the mismatch!) controls how long idle connections stay open. Four minutes is fine for most apps, but if you’re running something long-lived like streaming, you might want to bump it up to avoid dropped connections. Also, ensure your NAT Gateway is tied to your subnet (via &lt;code&gt;azurerm_subnet_nat_gateway_association&lt;/code&gt;) and has a public IP for internet access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;In a nutshell, plan your VNet and subnet sizes, keep your &lt;code&gt;service_cidr&lt;/code&gt; separate, and double-check for overlaps. AKS can auto-pick some settings, but explicit configs (like in my Terraform) save you from surprises. Oh, and don’t wing it like I did — read or, at least, use any appropriate AI tool of your choice to try to understand the relevant docs first!&lt;/p&gt;

&lt;p&gt;Here is the link to the project &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment" rel="noopener noreferrer"&gt;repo&lt;/a&gt; again if you want to try it out for yourself.&lt;/p&gt;

&lt;p&gt;See you on the next one.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>azure</category>
      <category>networking</category>
    </item>
    <item>
      <title>0 to K8s: How a Take-Home Assessment Took Over My Life 😭</title>
      <dc:creator>Great-Victor Anjorin</dc:creator>
      <pubDate>Tue, 05 Aug 2025 14:07:17 +0000</pubDate>
      <link>https://dev.to/victorthegreat7/0-to-k8s-how-a-take-home-assessment-took-over-my-life-2m0d</link>
      <guid>https://dev.to/victorthegreat7/0-to-k8s-how-a-take-home-assessment-took-over-my-life-2m0d</guid>
      <description>&lt;p&gt;It was September 2024 and I was in way over my head.&lt;/p&gt;

&lt;p&gt;I had just completed a DevOps Engineering program (offered by AltSchool Africa) with "moderate" confidence in the skills I had learned during the 12-month stint.&lt;/p&gt;

&lt;p&gt;I heard of a job opening and thought "why not" and applied. I was given a take-home assessment that looked very similar to my final project at AltSchool. I was so excited. I thought I would crush it and move on to the interview stage.&lt;/p&gt;

&lt;p&gt;I was &lt;strong&gt;SO WRONG&lt;/strong&gt;. &lt;/p&gt;




&lt;h3&gt;
  
  
  🚧 &lt;strong&gt;The Task That Took Me Nearly a Year&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Deploy a simple API on Google Kubernetes Engine (GKE) using &lt;strong&gt;Terraform&lt;/strong&gt; to provision &lt;strong&gt;both&lt;/strong&gt; the infrastructure &lt;em&gt;and&lt;/em&gt; define the Kubernetes (K8s) deployments.&lt;/p&gt;




&lt;p&gt;At AltSchool, I had defined K8s deployments using &lt;strong&gt;YAML manifests&lt;/strong&gt; — like a normal person. I didn’t even know Terraform could handle K8s manifests. But I figured, &lt;em&gt;how hard could it be?&lt;/em&gt; With some help from ChatGPT and a lot of grit, I could get the assessment done in a weekend.&lt;/p&gt;

&lt;p&gt;Well… I did. After &lt;strong&gt;10 months&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important to note&lt;/strong&gt;: I couldn't create a Google Cloud account, so I asked if I could use Microsoft Azure and I was permitted - so that's what I used.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧠 &lt;strong&gt;What I Had to Learn (the Hard Way)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This assessment snowballed into one of the most difficult and rewarding technical challenges I’ve ever faced. Here’s are some of the rabbit holes I fell down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configuring a virtual network&lt;/strong&gt; (something Azure Kubernetes Service usually handles by default) and creating a &lt;strong&gt;NAT gateway&lt;/strong&gt; for the cluster.&lt;/li&gt;
&lt;li&gt;Securing the K8s API server &lt;strong&gt;without using &lt;code&gt;authorized_ip_ranges&lt;/code&gt;&lt;/strong&gt; (this forced me to rethink access for my chosen CI/CD tool that would run &lt;code&gt;terraform apply&lt;/code&gt; and &lt;code&gt;destroy&lt;/code&gt; - GitHub Actions)&lt;/li&gt;
&lt;li&gt;Learning what Microsoft Azure services could help with monitoring and how to configure them.&lt;/li&gt;
&lt;li&gt;Learning what &lt;strong&gt;K8s Network Policies&lt;/strong&gt; were, what they protect, and why they matter.&lt;/li&gt;
&lt;li&gt;Learning how to issue a TLS certificate for my &lt;strong&gt;name.com domain&lt;/strong&gt; (that I intended to use to make the API accessible publicly, abstracting its IP address) using &lt;strong&gt;Let’s Encrypt with DNS challenge&lt;/strong&gt; — a mostly unpleasant experience.&lt;/li&gt;
&lt;li&gt;Figuring out how to keep any sensitive data (tokens and credentials) secure but accessible to my CI/CD pipeline using GitHub secrets, GitHub Actions workflow environment variables and a here-doc command?! (I just found out that was what it was called while writing the draft for this article)&lt;/li&gt;
&lt;li&gt;Writing reusable &lt;strong&gt;GitHub Actions&lt;/strong&gt; workflow scripts to automate deployment and destruction — and learned just how fragile CI/CD can be.&lt;/li&gt;
&lt;li&gt;I had clashes with NGINX Ingress on the cluster way too often - network policy misconfigurations, ingress creation blocked by absent admission controller endpoints, among other issues.&lt;/li&gt;
&lt;li&gt;My name.com domain expired {frustrated screams} - so I had to pivot to using the ingress IP directly.&lt;/li&gt;
&lt;li&gt;And bash scripting. Quite a bit of bash scripting.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🎯 &lt;strong&gt;What Changed?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This project broke me down and rebuilt me. I came in thinking I had a decent handle on DevOps. I came out realizing how &lt;em&gt;deep&lt;/em&gt; the field is — and how much more I still have to learn.&lt;/p&gt;

&lt;p&gt;But here's the key: I &lt;em&gt;did&lt;/em&gt; learn. Slowly. Painfully. Iteratively. And now, I know how to deploy a secure, observable, automated microservice stack from scratch — using tools I’d never even heard of when I started.&lt;/p&gt;




&lt;h3&gt;
  
  
  📝 &lt;strong&gt;Why I’m Writing This&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I'm not fully done with the project (I'm kind of a perfectionist and there are bonus points in the assessment), but I have decided to write a collection of articles that try to detail the issues I encountered that I can remember and how I worked through them. I don't intend to win like a Pulitzer Prize or anything (though I wouldn't mind) but I genuinely hope my insights can help others battle similar challenges.&lt;/p&gt;

&lt;p&gt;In the coming weeks, I’ll publish a series of articles breaking down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to deploy Kubernetes workload using Terraform,&lt;/li&gt;
&lt;li&gt;My solution to securing an AKS cluster,

&lt;ul&gt;
&lt;li&gt;Azure Networking and NAT Gateway configuration&lt;/li&gt;
&lt;li&gt;Securing the K8S API server (without using &lt;code&gt;authorized_ip_ranges&lt;/code&gt; block)&lt;/li&gt;
&lt;li&gt;Managing a Private Cluster&lt;/li&gt;
&lt;li&gt;K8S network policies&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;How to automate deployments,

&lt;ul&gt;
&lt;li&gt;Using GitHub Actions (for Continuous Integration and Deloyment) and Secrets (to securely pass sensitive tokens and credentials into Actions workflows),&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;What not to do with NGINX Ingress (please learn from my pain),&lt;/li&gt;

&lt;li&gt;Monitoring with Azure Managed Prometheus &amp;amp; Grafana,&lt;/li&gt;

&lt;li&gt;Issuing TLS certificates via Let’s Encrypt DNS-01 + name.com’s API,&lt;/li&gt;

&lt;li&gt;Remote Backend Management for Terraform,&lt;/li&gt;

&lt;li&gt;And many other topics.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Here is the &lt;a href="https://github.com/VictortheGreat7/Cloud_Engineering_Assessment" rel="noopener noreferrer"&gt;GitHub repository link&lt;/a&gt; for those that don't mind spoilers&lt;/p&gt;




&lt;h3&gt;
  
  
  🙌 &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No, I didn’t “crush” the take-home assessment. I never even submitted it. But I learned more in these 10 months than in 12 months at AltSchool — and I came out stronger, more capable, and more confident in my abilities.&lt;/p&gt;

&lt;p&gt;If you're in over your head right now — keep going. You’re not alone. And you might be a lot closer than you think.&lt;/p&gt;




&lt;p&gt;Stay tuned — the war stories (and lessons) are just beginning.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Automating User Creation and Management with Bash: A Step-by-Step Guide</title>
      <dc:creator>Great-Victor Anjorin</dc:creator>
      <pubDate>Tue, 02 Jul 2024 21:45:58 +0000</pubDate>
      <link>https://dev.to/victorthegreat7/automating-user-creation-and-management-with-bash-a-step-by-step-guide-2oma</link>
      <guid>https://dev.to/victorthegreat7/automating-user-creation-and-management-with-bash-a-step-by-step-guide-2oma</guid>
      <description>&lt;p&gt;Automating user creation and management can save time, reduce errors, and enhance security for SysOps engineers. In this article, we will go over a Bash script that automates the creation of users and groups, sets up home directories, and manages passwords securely. The script reads from a text file where each line consists of a username and their corresponding groups, logs every action in a log file, and saves the randomly generated passwords for each user in a secure &lt;code&gt;.csv&lt;/code&gt; file accessible only to the owner.&lt;/p&gt;

&lt;p&gt;Let's dive into the bash script and break it down step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Line-by-Line Explanation
&lt;/h2&gt;

&lt;p&gt;1.) First off, we have our shebang.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This specifies the type of interpreter script will be run with. Since it is a "bash" script, it should be run with the Bourne Again Shell (Bash) interpreter. Also, some commands in the script may not be interpreted correctly outside of Bash.&lt;/p&gt;

&lt;p&gt;2.) The paths for the log file and the password file are set to avoid unnecessary repetition in the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Define log and password storage files
LOG_FILE="/var/log/user_management.log"
PASSWORD_FILE="/var/secure/user_passwords.csv"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3.) To ensure that the bash script runs with root privileges, an if statement checks if the Effective User ID (EUID) is equal to zero. The EUID determines the permissions the script will use to run, and 0 represents the root user ID in Linux systems. Only users with administrative privileges (users who can use sudo or the root user itself) can run the script. If someone attempts to run it without such privileges, an error message and the script's run process will be terminated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Check if the script is run with root privileges
if [[ $EUID -ne 0 ]]; then
  echo "This script must be run with root privileges." &amp;gt;&amp;amp;2
  exit 1
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4.) To ensure that an input file is provided as an argument when running the script, this &lt;code&gt;if&lt;/code&gt; statement will terminate the script if no argument is provided. In this statement, &lt;code&gt;$#&lt;/code&gt; represents the argument provided when running the script. If it is equal to zero (no argument is provided) or if it is greater than or equal to 2, an error message will be printed and the script's execution is halted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Check if the input file is provided
if [[ $# -eq 0 || $# -ge 2 ]]; then
  echo "Usage: $0 &amp;lt;user_file&amp;gt;" &amp;gt;&amp;amp;2
  exit 1
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5.) Next is the &lt;code&gt;log_action&lt;/code&gt; function that records logs using bold lettering (formatted with ANSI escape codes: &lt;code&gt;\033[1m&lt;/code&gt; and &lt;code&gt;\033[0m&lt;/code&gt;) and a timestamp (using the &lt;code&gt;date&lt;/code&gt; command to get the current date and the specified date format: &lt;code&gt;'%Y-%m-%d %H:%M:%S'&lt;/code&gt;). This function is used to log important steps, success messages, and error messages in the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Log function
log_action() {
  echo "--------------------------------------------------" | tee -a "$LOG_FILE"
  echo -e "$(date +'%Y-%m-%d %H:%M:%S') - \033[1m$1\033[0m" | tee -a "$LOG_FILE"
  echo "--------------------------------------------------" | tee -a "$LOG_FILE"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;6.) Next is the &lt;code&gt;create_user_account&lt;/code&gt; function that manages the entire process of creating a user, setting up their home directories with appropriate permissions and ownership, adding them to specified groups, and assigning randomly generated passwords. Every important step is logged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;create_user_account&lt;/code&gt; function&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create_user_account() {
  local username="$1"
  local groups="$2"

  log_action "Creating user account '$username'..."

  # Check if user already exists
  if id "$username" &amp;amp;&amp;gt; /dev/null; then
    echo "User '$username' already exists. Skipping..." | tee -a "$LOG_FILE"
    return 1
  fi

  # Create user with home directory and set shell
  if useradd -m -s /bin/bash "$username"; then
    echo "User $username created successfully." | tee -a "$LOG_FILE"
  else
    echo "Error creating user $username." | tee -a "$LOG_FILE"
    return 1
  fi

  # Create user group if it does not exist (in case the script is run in other linux distributions that do not create user groups by default)
  if ! getent group "$username" &amp;gt;/dev/null; then
    groupadd "$username"
    usermod -g "$username" "$username"
    log_action "Group $username created."
  fi

  # Set up home directory permissions
  echo "Setting permissions for /home/$username..." | tee -a "$LOG_FILE"
  chmod 700 "/home/$username" &amp;amp;&amp;amp; chown "$username:$username" "/home/$username"
  if [[ $? -eq 0 ]]; then
    echo "Permissions set for /home/$username." | tee -a "$LOG_FILE"
  else
    echo "Error setting permissions for /home/$username." | tee -a "$LOG_FILE"
    return 1
  fi

  # Add user to additional groups (comma separated)
  echo "Adding user $username to specified additional groups..." | tee -a "$LOG_FILE"
  IFS=',' read -ra group_array &amp;lt;&amp;lt;&amp;lt; "$groups"
  for group in "${group_array[@]}"; do
    group=$(echo "$group" | xargs)

    # Check if group exists, if not create it
    if ! getent group "$group" &amp;amp;&amp;gt;/dev/null; then
      if groupadd "$group"; then
        echo "Group $group did not exist. Now created." | tee -a "$LOG_FILE"
      else
        echo "Error creating group $group." | tee -a "$LOG_FILE"
        continue
      fi
    fi

    # Add user to group
    if gpasswd -a "$username" "$group"; then
      echo "User $username added to group $group." | tee -a "$LOG_FILE"
    else
      echo "Error adding user $username to group $group." | tee -a "$LOG_FILE"
    fi
  done

  # Log if no additional groups are specified
  if [[ -z "$groups" ]]; then
    echo "No additional groups specified." | tee -a "$LOG_FILE"
  fi

  # Generate random password, set it for the user, and store it in a file
  echo "Setting password for user $username..." | tee -a "$LOG_FILE"
  password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12)
  echo "$username:$password" | chpasswd
  if [[ $? -eq 0 ]]; then
    echo "Password set for user $username." | tee -a "$LOG_FILE"
    echo "$username,$password" &amp;gt;&amp;gt; "$PASSWORD_FILE"
  else
    echo "Error setting password for user $username. Deleting $username user account" | tee -a "$LOG_FILE"
    userdel -r "$username"
    return 1
  fi
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In the function, I initially set local variables to hold the values of the specified username and groups, to avoid repetition.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local username="$1"
local groups="$2"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In this next section, I use the &lt;code&gt;log_action&lt;/code&gt; function to record the start of each user account creation. Additionally, I verify whether the user already exists. If the user does exist, an error message is displayed and the script's execution is stopped.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log_action "Creating user account '$username'..."

  # Check if user already exists
  if id "$username" &amp;amp;&amp;gt; /dev/null; then
    echo "User '$username' already exists. Skipping..." | tee -a "$LOG_FILE"
    return 1
  fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Next, there is an &lt;code&gt;if&lt;/code&gt; statement that uses the &lt;code&gt;useradd&lt;/code&gt; command with the &lt;code&gt;-m&lt;/code&gt; and &lt;code&gt;-s&lt;/code&gt; flags to create a user with a login shell (In this case, the login shell is set to be &lt;code&gt;/bin/bash&lt;/code&gt;. If you want, you can modify or remove the &lt;code&gt;-s /bin/bash&lt;/code&gt; part entirely) and assigns a home directory to the user. It stops the script's run process if an error occurs during the execution of the command.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create user with home directory and set shell
  if useradd -m -s /bin/bash "$username"; then
    echo "User $username created successfully." | tee -a "$LOG_FILE"
  else
    echo "Error creating user $username." | tee -a "$LOG_FILE"
    return 1
  fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Next, in the case that the script is run on other Linux distributions that do not create and assign a primary group with the same name as the newly created user, the &lt;code&gt;if&lt;/code&gt; statement here will create a group with the same name as the user and add it as the user's primary group.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create user group if it does not exist (in case the script is run in other linux distributions that do not create user groups by default)
  if ! getent group "$username" &amp;gt;/dev/null; then
    groupadd "$username"
    usermod -g "$username" "$username"
    log_action "Group $username created."
  fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In this section of the function, the newly created user is designated as the owner of the newly created home directory. The owner is also granted all possible permissions for the directory. This is all done using the &lt;code&gt;chmod&lt;/code&gt; and &lt;code&gt;chown&lt;/code&gt; commands. If this process is unsuccessful, an error message is printed and the execution is halted.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Set up home directory permissions
  echo "Setting permissions for /home/$username..." | tee -a "$LOG_FILE"
  chmod 700 "/home/$username" &amp;amp;&amp;amp; chown "$username:$username" "/home/$username"
  if [[ $? -eq 0 ]]; then
    echo "Permissions set for /home/$username." | tee -a "$LOG_FILE"
  else
    echo "Error setting permissions for /home/$username." | tee -a "$LOG_FILE"
    return 1
  fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In this section, the newly created user is added to additional groups specified in the input file. By setting the Internal Field Separator (IFS) to expect comma-separated values and using the &lt;code&gt;read&lt;/code&gt; command with the &lt;code&gt;-ra&lt;/code&gt; flags, the groups are individually placed inside an array called &lt;code&gt;group_array&lt;/code&gt; to be used in the subsequent &lt;code&gt;for&lt;/code&gt; loop. Within the loop, for every value in the group_array, the &lt;code&gt;xargs&lt;/code&gt; command removes any whitespace, creates the group if it does not exist, and finally adds the user to the group using the &lt;code&gt;gpasswd&lt;/code&gt; command with the &lt;code&gt;-a&lt;/code&gt; flag. In the case where no group is specified for the user in the input file, a message will be printed.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Add user to additional groups (comma separated)
  echo "Adding user $username to specified additional groups..." | tee -a "$LOG_FILE"
  IFS=',' read -ra group_array &amp;lt;&amp;lt;&amp;lt; "$groups"
  for group in "${group_array[@]}"; do
    group=$(echo "$group" | xargs)

    # Check if group exists, if not create it
    if ! getent group "$group" &amp;amp;&amp;gt;/dev/null; then
      if groupadd "$group"; then
        echo "Group $group did not exist. Now created." | tee -a "$LOG_FILE"
      else
        echo "Error creating group $group." | tee -a "$LOG_FILE"
        continue
      fi
    fi

    # Add user to group
    if gpasswd -a "$username" "$group"; then
      echo "User $username added to group $group." | tee -a "$LOG_FILE"
    else
      echo "Error adding user $username to group $group." | tee -a "$LOG_FILE"
    fi
  done

  # Log if no additional groups are specified
  if [[ -z "$groups" ]]; then
    echo "No additional groups specified." | tee -a "$LOG_FILE"
  fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For the final section of the function, a random 12-character password is generated and set for the user. The head command collects a stream of random bytes from the &lt;code&gt;/dev/urandom&lt;/code&gt; file. This stream is piped to the &lt;code&gt;tr&lt;/code&gt; command, which filters the bytes to include only alphanumeric characters (A-Z, a-z, 0-9) using the &lt;code&gt;-dc&lt;/code&gt; flag. The filtered result is then piped to another head command, which selects only the first 12 characters from the edited stream. The password is then set by piping the user's name and the randomly generated password to the &lt;code&gt;chpasswd&lt;/code&gt; command. The user and the generated password are saved in the designated password &lt;code&gt;.csv&lt;/code&gt; file. If setting the password fails, the script deletes the user account and logs the error, to avoid any possible security risk.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Generate random password, set it for the user, and store it in a file
  echo "Setting password for user $username..." | tee -a "$LOG_FILE"
  password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12)
  echo "$username:$password" | chpasswd
  if [[ $? -eq 0 ]]; then
    echo "Password set for user $username." | tee -a "$LOG_FILE"
    echo "$username,$password" &amp;gt;&amp;gt; "$PASSWORD_FILE"
  else
    echo "Error setting password for user $username. Deleting $username user account" | tee -a "$LOG_FILE"
    userdel -r "$username"
    return 1
  fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7.) After creating the create_user_account function, the script processes a file containing user information and creates user accounts accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Process the user file
user_file="$1"
while IFS=';' read -r username groups; do
  if create_user_account "$username" "${groups%%[ ;]}"; then
    log_action "User account '$username' created successfully."
  else
    log_action "Error creating user account '$username'."
  fi
done &amp;lt; "$user_file"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The script takes the user file as its argument and assigns it to the variable &lt;code&gt;user_file&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A while loop reads each line of the user file. The &lt;code&gt;IFS=';'&lt;/code&gt; part sets the Internal Field Separator to a semicolon (;), splitting each line at the semicolon. The &lt;code&gt;read -r&lt;/code&gt; username groups part reads the split parts into the username and groups variables.&lt;/li&gt;
&lt;li&gt;For each line in the file, the script calls the create_user_account function with the username and the groups (with trailing spaces removed using &lt;code&gt;${groups%%[ ;]}&lt;/code&gt;). The script also logs a message if the result of the &lt;code&gt;create_user_account&lt;/code&gt; was a success or failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;8.) After all that is done, the script gives only the owner (root) and those with root privileges access to the password file, logs the completion of the script's execution and prints the log file and password file location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Keep password file accessible only to those with root privileges
chmod 600 "$PASSWORD_FILE"

# Log completion
log_action "User creation script completed."

# Print log file and password file location
echo "Check $LOG_FILE for details."
echo "Check $PASSWORD_FILE for user passwords."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prerequisites for running the script&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Linux system&lt;/li&gt;
&lt;li&gt;A bash terminal (Optional. You can use any available shell terminal on the Linux system).&lt;/li&gt;
&lt;li&gt;Root privileges on your system.&lt;/li&gt;
&lt;li&gt;The text file containing the usernames and groups, must be formatted as &lt;code&gt;username;group1,group2&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To run the script:&lt;/strong&gt;&lt;br&gt;
1.) Copy the file from or clone the repository &lt;a href="https://github.com/VictortheGreat7/HNG_Stage1" rel="noopener noreferrer"&gt;HNG_Stage1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2.) Copy the text file containing the usernames and groups to the folder where the script is located.&lt;/p&gt;

&lt;p&gt;3.) Then, in the directory where both the script and the text file are now located, run &lt;code&gt;sudo ./create_users.sh &amp;lt;text file&amp;gt;&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;This script simplifies some administrative tasks. With such a script, SysOps engineers can automate user and group creation and management, allowing them to focus on more critical work while ensuring efficient and secure user management.&lt;/p&gt;

&lt;p&gt;This is one of the project assignments in the &lt;a href="https://hng.tech/internship" rel="noopener noreferrer"&gt;HNG Internship&lt;/a&gt; program designed to enhance your resume and deepen your knowledge of bash scripting. For the best experience, visit &lt;a href="https://hng.tech/premium" rel="noopener noreferrer"&gt;HNG Premium&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
