Skip to main content

Helm & GitOps: A Complete Beginner's Guide

· 27 min read
Ashik Mostofa Tonmoy
Senior DevOps Engineer & Platform Engineering Consultant

Orchestration Made Easy with Kubernetes

Table of Contents​

  1. Introduction to Kubernetes Package Management
  2. Helm Charts Fundamentals
  3. Helm for Microservices
  4. GitOps Concepts
  5. ArgoCD Configuration
  6. Complete Workflow Example
  7. Best Practices

Introduction to Kubernetes Package Management​

The problem Helm exists to solve​

A single service on Kubernetes is rarely a single object. It is a Deployment, a Service, a ConfigMap, a Secret, an Ingress or HTTPRoute, usually an HPA, and often a ServiceAccount with a role binding. Six to ten YAML files, most of which differ between development, staging and production by a handful of values — an image tag, a replica count, a hostname, a resource limit.

Copy that set per environment and you now maintain three near-identical trees that drift the moment someone patches one and forgets the others. Copy it per service as well and the drift compounds. That is the problem: not authoring the YAML, but keeping many near-copies of it honest.

Helm's answer is to make the difference between environments a set of values rather than a set of files.

Assumed knowledge​

This guide assumes you know what a Pod, Deployment and Service are, and that you can run kubectl against a cluster. If not, start with the Kubernetes learning guide and come back — the concepts below build directly on it.

The objects Helm will be packaging​

ObjectWhat it isWhat owns it
PodSmallest deployable unit; one or more containers sharing a network namespaceCreated by a controller, rarely by you
DeploymentDeclares replica count, image version and rollout strategyThe chart
ServiceStable in-cluster endpoint in front of a changing set of podsThe chart
ConfigMapNon-secret configuration, mounted or injected as env varsThe chart, values per environment
SecretSensitive values; base64 encoding is not encryptionExternal Secrets or Sealed Secrets, not the chart
NamespaceScope boundary for names, quotas and network policyThe platform, not the chart

Helm Charts Fundamentals​

What is Helm?​

Helm is the package manager for Kubernetes: it takes a directory of templated manifests plus a set of values, renders them, and installs the result as one named, versioned unit called a release.

Two properties matter more than the templating, and they are the reason Helm survived where plain templating tools did not:

  • A release is a unit. helm upgrade replaces a set of objects together, and helm rollback puts the previous set back. Neither is possible when each manifest is applied independently.
  • A chart has an interface. values.yaml is the contract between the chart's author and its consumer. Anything not exposed there is not tunable, which is a feature.

Why Do We Need Helm?​

Before Helm:

# You need to manually apply each file
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml
kubectl apply -f ingress.yaml
# Repeat for each environment (dev, staging, prod)
# Manually manage versions, rollbacks, dependencies

With Helm:

# One command to install everything
helm install my-app ./my-chart

Helm Architecture​

Core Helm Concepts​

ConceptAnalogyDescription
ChartRecipe BoxPackage containing all resources needed to run an app
ReleaseCooked MealRunning instance of a chart in your cluster
RepositoryRecipe WebsiteCollection of charts you can download
ValuesIngredient SubstitutionsConfiguration options to customize deployment
TemplatesRecipe InstructionsKubernetes YAML files with placeholders

Helm Chart Structure​

my-awesome-chart/
├── Chart.yaml # Chart metadata (name, version, description)
├── values.yaml # Default configuration values
├── charts/ # Dependency charts (sub-charts)
├── templates/ # Kubernetes resource templates
│ ├── deployment.yaml # Pod deployment template
│ ├── service.yaml # Service template
│ ├── ingress.yaml # Ingress template
│ ├── configmap.yaml # ConfigMap template
│ ├── _helpers.tpl # Reusable template functions
│ └── NOTES.txt # Post-installation instructions
├── .helmignore # Files to ignore (like .gitignore)
└── README.md # Documentation

Understanding Chart.yaml​

