├── README.md └── manifests ├── api-deployment.yaml ├── api-service.yaml ├── frontend-deployment.yaml ├── frontend-service.yaml ├── mongo-secret.yaml ├── mongo-service.yaml └── mongo-statefulset.yaml /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Cloud-Native Web Voting Application with Kubernetes 4 | 5 | This cloud-native web application is built using a mix of technologies. It's designed to be accessible to users via the internet, allowing them to vote for their preferred programming language out of six choices: C#, Python, JavaScript, Go, Java, and NodeJS. 6 | 7 | ## Technical Stack 8 | 9 | - **Frontend**: The frontend of this application is built using React and JavaScript. It provides a responsive and user-friendly interface for casting votes. 10 | 11 | - **Backend and API**: The backend of this application is powered by Go (Golang). It serves as the API handling user voting requests. MongoDB is used as the database backend, configured with a replica set for data redundancy and high availability. 12 | 13 | ## Kubernetes Resources 14 | 15 | To deploy and manage this application effectively, we leverage Kubernetes and a variety of its resources: 16 | 17 | - **Namespace**: Kubernetes namespaces are utilized to create isolated environments for different components of the application, ensuring separation and organization. 18 | 19 | - **Secret**: Kubernetes secrets store sensitive information, such as API keys or credentials, required by the application securely. 20 | 21 | - **Deployment**: Kubernetes deployments define how many instances of the application should run and provide instructions for updates and scaling. 22 | 23 | - **Service**: Kubernetes services ensure that users can access the application by directing incoming traffic to the appropriate instances. 24 | 25 | - **StatefulSet**: For components requiring statefulness, such as the MongoDB replica set, Kubernetes StatefulSets are employed to maintain order and unique identities. 26 | 27 | - **PersistentVolume and PersistentVolumeClaim**: These Kubernetes resources manage the storage required for the application, ensuring data persistence and scalability. 28 | 29 | ## Learning Opportunities 30 | 31 | Creating and deploying this cloud-native web voting application with Kubernetes offers a valuable learning experience. Here are some key takeaways: 32 | 33 | 1. **Containerization**: Gain hands-on experience with containerization technologies like Docker for packaging applications and their dependencies. 34 | 35 | 2. **Kubernetes Orchestration**: Learn how to leverage Kubernetes to efficiently manage, deploy, and scale containerized applications in a production environment. 36 | 37 | 3. **Microservices Architecture**: Explore the benefits and challenges of a microservices architecture, where the frontend and backend are decoupled and independently scalable. 38 | 39 | 4. **Database Replication**: Understand how to set up and manage a MongoDB replica set for data redundancy and high availability. 40 | 41 | 5. **Security and Secrets Management**: Learn best practices for securing sensitive information using Kubernetes secrets. 42 | 43 | 6. **Stateful Applications**: Gain insights into the nuances of deploying stateful applications within a container orchestration environment. 44 | 45 | 7. **Persistent Storage**: Understand how Kubernetes manages and provisions persistent storage for applications with state. 46 | 47 | By working through this project, you'll develop a deeper understanding of cloud-native application development, containerization, Kubernetes, and the various technologies involved in building and deploying modern web applications. 48 | 49 | 50 | ### **************************Steps to Deploy************************** 51 | 52 | Youtube Video to refer: 53 | 54 | [![Video Tutorial](https://img.youtube.com/vi/pTmIoKUeU-A/0.jpg)](https://youtu.be/pTmIoKUeU-A) 55 | 56 | Susbcribe: 57 | 58 | [https://www.youtube.com/@cloudchamp? 59 | ](https://www.youtube.com/@cloudchamp?sub_confirmation=1) 60 | 61 | 62 | Create EKS cluster with NodeGroup (2 nodes of t2.medium instance type) 63 | Create EC2 Instance t2.micro (Optional) 64 | 65 | ##IAM role for ec2 66 | ``` 67 | { 68 | "Version": "2012-10-17", 69 | "Statement": [{ 70 | "Effect": "Allow", 71 | "Action": [ 72 | "eks:DescribeCluster", 73 | "eks:ListClusters", 74 | "eks:DescribeNodegroup", 75 | "eks:ListNodegroups", 76 | "eks:ListUpdates", 77 | "eks:AccessKubernetesApi" 78 | ], 79 | "Resource": "*" 80 | }] 81 | } 82 | ``` 83 | 84 | Install Kubectl: 85 | ``` 86 | curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.24.11/2023-03-17/bin/linux/amd64/kubectl 87 | chmod +x ./kubectl 88 | sudo cp ./kubectl /usr/local/bin 89 | export PATH=/usr/local/bin:$PATH 90 | ``` 91 | 92 | Install AWScli: 93 | ``` 94 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 95 | unzip awscliv2.zip 96 | sudo ./aws/install 97 | ``` 98 | 99 | Once the Cluster is ready run the command to set context: 100 | ``` 101 | aws eks update-kubeconfig --name EKS_CLUSTER_NAME --region us-west-2 102 | ``` 103 | 104 | To check the nodes in your cluster run 105 | ``` 106 | kubectl get nodes 107 | ``` 108 | 109 | If using EC2 and getting the "You must be logged in to the server (Unauthorized)" error, refer this: https://repost.aws/knowledge-center/eks-api-server-unauthorized-error 110 | 111 | Clone the github repo 112 | ``` 113 | git clone https://github.com/N4si/K8s-voting-app.git 114 | ``` 115 | 116 | **Create CloudChamp Namespace** 117 | ``` 118 | kubectl create ns cloudchamp 119 | 120 | kubectl config set-context --current --namespace cloudchamp 121 | ``` 122 | 123 | **MONGO Database Setup** 124 | 125 | 126 | To create Mongo statefulset with Persistent volumes, run the command in manifests folder: 127 | ``` 128 | kubectl apply -f mongo-statefulset.yaml 129 | ``` 130 | 131 | Mongo Service 132 | ``` 133 | kubectl apply -f mongo-service.yaml 134 | ``` 135 | 136 | Create a temporary network utils pod. Enter into a bash session within it. In the terminal run the following command: 137 | ``` 138 | kubectl run --rm utils -it --image praqma/network-multitool -- bash 139 | ``` 140 | Within the new utils pod shell, execute the following DNS queries: 141 | ``` 142 | for i in {0..2}; do nslookup mongo-$i.mongo; done 143 | ``` 144 | Note: This confirms that the DNS records have been created successfully and can be resolved within the cluster, 1 per MongoDB pod that exists behind the Headless Service - earlier created. 145 | 146 | Exit the utils container 147 | ``` 148 | exit 149 | ``` 150 | 151 | On the `mongo-0` pod, initialise the Mongo database Replica set. In the terminal run the following command: 152 | ``` 153 | cat << EOF | kubectl exec -it mongo-0 -- mongo 154 | rs.initiate(); 155 | sleep(2000); 156 | rs.add("mongo-1.mongo:27017"); 157 | sleep(2000); 158 | rs.add("mongo-2.mongo:27017"); 159 | sleep(2000); 160 | cfg = rs.conf(); 161 | cfg.members[0].host = "mongo-0.mongo:27017"; 162 | rs.reconfig(cfg, {force: true}); 163 | sleep(5000); 164 | EOF 165 | ``` 166 | 167 | Note: Wait until this command completes successfully, it typically takes 10-15 seconds to finish, and completes with the message: bye 168 | 169 | 170 | To confirm run this in the terminal: 171 | ``` 172 | kubectl exec -it mongo-0 -- mongo --eval "rs.status()" | grep "PRIMARY\|SECONDARY" 173 | ``` 174 | 175 | Load the Data in the database by running this command: 176 | ## Note: use langdb not langdb() as shown in the video 177 | ``` 178 | cat << EOF | kubectl exec -it mongo-0 -- mongo 179 | use langdb; 180 | db.languages.insert({"name" : "csharp", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 5, "compiled" : false, "homepage" : "https://dotnet.microsoft.com/learn/csharp", "download" : "https://dotnet.microsoft.com/download/", "votes" : 0}}); 181 | db.languages.insert({"name" : "python", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 3, "script" : false, "homepage" : "https://www.python.org/", "download" : "https://www.python.org/downloads/", "votes" : 0}}); 182 | db.languages.insert({"name" : "javascript", "codedetail" : { "usecase" : "web, client-side", "rank" : 7, "script" : false, "homepage" : "https://en.wikipedia.org/wiki/JavaScript", "download" : "n/a", "votes" : 0}}); 183 | db.languages.insert({"name" : "go", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 12, "compiled" : true, "homepage" : "https://golang.org", "download" : "https://golang.org/dl/", "votes" : 0}}); 184 | db.languages.insert({"name" : "java", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 1, "compiled" : true, "homepage" : "https://www.java.com/en/", "download" : "https://www.java.com/en/download/", "votes" : 0}}); 185 | db.languages.insert({"name" : "nodejs", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 20, "script" : false, "homepage" : "https://nodejs.org/en/", "download" : "https://nodejs.org/en/download/", "votes" : 0}}); 186 | 187 | db.languages.find().pretty(); 188 | EOF 189 | ``` 190 | 191 | Create Mongo secret: 192 | ``` 193 | kubectl apply -f mongo-secret.yaml 194 | ``` 195 | 196 | **API Setup** 197 | 198 | Create GO API deployment by running the following command: 199 | ``` 200 | kubectl apply -f api-deployment.yaml 201 | ``` 202 | 203 | Expose API deployment through service using the following command: 204 | ``` 205 | kubectl expose deploy api \ 206 | --name=api \ 207 | --type=LoadBalancer \ 208 | --port=80 \ 209 | --target-port=8080 210 | ``` 211 | 212 | Next set the environment variable: 213 | 214 | ``` 215 | { 216 | API_ELB_PUBLIC_FQDN=$(kubectl get svc api -ojsonpath="{.status.loadBalancer.ingress[0].hostname}") 217 | until nslookup $API_ELB_PUBLIC_FQDN >/dev/null 2>&1; do sleep 2 && echo waiting for DNS to propagate...; done 218 | curl $API_ELB_PUBLIC_FQDN/ok 219 | echo 220 | } 221 | ``` 222 | 223 | Test and confirm that the API route URL /languages, and /languages/{name} endpoints can be called successfully. In the terminal run any of the following commands: 224 | ``` 225 | curl -s $API_ELB_PUBLIC_FQDN/languages | jq . 226 | curl -s $API_ELB_PUBLIC_FQDN/languages/go | jq . 227 | curl -s $API_ELB_PUBLIC_FQDN/languages/java | jq . 228 | curl -s $API_ELB_PUBLIC_FQDN/languages/nodejs | jq . 229 | ``` 230 | 231 | If everything works fine, go ahead with Frontend setup. 232 | ``` 233 | { 234 | API_ELB_PUBLIC_FQDN=$(kubectl get svc api -ojsonpath="{.status.loadBalancer.ingress[0].hostname}") 235 | echo API_ELB_PUBLIC_FQDN=$API_ELB_PUBLIC_FQDN 236 | } 237 | ``` 238 | 239 | **Frontend setup** 240 | 241 | Create the Frontend Deployment resource. In the terminal run the following command: 242 | ``` 243 | kubectl apply -f frontend-deployment.yaml 244 | ``` 245 | 246 | Create a new Service resource of LoadBalancer type. In the terminal run the following command: 247 | ``` 248 | kubectl expose deploy frontend \ 249 | --name=frontend \ 250 | --type=LoadBalancer \ 251 | --port=80 \ 252 | --target-port=8080 253 | ``` 254 | 255 | Confirm that the Frontend ELB is ready to recieve HTTP traffic. In the terminal run the following command: 256 | ``` 257 | { 258 | FRONTEND_ELB_PUBLIC_FQDN=$(kubectl get svc frontend -ojsonpath="{.status.loadBalancer.ingress[0].hostname}") 259 | until nslookup $FRONTEND_ELB_PUBLIC_FQDN >/dev/null 2>&1; do sleep 2 && echo waiting for DNS to propagate...; done 260 | curl -I $FRONTEND_ELB_PUBLIC_FQDN 261 | } 262 | ``` 263 | 264 | Generate the Frontend URL for browsing. In the terminal run the following command: 265 | ``` 266 | echo http://$FRONTEND_ELB_PUBLIC_FQDN 267 | ``` 268 | 269 | Test the full end-to-end cloud native application 270 | 271 | Using your local workstation's browser - browse to the URL created in the previous output. 272 | 273 | After the voting application has loaded successfully, vote by clicking on several of the **+1** buttons, this will generate AJAX traffic which will be sent back to the API via the API's assigned ELB. 274 | 275 | 276 | Query the MongoDB database directly to observe the updated vote data. In the terminal execute the following command: 277 | ``` 278 | kubectl exec -it mongo-0 -- mongo langdb --eval "db.languages.find().pretty()" 279 | ``` 280 | 281 | ## **Summary** 282 | 283 | In this Project, you learnt how to deploy a cloud native application into EKS. Once deployed and up and running, you used your local workstation's browser to test out the application. You later confirmed that your activity within the application generated data which was captured and recorded successfully within the MongoDB ReplicaSet back end within the cluster. 284 | -------------------------------------------------------------------------------- /manifests/api-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: api 5 | namespace: cloudchamp 6 | labels: 7 | role: api 8 | env: demo 9 | spec: 10 | replicas: 2 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxSurge: 1 15 | maxUnavailable: 25% 16 | selector: 17 | matchLabels: 18 | role: api 19 | template: 20 | metadata: 21 | labels: 22 | role: api 23 | spec: 24 | containers: 25 | - name: api 26 | image: cloudacademydevops/api:v3 27 | imagePullPolicy: Always 28 | env: 29 | - name: MONGO_CONN_STR 30 | value: mongodb://mongo-0.mongo,mongo-1.mongo,mongo-2.mongo:27017/langdb?replicaSet=rs0 31 | - name: MONGO_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: mongodb-secret 35 | key: username 36 | - name: MONGO_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: mongodb-secret 40 | key: password 41 | ports: 42 | - containerPort: 8080 43 | livenessProbe: 44 | httpGet: 45 | path: /ok 46 | port: 8080 47 | initialDelaySeconds: 2 48 | periodSeconds: 5 49 | readinessProbe: 50 | httpGet: 51 | path: /ok 52 | port: 8080 53 | initialDelaySeconds: 5 54 | periodSeconds: 5 55 | successThreshold: 1 -------------------------------------------------------------------------------- /manifests/api-service.yaml: -------------------------------------------------------------------------------- 1 | #Imperative command to create service to expose frontend deployment 2 | #kubectl expose deploy api \ --name=api \ --type=LoadBalancer \ --port=80 \ --target-port=8080 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: api 7 | labels: 8 | app: api 9 | spec: 10 | selector: 11 | app: api 12 | ports: 13 | - protocol: TCP 14 | port: 80 15 | targetPort: 8080 16 | type: LoadBalancer 17 | -------------------------------------------------------------------------------- /manifests/frontend-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend 5 | namespace: cloudchamp 6 | labels: 7 | role: frontend 8 | env: demo 9 | spec: 10 | replicas: 2 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxSurge: 1 15 | maxUnavailable: 25% 16 | selector: 17 | matchLabels: 18 | role: frontend 19 | template: 20 | metadata: 21 | labels: 22 | role: frontend 23 | spec: 24 | containers: 25 | - name: frontend 26 | image: cloudacademydevops/frontend:v11 27 | imagePullPolicy: Always 28 | env: 29 | - name: REACT_APP_APIHOSTPORT 30 | value: $API_ELB_PUBLIC_FQDN #add your API_Load_Balancer DNS manually here if app does not run 31 | ports: 32 | - containerPort: 8080 33 | livenessProbe: 34 | httpGet: 35 | path: /ok 36 | port: 8080 37 | initialDelaySeconds: 2 38 | periodSeconds: 5 39 | readinessProbe: 40 | httpGet: 41 | path: /ok 42 | port: 8080 43 | initialDelaySeconds: 5 44 | periodSeconds: 5 45 | successThreshold: 1 46 | -------------------------------------------------------------------------------- /manifests/frontend-service.yaml: -------------------------------------------------------------------------------- 1 | #Imperative command to create service to expose frontend deployment 2 | #kubectl expose deploy frontend \ --name=frontend \ --type=LoadBalancer \ --port=80 \ --target-port=8080 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: frontend 7 | labels: 8 | app: frontend 9 | spec: 10 | selector: 11 | app: frontend 12 | ports: 13 | - protocol: TCP 14 | port: 80 15 | targetPort: 8080 16 | type: LoadBalancer 17 | -------------------------------------------------------------------------------- /manifests/mongo-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: mongodb-secret 5 | namespace: cloudchamp 6 | data: 7 | username: YWRtaW4= 8 | password: cGFzc3dvcmQ= -------------------------------------------------------------------------------- /manifests/mongo-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: mongo 5 | namespace: cloudchamp 6 | labels: 7 | role: db 8 | env: demo 9 | spec: 10 | ports: 11 | - port: 27017 12 | targetPort: 27017 13 | clusterIP: None 14 | selector: 15 | role: db -------------------------------------------------------------------------------- /manifests/mongo-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: mongo 5 | namespace: cloudchamp 6 | spec: 7 | serviceName: mongo 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | role: db 12 | template: 13 | metadata: 14 | labels: 15 | role: db 16 | env: demo 17 | replicaset: rs0.main 18 | spec: 19 | affinity: 20 | podAntiAffinity: 21 | preferredDuringSchedulingIgnoredDuringExecution: 22 | - weight: 100 23 | podAffinityTerm: 24 | labelSelector: 25 | matchExpressions: 26 | - key: replicaset 27 | operator: In 28 | values: 29 | - rs0.main 30 | topologyKey: kubernetes.io/hostname 31 | terminationGracePeriodSeconds: 10 32 | containers: 33 | - name: mongo 34 | image: mongo:4.2 35 | command: 36 | - "numactl" 37 | - "--interleave=all" 38 | - "mongod" 39 | - "--wiredTigerCacheSizeGB" 40 | - "0.1" 41 | - "--bind_ip" 42 | - "0.0.0.0" 43 | - "--replSet" 44 | - "rs0" 45 | ports: 46 | - containerPort: 27017 47 | volumeMounts: 48 | - name: mongodb-persistent-storage-claim 49 | mountPath: /data/db 50 | volumeClaimTemplates: 51 | - metadata: 52 | name: mongodb-persistent-storage-claim 53 | spec: 54 | accessModes: 55 | - ReadWriteOnce 56 | storageClassName: gp2 57 | resources: 58 | requests: 59 | storage: 0.5Gi 60 | --------------------------------------------------------------------------------