DEV Community

Cover image for I redeployed an E-Commerce Monolith to a Zero-Trust Multi-AZ EKS ClusterHere Is the Step-by-Step Blueprint
manny300
manny300

Posted on

I redeployed an E-Commerce Monolith to a Zero-Trust Multi-AZ EKS ClusterHere Is the Step-by-Step Blueprint

Running an e-commerce platform on a single public virtual machine is an operational liability waiting to surface. In this guide, you will learn how to design, provision, and deploy a containerized PrestaShop e-commerce store on Amazon Elastic Kubernetes Service (EKS) across multiple Availability Zones, eliminate public compute and database exposure, and inject production database credentials into application pods without writing a single plaintext secret to disk.

This project is the fourth installation in my cloud engineering series documenting hands-on enterprise infrastructure deployments. If you have followed my previous projects, such as building an automated deployment pipeline for a 3-tier application to eliminate configuration drift or designing an automated IAM governance system that onboards and offboards users in under 60 seconds, you know that my focus is on building hardened, production-grade cloud systems. You can follow my journey on Dev.to and track all project codebases on my GitHub profile.


The Architectural Evolution: Moving Past the Monolith

Prior to this project, I deployed PrestaShop using a conventional monolithic architecture: a single Amazon EC2 instance provisioned in a public subnet, configured manually with Apache and PHP, connected to a single-AZ Amazon RDS instance.

While that monolithic deployment served its purpose as an initial proof of concept, it carried severe operational and security flaws that no business handling financial transactions should tolerate:

  1. Direct Public Attack Surface: The application server sat in a public subnet with an open SSH port (22) exposed to the entire internet, inviting brute-force attacks and credential scans.
  2. Single Point of Failure (SPOF): Both the compute layer and the database ran in a single Availability Zone. An outage in an AWS data center would bring the entire storefront down immediately.
  3. Configuration Drift: Software packages, Apache virtual hosts, and PHP modules were installed manually through ad-hoc package manager commands. Replicating that server identically in an incident recovery scenario would have been slow and error-prone.
  4. Credential Exposure: Database credentials lived in plaintext PHP configuration files on the local filesystem, vulnerable to server compromises or unauthorized access.

To resolve these risks, I redesigned the platform from the ground up around containerization, Kubernetes orchestration, zero-trust network boundaries, and automated secret delivery.

Below is the live application preview deployed through an AWS Network Load Balancer (NLB):

  • Live Storefront Preview: http://aad1534e7a20940148d569b875b1b9cd-82489033c55b4bb3.elb.us-east-1.amazonaws.com/

Note: If you are reading this post more than five hours after publication, this live endpoint has likely been decommissioned to prevent unnecessary cloud credit consumption. You can inspect the entire repository, configuration manifests, and implementation evidence on my GitHub repository.


High-Level Architecture Overview

The target architecture enforces complete separation of concerns and defense-in-depth across the following layers:

  1. Multi-AZ Network Foundation (VPC): A dedicated Virtual Private Cloud spanning two Availability Zones (us-east-1a and us-east-1c) divided into six subnets: two Public Subnets (hosting the Network Load Balancer and NAT Gateways), two Private Application Subnets (hosting the EKS worker nodes), and two Private Data Subnets (hosting Amazon RDS MySQL).
  2. Zero-Trust Administrative Access: Rather than provisioning a public bastion jump host or opening port 22 to the internet, secure administrative access and database initialization are conducted through an EC2 Instance Connect Endpoint (EICE) residing inside the private subnet.
  3. Automated Database Failover: Amazon RDS MySQL deployed in a Multi-AZ standby configuration across the private data subnets, completely unreachable from the public internet.
  4. Containerized Packaging: PrestaShop packaged into a hardened Docker image with customized PHP memory, upload, and execution parameters, hosted in Amazon Elastic Container Registry (ECR).
  5. Kubernetes Orchestration (Amazon EKS): An Amazon EKS cluster orchestrating container scheduling across private worker nodes.
  6. In-Memory Secret Delivery: Integration of AWS Secrets Manager with Kubernetes through IAM Roles for Service Accounts (IRSA) and the Secrets Store CSI Driver, mounting database credentials directly into the container as files and memory-backed environment variables without persisting them in cluster storage.
  7. Traffic Distribution: An AWS Network Load Balancer (NLB) accepting external client requests and distributing traffic across healthy pods in the private subnets.