apiVersion: v2 # Helm API version
name: my-awesome-chart # Chart name
description: A simple Helm chart # What this chart does
type: application # Type: application or library
version: 0.1.0 # Chart version (SemVer)
appVersion: "1.0" # Version of the app being deployed
keywords:
- nginx
- web
maintainers:
- name: Your Name
email: you@example.com
dependencies: # Other charts this needs
- name: redis
version: "~10.5.0"
repository: "https://charts.bitnami.com/bitnami"

Understanding values.yaml​

values.yaml is the chart's public interface and its default configuration at the same time. Anything a consumer is allowed to change appears here; anything that would break the chart if changed should not. The defaults should be the values that make the chart run locally without an override file.

# values.yaml - Configuration options

# Number of replicas
replicaCount: 3

# Container image
image:
repository: nginx
tag: "1.25.0"
pullPolicy: IfNotPresent

# Service configuration
service:
type: LoadBalancer
port: 80
targetPort: 8080

# Resource limits
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi

# Environment-specific settings
environment: production

# Feature flags
features:
monitoring: true
logging: true

Understanding Templates​

Templates use Go templating with special Helm functions:

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}
labels:
app: {{ .Chart.Name }}
version: {{ .Chart.AppVersion }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
metadata:
labels:
app: {{ .Chart.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.targetPort }}
resources:
{{- toYaml .Values.resources | nindent 10 }}

Template Variables Explained:

VariableDescriptionExample
{{ .Release.Name }}Name you give during installationmy-app
{{ .Chart.Name }}Chart name from Chart.yamlmy-awesome-chart
{{ .Chart.Version }}Chart version0.1.0
{{ .Values.xxx }}Value from values.yaml.Values.replicaCount

Hands-On: Creating Your First Helm Chart​

Step 1: Install Helm​

# macOS
brew install helm

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

# Windows (using Chocolatey)
choco install kubernetes-helm

# Verify installation
helm version

Step 2: Create a New Chart​

# Create a new chart called "my-nginx-app"
helm create my-nginx-app

# Navigate to the chart
cd my-nginx-app

# View the structure
tree .

Step 3: Customize values.yaml​

# Edit values.yaml
nano values.yaml

Replace with this simple configuration:

replicaCount: 2

image:
repository: nginx
tag: "1.25.0"
pullPolicy: IfNotPresent

service:
type: NodePort
port: 80
targetPort: 80

resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi

Step 4: Validate Your Chart​

# Lint the chart (check for errors)
helm lint .

# Dry-run to see what would be deployed
helm install --dry-run --debug my-nginx-app .

# Template rendering (see final YAML)
helm template my-nginx-app .

Step 5: Install the Chart​

# Install in your Kubernetes cluster
helm install my-nginx-app .

# Check the installation
helm list

# Check the pods
kubectl get pods

# Check the service
kubectl get svc

Expected Output:

NAME: my-nginx-app
LAST DEPLOYED: Thu Oct 16 2025 10:30:00
NAMESPACE: default
STATUS: deployed
REVISION: 1

Step 6: Upgrade Your Chart​

# Modify values.yaml (e.g., change replicas to 3)
nano values.yaml

# Upgrade the release
helm upgrade my-nginx-app .

# Check revision history
helm history my-nginx-app

Step 7: Rollback if Needed​

# Rollback to previous version
helm rollback my-nginx-app 1

# Check status
helm status my-nginx-app

Step 8: Uninstall​

# Uninstall the release
helm uninstall my-nginx-app

# Verify removal
kubectl get pods

Helm for Microservices​

Understanding Microservices Architecture​

The packaging problem with microservices is that the services are 90% alike. Each needs a Deployment, a Service, config, secrets and usually an ingress route; what differs is the image, the port, the resource envelope and a handful of environment-specific values. Charting each one separately reproduces the duplication Helm was supposed to remove — the useful patterns are a shared library chart, or one chart with a values file per service.

Real-World Example: Google's Online Boutique​

Let's use the Google Cloud microservices demo as our example.

Architecture Overview​

ServiceLanguagePurpose
frontendGoWeb UI
cartserviceC#Shopping cart
productcatalogserviceGoProduct information
currencyserviceNode.jsCurrency conversion
paymentserviceNode.jsPayment processing
shippingserviceGoShipping cost calculation
emailservicePythonOrder confirmation emails
checkoutserviceGoOrder checkout
recommendationservicePythonProduct recommendations
adserviceJavaAdvertisement
redis-cartRedisCart data storage

Setting Up the Microservices Demo​

Option 1: Quick Start (Using Pre-built Manifests)​

# Clone the repository
git clone --depth 1 --branch v0 https://github.com/GoogleCloudPlatform/microservices-demo.git
cd microservices-demo

# Apply all services at once
kubectl apply -f ./release/kubernetes-manifests.yaml

# Wait for pods to be ready
kubectl get pods -w

# Get the frontend URL
kubectl get service frontend-external

First, let's create a Helm chart for the Online Boutique.

Creating a Helm Chart for Microservices​

# Create the chart structure
helm create online-boutique
cd online-boutique

Chart.yaml​

apiVersion: v2
name: online-boutique
description: Google Cloud microservices demo application
type: application
version: 0.1.0
appVersion: "v0.10.0"
keywords:
- microservices
- demo
- ecommerce
maintainers:
- name: DevOps Team
email: devops@example.com

values.yaml for Microservices​

# Global settings
global:
environment: production
domain: boutique.example.com

# Frontend service
frontend:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/frontend
tag: v0.10.0
service:
type: LoadBalancer
port: 80
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi

# Product Catalog Service
productcatalog:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/productcatalogservice
tag: v0.10.0
service:
port: 3550
resources:
limits:
cpu: 200m
memory: 128Mi

# Cart Service
cartservice:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/cartservice
tag: v0.10.0
service:
port: 7070
resources:
limits:
cpu: 300m
memory: 256Mi

# Redis Cache
redis:
enabled: true
architecture: standalone
auth:
enabled: false
master:
resources:
limits:
cpu: 200m
memory: 256Mi

# Checkout Service
checkoutservice:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/checkoutservice
tag: v0.10.0
service:
port: 5050
resources:
limits:
cpu: 200m
memory: 128Mi

# Currency Service
currencyservice:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/currencyservice
tag: v0.10.0
service:
port: 7000
resources:
limits:
cpu: 200m
memory: 128Mi

# Payment Service
paymentservice:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/paymentservice
tag: v0.10.0
service:
port: 50051
resources:
limits:
cpu: 200m
memory: 128Mi

# Email Service
emailservice:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/emailservice
tag: v0.10.0
service:
port: 5000
resources:
limits:
cpu: 200m
memory: 128Mi

# Shipping Service
shippingservice:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/shippingservice
tag: v0.10.0
service:
port: 50051
resources:
limits:
cpu: 200m
memory: 128Mi

# Recommendation Service
recommendationservice:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/recommendationservice
tag: v0.10.0
service:
port: 8080
resources:
limits:
cpu: 200m
memory: 256Mi

# Ad Service
adservice:
replicaCount: 2
image:
repository: gcr.io/google-samples/microservices-demo/adservice
tag: v0.10.0
service:
port: 9555
resources:
limits:
cpu: 300m
memory: 300Mi

Template Example: templates/frontend-deployment.yaml​

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-frontend
labels:
app: frontend
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
spec:
replicas: {{ .Values.frontend.replicaCount }}
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}"
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
- name: PRODUCT_CATALOG_SERVICE_ADDR
value: "{{ .Release.Name }}-productcatalog:3550"
- name: CURRENCY_SERVICE_ADDR
value: "{{ .Release.Name }}-currencyservice:7000"
- name: CART_SERVICE_ADDR
value: "{{ .Release.Name }}-cartservice:7070"
- name: RECOMMENDATION_SERVICE_ADDR
value: "{{ .Release.Name }}-recommendationservice:8080"
- name: SHIPPING_SERVICE_ADDR
value: "{{ .Release.Name }}-shippingservice:50051"
- name: CHECKOUT_SERVICE_ADDR
value: "{{ .Release.Name }}-checkoutservice:5050"
- name: AD_SERVICE_ADDR
value: "{{ .Release.Name }}-adservice:9555"
resources:
{{- toYaml .Values.frontend.resources | nindent 10 }}
livenessProbe:
httpGet:
path: /_healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /_healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10

