<?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>Forem: Ijeawele Divine Nkwocha</title>
    <description>The latest articles on Forem by Ijeawele Divine Nkwocha (@ijeawele).</description>
    <link>https://forem.com/ijeawele</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%2F3015627%2Fa551e86e-f40d-4771-a62e-c97bc16e8a95.jpg</url>
      <title>Forem: Ijeawele Divine Nkwocha</title>
      <link>https://forem.com/ijeawele</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ijeawele"/>
    <language>en</language>
    <item>
      <title>Deploying a Production-Grade Microservices Platform on AWS EKS, Every Decision, Every Error, Every Lesson</title>
      <dc:creator>Ijeawele Divine Nkwocha</dc:creator>
      <pubDate>Thu, 23 Apr 2026 19:12:53 +0000</pubDate>
      <link>https://forem.com/ijeawele/deploying-a-production-grade-microservices-platform-on-aws-eks-every-decision-every-error-every-29mm</link>
      <guid>https://forem.com/ijeawele/deploying-a-production-grade-microservices-platform-on-aws-eks-every-decision-every-error-every-29mm</guid>
      <description>&lt;p&gt;Most Kubernetes tutorials stop at "your pod is running." That's not production.&lt;/p&gt;

&lt;p&gt;Production is secrets management, autoscaling, TLS automation, persistent storage across availability zones, and an ingress layer that handles real traffic patterns. This article walks through a full microservices deployment on AWS EKS, the architecture decisions, the errors that will humble you if you skip the fundamentals, and the things worth doing differently on the next project.&lt;/p&gt;

&lt;p&gt;The platform is RideShare Pro. Six independent services, a centralised ingress layer, managed data stores, and a live domain. &lt;a href="https://github.com/ijeawele-divine/rideshare-app/tree/master" rel="noopener noreferrer"&gt;GitHub repo here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're a DevOps engineer working toward production-grade Kubernetes, this is the kind of breakdown you won't find in a quickstart guide.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  What's Being Built
&lt;/h2&gt;

&lt;p&gt;RideShare Pro is a microservices-based rideshare application where each business capability lives in its own independent service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rider Service&lt;/strong&gt;, rider profiles, ride requests, status tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driver Service&lt;/strong&gt;, driver profiles, vehicle management, availability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trips Service&lt;/strong&gt;, trip lifecycle from creation to completion, trip history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matching Service&lt;/strong&gt;, real-time matching of riders with the nearest available driver&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email Service&lt;/strong&gt;, transactional emails triggered by events from other services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;, the user-facing web application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each service communicates over HTTP APIs. All external traffic is routed through a centralised NGINX Ingress Controller acting as the API gateway.&lt;/p&gt;

&lt;p&gt;The goal: deploy this on AWS EKS in a way that is scalable, highly available, and extremely production-ready.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 1, Deploy Locally Before Touching the Cluster
&lt;/h2&gt;

&lt;p&gt;Before writing a single Kubernetes manifest, deploy the entire application locally.&lt;/p&gt;

&lt;p&gt;This one decision saves hours.&lt;/p&gt;

&lt;p&gt;Local deployment surfaces errors in the codebase itself, bugs that would later appear as &lt;code&gt;CrashLoopBackOff&lt;/code&gt; pods with no obvious cause. Catching them at the local stage means you're not debugging application code and infrastructure configuration simultaneously. That combination is one of the most frustrating debugging scenarios in DevOps work.&lt;/p&gt;

&lt;p&gt;The cluster will surface problems. It won't always tell you clearly why. Local validation first means the only thing the cluster is testing is the infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2, Build Docker Images and Push to ECR
&lt;/h2&gt;

&lt;p&gt;Once the application is running locally, build Docker images for each service and store them in Amazon ECR (Elastic Container Registry).&lt;/p&gt;

&lt;p&gt;ECR is the right choice when you're already in the AWS ecosystem. It integrates natively with EKS, uses IAM-based access control, and there's no friction pulling images at deploy time. No separate registry credentials to manage. No external dependency at cluster startup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3, Kubernetes Manifest Structure
&lt;/h2&gt;