What You Will Need

Before starting, ensure you have the following accounts, permissions, and command-line tools ready:

  • An active AWS Account with administrative permissions for VPC, EC2, RDS, IAM, ECR, EKS, and Secrets Manager.
  • AWS CLI (v2) installed and authenticated (aws login).
  • Docker installed and running on your local machine (or an EC2 build environment).
  • kubectl (Kubernetes command-line client) matching your target cluster version (v1.30 or v1.31).
  • eksctl (the official CLI for Amazon EKS).
  • Helm (v3) for package management on Kubernetes.
  • A terminal shell (Bash or Zsh).

Step 1: Provisioning the Multi-AZ Network Topology

The foundation of a secure cloud architecture is strict network segmentation. Compute and database resources must never share subnets with public-facing ingress points.

Subnet Layout and IP Allocation

Create a custom VPC (10.0.0.0/16) with the following subnet CIDR blocks:

  • public-subnet-1 (10.0.1.0/24) in us-east-1a
  • public-subnet-2 (10.0.2.0/24) in us-east-1c
  • private-app-subnet-1 (10.0.10.0/24) in us-east-1a
  • private-app-subnet-2 (10.0.11.0/24) in us-east-1c
  • private-data-subnet-1 (10.0.20.0/24) in us-east-1a
  • private-data-subnet-2 (10.0.21.0/24) in us-east-1c

Routing Rules and Gateway Integration

  1. Attach an Internet Gateway (IGW) to the VPC.
  2. Route 0.0.0.0/0 in the Public Route Table directly to the Internet Gateway.
  3. Deploy an AWS NAT Gateway inside public-subnet-1 and associate it with an Elastic IP.
  4. Route 0.0.0.0/0 in the Private Route Table to the NAT Gateway. Associate both private application subnets with this route table so EKS worker nodes can download system updates and container images without accepting inbound traffic.
  5. Create an EC2 Instance Connect Endpoint (EICE) inside private-app-subnet-1. This enables zero-trust private SSH tunneling into private instances directly through AWS IAM credentials without needing public IP addresses or internet gateways.

Step 2: Deploying Multi-AZ Amazon RDS and Storing Secrets

"Architecturally, the enterprise production blueprint specifies an Amazon RDS Multi-AZ deployment with synchronous standby replication across us-east-1a and us-east-1c for automated failover. However, to respect AWS Free Tier financial guardrails and prevent billing surprises during our development phase, I deployed a Single-AZ instance while establishing the DB Subnet Group across multiple availability zones. This delivers the exact network foundation required so that promoting the database to Multi-AZ for production requires zero architectural rework."

PrestaShop requires a reliable relational database backend. Instead of hosting MySQL directly inside a pod with volatile local storage, we delegate the persistence layer to managed Amazon RDS.

Creating the Multi-AZ Database (below are still the steps that would help you provision a multi-AZ DB if you have a paid account)

  1. Create a DB Subnet Group containing private-data-subnet-1 and private-data-subnet-2.
  2. Provision an Amazon RDS MySQL instance (version 8.0) using the Multi-AZ deployment option. This provisions a primary database in one Availability Zone and an active-standby replica in the second zone, handling synchronous replication and automated failover.
  3. Configure the database security group (rds-db-sg) to reject all inbound traffic by default.

Storing Credentials in AWS Secrets Manager

Create a secret named prestashop/db/credentials in AWS Secrets Manager:

