A video for this project is coming soon (like this). The source code is split into three repos: (api), (frontend), and (manifests).


What I Built

I built a voting app where people pick their favorite of three movies: Star Wars, Avatar, or Avengers. I hosted the whole thing on AWS with Elastic Kubernetes Service (EKS).

  1. Frontend: A React interface where people submit their votes.

  2. Backend: A Go API that takes votes and updates the database.

  3. Database: MongoDB, which stores the vote records.

Architecture

VPC (Virtual Private Cloud)

The private network that holds everything. Mine runs inside AWS.

Cluster

A Kubernetes cluster is a group of nodes (machines) that run containers together. It’s the core of the whole setup.

Namespace

A namespace splits up cluster resources. Think of it as a smaller cluster inside the real one.

Services

A Service defines a set of Pods and how to reach them. I used a ClusterIP service for the MongoDB StatefulSet and LoadBalancer services for the API and frontend.

  • mongo: A ClusterIP service. It’s only reachable from inside the cluster. The API uses it to reach the MongoDB StatefulSet.
  • api: A LoadBalancer service. It’s reachable from outside the cluster. It points to the API Deployment.
  • frontend: A LoadBalancer service. It’s reachable from outside the cluster. It points to the frontend Deployment.

StatefulSet

A StatefulSet is built for stateful apps like databases. I used it for MongoDB because it needs persistent storage and stable network names. Mine runs three replica pods for availability. The primary pod handles reads and writes. The secondaries keep copies of the data.

  • pod1, pod2, pod3: Each one is a MongoDB instance inside the StatefulSet.
  • pvc0, pvc1, pvc2 and pv0, pv1, pv2: These are Persistent Volume Claims (PVC) and Persistent Volumes (PV).
    • PVC (Persistent Volume Claim): A request for storage. A PVC uses up PV resources, the same way a Pod uses up node resources.
    • PV (Persistent Volume): A chunk of storage added to the cluster through a StorageClass. Mine uses gp2.

Deployments

Deployments run stateless apps. They create Pods and scale them. I have two Deployments, each with a ReplicaSet. One is for the API and one is for the frontend.

  • api: Runs the Go API that handles votes and talks to the database.

  • frontend: Runs the React interface people use to vote.

    • ReplicaSet: Keeps the right number of Pod replicas running at all times. This gives you self-healing and scaling.

How a Vote Flows

  1. A user submits a vote on the frontend.
  2. The frontend sends it to the API.
  3. The API updates the database.
  4. The database saves it and responds to the API.
  5. The API tells the frontend the vote went through.
  6. The user sees the new count.

What You Need

You need an AWS account with the AWS CLI installed. You also need kubectl and Docker.

Install links:

Initial Setup

IAM User

I followed least privilege and created a new IAM user just for this. Mine is called ‘kubectl’ and has my kubectl-policy custom policy attached. I used this user to configure AWS in my terminal.

The kubectl-policy policy lets the user describe, list, and access the EKS cluster.

Here’s the policy in JSON:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "eks:DescribeCluster",
      "eks:ListClusters",
      "eks:DescribeNodegroup",
      "eks:ListNodegroups",
      "eks:ListUpdates",
      "eks:AccessKubernetesApi"
    ],
    "Resource": "*"
  }]
}

IAM Cluster Role

For the cluster role, I created a new role and attached the Amazon EKS Cluster IAM Role policy.

IAM Node Group Role

For the node group role, I created a new role and attached four AWS managed policies.

Setting Up the EKS Cluster

Creating the Cluster

I created a new EKS cluster with the default settings and attached my cluster role.

Once it was up, I made a node group with two t2.medium nodes and attached my node group role.

I also added the Amazon EBS CSI Driver under add-ons. It creates EBS volumes on demand when a PVC asks for storage. MongoDB’s StatefulSet won’t work without it.

Connecting kubectl and Fixing the Config Map

With the cluster and node group ready, I had to connect kubectl to the cluster. kubectl is the command line tool for working with Kubernetes.

First I configured the AWS CLI with my IAM user’s access keys.

aws configure

I checked that it worked with:

aws sts get-caller-identity

It returned the ARN of my IAM user.