&lt;p&gt;The manifests are organised into four directories: &lt;code&gt;aws/&lt;/code&gt;, &lt;code&gt;platform/&lt;/code&gt;, &lt;code&gt;stateful/&lt;/code&gt;, and &lt;code&gt;applications/&lt;/code&gt;. Each one has a clear separation of concern.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;aws/&lt;/code&gt;, Cluster-Level Infrastructure
&lt;/h3&gt;

&lt;p&gt;These manifests interact directly with the AWS API to provision cluster-level resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;node-groups.yaml&lt;/strong&gt; creates managed node groups, collections of EC2 instances that Kubernetes schedules pods onto. Managed node groups mean AWS handles provisioning, scaling, and lifecycle management. &lt;code&gt;t3.medium&lt;/code&gt; instances across multiple Availability Zones cover general-purpose workloads well without over-provisioning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iam-roles.yaml&lt;/strong&gt; sets up IAM Roles for Service Accounts (IRSA). IRSA is how specific Kubernetes service accounts get permission to call AWS APIs, in this case, permission to create EBS volumes for persistent storage. This is the correct approach. Giving broad IAM permissions to nodes is a security anti-pattern. IRSA scopes permissions to exactly what each service account needs and nothing more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;storage-classes.yaml&lt;/strong&gt; creates an EBS gp3 StorageClass using the IRSA role above. The critical setting here is &lt;code&gt;volumeBindingMode: WaitForFirstConsumer&lt;/code&gt;. More on why this matters in the errors section.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;platform/&lt;/code&gt;, Shared Cluster-Wide Components
&lt;/h3&gt;

&lt;p&gt;This directory sets up everything that makes the cluster secure and scalable, autoscaling, ingress, secrets, and security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autoscaling&lt;/strong&gt;, Two autoscaling mechanisms work together here. The Horizontal Pod Autoscaler (HPA) scales pods when CPU or memory thresholds are hit. When all nodes are full and more pods need scheduling, the Cluster Autoscaler adds nodes to accommodate them. Both are necessary. HPA without the Cluster Autoscaler means pod scaling stalls when node capacity runs out.&lt;/p&gt;

&lt;p&gt;One thing worth knowing about HPA: it requires both resource &lt;strong&gt;requests&lt;/strong&gt; and &lt;strong&gt;limits&lt;/strong&gt; to be set on containers. HPA measures CPU utilisation as a percentage of the &lt;em&gt;requested&lt;/em&gt; CPU, not the limit. Without requests, it has no baseline and effectively does nothing. Always set both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingress&lt;/strong&gt;, NGINX Ingress Controller with path-based routing. The backend ingress rules (&lt;code&gt;ingress-api.yaml&lt;/code&gt;) are separated from the frontend (&lt;code&gt;ingress-frontend.yaml&lt;/code&gt;) deliberately. API paths need specific annotations, rate limiting, and authentication headers that shouldn't bleed over to the frontend. Separating them gives cleaner, more targeted control and makes future changes safer.&lt;/p&gt;

&lt;p&gt;For HTTPS, cert-manager with a ClusterIssuer pointing to Let's Encrypt handles certificate provisioning and renewal automatically. Production deployments need HTTPS. This is the cleanest way to handle it without any manual certificate management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets&lt;/strong&gt;, This is where most engineers either get it right or create a future security incident.&lt;/p&gt;

&lt;p&gt;Native Kubernetes Secrets are base64 encoded, not encrypted. Anyone with cluster access can decode them. The production-grade approach is the &lt;strong&gt;External Secrets Operator (ESO)&lt;/strong&gt; with &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it works: secrets, database URLs, Redis connection strings, JWT keys and other sensitive credentials are stored in AWS Secrets Manager. ESO creates a SecretStore pointing to that service. ExternalSecret resources reference the store and map specific secrets into pods as environment variables. ESO syncs on a configurable schedule, so rotating a secret in AWS Secrets Manager propagates to the cluster automatically.&lt;/p&gt;