Template Example: templates/frontend-service.yaml​

apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-frontend-external
labels:
app: frontend
spec:
type: {{ .Values.frontend.service.type }}
selector:
app: frontend
ports:
- name: http
port: {{ .Values.frontend.service.port }}
targetPort: 8080

Installing Microservices with Helm​

# Install the entire application
helm install boutique ./online-boutique

# Check deployment status
helm status boutique

# Watch pods coming up
kubectl get pods -l "release=boutique" -w

# Get the frontend URL
kubectl get service boutique-frontend-external

# Test different configurations for different environments
# Development environment
helm install boutique-dev ./online-boutique \
--set frontend.replicaCount=1 \
--set global.environment=development

# Production environment with more replicas
helm install boutique-prod ./online-boutique \
--set frontend.replicaCount=5 \
--set productcatalog.replicaCount=5 \
--set global.environment=production

Environment-Specific Values Files​

Create separate values files for each environment:

values-dev.yaml

global:
environment: development

frontend:
replicaCount: 1
resources:
limits:
cpu: 100m
memory: 128Mi

productcatalog:
replicaCount: 1

values-prod.yaml

global:
environment: production

frontend:
replicaCount: 5
resources:
limits:
cpu: 500m
memory: 512Mi

productcatalog:
replicaCount: 5

