Docker Kubernetes and the rest - A Practical
Rajesh Pillai
Posted on September 17, 2023
Before we begin the journey of deployment, I do want you to consider this article as a checklist (as things may change but the process and steps may more or less remain the same).
I am putting down the steps for the future me and for the devs and tech people around me who has to work with this aspect of the tech day in and day out.
NOTE: For running k8 locally you can try using minikube, docker desktop or other tools. But here it's depicted for a production scenario. So here k8 is running in cloud. Depending on the cloud provider you can use respective k8 offering. For Azure, it will be AKS. For AWS it's EKS and for GCP it's, GKE.
Let's begin at the beginning by taking a high level look at how things are arranged in the cloud stack.
Brief explanation of the above diagram.
- Kubernetes Cluster: Represents the entire Kubernetes environment.
- Nodes: Physical or virtual machines that run your workloads. In this diagram, we have two nodes (Node 1 and Node 2).
- Pods: The smallest deployable units in Kubernetes that can be created and managed. Each pod contains one or more containers.
- Docker Containers: Containers that run your applications. They are managed by the pods.
Now, let's deploy a React application (take any react application that you get your hands at, any opensource from github or any sample project) that has Node (Express, Fastify or other), and deploy it to Google Cloud using Kubernetes and Docker and understand how the support ecosystem helps in optimizing this workflow.
Here is a diagram illustrating what we want to achieve.
Brief explanation of the diagram.
- Kubernetes Cluster: Represents the entire Kubernetes environment.
- Nodes: Physical or virtual machines that run your workloads. In this diagram, we have two nodes (Node 1 and Node 2).
- Fastify Pod: Contains the Docker container running the Fastify server.
- React Pod: Contains the Docker container running the React app.
- Docker Containers: Containers that run your applications. They are managed by the pods.
- Nginx: Used for serving static HTML generated from the React app.
Some technical explanation for the beginner in this area of work or exploration.
Container: A lightweight, standalone, and executable software package that includes everything needed to run a piece of software, including the code, runtime, system tools, libraries, and settings. Containers are isolated from each other and the host system. This is where your application code runs.
Pod: In the Kubernetes ecosystem, the smallest and simplest unit is the pod. A pod represents a single instance of a running process in a cluster and can contain one or more containers. Containers within the same pod share the same network IP, port space, and storage, which allows them to communicate easily through localhost and share data if needed.
Node: A node is a worker machine, VM, or physical computer that serves as a hosting environment for pods. Each node is managed by the master node and contains the services necessary to run pods. A node can host multiple pods.
What does this architecture gives us?
Isolation: Pods provide a level of isolation between applications. If one application crashes or has an issue, it won't directly impact other applications running in separate pods.
Scalability: Deploying applications in separate pods allows you to scale each application independently based on its own demand and resource requirements.
Resource Management: Kubernetes allows you to set resource requests and limits at the pod level. By deploying each application in its own pod, you can fine-tune the CPU and memory allocations for each application based on its specific needs.
Versioning and Updates: Deploying applications in separate pods makes it easier to manage versioning and updates. You can update or roll back a specific application without affecting others.
Logging and Monitoring: With applications in separate pods, it's easier to set up logging and monitoring specific to each application, helping in troubleshooting and performance analysis.
Security: Pods provide a level of security isolation. By deploying applications in separate pods, you can apply specific security policies, network policies, and access controls to each application.
While it's a best practice to deploy each application in its own pod, there are scenarios where multi-container pods (pods with more than one container) make sense. This is typically when containers in the pod need to share storage or network namespaces and have tightly coupled application components. However, such scenarios are less common and are used for specific use cases.
Steps to achieve this deployment
- Dockerize the Applications: Create Docker images for both the React and Fastify applications.
- Push Images to Container Registry: Push the Docker images to Google Container Registry (GCR) or another container registry of your choice.
- Create Kubernetes Deployment & Service YAMLs: Define the deployment and service configurations for both applications.
- Deploy on GKE: Use kubectl to deploy the applications on GKE.
(You can adapt this to other cloud providers like AWS, Azure etc)
The steps to achieve this is
- Dockerize the applications
- Push images to Google Container Registry (or other registry as per the cloud provider)
- Create Kubernetes Deployment & Service YAMLs.
Here are the sample deployment scripts (Do note that the cluster setup, security etc has to be setup as per the respective cloud providers)
Step 1 - Dockerize the Application
Let's explore the steps further.
Dockerfile for React App
# React App Dockerfile
FROM node:14-slim AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine AS production-stage
COPY --from=build-stage /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
We are using a multistage build so that output from stage 1 can be used for further processing. In mutistage build each FROM instruction can use a different base image and starts a new stage of the build. You can also selectively copy artifacts from one stage to another, leaving behind everything you don't want in the final image. If you don't specify a stagename you can use the default stage index starting from 0.
Explanation of the above Docker file
- Base image for Node.js
FROM node:14-slim AS build-stage
The image to be used for subsequent instuctions.
- Set working directiory
WORKDIR /app
This sets the working directory inside the container to '/app'. All subsequent commands will be run from this directory.
- COPY 'package.json' and 'package-lock.json'
COPY package*.json ./
This copies both 'package.json' and 'package-lock.json' (if it exists) from the host machine to the current directory ('/app') inside the container.
- Install Dependencies
RUN npm install
This installs the Node.js dependencies specified in 'package.json'. Since we've already copied 'package.json' into the container, this command installs the required dependencies.
- Copy Application Source code
COPY . .
This copies the entire content of the current directory on the host machine (i.e. your React app's source code) into the '/app' directory inside the container.
- Build the React app
RUN npm run build
This runs the 'build' script specified in package.json
, which typically compiles and bundles the React app for production (You can verify your package.json scripts section for the appropriate commands). I am using the generally used commands here.
- Base Image for Nginx
FROM nginx:alpine
After building the React app, we start a new stage with the nginx:alpine base image. This is a lightweight image with Nginx installed, which will serve our static React app.
- Copy build React App to Nginx Directory
COPY --FROM=build-stage /app/build /usr/share/nginx/html
This copies the built React app (from the /app/build directory of the previous stage, names as build-stage) to the Nginx default directory for serving static content (/usr/share/nginx/html).
- Expose Port 80
EXPOSE 80
This informs Docker that the container will be listening on port 80, which is the default port for HTTP and where Nginx serves content by default. Treat this command like a documentation to understand where the container will be listening.
- Start Nginx
CMD ["nginx", "-g", "daemon off;"]
This is the command that will be executed when the container starts. It starts the Nginx server with the daemon off; configuration, ensuring Nginx stays in the foreground so the container keeps running.
Dockerfile for Fastify API (You can use Express or other framework as needed)
# Fastify API Dockerfile
FROM node:14-slim
WORKDIR /api
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5000
CMD ["npm", "start"]
I am not explaining the above commands in details as it should be quite easy to follow (If you need explanation do let me no in the comments/feedback section)
Sample Nginx configuration
Here is the sample nginx conf for the above application (nginx.conf or custom config in sites-available directory which is symlinked to 'sites-enabled' directory.
server {
listen 80;
server_name your-domain.com;
location / {
root /usr/share/nginx/html; # This is where the React app's build files are
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://fastify-service:5000; # Assuming Fastify runs on port 5000 and is named "fastify-service" in Kubernetes
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Step 2 - Kubernetes Configuration (configmap.yaml)
ConfigMap for Environment Variables
apiVersion: v1
kind: ConfigMap
metadata:
name: todo-config
data:
NODE_ENV: "production"
Let's understand the ConfigMap
apiVersion: Specifies the API version to use. v1 is the API version for core Kubernetes resources, including ConfigMap.
kind: Specifies the kind of Kubernetes resource you want to create. In this case, it's a ConfigMap.
metadata:
*- name: The name of the ConfigMap. It's named todo-config in this instance.data: This section contains the actual configuration data. Each key-value pair under data represents a configuration item.
*-NODE_ENV: This is a common environment variable used in many applications to specify the runtime environment. Setting it to "production" typically means the application will run with optimizations for production use and won't display detailed error messages that might be shown in a development environment.
Secrets for Sensitive Data
First, create a secret. For e.g. for a database password:
kubectl create secret generic todo-secrets --from-literal=DB_PASSWORD=mysecretpassword
This will create a secret named todo-secrets with a key DB_PASSWORD
Step 3 - Kubernetes Deployment
Fastify API Deployment (fastify-deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastify-deployment
spec:
replicas: 3
selector:
matchLabels:
app: fastify
template:
metadata:
labels:
app: fastify
spec:
containers:
- name: fastify
image: gcr.io/your_project_id/fastify-app:v1
env:
- name: NODE_ENV
valueFrom:
configMapKeyRef:
name: todo-config
key: NODE_ENV
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: todo-secrets
key: DB_PASSWORD
Let's understand the above deployment script.
Here's a breakdown of the YAML:
apiVersion: Specifies the API version to use. apps/v1 is the API version for Deployments.
kind: Specifies the kind of Kubernetes resource you want to create. In this case, it's a Deployment.
metadata:
-*name: The name of the Deployment. It's named fastify-deployment.
-spec:
-*replicas: Specifies the number of pod replicas you want to maintain. Here, it's set to 3, meaning Kubernetes will try to maintain three running instances of your Fastify app at all times.
-*selector: This is used to find which Pods the Deployment should manage.
--*matchLabels: A map of key-value pairs. The Deployment manages any pods with labels that match this map. Here, it's looking for Pods with the label app: fastify.
--*template: Describes the desired state for Pods the Deployment manages.
---*metadata.labels: Labels to apply to the Pod. This should match the selector.matchLabels above.
--*spec: The specification for the containers to run in the Pod.
---*containers: A list of containers to run in the Pod.
----*name: The name of the container, fastify in this case.
----*image: The Docker image to use for the container. Here, it's pulling from Google Container Registry (gcr.io) from a specified project and using the fastify-app:v1 image.
----*env: A list of environment variables to set in the container.
The first environment variable, NODE_ENV, is set using a value from a ConfigMap named todo-config with a key of NODE_ENV.
The second environment variable, DB_PASSWORD, is set using a value from a Secret named todo-secrets with a key of DB_PASSWORD.
In summary, this YAML defines a Kubernetes Deployment for the Fastify app. It ensures that three replicas of the Fastify app are running at all times. The Fastify app container uses the image gcr.io/your_project_id/fastify-app:v1 and sets environment variables from a ConfigMap and a Secret.
React App Deployment (frontend-deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: react-deployment
spec:
replicas: 3
selector:
matchLabels:
app: react
template:
metadata:
labels:
app: react
spec:
containers:
- name: react
image: gcr.io/your_project_id/react-app:v1
Let's understand the React deployment yaml
- apiVersion: Specifies the API version to use. apps/v1 is the API version for Deployments.
- kind: Specifies the kind of Kubernetes resource you want to create. In this case, it's a Deployment.
- metadata: -*name: The name of the Deployment. It's named react-deployment. -spec: --*replicas: Specifies the number of pod replicas you want to maintain. Here, it's set to 3, meaning Kubernetes will try to maintain three running instances of your React app at all times. --*selector: This is used to find which Pods the Deployment should manage. ---*matchLabels: A map of key-value pairs. The Deployment manages any pods with labels that match this map. Here, it's looking for Pods with the label app: react. --*template: Describes the desired state for Pods the Deployment manages. ---*metadata.labels: Labels to apply to the Pod. This should match the selector.matchLabels above. ---*spec: The specification for the containers to run in the Pod. ----*containers: A list of containers to run in the Pod. -----*name: The name of the container, react in this case. -----*image: The Docker image to use for the container. Here, it's pulling from Google Container Registry (gcr.io) from a specified project and using the react-app:v1 image.
In summary, this YAML defines a Kubernetes Deployment for the React app. It ensures that three replicas of the React app are running at all times. The React app container uses the image gcr.io/your_project_id/react-app:v1. Unlike the Fastify deployment, this React deployment doesn't have environment variables set from ConfigMaps or Secrets, making it a bit simpler.
Deployment Scripts
# Authenticate with GCR
gcloud auth configure-docker
# Build Docker images
docker build -t gcr.io/your_project_id/fastify-app:v1 ./path_to_fastify_app
docker build -t gcr.io/your_project_id/react-app:v1 ./path_to_react_app
# Push to GCR
docker push gcr.io/your_project_id/fastify-app:v1
docker push gcr.io/your_project_id/react-app:v1
# Create ConfigMaps and Secrets (if any)
kubectl apply -f path_to_configmap.yaml
# If there are secrets, create them here
# kubectl create secret ...
# Apply Kubernetes configurations
kubectl apply -f path_to_fastify_deployment.yaml
kubectl apply -f path_to_react_deployment.yaml
# Expose services
kubectl expose deployment fastify-deployment --type=LoadBalancer --port 5000
kubectl expose deployment react-deployment --type=LoadBalancer --port 80
Verify Deployments
To verify the deployments you've done, you can use the kubectl command-line tool to inspect the state and configuration of your Kubernetes cluster. Here's a step-by-step guide:
- Check Deployments: This will show you the status of all deployments, including the number of replicas, updated replicas, available replicas, etc. (In the terminal you can run the command)
kubectl get deployments
- Check Pods: This will show you the status of all the pods. You can verify if the pods are running, if they've restarted, etc.
kubectl get pods
- Check Services: This will show you the services you've exposed. For LoadBalancer type services, you can also see the external IP addresses once they're provisioned
kubectl get services
- Check ConfigMaps: To ensure your ConfigMaps have been correctly applied:
kubectl describe configmap todo-config
- Check Secrets
kubectl get secrets
- Check Logs For any specific pod, especially if there are issues, you can check it's logs
kubectl logs pod_name_here
- Access the application
http://EXTERNAL_IP
- Additional Debugging If any pod is not running or is in an error state, you can describe it to get more details.
kubectl describe pod pod_name_here
- Check Events To see a stream of all events in the cluster, which can be helpful for debugging
kubectl get events --sort-by=.metadata.creationTimestamp
Additional Steps:
- ConfigMap and Secrets: If your Kubernetes Deployment references ConfigMaps or Secrets (like the todo-config and todo-secrets in the previous examples), ensure they are created in the Kubernetes cluster before deploying the application.
# Create the ConfigMap
kubectl create configmap todo-config --from-literal=NODE_ENV=production
# If you have secrets, create the Secret (replace 'your-secret-value' with the actual secret)
kubectl create secret generic todo-secrets --from-literal=DB_PASSWORD=your-secret-value
GitOps using ArgoCD
Let's automate the deployments using ArgoCD.
- Install ArgoCD First, install ArgoCD on your Kubernetes cluster
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
- Expose ArgoCD Server For simplicity, you can expose the ArgoCD server using a LoadBalancer
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
- Access ArgoCD Once the LoadBalancer is provisioned, you can access the ArgoCD UI using external IP
kubectl get svc -n argocd argocd-server
The default username is 'admin', and the password is the name of the ArgoCD server pod
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
Store Kubernetes Manifests in Git
ArgoCD uses a Git repository as the source of truth for your Kubernetes manifests. Ensure all your deployment manifests (for Fastify, React, ConfigMaps, Secrets, etc.) are stored in a Git repository.Create an ArgoCD Application
An ArgoCD "Application" defines the relationship between a Git repository (which contains your Kubernetes manifests) and a deployment destination (namespace, cluster).
You can create an application using the ArgoCD CLI or the UI. Here's a CLI example:
argocd app create fastify-app \
--repo https://github.com/your_username/your_repo.git \
--path path_in_repo_to_k8s_manifests \
--dest-namespace default \
--dest-server https://kubernetes.default.svc \
--sync-policy automated
Replace placeholders with your actual values.
- Sync the Application If you've set the sync policy to automated, ArgoCD will automatically deploy changes made to the Git repository. Otherwise, you can manually sync using:
argocd app sync fastify-app
OR through ArgoCD UI.
In summmary the changes needed to move your manual deployments to ArgoCD is given below.
Git Repository: All your Kubernetes manifests should be in a Git repository. This includes Deployments, Services, ConfigMaps, Secrets (though consider encrypting secrets or using external secret management).
ArgoCD Application: You'll define an ArgoCD application for each logical application (e.g., Fastify backend, React frontend).
Sync Policy: Decide if you want automatic deployments on Git changes (automated) or manual deployments (manual).
That's it for this article. I may revist and revise this from time to time. Happy Deploying!
Posted on September 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.