&lt;p&gt;This eliminates an entire class of security risk. The alternative, base64 encoded secrets committed to YAML files, allows your secrets to be revealed with just a Google search.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fnd0bz5zz255qapw2te.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fnd0bz5zz255qapw2te.png" alt="Secret Store for Trips Service in AWS Secrets Manager" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;, Pod Disruption Budgets (PDB) for all critical services. A PDB ensures that during node maintenance or cluster upgrades, Kubernetes cannot take down more than a defined number of pods simultaneously. Setting &lt;code&gt;minAvailable: 2&lt;/code&gt; means regardless of what's happening at the node level, at least 2 pods of that service stay running. This is the difference between a cluster that survives a rolling upgrade and one that causes an outage during routine maintenance.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;stateful/&lt;/code&gt;, Persistent Data
&lt;/h3&gt;

&lt;p&gt;This is the most significant architectural decision in the project, and it's a deliberate departure from the typical tutorial approach.&lt;/p&gt;

&lt;p&gt;A standard spec would call for PostgreSQL and Redis deployed as StatefulSets inside the cluster. Here, both are replaced with managed AWS services: &lt;strong&gt;Aurora RDS&lt;/strong&gt; for PostgreSQL and &lt;strong&gt;Amazon ElastiCache&lt;/strong&gt; for Redis.&lt;/p&gt;

&lt;p&gt;The reasoning is operational reality.&lt;/p&gt;

&lt;p&gt;StatefulSets in Kubernetes are powerful but come with real overhead. Database replication, node failure recovery, volume reattachment, version upgrades, all of that falls on the team. For most production systems, that's engineering time that isn't being spent on the product.&lt;/p&gt;

&lt;p&gt;Aurora RDS changes the equation. Replication across Availability Zones is automatic. Storage scales without intervention. Automated backups, failover, and read replicas are built in. ElastiCache gives the same model for Redis, managed, highly available, secure, with automatic failover and no operational burden.&lt;/p&gt;

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

&lt;p&gt;The tradeoff is cost and cloud portability. Managed services cost more than self-hosted, and you're tied to AWS. For a production system where reliability and engineering time both matter, this is the right call. Know the tradeoff, make the decision consciously.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;applications/&lt;/code&gt;, The Microservices
&lt;/h3&gt;

&lt;p&gt;Each service directory contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deployment.yaml&lt;/code&gt;, pod specs, container definitions, resource requests/limits, environment variables pulled from ExternalSecrets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service.yaml&lt;/code&gt;, ClusterIP service exposing the deployment internally&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hpa.yaml&lt;/code&gt;, Horizontal Pod Autoscaler targeting CPU and memory thresholds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;configmap.yaml&lt;/code&gt;, non-sensitive configuration like service URLs and feature flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All services are ClusterIP. External traffic flows through NGINX Ingress only. Exposing individual services directly to the internet through LoadBalancer type is both a cost and security problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4, Real Domain, Real HTTPS
&lt;/h2&gt;

&lt;p&gt;Deploying to a cluster is one thing. A live URL that external traffic can hit is another, and it's what makes a portfolio project credible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding the Load Balancer IP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When NGINX Ingress Controller deploys on EKS, it provisions an AWS Load Balancer automatically. To get the external IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get svc &lt;span class="nt"&gt;-n&lt;/span&gt; ingress-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;EXTERNAL-IP&lt;/code&gt; column on the &lt;code&gt;ingress-nginx-controller&lt;/code&gt; service is the cluster's entry point. That's what DNS points at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In GoDaddy (or whichever registrar), add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type: &lt;code&gt;A&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;rideshare&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Value: the Load Balancer IP from above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;rideshare.ijeaweledivine.online&lt;/code&gt; now routes to the NGINX Ingress Controller, which applies path rules to reach the correct service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS Automation with cert-manager&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Certificate management - cert-manager with a Let's Encrypt ClusterIssuer handles provisioning and renewal automatically.&lt;/p&gt;