Then I set the kubectl context:

aws eks update-kubeconfig --name <eks-cluster-name> --region <aws-region>

Next I wanted to create a namespace and deploy MongoDB. But my IAM user didn’t have permission. Running kubectl get nodes gave me error: You must be logged in to the server (Unauthorized). The user had no access to the cluster yet.

If I ran aws configure with the root user’s keys instead, kubectl get nodes worked fine. That’s because the root user created the cluster.

To fix it, I edited the aws-auth config map. The easiest way was AWS Cloud Shell, which already has root credentials. I ran kubectl edit configmap aws-auth -n kube-system

It returned:

apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::007575380756:role/<IAM-role> 
      username: system:node:{{EC2PrivateDNSName}}
kind: ConfigMap
metadata:
  creationTimestamp: "2024-01-25T09:43:30Z"
  name: aws-auth
  namespace: kube-system
  resourceVersion: "1366"
  uid: 1e2ae7a7-db28-4fb7-aa8e-9ce9b8cc98b0

I added a mapUsers section under mapRoles like this:

apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::007575380756:role/<IAM-role>
      username: system:node:{{EC2PrivateDNSName}}
  mapUsers: |
    - userarn: arn:aws:iam::007575380756:user/<IAM-user>
      username: <IAM-user>
      groups:
        - system:masters    
kind: ConfigMap
metadata:
  creationTimestamp: "2024-01-25T09:43:30Z"
  name: aws-auth
  namespace: kube-system

Back in my terminal with the IAM user’s credentials, kubectl get nodes worked. The nodes showed as ready:

NAME                            STATUS   ROLES    AGE   VERSION
ip-172-31-33-204.ec2.internal   Ready    <none>   53m   v1.29.0-eks-5e0fdde
ip-172-31-7-200.ec2.internal    Ready    <none>   53m   v1.29.0-eks-5e0fdde

Deploying the Resources

I deployed everything with manifest files. A manifest is a YAML file that describes a Kubernetes resource. You apply one with kubectl apply -f <file-name>.

Here’s what each manifest does:

  • mongo-statefulset.yaml: Defines the MongoDB StatefulSet. It sets the image, replica count, storage claims, and the replica set config.
  • mongo-service.yaml: Creates a ClusterIP Service for MongoDB. It works like an internal load balancer so other pods can reach the MongoDB pods. Its selectors match the labels on the StatefulSet.
  • mongo-secret.yaml: Creates a Secret for the MongoDB username and password. The API reads it so the credentials aren’t hardcoded.
  • api-deployment.yaml: Defines the Deployment for the Go API. It sets the image, replica count, and the environment variables the API needs to reach MongoDB. Those point to the MongoDB secret.
  • api-service.yaml: Creates a LoadBalancer Service for the API, since it needs outside access. It maps an external port to the container port.
  • frontend-deployment.yaml: Defines the Deployment for the React frontend. It sets the image, replica count, and environment variables, including the API host so the frontend can reach the API.
  • frontend-service.yaml: Creates a LoadBalancer Service for the frontend so users can reach it from outside the cluster.

Creating a Namespace

I ran kubectl create ns <namespace-name> to create the namespace.

Then kubectl get ns to confirm it was there.

Deploying the MongoDB StatefulSet

Next I ran kubectl apply -f mongo-statefulset.yaml to apply the StatefulSet to the cluster.

Here’s mongo-statefulset.yaml with comments:

apiVersion: apps/v1 

kind: StatefulSet # Defines the type of Kubernetes object (StatefulSet)

metadata: 
  name: mongo 
  namespace: <namespace-name> 