aws secretsmanager create-secret \
    --name prestashop/db/credentials \
    --description "Database credentials for PrestaShop" \
    --secret-string '{"username":"admin","password":"YourStrongSecretPassword123!"}' \
    --region us-east-1
Enter fullscreen mode Exit fullscreen mode

By storing credentials in AWS Secrets Manager, we establish a single source of truth for sensitive values. We will authorize Kubernetes pods to read this secret dynamically at runtime.


Step 3: Containerizing PrestaShop and Pushing to Amazon ECR

The official PrestaShop image requires specific PHP runtime adjustments to prevent file upload bottlenecks and database timeouts during initial installation and catalog operations.

The Dockerfile

Create a Dockerfile that layers our required PHP configuration adjustments on top of the official PrestaShop image:

# Base image: Official PrestaShop 8.1 with Apache & PHP 8.1
FROM prestashop/prestashop:8.1-apache

# Custom PHP configuration for high performance
RUN echo "memory_limit = 512M\n\
upload_max_filesize = 64M\n\
post_max_size = 64M\n\
max_execution_time = 300\n\
max_input_vars = 5000" > /usr/local/etc/php/conf.d/custom-prestashop.ini

# Expose standard web port
EXPOSE 80
Enter fullscreen mode Exit fullscreen mode

Explaining the Technical Rationale

  • memory_limit = 512M: PrestaShop module compilations and database migrations exhaust PHP default limits (128M), causing fatal memory allocation errors during installation.
  • max_input_vars = 5000: Back-office catalog menus and product variations post hundreds of form parameters in a single HTTP request. The PHP default (1000) causes silent data truncation.
  • max_execution_time = 300: Allows long-running database population steps to complete without Apache terminating the gateway worker thread.

Building and Pushing to Amazon ECR

Create an Amazon ECR repository and publish the image with an explicit release tag:

# 1. Create the repository
aws ecr create-repository --repository-name prestashop --region us-east-1

# 2. Authenticate Docker with Amazon ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com

# 3. Build the container image
docker build -t prestashop:8.1 .

# 4. Tag the container image for ECR
docker tag prestashop:8.1 <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/prestashop:8.1

# 5. Push the image
docker push <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/prestashop:8.1
Enter fullscreen mode Exit fullscreen mode

Always tag container images with a semantic version (such as :8.1) rather than relying on :latest. As I will demonstrate in the troubleshooting section, relying on untagged references creates immediate deployment failures in Kubernetes.


Step 4: Provisioning the Amazon EKS Cluster and Worker Nodes

With the network and container repository established, we provision the Kubernetes control plane and compute nodes.

Required IAM Roles

EKS operates on two distinct permission layers:

  1. Cluster Role (prestashop-eks-cluster-role): Allows Kubernetes control plane components to manage AWS resources (such as load balancers and elastic network interfaces). Attach the AWS-managed policy AmazonEKSClusterPolicy.
  2. Node Instance Role (prestashop-eks-node-role): Assigned to the EC2 worker nodes. Attach three mandatory AWS-managed policies:
    • AmazonEKSWorkerNodePolicy: Allows worker nodes to register with the control plane.
    • AmazonEC2ContainerRegistryReadOnly: Permits pulling images from Amazon ECR.
    • AmazonEKS_CNI_Policy: Enables the AWS VPC CNI plugin to allocate and assign VPC IP addresses to pods.

Cluster Creation and Endpoint Access Architecture

Create the EKS cluster within your custom VPC.

A vital architectural setting is Cluster Endpoint Access:

  • Private Access: MUST be enabled. This creates cross-account Elastic Network Interfaces (ENIs) directly inside your private subnets, allowing worker node kubelet daemons to communicate with the Kubernetes API server internally without traversing the public internet.
  • Public Access: Enabled with CIDR restrictions so you can execute administrative kubectl commands from your local machine.

Node Group Sizing and the Private Subnet Placement

Provision the Managed Node Group placed exclusively inside private-app-subnet-1 and private-app-subnet-2:

  • Instance type: t3.small (Free Tier eligible on modern accounts, supporting 110 pods per node and 2 GiB of memory).
  • Desired capacity: 2 nodes. Minimum: 1, Maximum: 3.