&lt;p&gt;The annotations that drive this on the Ingress manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nginx.ingress.kubernetes.io/use-regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
  &lt;span class="na"&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
  &lt;span class="na"&gt;nginx.ingress.kubernetes.io/proxy-read-timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;
  &lt;span class="na"&gt;nginx.ingress.kubernetes.io/proxy-send-timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;
  &lt;span class="na"&gt;nginx.ingress.kubernetes.io/websocket-services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trip-service"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Understanding what they mean is a Nice-to-Know.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;use-regex: "true"&lt;/code&gt;, enables regex path matching. Without this, path rules are basic prefix matching only. With it, you can write precise rules like &lt;code&gt;/api/trips/.*&lt;/code&gt; to catch all trip-related routes cleanly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cluster-issuer: letsencrypt-prod&lt;/code&gt;, the key annotation. cert-manager sees this, creates a CertificateRequest, runs the ACME challenge with Let's Encrypt, gets a signed certificate, stores it as a Kubernetes Secret, and handles renewal before expiry. One annotation. Permanent HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;proxy-read-timeout&lt;/code&gt; and &lt;code&gt;proxy-send-timeout: "3600"&lt;/code&gt;, sets timeout values to 1 hour. The default NGINX timeout is 60 seconds. For a rideshare platform where an active trip can last 45 minutes, 60 seconds kills live connections mid-trip. Match your timeout values to your actual usage patterns.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;websocket-services: "trip-service"&lt;/code&gt;, the trips service uses WebSockets for real-time communication: live trip status updates, driver location tracking. Standard HTTP is request-response and closes. WebSockets stay open. Without this annotation, NGINX doesn't handle the connection upgrade correctly, and real-time features fail silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cert-manager Automation Flow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the Ingress is applied with the cert-manager annotation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;cert-manager detects the annotation and creates a &lt;code&gt;CertificateRequest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Let's Encrypt issues an ACME challenge, proof that the domain is under your control&lt;/li&gt;
&lt;li&gt;cert-manager creates a temporary pod and Ingress rule to respond to the challenge&lt;/li&gt;
&lt;li&gt;Let's Encrypt verifies, issues the certificate&lt;/li&gt;
&lt;li&gt;cert-manager stores it as a Kubernetes Secret and mounts it into the Ingress&lt;/li&gt;
&lt;li&gt;NGINX serves HTTPS traffic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Watch it happen in real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get certificaterequest &lt;span class="nt"&gt;-n&lt;/span&gt; your-namespace
kubectl describe certificaterequest &amp;lt;name&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; your-namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Status shows &lt;code&gt;Approved&lt;/code&gt; and &lt;code&gt;Issued&lt;/code&gt; in about 2 minutes. The whole process is hands-off after the initial annotation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Errors That May Stress You OUT
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error 1, &lt;code&gt;no topology key found for node&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The EBS CSI Driver couldn't identify which Availability Zone the worker node was in, so it couldn't safely create a persistent volume. EBS volumes are AZ-specific; a volume in &lt;code&gt;eu-north-1a&lt;/code&gt; cannot be mounted by a pod in &lt;code&gt;eu-north-1b&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two fixes:&lt;/p&gt;

&lt;p&gt;EKS nodes need the label &lt;code&gt;topology.ebs.csi.aws.com/zone&lt;/code&gt; for the storage driver to identify the AZ. Apply this to your node groups.&lt;/p&gt;

&lt;p&gt;More importantly: set &lt;code&gt;volumeBindingMode: WaitForFirstConsumer&lt;/code&gt; in your StorageClass. Without this, Kubernetes creates the EBS volume before it knows which node the pod will land on. &lt;code&gt;WaitForFirstConsumer&lt;/code&gt; delays volume creation until the pod is scheduled to a node, then creates the volume in the same AZ. This single setting eliminates an entire class of storage scheduling problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 2, &lt;code&gt;secret "postgres-credentials" not found&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Namespace isolation. Kubernetes secrets are namespace-scoped. A pod in the &lt;code&gt;rideshare-app&lt;/code&gt; namespace cannot access a secret created in the &lt;code&gt;default&lt;/code&gt; namespace. The credentials existed, they were just invisible from where the pod was looking.&lt;/p&gt;

&lt;p&gt;When pods hit &lt;code&gt;CreateContainerConfigError&lt;/code&gt; or &lt;code&gt;CrashLoopBackOff&lt;/code&gt; and the image is confirmed healthy, check namespace alignment before anything else. It's almost always either a namespace mismatch or a missing secret.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Do Differently
&lt;/h2&gt;