Install with environment-specific values:

# Development
helm install boutique-dev ./online-boutique -f values-dev.yaml

# Production
helm install boutique-prod ./online-boutique -f values-prod.yaml

GitOps Concepts​

What is GitOps?​

GitOps is a deployment model with one defining property: the cluster's desired state lives in Git, and a controller inside the cluster continuously reconciles reality against it. Nothing pushes to the cluster from CI.

That inversion is what buys you the properties people want from GitOps. Drift is corrected rather than merely detected, because reconciliation is continuous. Rollback is a revert, because the previous state is a commit. And CI needs no cluster credentials at all, because it never talks to the cluster — which removes the most over-privileged secret in a typical pipeline.

Traditional Deployment vs GitOps:

GitOps Principles​

PrincipleDescriptionBenefit
DeclarativeSystem state described declarativelyClear, version-controlled desired state
Versioned & ImmutableEverything in Git, never modified in placeComplete audit trail, easy rollback
Pulled AutomaticallySoftware agents pull desired stateNo external credentials needed
Continuously ReconciledSoftware agents verify actual stateSelf-healing, drift detection

Why GitOps?​

Without GitOps:

# Developer manually deploys
kubectl apply -f deployment.yaml
# Who deployed? When? Why? What was the previous state?
# No audit trail, hard to rollback, risky

With GitOps:

# Developer commits change to Git
git add deployment.yaml
git commit -m "Increase replicas to 5 for Black Friday"
git push

# ArgoCD automatically:
# 1. Detects the change
# 2. Validates the change
# 3. Applies to cluster
# 4. Monitors for drift
# Complete audit trail in Git history!

GitOps Workflow​

Repository Structure for GitOps​

Two Repository Approach (Recommended):

app-source-repo/ # Application Code
├── src/
│ └── main.go
├── Dockerfile
└── .github/
└── workflows/
└── ci.yaml

app-config-repo/ # Kubernetes Manifests
├── environments/
│ ├── dev/
│ │ ├── values.yaml
│ │ └── kustomization.yaml
│ ├── staging/
│ │ └── values.yaml
│ └── prod/
│ └── values.yaml
├── helm-charts/
│ └── my-app/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
└── argocd/
└── applications/
├── dev-app.yaml
├── staging-app.yaml
└── prod-app.yaml

Why Separate Repositories?

  • Security: Different access controls
  • Clarity: Code changes vs config changes
  • Automation: CI/CD pipelines are simpler
  • Auditability: Clear separation of concerns

ArgoCD Configuration​

What is ArgoCD?​