Authorizing EKS Pods to Access RDS

Update the inbound rules of the RDS security group (rds-db-sg):

  • Protocol: TCP
  • Port: 3306 (MySQL)
  • Source: The Security Group ID of the EKS Worker Nodes.

This ensures that only workloads running on verified cluster nodes can establish socket connections with the database.


Step 5: Kubernetes Manifests and Zero-Trust Secrets Integration

Rather than creating Kubernetes secrets manually with base64-encoded strings, we integrate the cluster directly with AWS Secrets Manager using the Secrets Store CSI Driver and IAM Roles for Service Accounts (IRSA).

Step 5.1: Configure IAM Roles for Service Accounts (IRSA)

IRSA eliminates the need to store long-lived AWS credentials inside Kubernetes worker nodes.

# 1. Associate the IAM OIDC Provider with your cluster
eksctl utils associate-iam-oidc-provider \
    --region us-east-1 \
    --cluster your-cluster-name \
    --approve

# 2. Create the IAM policy granting access to Secrets Manager
aws iam create-policy \
    --policy-name prestashop-secret-access-policy \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "secretsmanager:GetSecretValue",
                    "secretsmanager:DescribeSecret"
                ],
                "Resource": "arn:aws:secretsmanager:us-east-1:441356679525:secret:prestashop/db/credentials-8pAAsz"
            }
        ]
    }'

# 3. Create the Kubernetes namespace
kubectl create namespace prestashop

# 4. Bind the Kubernetes Service Account to the IAM Policy
eksctl create iamserviceaccount \
    --name prestashop-service-account \
    --namespace prestashop \
    --region us-east-1 \
    --cluster your-eks-cluster-name \
    --attach-policy-arn "arn:aws:iam::<ACCOUNT_ID>:policy/prestashop-secret-access-policy" \
    --approve \
    --override-existing-serviceaccounts
Enter fullscreen mode Exit fullscreen mode

Step 5.2: Install the Secrets Store CSI Driver with STS Support

Install the driver using Helm, passing the STS token audience flag:

# Add the Helm repository
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm repo update

# Install the CSI driver with secret synchronization and STS tokens enabled
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
    --namespace kube-system \
    --set syncSecret.enabled=true \
    --set "tokenRequests[0].audience=sts.amazonaws.com"

# Install the official AWS Provider plugin
kubectl apply -f https://raw.githubusercontent.com/aws/secrets-store-csi-driver-provider-aws/main/deployment/aws-provider-installer.yaml
Enter fullscreen mode Exit fullscreen mode

Step 5.3: The SecretProviderClass Manifest (secret-provider-class.yaml)

This custom resource instructs the CSI driver which secret object to retrieve from AWS Secrets Manager and maps the JSON keys (username and password) into a synchronized Kubernetes Secret:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: prestashop-secret-provider-class
  namespace: prestashop
spec:
  provider: aws
  secretObjects:
  - secretName: prestashop-db-secret
    type: Opaque
    data:
    - objectName: DB_PASSWORD
      key: password
    - objectName: DB_USER
      key: username
  parameters:
    objects: |
      - objectName: "arn:aws:secretsmanager:us-east-1:<ACCOUNT_ID>:secret:prestashop/db/credentials-8pAAsz"
        jmesPath:
          - path: password
            objectAlias: DB_PASSWORD
          - path: username
            objectAlias: DB_USER
Enter fullscreen mode Exit fullscreen mode

Why This Design Matters

The CSI driver retrieves the secret from AWS over an encrypted channel using short-lived tokens. The secretObjects directive synchronizes these values into a native Kubernetes Secret (prestashop-db-secret), allowing pods to consume them as environment variables without exposing raw values in Git repositories.

Step 5.4: The Deployment Manifest (deployment.yaml)

