Introduction: Efficient Kubernetes Application Deployment

So far, we've learned various Kubernetes resources and configuration methods. However, in real production environments, you need to manage dozens or hundreds of YAML files and apply different configurations for each environment. Helm, a package manager, emerged to solve this complexity.

Also, in modern software development, CI/CD pipelines that automate the process from code changes to production deployment are essential. In this part, we'll explore how to use Helm and build GitOps-based CI/CD pipelines in detail.

1. What is Helm?

1.1 Helm Overview

Helm is the package manager for Kubernetes. Like apt or yum on Linux, or Homebrew on macOS, Helm makes it easy to install, upgrade, and manage Kubernetes applications.

Core Helm concepts:

  • Chart: A collection of files that define a Kubernetes application
  • Release: An instance of a Chart installed in the cluster
  • Repository: A storage location for sharing and storing Charts
  • Values: Values that customize a Chart's default settings

1.2 Why Use Helm?

Problem Helm Solution
Managing numerous YAML files Package into a single Chart
Environment-specific configuration differences Override with values.yaml
Deployment history management Release version management and rollback
Complex dependencies Chart dependency management
Lack of reusability Templates and shareable Charts

2. Helm Installation and Basic Usage

2.1 Installing Helm

# macOS
brew install helm

# Linux (script)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Windows (Chocolatey)
choco install kubernetes-helm

# Verify installation
helm version

2.2 Repository Management

# Add official Helm Chart repositories
helm repo add stable https://charts.helm.sh/stable
helm repo add bitnami https://charts.bitnami.com/bitnami

# List repositories
helm repo list

# Update repositories
helm repo update

# Search for Charts
helm search repo nginx
helm search repo mysql

# Search Hub
helm search hub prometheus

2.3 Basic Commands

# Install Chart
helm install my-release bitnami/nginx

# Install to specific namespace
helm install my-release bitnami/nginx -n my-namespace --create-namespace

# Install with values file
helm install my-release bitnami/nginx -f custom-values.yaml

# Specify values on command line
helm install my-release bitnami/nginx --set replicaCount=3

# List releases
helm list
helm list -A  # All namespaces

# Check release status
helm status my-release

# Upgrade release
helm upgrade my-release bitnami/nginx --set replicaCount=5

# Rollback release
helm rollback my-release 1  # Rollback to revision 1

# View release history
helm history my-release

# Delete release
helm uninstall my-release

3. Understanding Chart Structure

3.1 Chart Directory Structure

my-chart/
├── Chart.yaml          # Chart metadata
├── values.yaml         # Default configuration values
├── charts/             # Dependency Charts
├── templates/          # Kubernetes manifest templates
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── _helpers.tpl    # Template helper functions
│   ├── NOTES.txt       # Message displayed after installation
│   └── tests/          # Test files
│       └── test-connection.yaml
├── .helmignore         # Files to exclude when packaging
└── README.md           # Documentation

3.2 Chart.yaml

# Chart.yaml
apiVersion: v2
name: my-app
description: A Helm chart for my application
type: application
version: 1.0.0          # Chart version
appVersion: "2.0.0"     # Application version

# Dependencies definition
dependencies:
  - name: mysql
    version: "9.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: mysql.enabled
  - name: redis
    version: "17.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled

# Keywords and metadata
keywords:
  - web
  - application
home: https://example.com
sources:
  - https://github.com/example/my-app
maintainers:
  - name: DevOps Team
    email: devops@example.com

3.3 values.yaml

# values.yaml
# Default configuration values

# Application settings
replicaCount: 2

image:
  repository: myregistry/my-app
  tag: "latest"
  pullPolicy: IfNotPresent

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

# Service settings
service:
  type: ClusterIP
  port: 80
  targetPort: 8080

# Ingress settings
ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: myapp-tls
      hosts:
        - myapp.example.com

# Resource limits
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 128Mi

# Autoscaling
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

# Environment variables
env:
  - name: APP_ENV
    value: production
  - name: LOG_LEVEL
    value: info