ArgoCD is a controller that runs in the cluster and implements that reconciliation loop. It watches one or more Git repositories, renders whatever they contain (plain manifests, Kustomize overlays, Helm charts), diffs the result against live objects, and either reports the difference or corrects it, depending on the sync policy.

ArgoCD Architecture Components​

ComponentPurposeAnalogy
API ServerHandles API calls, UI, CLIReceptionist
Repository ServerFetches manifests from GitLibrarian
Application ControllerMonitors apps, syncs stateQuality Inspector

Installing ArgoCD​

Step 1: Create Namespace and Install​

Better use helm https://github.com/argoproj/argo-helm/tree/main/charts/argo-cd

# Create namespace
kubectl create namespace argocd

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

# Wait for pods to be ready
kubectl get pods -n argocd -w

Step 2: Access ArgoCD UI​

Option 1: Port Forward (Quick Test)

# Forward port 8080 to ArgoCD server
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Access at: https://localhost:8080

Option 2: LoadBalancer (Production)

# Change service type to LoadBalancer
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'

# Get external IP
kubectl get svc argocd-server -n argocd

Option 3: Ingress (Recommended for Production)

# argocd-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server-ingress
namespace: argocd
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
ingressClassName: nginx
rules:
- host: argocd.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 443
tls:
- hosts:
- argocd.example.com
secretName: argocd-tls

Apply ingress:

kubectl apply -f argocd-ingress.yaml

Step 3: Get Initial Admin Password​

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

Default Credentials:

  • Username: admin
  • Password: (from the command above)

Step 4: Login with CLI​

# Install ArgoCD CLI
# macOS
brew install argocd

# Linux
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd
sudo mv argocd /usr/local/bin/

# Login
argocd login localhost:8080 --username admin --password <initial-password>

# Change password
argocd account update-password

Configuring Your First Application​

Method 1: Using CLI​

# Create application
argocd app create boutique-dev \
--repo https://github.com/your-org/app-config-repo.git \
--path helm-charts/online-boutique \
--dest-server https://kubernetes.default.svc \
--dest-namespace default \
--values values-dev.yaml \
--sync-policy automated \
--auto-prune \
--self-heal

# Check application status
argocd app get boutique-dev

# Sync manually (if not auto-sync)
argocd app sync boutique-dev

# View logs
argocd app logs boutique-dev
# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: boutique-dev
namespace: argocd
# Finalizer that ensures cascading deletes
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
# The project the application belongs to
project: default

# Source of the application manifests
source:
repoURL: https://github.com/your-org/app-config-repo.git
targetRevision: HEAD # Can be branch, tag, or commit SHA
path: helm-charts/online-boutique

# Helm specific config
helm:
# Use specific values file
valueFiles:
- values-dev.yaml

# Override specific values
parameters:
- name: frontend.replicaCount
value: "2"
- name: global.environment
value: "development"

# Destination cluster and namespace
destination:
server: https://kubernetes.default.svc
namespace: default

# Sync policy
syncPolicy:
automated:
# Automatically sync when Git changes
prune: true # Delete resources not in Git
selfHeal: true # Force sync if cluster state changes
allowEmpty: false # Don't sync if repo is empty

syncOptions:
- CreateNamespace=true # Auto-create namespace if needed

# Retry config
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m

# Health check configuration
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Ignore if HPA modifies replicas

Apply the application:

kubectl apply -f argocd-application.yaml

# Watch it sync
kubectl get applications -n argocd -w

ArgoCD Sync Policies​

PolicyDescriptionUse Case
ManualRequire manual trigger to syncProduction deployments requiring approval
AutomatedAuto-sync on Git changesDevelopment/staging environments
PruneDelete resources not in GitKeep cluster clean
Self-HealRevert manual cluster changesEnforce GitOps principles

Advanced ArgoCD Configurations​

Multi-Environment Setup​