Deploy PrestaShop into the prestashop namespace, referencing the Service Account and mounting the inline CSI volume:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: prestashop-deployment
  namespace: prestashop
  labels:
    app: prestashop
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prestashop
  template:
    metadata:
      labels:
        app: prestashop
    spec:
      serviceAccountName: prestashop-service-account
      volumes:
      - name: secrets-store-inline
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: prestashop-secret-provider-class
      containers:
      - name: prestashop
        image: 441356679525.dkr.ecr.us-east-1.amazonaws.com/prestashop:8.1
        imagePullPolicy: Always
        ports:
        - containerPort: 80
        env:
        - name: DB_SERVER
          value: "prestashop-db.cepkyuma02s4.us-east-1.rds.amazonaws.com"
        - name: DB_NAME
          value: "prestashop"
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: prestashop-db-secret
              key: username
        - name: DB_PASSWD
          valueFrom:
            secretKeyRef:
              name: prestashop-db-secret
              key: password
        - name: PS_HANDLE_REVERSE_PROXY
          value: "1"
        volumeMounts:
        - name: secrets-store-inline
          mountPath: "/mnt/secrets-store"
          readOnly: true
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "500m"
Enter fullscreen mode Exit fullscreen mode

Explaining the Configuration

  • volumeMounts: In Kubernetes Secrets Store CSI architecture, secrets are only fetched from AWS when a pod mounts the CSI volume. Mounting the volume triggers the retrieval, after which the synchronized Kubernetes secret is generated.
  • PS_HANDLE_REVERSE_PROXY=1: Informs PrestaShop that client SSL termination occurs upstream at the AWS load balancer. Without this setting, PrestaShop enters infinite redirection loops.
  • Explicit CPU and memory limits ensure that the PHP worker threads do not cause node-level memory exhaustion.

Step 5.5: The Service Manifest (service.yaml)

Expose the application through an AWS Network Load Balancer (NLB):

apiVersion: v1
kind: Service
metadata:
  name: prestashop-service
  namespace: prestashop
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-internal: "false"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: prestashop
Enter fullscreen mode Exit fullscreen mode

Apply all three manifests:

kubectl apply -f secret-provider-class.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
Enter fullscreen mode Exit fullscreen mode

Step 6: Post-Installation Hardening inside the Running Pod

Once the PrestaShop installation assistant completes database population via the browser, PrestaShop locks access to the administration dashboard until two mandatory security steps are performed:

  1. Deleting the /install directory to prevent unauthorized re-installation.
  2. Renaming the default /admin directory to an unguessable unique identifier.

In a traditional VM environment, you would SSH into the server to execute filesystem changes. In Kubernetes, we execute these commands directly inside the active container using kubectl exec:

# 1. Obtain the running pod name
POD_NAME=$(kubectl get pods -n prestashop -l app=prestashop -o jsonpath="{.items[0].metadata.name}")

# 2. Delete the install directory
kubectl exec -it -n prestashop $POD_NAME -- rm -rf /var/www/html/install

# 3. Rename the admin directory to the suggested unique path
kubectl exec -it -n prestashop $POD_NAME -- mv /var/www/html/admin /var/www/html/admin107nz3byc5pv2nr5xgj
Enter fullscreen mode Exit fullscreen mode

Once executed, the administrative back-office becomes immediately accessible at:
http://<LOAD_BALANCER_DNS>/admin107nz3byc5pv2nr5xgj/


The Build Log: Real-World Hiccups and How I Resolved Them

Infrastructure projects rarely proceed without obstacles. Production engineering is defined by how you diagnose and resolve unexpected failures. Below are the actual roadblocks encountered during this deployment, along with the diagnostic processes and technical resolutions.