spec: # specification defining the desired state of the StatefulSet.
  serviceName: mongo # name used to connect to the pods 

  replicas: 3 # number of pods

  selector: # identifies the set of pods 
    matchLabels:
      role: db # this label selector determines which pods are part of the group
  template: #  describes the pods that will be created.
    metadata:
      labels:
        role: db # Labels assigned to the pods
        env: demo # env label
        replicaset: rs0.main # the replica set label (mongo replicaset)

    spec: # pod specification
      affinity: # Affinity settings for the pod placement.
        podAntiAffinity: # Defines rules to separate pods from each other.
          preferredDuringSchedulingIgnoredDuringExecution: # Soft rule for anti-affinity.
            - weight: 100 # Weight associated with the scheduling preference.
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: replicaset
                      operator: In
                      values:
                        - rs0.main # Ensures pods with the same replicaset label (rs0.main) are not on the same host.
                topologyKey: kubernetes.io/hostname # Focuses on the hostname of the node for pod placement. 

      terminationGracePeriodSeconds: 10 

      containers:
        - name: mongo # Name of the container.
          image: mongo:4.2 # The Docker image to use for the container.
          command: # Command to run inside the container.
            - "numactl" #Command to control NUMA policy
            - "--interleave=all" #Option to interleave memory allocations across all NUMA nodes
            - "mongod" #Command to run the MongoDB daemon
            - "--wiredTigerCacheSizeGB" #Option to specify WiredTiger cache size (GB)
            - "0.1" #Cache size
            - "--bind_ip" # Option to specify the IP address to bind to
            - "0.0.0.0" # MongoDB will listen for incoming connections on all network interfaces
            - "--replSet" # Used to specify the name of the replica set in MongoDB.
            - "rs0" # Name of the MongoDB replica set
          ports:
            - containerPort: 27017 # The port on which the container is listening.
  
          volumeMounts: #attaches the persistent volume to the container
            - name: mongodb-persistent-storage-claim 
              mountPath: /data/db 

  volumeClaimTemplates: # Templates for creating persistent volume claims(PVC).
    - metadata:
        name: mongodb-persistent-storage-claim 
      spec:
        accessModes:
          - ReadWriteOnce 
        storageClassName: gp2 #type of storage that is provided by AWS EBS
        resources:
          requests:
            storage: 0.5Gi # Amount

To watch the pods come up, I ran kubectl get pods -n <namespace-name> -w. The -w flag watches and -n sets the namespace. The output looked like this:

AME      READY   STATUS              RESTARTS   AGE
mongo-0   0/1     ContainerCreating   0          6s
mongo-0   1/1     Running             0          7s
mongo-1   0/1     Pending             0          0s
mongo-1   0/1     Pending             0          0s
mongo-1   0/1     ContainerCreating   0          0s
mongo-1   1/1     Running             0          25s
mongo-2   0/1     Pending             0          0s
mongo-2   0/1     Pending             0          0s
mongo-2   0/1     ContainerCreating   0          0s
mongo-2   1/1     Running             0          14s

I also checked the persistent volume claims with kubectl get pvc -n <namespace-name>

NAME                                       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
mongodb-persistent-storage-claim-mongo-0   Bound    pvc-d93f7cc3-c922-4f0d-825f-3ab261e10594   1Gi        RWO            gp2            <unset>                 25m
mongodb-persistent-storage-claim-mongo-1   Bound    pvc-6f411063-8946-4e57-aa91-456733a33100   1Gi        RWO            gp2            <unset>                 4m33s
mongodb-persistent-storage-claim-mongo-2   Bound    pvc-ca7aa5aa-0f8b-41b2-9a92-931ab1f7e567   1Gi        RWO            gp2            <unset>                 4m8s

Creating a Service for the StatefulSet

I exposed MongoDB with a headless service (no ClusterIP). It’s internal only and not reachable from the internet. It lets other pods connect to each StatefulSet pod directly.

Here’s mongo-service.yaml:

apiVersion: v1                            
kind: Service                            
metadata:
  name: mongo                             
  namespace: <namespace-name>                       
  labels:                                
    role: db
    env: demo
spec:
  ports:
  - port: 27017      #where the service will be listening
    targetPort: 27017  #where the service will be routing traffic to
  clusterIP: None      #what makes the service headless
  selector:    #identifies which pods this service will route traffic to
    role: db

I created it with kubectl apply -f mongo-service.yaml and got back service/mongo created

Then I checked it:

kubectl get svc -n <namespace-name>

The output:

NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE
mongo   ClusterIP   None         <none>        27017/TCP   2m10s

Setting Up Replication and Adding Data

With the service in place, I set up replication. I opened the mongo shell inside the mongo-0 pod.