# argocd-apps/dev-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: boutique-dev
namespace: argocd
spec:
project: boutique
source:
repoURL: https://github.com/your-org/app-config-repo.git
targetRevision: develop
path: helm-charts/online-boutique
helm:
valueFiles:
- values-dev.yaml
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
---
# argocd-apps/staging-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: boutique-staging
namespace: argocd
spec:
project: boutique
source:
repoURL: https://github.com/your-org/app-config-repo.git
targetRevision: main
path: helm-charts/online-boutique
helm:
valueFiles:
- values-staging.yaml
destination:
server: https://kubernetes.default.svc
namespace: staging
syncPolicy:
automated:
prune: true
selfHeal: false # Require approval for staging
---
# argocd-apps/prod-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: boutique-prod
namespace: argocd
spec:
project: boutique
source:
repoURL: https://github.com/your-org/app-config-repo.git
targetRevision: v1.0.0 # Use tags for prod
path: helm-charts/online-boutique
helm:
valueFiles:
- values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: prod
syncPolicy:
automated: false # Manual sync for production

App of Apps Pattern​

Create a master app that manages other apps:

# argocd-apps/root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/app-config-repo.git
targetRevision: HEAD
path: argocd-apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true

Now all your apps are managed by this root app!

kubectl apply -f argocd-apps/root-app.yaml

ArgoCD Projects (Multi-Tenancy)​

# argocd-project.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: boutique
namespace: argocd
spec:
description: Online Boutique Application

# Allow deployments to these clusters
destinations:
- namespace: dev
server: https://kubernetes.default.svc
- namespace: staging
server: https://kubernetes.default.svc
- namespace: prod
server: https://kubernetes.default.svc

# Allow pulling from these Git repos
sourceRepos:
- https://github.com/your-org/app-config-repo.git

# Allowed resource types
clusterResourceWhitelist:
- group: ''
kind: Namespace

namespaceResourceWhitelist:
- group: 'apps'
kind: Deployment
- group: ''
kind: Service
- group: ''
kind: ConfigMap
- group: ''
kind: Secret

# RBAC roles
roles:
- name: developer
description: Developers can view and sync
policies:
- p, proj:boutique:developer, applications, get, boutique/*, allow
- p, proj:boutique:developer, applications, sync, boutique/dev-*, allow
- name: admin
description: Admins have full access
policies:
- p, proj:boutique:admin, applications, *, boutique/*, allow

Complete Workflow Example​

Let's put everything together with a real-world scenario!

Scenario: Deploying Online Boutique with GitOps​

Step-by-Step Implementation​

1. Repository Setup​

App Source Repository:

# Create and clone your app repo
git clone https://github.com/your-org/online-boutique-app.git
cd online-boutique-app

# Structure
# online-boutique-app/
# ├── src/
# │ ├── frontend/
# │ ├── cartservice/
# │ └── productcatalogservice/
# ├── Dockerfile
# └── .github/
# └── workflows/
# └── ci.yaml

Config Repository:

# Create and clone your config repo
git clone https://github.com/your-org/online-boutique-config.git
cd online-boutique-config

# Structure
# online-boutique-config/
# ├── helm-charts/
# │ └── online-boutique/
# ├── environments/
# │ ├── dev/
# │ ├── staging/
# │ └── prod/
# └── argocd-apps/

2. CI Pipeline (GitHub Actions)​

Create .github/workflows/ci.yaml:

name: CI/CD Pipeline

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

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

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write

steps:
- name: Checkout code
uses: actions/checkout@v3

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

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

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

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

- name: Update Helm values
env:
IMAGE_TAG: ${{ github.sha }}
run: |
# Clone config repo
git clone https://github.com/your-org/online-boutique-config.git
cd online-boutique-config

# Update image tag in values file
sed -i "s/tag: .*/tag: \"$IMAGE_TAG\"/" \
environments/dev/values.yaml

# Commit and push
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add environments/dev/values.yaml
git commit -m "Update image tag to $IMAGE_TAG"
git push https://x-access-token:${{ secrets.GH_PAT }}@github.com/your-org/online-boutique-config.git

3. Helm Chart Configuration​

helm-charts/online-boutique/values.yaml:

# Default values
global:
environment: development
imageRegistry: ghcr.io/your-org

frontend:
enabled: true
replicaCount: 2
image:
repository: online-boutique-app/frontend
tag: "latest"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80