Hiccup 1: Homebrew Dependency Deadlocks on macOS

  • The Symptom: Running brew install kubectl on macOS 13 triggered Homebrew to build eleven low-level GNU dependencies (ncurses, bash, gettext, libunistring, coreutils) from source. The build stalled indefinitely due to network timeouts when connecting to ftp.gnu.org.
  • The Root Cause: Homebrew deprecates older macOS releases and ceases distributing pre-compiled binary packages ("bottles"), forcing source compilation on missing dependencies.
  • The Solution: Bypass package managers entirely for standalone Go and compiled binaries. I installed official pre-built binaries directly into /usr/local/bin using curl:
# Direct install for kubectl
curl -LO "https://dl.k8s.io/release/v1.31.0/bin/darwin/amd64/kubectl"
chmod +x ./kubectl && sudo mv ./kubectl /usr/local/bin/kubectl

# Direct install for eksctl
curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_Darwin_amd64.tar.gz"
tar -xzf eksctl_Darwin_amd64.tar.gz -C /tmp && sudo mv /tmp/eksctl /usr/local/bin/eksctl

# Direct install for helm
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh && ./get_helm.sh && rm get_helm.sh
Enter fullscreen mode Exit fullscreen mode

Hiccup 2: The AWS Free Tier Pod Density Bottleneck (Too many pods)(NodeCreationFailure)

  • The Symptom: Core Kubernetes components (coredns, metrics-server, and the Secrets Store CSI driver) remained permanently stuck in Pending state. Describing the pending pods returned: 0/2 nodes are available: 2 Too many pods.
  • The Root Cause: When initially attempting to stay within Free Tier limits using t3.micro instances, AWS hardcodes a maximum limit of 4 pods per node. AWS calculates this limit based on the physical network cards (ENIs) of the instance:

$$\text{Max Pods} = (\text{Number of ENIs} \times (\text{IPs per ENI} - 1)) + 2$$

For t3.micro, $(2 \times (2 - 1)) + 2 = 4\text{ pods}$. Because AWS system daemons (aws-node, kube-proxy, eks-pod-identity-agent, eks-node-monitoring-agent) occupied all four slots on both nodes, the cluster was operating at 100% capacity before application pods were even scheduled.

  • The Solution: Rather than launching excessive micro nodes, I reviewed AWS instance policies and identified that modern AWS accounts include t3.small in their Free Tier allowance. Moving to t3.small provided:
    • 110 pods per node, accommodating all system agents, CoreDNS, CSI drivers, and PrestaShop.
    • 2 GiB of RAM per node, providing necessary memory margins for Apache and PHP worker processes.

Hiccup 3: Secrets Store CSI Driver STS Token Trap

  • The Symptom: The PrestaShop pod remained stuck in ContainerCreating for hours. Inspecting pod events (kubectl describe pod) revealed: Warning FailedMount: MountVolume.SetUp failed for volume "secrets-store-inline" : rpc error: code = Unknown desc = CSI token error: serviceAccount.tokens not provided - ensure tokenRequests is configured in CSIDriver spec
  • The Root Cause: Modern Kubernetes releases require the Secrets Store CSI Driver to request short-lived projected service account tokens (sts.amazonaws.com) from the Kubernetes API server before communicating with the AWS Provider. The CSI driver had been installed without the STS token audience configured, preventing the provider from assuming the IAM role via Web Identity Federation.
  • The Solution: Upgraded the Helm release to inject the STS audience configuration and recycled the stalled pod:
# Upgrade Helm release with STS token request configured
helm upgrade csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
    --namespace kube-system \
    --set syncSecret.enabled=true \
    --set "tokenRequests[0].audience=sts.amazonaws.com"

# Delete the blocked pod to trigger an immediate fresh mount attempt
kubectl delete pod -n prestashop -l app=prestashop
Enter fullscreen mode Exit fullscreen mode