&lt;p&gt;Two gaps from the project review are worth calling out explicitly, because they're easy to miss and impactful to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health probes&lt;/strong&gt;, &lt;code&gt;livenessProbe&lt;/code&gt; and &lt;code&gt;readinessProbe&lt;/code&gt; tell Kubernetes whether a pod is healthy and ready to receive traffic. Without them, Kubernetes has no mechanism to automatically restart a stuck pod or remove it from rotation when it's not ready. The result: a broken pod silently receives live traffic and returns errors until someone notices manually. Adding probes is a small amount of YAML that meaningfully improves reliability. Don't skip them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource requests on HPA&lt;/strong&gt;, As covered above: HPA uses requests as the baseline for utilization calculations. Limits without requests gives the autoscaler nothing to measure against. Set both, always.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The infrastructure setup is the visible part. The real depth is in the operational decisions, why managed services over StatefulSets, why External Secrets over native Kubernetes Secrets, why separate ingress manifests for API and frontend.&lt;/p&gt;

&lt;p&gt;Those decisions are what separate someone who knows Kubernetes syntax from someone who can design a system that holds up under real conditions.&lt;/p&gt;

&lt;p&gt;For anyone working through something similar: deploy locally first, understand service communication before touching the cluster, and don't treat health probes and resource requests as optional polish. They're not. The cluster runs without them until it doesn't.&lt;/p&gt;




&lt;p&gt;*Ijeawele is a DevOps Engineer building production-grade infrastructure and writing about it in plain terms. More projects coming.&lt;/p&gt;

&lt;p&gt;Reach out to me for questions or any opportunities on my &lt;a href="https://linkedin.com/in/ijeawele-nkwocha" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt;&lt;br&gt;
Check out my other Projects on &lt;a href="https://github.com/ijeawele-divine" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>eks</category>
      <category>microservices</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Azure Disaster Recovery with Terraform: Complete RTO/RPO Guide (2025)</title>
      <dc:creator>Ijeawele Divine Nkwocha</dc:creator>
      <pubDate>Thu, 27 Nov 2025 00:27:40 +0000</pubDate>
      <link>https://forem.com/ijeawele/building-resilient-cloud-infrastructure-with-terraform-and-azure-4637</link>
      <guid>https://forem.com/ijeawele/building-resilient-cloud-infrastructure-with-terraform-and-azure-4637</guid>
      <description>&lt;p&gt;I recently architected a dual-variant infrastructure testing environment using Terraform. I built both resilient and non-resilient cloud resources to benchmark a cloud resilience monitoring tool. The objective was to calculate precise Recovery Time Objective (RTO) and Recovery Point Objective (RPO) metrics for individual Azure resources across 12 service types.&lt;/p&gt;

&lt;p&gt;As an AWS-native DevOps engineer, I approached this Azure implementation strategically. Rather than relying solely on documentation, I took time to understand Azure's disaster recovery approach at a very low level, which differs significantly from AWS's approach. And what I got was A production-grade infrastructure that demonstrates how DR strategies must be tailored to each cloud provider's architecture, not simply translated across platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Disaster Recovery: RTO and RPO
&lt;/h2&gt;

&lt;p&gt;The main differentiators between resilient and non-resilient resources came down to two key factors: disaster recovery configuration and the chosen tier (Basic, Standard, or Premium). What you deploy your resources with determines how much they can withstand during a disaster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery Time Objective (RTO)&lt;/strong&gt; is how long it takes to get operations back to normal and restore systems after a disruption. It's the acceptable amount of time recovery must be achieved to avoid a significant business impact. For example, a system with an RTO of 5 hours means it must be back online within 5 hours after a failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery Point Objective (RPO)&lt;/strong&gt; defines the maximum amount of data loss tolerable, measured in time. It indicates how frequently data backups or replication occur. For example, an RPO of 15 minutes means data is backed up or replicated at least every 15 minutes. It shows how much data an organisation can afford to lose if systems go down.&lt;/p&gt;

&lt;p&gt;RTO and RPO differ for every organisation, and the goal of disaster recovery is to ensure these objectives are met in case of any disaster. How do you ensure this? By provisioning resources with failure in mind. &lt;em&gt;You can't build a perfect system, but you can build a perfect recovery system&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;How long will it take to bounce back after downtime, and how much data would you lose? Those are things within your control.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes Cloud Resources Resilient
&lt;/h2&gt;