# Similar blocks for other services...
productcatalog:
enabled: true
replicaCount: 2
# ... config

cartservice:
enabled: true
replicaCount: 2
# ... config

# etc.

environments/dev/values.yaml:

global:
environment: development

frontend:
replicaCount: 1
resources:
limits:
cpu: 100m
memory: 128Mi

productcatalog:
replicaCount: 1

environments/prod/values.yaml:

global:
environment: production

frontend:
replicaCount: 5
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20
resources:
limits:
cpu: 500m
memory: 512Mi

productcatalog:
replicaCount: 5
autoscaling:
enabled: true

4. ArgoCD Application Setup​

argocd-apps/boutique-dev.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: boutique-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-org/online-boutique-config.git
targetRevision: develop
path: helm-charts/online-boutique
helm:
valueFiles:
- ../../environments/dev/values.yaml
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

5. Deploy Everything​

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

# 2. Apply ArgoCD applications
kubectl apply -f argocd-apps/boutique-dev.yaml

# 3. Watch the magic happen!
kubectl get applications -n argocd
argocd app get boutique-dev --watch

# 4. Access the application
kubectl get svc -n dev

6. Make a Change and Watch GitOps in Action​

# In your app source repo
cd online-boutique-app
echo "console.log('New feature!');" >> src/frontend/app.js

git add .
git commit -m "Add new feature"
git push origin main

# GitHub Actions will:
# 1. Build new image
# 2. Push to registry
# 3. Update config repo

# ArgoCD will:
# 1. Detect the change in config repo
# 2. Pull new manifests
# 3. Apply to cluster
# 4. Show sync status in UI

# Watch it happen:
argocd app watch boutique-dev

Troubleshooting Common Issues​

Issue 1: Application Out of Sync

# Check differences
argocd app diff boutique-dev

# Manual sync
argocd app sync boutique-dev

# Force sync (if needed)
argocd app sync boutique-dev --force

Issue 2: Image Pull Errors

# Create image pull secret
kubectl create secret docker-registry regcred \
--docker-server=ghcr.io \
--docker-username=your-username \
--docker-password=your-token \
-n dev

# Reference in deployment
# Add to values.yaml:
imagePullSecrets:
- name: regcred

Issue 3: Health Check Failures

# Check pod logs
kubectl logs -n dev deployment/boutique-dev-frontend

# Check events
kubectl get events -n dev --sort-by='.lastTimestamp'

# Describe deployment
kubectl describe deployment boutique-dev-frontend -n dev

Best Practices​