# Dependency enablement
mysql:
  enabled: true
  auth:
    database: myapp
    username: appuser

redis:
  enabled: false

3.4 Writing Templates

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-app.fullname" . }}
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-app.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          {{- if .Values.env }}
          env:
            {{- toYaml .Values.env | nindent 12 }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5

4. values.yaml Customization

4.1 Environment-specific values Files

# Environment-specific values files structure
values/
├── values.yaml           # Default values
├── values-dev.yaml       # Development environment
├── values-staging.yaml   # Staging environment
└── values-prod.yaml      # Production environment

values-prod.yaml

# values-prod.yaml - Production environment settings
replicaCount: 5

image:
  tag: "1.2.3"  # Fixed specific version

resources:
  limits:
    cpu: 1000m
    memory: 1Gi
  requests:
    cpu: 500m
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 5
  maxReplicas: 20
  targetCPUUtilizationPercentage: 70

ingress:
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: api-prod-tls
      hosts:
        - api.example.com

env:
  - name: APP_ENV
    value: production
  - name: LOG_LEVEL
    value: warn
  - name: DB_HOST
    valueFrom:
      secretKeyRef:
        name: db-credentials
        key: host
# Combine multiple values files
helm install my-app ./my-chart \
  -f values.yaml \
  -f values-prod.yaml \
  --set image.tag=1.2.4

4.2 Verifying Template Rendering

# Check rendered templates (without actual deployment)
helm template my-release ./my-chart -f values-prod.yaml

# Check specific template only
helm template my-release ./my-chart -s templates/deployment.yaml

# Debug mode
helm install my-release ./my-chart --dry-run --debug

5. Useful Helm Charts

5.1 Installing Prometheus Stack

# Add prometheus-community repository
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Install kube-prometheus-stack (Prometheus + Grafana + AlertManager)
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.adminPassword=admin123

5.2 Installing Grafana Standalone

# Add Grafana repository
helm repo add grafana https://grafana.github.io/helm-charts

# Install Grafana
helm install grafana grafana/grafana \
  --namespace monitoring \
  --set persistence.enabled=true \
  --set adminPassword='admin123'

5.3 Other Useful Charts

# Nginx Ingress Controller
helm install nginx-ingress ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace

# cert-manager (TLS certificate automation)
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set installCRDs=true

# Redis
helm install redis bitnami/redis \
  --set auth.password=mypassword

# PostgreSQL
helm install postgresql bitnami/postgresql \
  --set auth.postgresPassword=mypassword

# Elasticsearch
helm install elasticsearch elastic/elasticsearch \
  --set replicas=3

6. CI/CD Concept Review

6.1 What is CI/CD?

  • CI (Continuous Integration): Frequently integrate code changes with automated builds and tests
  • CD (Continuous Delivery): Always maintain a state ready to deploy to production
  • CD (Continuous Deployment): All changes are automatically deployed to production

6.2 Traditional CI/CD vs GitOps

Feature Traditional CI/CD GitOps
Deployment trigger CI pipeline pushes Pulls Git changes
Source of truth CI server state Git repository
Rollback Requires redeployment Git revert
Audit trail CI logs Git history
Security CI needs cluster access Operates only within cluster

7. Introduction to GitOps

7.1 GitOps Principles

  1. Declarative configuration: Define all infrastructure and application state declaratively
  2. Version control: Use Git as the single source of truth
  3. Automatic synchronization: Approved changes are automatically applied to the cluster
  4. Continuous reconciliation: Continuously detect and correct differences between actual and desired state

7.2 ArgoCD

ArgoCD is the most widely used GitOps tool.

# Install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Install ArgoCD CLI (macOS)
brew install argocd

# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Access via port forwarding
kubectl port-forward svc/argocd-server -n argocd 8080:443

ArgoCD Application Definition

# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/my-app-config.git
    targetRevision: main
    path: kubernetes/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true        # Automatically clean deleted resources
      selfHeal: true     # Auto-recover manual changes
    syncOptions:
      - CreateNamespace=true

7.3 Flux CD

# Install Flux CLI
curl -s https://fluxcd.io/install.sh | sudo bash

# Bootstrap Flux (using GitHub)
flux bootstrap github \
  --owner=myorg \
  --repository=fleet-infra \
  --branch=main \
  --path=./clusters/production \
  --personal

8. Automating K8s Deployment with GitHub Actions

8.1 GitHub Actions Workflow Basic Structure

# .github/workflows/ci-cd.yaml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # 1. Test and Build
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Run linting
        run: npm run lint

  # 2. Build and Push Docker Image
  build:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # 3. Kubernetes Deployment
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Set up kubectl
        uses: azure/setup-kubectl@v3
        with:
          version: 'latest'

      - name: Configure kubeconfig
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config
          chmod 600 $HOME/.kube/config

      - name: Set up Helm
        uses: azure/setup-helm@v3
        with:
          version: 'latest'

      - name: Deploy with Helm
        run: |
          helm upgrade --install my-app ./charts/my-app \
            --namespace production \
            --create-namespace \
            --set image.tag=${{ github.sha }} \
            --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
            --wait \
            --timeout 5m

      - name: Verify deployment
        run: |
          kubectl rollout status deployment/my-app -n production
          kubectl get pods -n production -l app.kubernetes.io/name=my-app

8.2 GitOps-style GitHub Actions

# .github/workflows/gitops-ci.yaml
name: GitOps CI Pipeline

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'Dockerfile'

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  CONFIG_REPO: myorg/k8s-config

jobs:
  build-and-update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

      # GitOps: Update config repository
      - name: Checkout config repo
        uses: actions/checkout@v4
        with:
          repository: ${{ env.CONFIG_REPO }}
          token: ${{ secrets.CONFIG_REPO_TOKEN }}
          path: config-repo

      - name: Update image tag
        run: |
          cd config-repo
          # If using kustomize
          cd kubernetes/overlays/production
          kustomize edit set image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

      - name: Commit and push
        run: |
          cd config-repo
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add .
          git commit -m "Update image to ${{ github.sha }}"
          git push

9. Practical Pipeline Building Example

9.1 Complete Project Structure

my-project/
├── .github/
│   └── workflows/
│       ├── ci.yaml           # CI pipeline
│       ├── cd-staging.yaml   # Staging deployment
│       └── cd-production.yaml # Production deployment
├── src/                      # Application source
├── tests/                    # Test code
├── charts/
│   └── my-app/              # Helm Chart
│       ├── Chart.yaml
│       ├── values.yaml
│       ├── values-staging.yaml
│       ├── values-production.yaml
│       └── templates/
├── Dockerfile
└── package.json

10. Conclusion and Series Wrap-up

In this part, we explored Helm for streamlining Kubernetes application deployment and automated CI/CD pipelines:

  • Helm: Kubernetes package manager that simplifies complex application deployment
  • Chart structure: Reusable packages utilizing templates, values, and helper functions
  • GitOps: Modern deployment approach using Git as the source of truth
  • ArgoCD/Flux: Representative GitOps tools
  • GitHub Actions: Building complete CI/CD pipelines

Docker & Kubernetes Complete Guide Series Summary

Throughout 10 parts, we've covered the core concepts and practical applications of Docker and Kubernetes:

  1. Part 1: Docker basics and container concepts
  2. Part 2: Docker images and Dockerfile
  3. Part 3: Multi-container management with Docker Compose
  4. Part 4: Kubernetes basics and architecture
  5. Part 5: Pod, Deployment, Service
  6. Part 6: Kubernetes Basics - Concepts and Architecture
  7. Part 7: Kubernetes Installation and Cluster Configuration
  8. Part 8: Pod, Deployment, Service Deep Dive
  9. Part 9: ConfigMap, Secret, Ingress
  10. Part 10: Helm and CI/CD Pipeline

We hope this series has given you comprehensive knowledge from container technology basics to production-level Kubernetes operations. Container and orchestration technologies continue to evolve, so keep following official documentation and the community for the latest developments.

Practice and experience are the best teachers. Apply what you've learned to your own projects!