Skip to content
Rabieh Fashwall edited this page Jan 21, 2025 · 3 revisions

Finally we will work on migrating our go-task to a Kubernetes cluster using Kind for local development and Helm for package management.

Setting up

Kind

Kind allows you to run a local Kubernetes cluster using Docker containers, making it ideal for development and testing.

  1. Install Kind:

  2. Create a Kind Cluster:

    kind create cluster --name go-task-cluster
    • This will create a local Kubernetes cluster named go-task-cluster.
    • You can verify the cluster is running using kubectl cluster-info.
  3. Verify kubectl context:

    kubectl config get-contexts
    • Make sure the context for your Kind cluster is set as the current context. You can switch contexts using kubectl config use-context <context-name>.

Helm

Helm is a package manager for Kubernetes that helps you define, install, and manage applications.

  1. Download Helm:

  2. Verify Installation:

    helm version

3. Containerizing Your Services

I push the images to my public registry so we save some time.

4. Creating Helm Charts

A Helm chart is a collection of files that describe a Kubernetes application. Create a Helm chart for each of your microservices:

  1. Create a Chart: To Create a chart, we use helm create command which will generate a basic helm chart, but since we have a special setup, we will work on our own copy.

    helm create user-service 
  2. Modify Chart Files:

    • values.yaml: Define the configuration values for your service, including image names, ports, environment variables, resource limits, etc.
    • templates/: This directory contains Kubernetes resource definition templates (YAML files) that will be used to create deployments, services, configmaps, etc. You'll need to modify these templates to match your service's requirements.
  • user-service/values.yaml:

     service:
     name: user-service 
     commandPort: "3001"
     queryPort: "3000"
    
     image:
     commandRepo: rfashwal/go-task-user-command-service
     queryRepo: rfashwal/go-task-user-query-service  
     tag: latest
     pullPolicy: Always
    
     environment:
     USER_SERVICE_COMMAND_NAME: user-command-service
     USER_SERVICE_COMMAND_PORT: "3001"
     USER_SERVICE_QUERY_NAME: user-query-service
     USER_SERVICE_QUERY_PORT: "3000"
     USER_SERVICE_DB_HOST: mysql.default.svc.cluster.local
     USER_SERVICE_DB_PORT: "3306"
     USER_SERVICE_DB_NAME: user_tracker
     USER_SERVICE_NATS_PORT: "4222" 
     USER_SERVICE_NATS_SERVICE: nats.default.svc.cluster.local
    
     resources: {}
    
  • user-service/templates/deployment.yaml:

     apiVersion: apps/v1
     kind: Deployment
     metadata:
     name: {{ include "user-service.fullname" . }}-command
     labels:
         {{- include "user-service.labels" . | nindent 4 }}
     spec:
     replicas: 1
     selector:
         matchLabels:
         {{- include "user-service.selectorLabels" . | nindent 6 }}
     template:
         metadata:
         {{- with .Values.podAnnotations }}
         annotations:
             {{- toYaml . | nindent 8 }}
         {{- end }}
         labels:
             {{- include "user-service.selectorLabels" . | nindent 8 }}
         spec:
         containers:
             - name: {{ include "user-service.name" . }}-command
             image: "{{ .Values.image.commandRepo }}:{{ .Values.image.tag }}"
             imagePullPolicy: {{ .Values.image.pullPolicy }}
             ports:
                 - containerPort: {{ .Values.service.commandPort }}
             envFrom:
                 - configMapRef:
                     name: {{ include "user-service.fullname" . }}-command-config
                 - secretRef:
                     name: {{ include "user-service.fullname" . }}-secret
             resources:
                 {{- toYaml .Values.resources | nindent 12 }}
     ---
     apiVersion: apps/v1
     kind: Deployment
     metadata:
     name: {{ include "user-service.fullname" . }}-query
     labels:
         {{- include "user-service.labels" . | nindent 4 }}
     spec:
     replicas: 1
     selector:
         matchLabels:
         {{- include "user-service.selectorLabels" . | nindent 6 }}
     template:
         metadata:
         {{- with .Values.podAnnotations }}
         annotations:
             {{- toYaml . | nindent 8 }}
         {{- end }}
         labels:
             {{- include "user-service.selectorLabels" . | nindent 8 }}
         spec:
         containers:
             - name: {{ include "user-service.name" . }}-query
             image: "{{ .Values.image.queryRepo }}:{{ .Values.image.tag }}"
             imagePullPolicy: {{ .Values.image.pullPolicy }}
             ports:
                 - containerPort: {{ .Values.service.queryPort }}
             envFrom:
                 - configMapRef:
                     name: {{ include "user-service.fullname" . }}-query-config
                 - secretRef:
                     name: {{ include "user-service.fullname" . }}-secret
             resources:
                 {{- toYaml .Values.resources | nindent 12 }}
  • user-service/templates/services.yaml:

     apiVersion: v1
     kind: Service
     metadata:
     name: {{ include "user-service.fullname" . }}-command-service
     labels:
         {{- include "user-service.labels" . | nindent 4 }}
     spec:
     type: ClusterIP
     ports:
         - port: {{ .Values.service.commandPort }}
         targetPort: {{ .Values.service.commandPort }}
         protocol: TCP
         name: http-command
     selector:
         {{- include "user-service.selectorLabels" . | nindent 4 }}
     ---
     apiVersion: v1
     kind: Service
     metadata:
     name: {{ include "user-service.fullname" . }}-query-service
     labels:
         {{- include "user-service.labels" . | nindent 4 }}
     spec:
     type: ClusterIP
     ports:
         - port: {{ .Values.service.queryPort }}
         targetPort: {{ .Values.service.queryPort }}
         protocol: TCP
         name: http-query
     selector:
         {{- include "user-service.selectorLabels" . | nindent 4 }}
    
  • user-service/templates/cm.yaml:

    apiVersion: v1
    kind: ConfigMap
    metadata:
    name: {{ include "user-service.fullname" . }}-command-config
    data:
    USER_SERVICE_DB_HOST: {{ .Values.environment.USER_SERVICE_DB_HOST }}
    USER_SERVICE_DB_PORT: "{{ .Values.environment.USER_SERVICE_DB_PORT }}"
    USER_SERVICE_DB_NAME: {{ .Values.environment.USER_SERVICE_DB_NAME }}
    USER_SERVICE_NATS_SERVICE: {{ .Values.environment.USER_SERVICE_NATS_SERVICE }}
    USER_SERVICE_NATS_PORT: "{{ .Values.environment.USER_SERVICE_NATS_PORT }}" 
    USER_SERVICE_SERVICE_NAME: {{ .Values.environment.USER_SERVICE_COMMAND_NAME }}
    USER_SERVICE_SERVICE_PORT: "{{ .Values.environment.USER_SERVICE_COMMAND_PORT }}"
    ---
    apiVersion: v1
    kind: ConfigMap
    metadata:
    name: {{ include "user-service.fullname" . }}-query-config
    data:
    USER_SERVICE_DB_HOST: {{ .Values.environment.USER_SERVICE_DB_HOST }}
    USER_SERVICE_DB_PORT: "{{ .Values.environment.USER_SERVICE_DB_PORT }}"
    USER_SERVICE_DB_NAME: {{ .Values.environment.USER_SERVICE_DB_NAME }}
    USER_SERVICE_SERVICE_NAME: {{ .Values.environment.USER_SERVICE_QUERY_NAME }}
    USER_SERVICE_SERVICE_PORT: "{{ .Values.environment.USER_SERVICE_QUERY_PORT }}"
    
  • user-service/templates/_helpers.tpl

    {{/*
    Expand the name of the chart.
    */}}
    {{- define "user-service.name" -}}
    {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
    {{- end }}
    
    {{/*
    Create a default fully qualified app name.
    We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
    If release name contains chart name it will be used as a full name.
    */}}
    {{- define "user-service.fullname" -}}
    {{- if .Values.fullnameOverride }}
    {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
    {{- else }}
    {{- $name := default .Chart.Name .Values.nameOverride }}
    {{- if contains $name .Release.Name }}
    {{- .Release.Name | trunc 63 | trimSuffix "-" }}
    {{- else }}
    {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
    {{- end }}
    {{- end }}
    {{- end }}
    
    {{/*
    Create chart name and version as used by the chart label.
    */}}
    {{- define "user-service.chart" -}}
    {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
    {{- end }}
    
    {{/*
    Common labels
    */}}
    {{- define "user-service.labels" -}}
    helm.sh/chart: {{ include "user-service.chart" . }}
    {{ include "user-service.selectorLabels" . }}
    {{- if .Chart.AppVersion }}
    app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
    {{- end }}
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    {{- end }}
    
    {{/*
    Selector labels
    */}}
    {{- define "user-service.selectorLabels" -}}
    app.kubernetes.io/name: {{ include "user-service.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    {{- end }}
    
    {{/*
    Create the name of the service account to use
    */}}
    {{- define "user-service.serviceAccountName" -}}
    {{- if .Values.serviceAccount.create }}
    {{- default (include "user-service.fullname" .) .Values.serviceAccount.name }}
    {{- else }}
    {{- default "default" .Values.serviceAccount.name }}
    {{- end }}
    {{- end }}
    

Deploying to Kubernetes

  1. Install Required Charts:

First we need to install mysql and nats charts

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add nats https://nats-io.github.io/k8s/helm/charts/  

helm install mysql bitnami/mysql
helm install nats nats/nats
  1. Fetsh mysql password and create secret:

    MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace default mysql -o jsonpath="{.data.mysql-root-password}" | base64 -d)
    
    kubectl create secret generic user-service-secret \
    --from-literal=USER_SERVICE_DB_USER=root \
    --from-literal=USER_SERVICE_DB_PASS=$MYSQL_ROOT_PASSWORD
    
    kubectl create secret generic task-service-secret \
     --from-literal=TASK_SERVICE_DB_USER=root \
     --from-literal=TASK_SERVICE_DB_PASS=$MYSQL_ROOT_PASSWORD
  2. Init DB

    • Access mysql instance

      kubectl run mysql-client --rm --tty -i --restart='Never' --image  docker.io/bitnami/mysql:8.4.3-debian-12-r5 --namespace default --env MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD --command -- bash
    • Access mysql CLI

      mysql -h mysql.default.svc.cluster.local -uroot -p"$MYSQL_ROOT_PASSWORD"
    • Create the DBs

      CREATE DATABASE IF NOT EXISTS user_tracker;
      USE user_tracker;
      
      CREATE TABLE IF NOT EXISTS users (
          id INT AUTO_INCREMENT PRIMARY KEY,
          name VARCHAR(100) NOT NULL,
          email VARCHAR(100) NOT NULL UNIQUE,
          password VARCHAR(255) NOT NULL
      );
      
      CREATE DATABASE IF NOT EXISTS task_tracker;
      USE task_tracker;
      
      CREATE TABLE IF NOT EXISTS tasks (
          id INT AUTO_INCREMENT PRIMARY KEY,
          user_id INT NOT NULL,
          assignee_id INT NULL,
          title VARCHAR(255) NOT NULL,
          description TEXT,
          status VARCHAR(50)
      );
    • Install User Service

      helm install user-service deploy/kubernetes/user-service/.            

Accessing User Service

  • Port-Forwarding: If you're using Kind, you can use kubectl port-forward to access services locally.
    kubectl port-forward service/user-service-query-service 3000:3000
  • Ingress: For production deployments, you would typically set up an Ingress controller to expose your services externally.

Deploy all services

We successfully deployed deployed our first service on a local k8s cluster, we can check and deploy other services with our final solution including all charts in the below branch

git checkout 07-complete

helm install notification-service deploy/kubernetes/notification-service/.
helm install task-service deploy/kubernetes/task-service/.

Clone this wiki locally