Helm Best Practices​

  1. Version Everything

    # Always specify versions
    apiVersion: v2
    version: 1.2.3
    appVersion: "2.0.1"
  2. Use Semantic Versioning

    • MAJOR.MINOR.PATCH (1.2.3)
    • MAJOR: Breaking changes
    • MINOR: New features
    • PATCH: Bug fixes
  3. Document Your Charts

    # README.md
    ## Configuration
    | Parameter | Description | Default |
    |-----------|-------------|---------|
    | replicaCount | Number of replicas | 3 |
  4. Use _helpers.tpl for Reusable Templates

    {{- define "myapp.fullname" -}}
    {{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
    {{- end }}
  5. Always Set Resource Limits

    resources:
    limits:
    cpu: 500m
    memory: 512Mi
    requests:
    cpu: 250m
    memory: 256Mi

GitOps Best Practices​

  1. Separate Code from Config

    • One repo for application code
    • One repo for Kubernetes manifests
  2. Use Branches for Environments

    • develop → dev environment
    • main → staging environment
    • tags → production environment
  3. Implement PR Reviews

    # GitHub branch protection
    - Require pull request reviews
    - Require status checks
    - Require signed commits
  4. Never Commit Secrets

    # Use Sealed Secrets or External Secrets
    # Never in Git:
    password: mypassword # never do this

    # Instead:
    apiVersion: bitnami.com/v1alpha1
    kind: SealedSecret
  5. Tag Production Releases

    git tag -a v1.0.0 -m "Production release"
    git push origin v1.0.0

ArgoCD Best Practices​

  1. Use Projects for Multi-Tenancy

    # Isolate teams
    spec:
    project: team-a
  2. Enable Automated Sync with Caution

    # Dev: auto-sync on
    # Staging: auto-sync with approval
    # Prod: manual sync
  3. Configure Notifications

    # Slack, email notifications
    apiVersion: v1
    kind: ConfigMap
    metadata:
    name: argocd-notifications-cm
    data:
    service.slack: |
    token: $slack-token
  4. Use Health Checks

    # Custom health assessments
    healthCheck:
    http:
    path: /healthz
  5. Implement RBAC

    # Role-based access
    p, role:developer, applications, get, */*, allow
    p, role:developer, applications, sync, */dev-*, allow

Security Best Practices​

  1. Scan Images

    # In CI pipeline
    - name: Scan image
    uses: aquasecurity/trivy-action@master
  2. Use Non-Root Containers

    securityContext:
    runAsNonRoot: true
    runAsUser: 1000
  3. Implement Network Policies

    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
    name: deny-all
    spec:
    podSelector: {}
    policyTypes:
    - Ingress
    - Egress
  4. Rotate Credentials

    # Regular rotation
    argocd account update-password
  5. Audit Logs

    # Monitor ArgoCD activity
    kubectl logs -n argocd deployment/argocd-server

Quick Reference Commands​

Helm Commands​

# Chart Management
helm create <chart-name> # Create new chart
helm lint <chart> # Validate chart
helm template <chart> # Render templates
helm package <chart> # Package chart

# Repository Management
helm repo add <name> <url> # Add repository
helm repo update # Update repositories
helm search repo <keyword> # Search charts

# Release Management
helm install <name> <chart> # Install chart
helm upgrade <name> <chart> # Upgrade release
helm rollback <name> <revision> # Rollback release
helm uninstall <name> # Delete release
helm list # List releases
helm history <name> # View release history

# Debugging
helm get values <name> # Show values
helm get manifest <name> # Show manifests
helm get all <name> # Show everything

ArgoCD Commands​

# Application Management
argocd app create <name> # Create app
argocd app get <name> # Get app details
argocd app list # List apps
argocd app delete <name> # Delete app

# Sync Operations
argocd app sync <name> # Sync app
argocd app sync <name> --force # Force sync
argocd app wait <name> # Wait for sync

# Monitoring
argocd app watch <name> # Watch app status
argocd app logs <name> # View logs
argocd app diff <name> # Show differences

# Repository Management
argocd repo add <url> # Add repository
argocd repo list # List repositories

Kubectl Commands​

# Pod Management
kubectl get pods # List pods
kubectl logs <pod> # View logs
kubectl exec -it <pod> -- /bin/bash # Shell into pod
kubectl describe pod <pod> # Describe pod

# Deployment Management
kubectl get deployments # List deployments
kubectl scale deployment <name> --replicas=5
kubectl rollout status deployment/<name>
kubectl rollout history deployment/<name>

# Service Management
kubectl get services # List services
kubectl expose deployment <name> # Expose deployment

# Namespace Management
kubectl get namespaces # List namespaces
kubectl create namespace <name> # Create namespace
kubectl config set-context --current --namespace=<name>

Conclusion​

Congratulations! You now understand:

  • Kubernetes fundamentals - The building blocks
  • Helm Charts - Package management for K8s
  • Microservices deployment - Complex apps made simple
  • GitOps principles - Git as the source of truth
  • ArgoCD - Automated continuous delivery
  • Complete workflow - From code to production

Next Steps​

  1. Practice: Set up a test cluster and try the examples
  2. Explore: Check out Artifact Hub for more charts
  3. Contribute: Share your charts with the community
  4. Learn More: Dive into advanced topics like:
    • Helm hooks
    • ArgoCD ApplicationSets
    • Progressive delivery with Argo Rollouts
    • Secret management with Sealed Secrets

Additional Resources​


The part of this that survives contact with production is the two-repository split — control plane in one, values in another. The rest is mechanism you can look up. Get that boundary wrong and you will be untangling it a year later.