&lt;p&gt;One key thing that makes cloud resources resilient is redundancy, how many times it's replicated and where it's being replicated. You can have resources replicated in the same zone, but that doesn't make them resilient because when something happens in that zone, all the resources there are affected.&lt;/p&gt;

&lt;p&gt;Azure has different redundancy options, each with different percentages of uptime represented from 99.9% (three 9s) to 99.999999999% (twelve 9s). When it comes to storage, it's critical to replicate your data across different data centers, zones, and regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure Storage Redundancy Options
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Locally Redundant Storage (LRS):&lt;/strong&gt; Data is replicated three times within a single data centre to protect against hardware failures, with a durability of 99.99%. It protects against local hardware failures but not data centre-level faults.&lt;br&gt;
&lt;strong&gt;Zone-Redundant Storage (ZRS):&lt;/strong&gt; Data is synchronously replicated across three separate Availability Zones within a region, enhancing resilience against zone or data centre failures. Your data remains accessible even if one zone fails.&lt;br&gt;
&lt;strong&gt;Geo-Redundant Storage (GRS):&lt;/strong&gt; Combines LRS in the primary region with asynchronous replication to a secondary region to guard against entire regional failures.&lt;br&gt;
&lt;strong&gt;Geo-Zone-Redundant Storage (GZRS):&lt;/strong&gt; Combines ZRS in the primary region with asynchronous replication to a secondary region for both zonal and regional fault tolerance.&lt;br&gt;
The kind of storage you choose determines how resilient the whole system is. It's literally how your data is stored and protected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resilience Is Component-Specific
&lt;/h2&gt;

&lt;p&gt;Each component has different ways to be resilient, so how do you compare them? You don't. Resilience is calculated individually as a result of the efficiency of each component.&lt;/p&gt;

&lt;p&gt;The resources I deployed include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Virtual Machines&lt;/li&gt;
&lt;li&gt;SQL Database&lt;/li&gt;
&lt;li&gt;Azure Kubernetes Service (AKS)&lt;/li&gt;
&lt;li&gt;Azure Container Registry (ACR)&lt;/li&gt;
&lt;li&gt;Managed Disks&lt;/li&gt;
&lt;li&gt;Azure Data Lake Storage (ADLS)
Ensuring they're all resilient is different for each and must be worked on individually. Each service has its own disaster recovery mechanism - SQL failover groups, ADLS native replication, and so on.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Infrastructure as Code with Terraform
&lt;/h2&gt;