kubectl exec -it mongo-0 -n <namespace-name> -- mongo

I made mongo-0 the primary. mongo-1 and mongo-2 became the secondaries.

I ran these commands in the mongo shell:

# initialize the MongoDB replica set
rs.initiate();

# set a delay of 2 seconds to allow rs.initiate() to complete
sleep(2000);

# add a secondary node to the replica set along with its hostname and port
rs.add("mongo-1.mongo:27017");

# add another secondary node 
rs.add("mongo-2.mongo:27017");

# set a delay to allow secondary nodes to join the replica set and sync data
sleep(2000);

# fetch the current replica set configuration and store it in the 'cfg' variable
cfg = rs.conf();

# change configuration to specify the hostname of the primary node
cfg.members[0].host = "mongo-0.mongo:27017";

# apply the new configuration to the replica set
rs.reconfig(cfg, {force: true});

# set a 5 second delay for the replica set to stabilize and complete the reconfiguration process
sleep(5000);

Then I switched to the moviedb database with use moviedb; and inserted the movie data. The database gets created automatically on the first insert.


db.movies.insert({
  "title": "avengers",
  "moviedetail": {
    "director": "Joss Whedon",
    "genre": "Action, Adventure, Sci-Fi",
    "releaseYear": 2012,
    "votes": 0
  }
});

db.movies.insert({
  "title": "avatar",
  "moviedetail": {
    "director": "James Cameron",
    "genre": "Action, Adventure, Fantasy",
    "releaseYear": 2009,
    "votes": 0
  }
});

db.movies.insert({
  "title": "starwars",
  "moviedetail": {
    "director": "George Lucas",
    "genre": "Action, Adventure, Fantasy",
    "releaseYear": 1977,  
    "votes": 0
  }
});

I checked the data with db.movies.find().pretty(); and got:


rs0:PRIMARY> db.movies.find().pretty();
{
        "_id" : ObjectId("65b393287f665c0281d522e5"),
        "title" : "avengers",
        "moviedetail" : {
                "director" : "Joss Whedon",
                "genre" : "Action, Adventure, Sci-Fi",
                "releaseYear" : 2012,
                "votes" : 0
        }
}
{
        "_id" : ObjectId("65b393377f665c0281d522e6"),
        "title" : "avatar",
        "moviedetail" : {
                "director" : "James Cameron",
                "genre" : "Action, Adventure, Fantasy",
                "releaseYear" : 2009,
                "votes" : 0
        }
}
{
        "_id" : ObjectId("65b3933f7f665c0281d522e7"),
        "title" : "starwars",
        "moviedetail" : {
                "director" : "George Lucas",
                "genre" : "Action, Adventure, Fantasy",
                "releaseYear" : 1977,
                "votes" : 0
        }
}

Creating the MongoDB Secret

Next I created a secret to hold the MongoDB username and password.

Here’s mongo-secret.yaml:

apiVersion: v1                    
kind: Secret    
metadata:
  name: mongodb-secret                   
  namespace: <namespace-name>                
data:
  username: YWRtaW4=    # This is 'admin' in Base64 encoding
  password: cGFzc3dvcmQ=  # This is 'password' in Base64 encoding
# in kubernetes secrets are stored in base64 encoding

I ran kubectl apply -f mongo-secret.yaml and got back secret/mongodb-secret created

Deploying the API

The API updates the database every time someone votes. I wrote it in Go.

I deployed it with kubectl apply -f api-deployment.yaml and got back deployment.apps/api created

Here’s api-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: <namespace-name>
  labels:
    app: api  
    env: demo