Hiccup 4: Rolling Update Slot Deadlock and Image Tag Mismatch

  • The Symptom: Following the CSI fix, the new pod entered ImagePullBackOff. When the deployment was updated, a new pod remained stuck in Pending while the old pod sat in ImagePullBackOff.
  • The Root Cause: Two issues compounded here:
    1. The container image specified in deployment.yaml lacked an explicit tag, causing Kubernetes to request :latest from Amazon ECR. Because the image was pushed as :8.1, ECR rejected the pull.
    2. When the image was updated, Kubernetes initiated a rolling update by attempting to start the new replica before terminating the old replica. Because the node was near its pod allocation ceiling, the failed pod occupied the final slot, preventing the new pod from scheduling.
  • The Solution: Updated the deployment with the explicit :8.1 image tag and manually deleted the stuck pod so the new replica could claim the available node slot immediately:
# Update image tag
kubectl set image deployment/prestashop-deployment prestashop=441356679525.dkr.ecr.us-east-1.amazonaws.com/prestashop:8.1 -n prestashop

# Remove the failed pod occupying the node slot
kubectl delete pod -n prestashop prestashop-deployment-686796b775-s7k2z
Enter fullscreen mode Exit fullscreen mode

Hiccup 5: The Reverse Proxy SSL Enforcement Loop

  • The Symptom: On the day following initial deployment, the storefront suddenly stopped loading in web browsers, presenting a connection timeout error.
  • The Root Cause: Inspecting raw HTTP headers with curl -IL revealed that Apache was returning: Status: 301 Moved Permanently Location: https://aad1534e7...elb.amazonaws.com/

PrestaShop had internal SSL enforcement enabled (PS_SSL_ENABLED=1) in its database. Because the AWS Network Load Balancer was currently configured to listen only on HTTP port 80, the browser followed the 301 redirect to HTTPS port 443, where no listener existed, resulting in a connection timeout.

  • The Solution: Executed a PHP one-liner directly inside the pod to toggle SSL enforcement off in the database until an ACM SSL certificate and HTTPS listener were attached to the load balancer:
kubectl exec -it -n prestashop $(kubectl get pods -n prestashop -l app=prestashop -o jsonpath="{.items[0].metadata.name}") -- php -r '
$db = new PDO("mysql:host=" . getenv("DB_SERVER") . ";dbname=" . getenv("DB_NAME"), getenv("DB_USER"), getenv("DB_PASSWD"));
$db->exec("UPDATE ps_configuration SET value = 0 WHERE name IN (\"PS_SSL_ENABLED\", \"PS_SSL_ENABLED_EVERYWHERE\");");
echo "SSL Disabled successfully\n";
'
Enter fullscreen mode Exit fullscreen mode

Once executed and tested in a private browser session to avoid cached redirects, the storefront loaded immediately over HTTP.


What I Would Improve in a v2

While this deployment satisfies zero-trust security and multi-AZ availability requirements, production engineering requires continuous iteration. For a version 2 of this architecture, I would implement:

  1. Shared Persistent Storage via Amazon EFS: PrestaShop stores customer uploads, module configurations, and product imagery on the local container filesystem. Scaling this deployment to multiple pod replicas across Availability Zones requires decoupling state from the container using the Amazon EFS CSI Driver with ReadWriteMany (RWX) volume mounts.
  2. Infrastructure as Code (IaC) with Terraform: Replace manual AWS CLI and eksctl commands with modular Terraform configurations, managing VPC topology, RDS instances, IAM roles, and EKS clusters declaratively with remote state locking.
  3. Automated GitOps Continuous Delivery: Implement ArgoCD to synchronize Kubernetes manifests directly from a Git repository, eliminating manual kubectl apply commands and enforcing automated drift correction.
  4. End-to-End TLS Termination with AWS Load Balancer Controller: Provision an AWS Application Load Balancer (ALB) using the AWS Load Balancer Controller, automatically requesting and renewing SSL certificates via AWS Certificate Manager (ACM) and managing DNS records with Route 53.

What Is Next

In my next project, I will transition from script-driven and CLI deployments to fully declarative Infrastructure as Code (IaC) using Terraform, provisioning multi-environment cloud platforms with automated testing and continuous integration pipelines.

You can view the complete source code, Docker configuration, and Kubernetes manifests for this project in the repository below:

Top comments (0)