&lt;p&gt;I provisioned each component using Terraform and used modules to set up individual resources. I configured a remote backend using Azure Blob Storage and implemented state locking with Azure storage to prevent concurrent modifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disaster Recovery Solutions by Resource
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Virtual Machines&lt;/strong&gt;&lt;br&gt;
I configured Azure Site Recovery (ASR) for the resilient VM with replication to a paired region, set up a backup policy with scheduled snapshots in a Recovery Services Vault, used Premium SSDs, and deployed across multiple Availability Zones.&lt;br&gt;
&lt;strong&gt;Azure Kubernetes Service (AKS)&lt;/strong&gt;&lt;br&gt;
The disaster recovery solution for AKS included multiple node pools spread across Availability Zones and a backup solution using Azure Backup. Interestingly, the backup vault couldn't be deleted even after running terraform destroy until after a week, a safety feature to prevent accidental data loss.&lt;br&gt;
&lt;strong&gt;Managed Disks&lt;/strong&gt;&lt;br&gt;
I configured GRS storage with Premium SSD, automated snapshot policies to back up the disks, and cross-region replication for both the managed disk and snapshots.&lt;br&gt;
&lt;strong&gt;Azure Container Registry (ACR)&lt;/strong&gt;&lt;br&gt;
For ACR, I set up geo-replication to a secondary region (from westus2 to centralus), ensuring container images and tags are automatically copied and synchronised.&lt;br&gt;
&lt;strong&gt;Azure Data Lake Storage Gen2&lt;/strong&gt;&lt;br&gt;
ADLS was configured with GRS replication and cross-region failover readiness.&lt;br&gt;
&lt;strong&gt;SQL Database&lt;/strong&gt;&lt;br&gt;
The SQL database was set up with zone redundancy, active geo-replication to a paired region, and failover groups for automatic coordinated failover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive into Key DR Solutions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Azure Site Recovery (ASR)&lt;/strong&gt;&lt;br&gt;
Azure Site Recovery is a Disaster Recovery as a Service solution provided by Azure. I configured it separately from the resources and passed it as a data source in the root module. I created a separate directory named asr-setup-vault with its own state file and provider block.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ASR setup &lt;br&gt;
involves configuring the ASR fabric, ASR protection container, replication policy, and ASR protection container mapping. A shared vault was configured for both the managed disk and VM, and a backup policy was set up for the shared vault. The expected outcome is cross-region replicated VMs and managed disks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Data Protection Backup Vault&lt;/strong&gt;&lt;br&gt;
I used a Data Protection backup vault to store snapshots and AKS backups. A data protection backup vault is a secure, centralised storage entity designed to store and manage backup data and recovery points over time. It acts as a container for backups, providing protection through encryption, data isolation, and access control mechanisms to ensure the integrity and availability of backup data even if production systems are compromised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple Node Pools Across Availability Zones&lt;/strong&gt;&lt;br&gt;
When multiple node pools in an AKS cluster are spread across Availability Zones, it means that each node pool's virtual machines are distributed across different isolated physical locations within the same Azure region. This configuration boosts the cluster's resilience and availability because even if one AZ experiences an outage, nodes in other AZs remain functional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geo-Replication for Container Registry&lt;/strong&gt;&lt;br&gt;
Geo-replication for ACR means that the container registry's contents, including container images and tags, are automatically copied and synchronized from the primary region to one or more secondary Azure regions. This ensures high availability and reduces latency for pulling images from different geographic locations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL Database Failover Groups&lt;/strong&gt;&lt;br&gt;
Failover groups for Azure SQL Database enable automatic and coordinated failover of a group of databases from a primary server in one Azure region to a secondary server in another region. This ensures high availability and disaster recovery by replicating databases geo-redundantly and allowing seamless switching to the secondary region if the primary becomes unavailable due to an outage or disaster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Framework for Disaster Recovery
&lt;/h2&gt;

&lt;p&gt;Here's a simple decision tree I used to determine the right DR strategy:&lt;/p&gt;

&lt;p&gt;Is this data critical to business operations?&lt;br&gt;
If YES: Can you afford to lose ANY data?&lt;br&gt;
No data loss acceptable: Use active geo-replication (SQL failover groups, GZRS storage)&lt;br&gt;
Some data loss is acceptable:&lt;br&gt;
• Under 1 hour data loss: ASR for VMs, GZRS for storage&lt;br&gt;
• 1-24 hours data loss: Daily backups, GRS storage&lt;br&gt;
If NO (data not critical): Use the most cost-effective option, LRS storage, Basic SKUs, no ASR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Building resilient infrastructure isn't about making everything highly available; it's about understanding your business requirements and making informed decisions about where to invest in redundancy and disaster recovery. Each Azure service has its own DR mechanisms, and you need to understand them individually to build a truly resilient system.&lt;/p&gt;

&lt;h2&gt;
  
  
  This project taught me that moving from AWS to Azure isn't just about learning new service names; it's about understanding fundamentally different approaches to disaster recovery and resilience. The skills are transferable, but the implementation details matter.
&lt;/h2&gt;

&lt;p&gt;Got questions about building resilient infrastructure or want to discuss disaster recovery strategies? I'm always happy to discuss!&lt;/p&gt;

&lt;p&gt;I'm a DevOps engineer and technical writer currently open to new opportunities. &lt;em&gt;If you're hiring or want to connect&lt;/em&gt;, reach out on &lt;a href="https://linkedin.com/in/ijeawele-nkwocha" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or drop me an email at [&lt;a href="mailto:nkwochaijeawele@gmail.com"&gt;nkwochaijeawele@gmail.com&lt;/a&gt;].&lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
      <category>devops</category>
      <category>cloudcomputing</category>
    </item>
  </channel>
</rss>