spec:
  replicas: 2  #two pods (containers) created
  strategy:
    type: RollingUpdate #rolling update for zero downtime
    rollingUpdate:
      maxSurge: 1  #pods that can be created above 2
      maxUnavailable: 25% # % of pods that can be unavailable during update
  selector:
    matchLabels:
      app: api  
  template:
    metadata:
      labels:
        app: api 
    spec:
      containers:
      - name: api
        image: davidsvida/api-deploy:v7 #docker image
        imagePullPolicy: Always  # ensure latest image
        env: #env variables for the container
          - name: MONGO_CONN_STR
            value: mongodb://mongo-0.mongo,mongo-1.mongo,mongo-2.mongo:27017/moviedb?replicaSet=rs0
          - name: MONGO_USERNAME
            valueFrom:
              secretKeyRef:  #to retrieve value from k8s secret
                name: mongodb-secret
                key: username
          - name: MONGO_PASSWORD
            valueFrom:
              secretKeyRef:
                name: mongodb-secret
                key: password
        ports:
        - containerPort: 8080 #listening port
        livenessProbe:  #check container health
          httpGet:
            path: /ok
            port: 8080
          initialDelaySeconds: 2  #wait 2 seconds before checking  
          periodSeconds: 5 #check every 5 seconds
        readinessProbe: # check if container is ready to receive traffic
          httpGet:
            path: /ok
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
          successThreshold: 1  #after 1 successful check, container is ready

I checked with kubectl get pods -n <namespace-name> and saw:

NAME                  READY   STATUS    RESTARTS   AGE
api-579856b9cd-f255n  1/1     Running   0          88s
api-579856b9cd-kgch8  1/1     Running   0          88s
mongo-0               1/1     Running   0          4h54m
mongo-1               1/1     Running   0          4h53m
mongo-2               1/1     Running   0          4h53m

Creating a Service for the API

This step needs an SSL certificate from AWS Certificate Manager (ACM). ACM certificates for your own domain are free. I grabbed the certificate ARN and swapped it in for arn:aws:acm:region:account-id:certificate/certificate-id in api-service.yaml.

Here’s api-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: <namespace-name>
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn:aws:acm:region:account-id:certificate/certificate-id" #for our ELB to use SSL
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http" #for internal communication
  labels:
    app: api 
spec:
  selector:
    app: api  
  ports:
    - protocol: TCP
      port: 443 #indicates listening HTTPS traffic
      targetPort: 8080
  type: LoadBalancer  #type of service(LoadBalancer)

I exposed the API through a load balancer with kubectl apply -f api-service.yaml and got back service/api created

To check it, I ran kubectl get svc -n <namespace-name> and saw both services:

NAME    TYPE           CLUSTER-IP      EXTERNAL-IP                                                              PORT(S)        AGE
api     LoadBalancer   10.100.120.76    af6d6ac51fafc48cc92ac450ac73c1b0-936014577.us-east-1.elb.amazonaws.com    443:31938/TCP   4s
mongo   ClusterIP      None            <none>                                                                   27017/TCP      132m

Pointing DNS at the Load Balancer

The api-service.yaml annotation service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn:aws:acm:region:account-id:certificate/certificate-id" tells the ELB to use my ACM certificate. For that certificate to actually work, the load balancer needs a subdomain on the domain the certificate covers. So I created a CNAME record in Route 53 pointing to the EXTERNAL-IP of the API service.

It took a few minutes for the load balancer and DNS to go live. Then I added /movies to the end of the DNS name and got back the data I inserted earlier.

Building the Frontend Image

The React frontend reads the API address from the .env file. Since React compiles into a static site, I had to set this before building the Docker image.

Clone the Repo

  • I cloned the frontend repo:
  git clone https://github.com/davidsvida/frontend-deploy

Update the .env File

  • I opened the .env file:
  vim .env
  • Then I set REACT_APP_APIHOSTPORT to the API subdomain I created in Route 53:
REACT_APP_APIHOSTPORT=<subdomain>.<domain>.<tld>

Build and Push the Image

Next I built the image and pushed it to Docker Hub. I was on a Mac, so I used buildx to build for both arm64 (Apple Silicon) and amd64 (Intel).

Check that buildx is installed with docker buildx version first. If it isn’t, run docker buildx install

docker buildx create --name mybuilder --use
docker buildx inspect --bootstrap
docker buildx build --platform linux/amd64,linux/arm64 -t <your-dockerhub-username>/<your-dockerhub-repo-name>:<version-number> . --push

What each line does:

  • docker buildx create --name mybuilder --use: Creates a builder called mybuilder and makes it the active one.
  • docker buildx inspect --bootstrap: Inspects the builder and gets it ready to use.
  • docker buildx build --platform linux/amd64,linux/arm64 -t <your-dockerhub-username>/<your-dockerhub-repo-name>:<version-number> . --push: Builds the image for arm64 and amd64 and pushes it to Docker Hub.

A Note on This Step

Updating the .env file by hand isn’t ideal. A serverless API or a CI/CD pipeline could automate it. I didn’t have time to build either for this project.


Deploying the Frontend

With the image on Docker Hub, I deployed the frontend and pointed it at that image.

Here’s frontend-deployment.yaml. It’s close to api-deployment.yaml. The main differences are the image, environment variables, and label selectors.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: <namespace-name>
  labels:
    app: frontend
    env: demo
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 25%
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: <your-dockerhub-username>/<your-dockerhub-repo-name>:<version-number>  #the docker image we built and pushed to docker hub
        imagePullPolicy: Always
        env:
          - name: API_HOST_PORT
            value: "<subdomain>.<domain>.<tld>" #the environment variable previously set in the .env file
        ports:
        - containerPort: 8080 
        livenessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 3

I ran kubectl apply -f frontend-deployment.yaml and got back deployment.apps/frontend created

Creating a Service for the Frontend

Before exposing the frontend, I replaced the placeholder with my certificate ARN. I used the same ACM certificate as the API.

Then I exposed it through another load balancer with kubectl apply -f frontend-service.yaml and got back service/frontend created

It’s basically the same as api-service.yaml, just pointed at the frontend Deployment.

apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: <namespace-name>
  labels:
    app: frontend
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn:aws:acm:region:account-id:certificate/certificate-id"
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
spec:
  selector:
    app: frontend
  ports:
    - protocol: TCP
      port: 443
      targetPort: 8080
  type: LoadBalancer

That finished all the deployments and services. I checked everything with kubectl get all -n <namespace-name> and got:

david@dhcp-10-249-202-118 manifests % kubectl get all -n <namespace-name>
NAME                            READY   STATUS    RESTARTS   AGE
pod/api-579856b9cd-f255n        1/1     Running   0          169m
pod/api-579856b9cd-kgch8        1/1     Running   0          169m
pod/frontend-6f76b694f4-s7q98   1/1     Running   0          28m
pod/frontend-6f76b694f4-w9mmz   1/1     Running   0          28m
pod/mongo-0                     1/1     Running   0          173m
pod/mongo-1                     1/1     Running   0          172m
pod/mongo-2                     1/1     Running   0          172m

NAME               TYPE           CLUSTER-IP       EXTERNAL-IP                                                               PORT(S)        AGE
service/api        LoadBalancer   10.100.120.76    af6d6ac51fafc48cc92ac450ac73c1b0-936014577.us-east-1.elb.amazonaws.com    443:31938/TCP   162m
service/frontend   LoadBalancer   10.100.175.254   adb0e29e2324d48da8dd64699t-936014577.us-east-1.elb.amazonaws.com   443:32493/TCP   28m
service/mongo      ClusterIP      None             <none>                                                                    27017/TCP      172m

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/api        2/2     2            2           169m
deployment.apps/frontend   2/2     2            2           28m

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/api-579856b9cd        2         2         2       169m
replicaset.apps/frontend-6f76b694f4   2         2         2       28m

NAME                     READY   AGE
statefulset.apps/mongo   3/3     173m

If something looks off, double check frontend-deployment.yaml and frontend-service.yaml.

Pointing DNS at the Frontend Load Balancer

Same as the API. I went to Route 53 and gave the frontend load balancer a subdomain on the domain tied to the certificate.

Testing the Voting App

I opened the frontend’s DNS name and the voting app loaded. I could vote for Star Wars, Avatar, or Avengers.

To confirm votes were hitting the database, I opened the mongo-0 pod with kubectl exec -it mongo-0 -n <namespace-name> -- mongo. I switched to the database with use moviedb; and ran db.movies.find().pretty();. The vote counts were going up.

You can also add /movies to the API’s DNS name to see the updated votes.

Wrapping Up

I know not many people will read this, but I’m proud of this project. I learned a lot about Kubernetes, EKS, and AWS. It was fun to build and I’m happy with how it turned out.

Thanks for reading.