Blog Post

Apps on Azure Blog
7 MIN READ

Deploy Cloud Native Apps on AKS - Happy Helming

monojit18's avatar
monojit18
Icon for Microsoft rankMicrosoft
Jan 26, 2022

Introduction

Helm is the package manager for the applications built for Kubernetes. Helm helps to manage complex Kubernetes applications with minimal effort. Helm packages are bundled as Charts which are easy to Create, Manage, Deploy and Rollback onto K8s clusters.

 

What this article covers

  • An overview of Helm chart and its components

  • An insight into the Architecture of Helm

  • A deep look at how Helm chart can be used to deploy Containerised applications onto K8s clusters

  • Anatomy of Helm chart templates and configurations

  • Some hands-on examples

  • A quick showcase of how Helm can work with Azure DevOps to automate application deployment

Components

Chart.yaml

  • Contains information about the chart

  • Modify Description

  • Modify Chart Version

  • Modify App Version

apiVersion: v2
name: ratingsapi-chart
description: A Helm chart for Ratings API

# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 1.0.0

 

values.yaml

  • Configuration values for the chart

deployment:
name: ratingsapi-deploy
namespace: aks-workshop-dev
labels:
  app: ratingsapi-deploy
selectorLabels:
  app: ratingsapi-pod    
replicas: 2
strategyType: RollingUpdate
maxSurge: 1
maxUnavailable: 1
nodeSelector:
  agentpool: aksapipool
containers:
- name: ratingsapi-app
  image: <acr_name>/<image_name>:<tag>
  imagePullPolicy: IfNotPresent
  readinessPort: 3000
  readinessPath: /healthz
  livenessPort: 3000
  livenessPath: /healthz
  memoryRequest: "64Mi"
  cpuRequest: "100m"
  memoryLimit: "256Mi"
  cpuLimit: "200m"
  containerPorts: [3000]    
  env:          
  - name: MONGODB_URI
    valueKey: MONGOCONNECTION
    valueSecret: aks-workshop-mongo-secret
service:
name: ratingsapi-service
namespace: aks-workshop-dev
selector:
  app: ratingsapi-pod
type: ClusterIP
ports:
- protocol: TCP
  port: 80
  targetPort: 3000

 

Chart Templates

  • Contains multiple Chart templates

  • All templates are deployed at once

  • Each Template contains the definition of a K8s object

    • Same format as original Deployment YAML file

    • Values are replaced by templated values

    • Filled up by corresponding values from Values YAML file at runtime

Let us Get into some Action

Create Helm Chart

helm create <chart-name>

 

 

Folder Structure

  • templates

    • Contains multiple template yamls

  • .helmignore

    • File containing entries that should NOT be included in the helm package

  • values-xxx.yaml

    • Configuration values for the chart

  • Configure Chart Release

    • Chart.yaml

       

       

      • Contains information about the chart

      • Modify Description

      • Modify Chart Version

      • Modify App Version

         

Define a Chart Template

  • Conditions

    • Check existence of an item in yaml

      {{ if $ingress.annotations.rewriteTarget }}
        nginx.ingress.kubernetes.io/rewrite-target: {{ $ingress.annotations.rewriteTarget }}
      {{ end }}

       

  • Loops

    • Repetitive entries in yaml

      {{- range $host := $ingress.hosts }}
      - host: {{ $host.name}}
        http:
          paths:
           {{- range $path := $host.paths }}
          - path: {{ $path.path }}
            pathType: {{ $path.pathType }}
            backend:
              service:
                name: {{ $path.service }}
                port:
                  number: {{ $path.port }}
           {{- end }}
      {{- end }}
    • range denotes the start of the Loop block

    • {{- end }} denotes the end of the Loop block

    • Assign items in the Loop block in a variable

      $host := $ingress.hosts
      ......
      $path := $host.paths

       

  • Assign Single Key/Value pair

    • Single Value assignments are straight forward

      pathType: {{ $path.pathType }}

       

  • Assign Multiple Key/Value pairs

    • Multiple Value assignments are performed using toYaml keyword

    • Indentation is for successful assignment

      Note nindent value - 6

      spec:
      selector:
        matchLabels:
      {{ toYaml $deployment.selectorLabels | nindent 6 }}
    • 4 Blank Spaces from Left + 2 TAB Spaces (standard K8s object hierarchy)

    • Few More Examples

      Note nindent value = 8

        template:
        metadata:
          labels:
           {{ toYaml $deployment.selectorLabels | nindent 8 }}

      Note nindent value = 4

      metadata:
      name: {{ $deployment.name }}
      namespace: {{ $deployment.namespace }}
      labels:
       {{ toYaml $deployment.labels | nindent 4 }}

     

  • Lookup

    • Format = lookup (apiVersion, kind, namespace, name)

    • apiVersion and kind are mandatory parameters

    • namespace parameter empty implies all k8s namespaces

    • name parameter empty implies all k8s resources of the specified kind

  • Examples

    Deployment
    {{ $deployment := .Values.deployment }}
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: {{ $deployment.name }}
    namespace: {{ $deployment.namespace }}
    labels:
     {{ toYaml $deployment.labels | nindent 4 }}
    spec:
    selector:
      matchLabels:
       {{ toYaml $deployment.selectorLabels | nindent 6 }}
    replicas: {{ $deployment.replicas }}
    strategy:
      type: {{ $deployment.strategyType }}
      rollingUpdate:
        maxSurge: {{ $deployment.maxSurge }}
        maxUnavailable: {{ $deployment.maxUnavailable }}
    template:
      metadata:
        labels:
         {{ toYaml $deployment.selectorLabels | nindent 8 }}
      spec:
        nodeSelector:
         {{ toYaml $deployment.nodeSelector | nindent 8 }}
         {{ if $deployment.imagePullSecrets }}
         {{- range $secret := $deployment.imagePullSecrets }}
        imagePullSecrets:
        - name: {{ $secret.name }}
         {{- end}}
         {{ end }}
        containers:
         {{- range $container := $deployment.containers }}
        - name: {{ $container.name }}
          image: {{ $container.image }}
          imagePullPolicy: {{ $container.imagePullPolicy }}
          resources:
            requests:
              memory: {{ $container.memoryRequest }}
              cpu: {{ $container.cpuRequest }}
            limits:
              memory: {{ $container.memoryLimit }}
              cpu: {{ $container.cpuLimit }}
           {{ if and $container.readinessPort $container.readinessPath}}
          readinessProbe:
            httpGet:
              port: {{ $container.readinessPort }}
              path: {{ $container.readinessPath }}
           {{ end }}
           {{ if and $container.livenessPort $container.livenessPath }}
          livenessProbe:
            httpGet:
              port: {{ $container.livenessPort }}
              path: {{ $container.livenessPath }}
           {{ end }}
          env:
           {{- range $env := $container.env }}
          - name: {{ $env.name }}
            valueFrom:
              secretKeyRef:
                key: {{ $env.valueKey }}
                name: {{ $env.valueSecret }}
           {{- end }}
          ports:
           {{- range $containerPort := $container.containerPorts }}
          - containerPort: {{ $containerPort }}
           {{- end }}
         {{- end }}      

     

    Service
    {{ $service := .Values.service }}
    apiVersion: v1
    kind: Service
    metadata:
    name: {{ $service.name }}
    namespace: {{ $service.namespace }}
    spec:
    selector:
     {{ toYaml $service.selector | nindent 4}}
    ports:
     {{ range $port := $service.ports }}
    - protocol: {{ $port.protocol }}
      port: {{ $port.port }}
      targetPort: {{ $port.targetPort }}
     {{ end }}
    type: ClusterIP

     

    RBAC
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
    name: {{ .Values.developer.roleName }}
    namespace: {{ .Values.developer.roleNamespace }}
    rules:
    {{- range $rule := .Values.developer.rules}}
    - apiGroups: {{ $rule.apiGroups }}
    resources: {{ $rule.resources }}
    verbs: {{ $rule.verbs }}
    {{- end }}

    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
    name: {{ .Values.developer.bindingName }}
    namespace: {{ .Values.developer.bindingNamespace }}
    roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: Role
    name: {{ .Values.developer.roleName }}
    subjects:
    {{- range $subject := .Values.developer.subjects}}
    - apiGroup: rbac.authorization.k8s.io
    name: {{ $subject.name }}
    kind: {{ $subject.kind }}
    {{- end }}
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
    name: {{ .Values.manager.roleName }}
    namespace: {{ .Values.manager.roleNamespace }}
    rules:
    {{- range $rule := .Values.developer.rules}}
    - apiGroups: {{ $rule.apiGroups }}
    resources: {{ $rule.resources }}
    verbs: {{ $rule.verbs }}
    {{- end }}

    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
    name: {{ .Values.manager.bindingName }}
    namespace: {{ .Values.manager.bindingNamespace }}
    roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: Role
    name: {{ .Values.manager.roleName }}
    subjects:
    {{- range $subject := .Values.manager.subjects}}
    - apiGroup: rbac.authorization.k8s.io
    name: {{ $subject.name }}
    kind: {{ $subject.kind }}
    {{- end }}
    # Check for existence of the ClusterRoleBinding resource with the name - aks-workshop-cluster-admin-bindings in all k8s namespaces
    {{if not (lookup "rbac.authorization.k8s.io/v1" "ClusterRoleBinding" "" "aks-workshop-cluster-admin-bindings") }}
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
    name: {{ .Values.clusteradmin.name }}
    roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: ClusterRole
    name: {{ .Values.clusteradmin.roleName }}
    subjects:
    - apiGroup: rbac.authorization.k8s.io
    name: {{ (index .Values.clusteradmin.subjects 0).name}}
    kind: {{ (index .Values.clusteradmin.subjects 0).kind}}
    {{ end }}

     

    Ingress
    {{ $ingress := .Values.ingress }}
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
    name: {{ $ingress.name }}    
    namespace: {{ $ingress.namespace }}  
    annotations:
      kubernetes.io/ingress.class: {{ $ingress.annotations.ingressClass }}
       {{ if $ingress.annotations.rewriteTarget }}
      nginx.ingress.kubernetes.io/rewrite-target: {{ $ingress.annotations.rewriteTarget }}
       {{ end }}  
       {{ if $ingress.annotations.enableCors }}
      nginx.ingress.kubernetes.io/enable-cors: {{ $ingress.annotations.enableCors | quote }}
       {{ end }}  
       {{ if $ingress.annotations.proxyBodySize }}
      nginx.ingress.kubernetes.io/proxy-body-size: {{ $ingress.annotations.proxyBodySize }}
       {{ end }}
       {{ if $ingress.annotations.backendProtocol }}
      nginx.ingress.kubernetes.io/backend-protocol: {{ $ingress.annotations.backendProtocol }}
       {{ end }}
       {{ if $ingress.annotations.sslpassThrough }}
      nginx.ingress.kubernetes.io/ssl-passthrough: {{ $ingress.annotations.sslpassThrough | quote }}
       {{ end }}    
    spec:
    rules:
     {{- range $host := $ingress.hosts }}
    - host: {{ $host.name}}
      http:
        paths:
         {{- range $path := $host.paths }}
        - path: {{ $path.path }}
          pathType: {{ $path.pathType }}
          backend:
            service:
              name: {{ $path.service }}
              port:
                number: {{ $path.port }}
         {{- end }}
     {{- end }}
     {{ if $ingress.tls }}
    tls:
     {{- range $tls := $ingress.tls }}
    - hosts:
       {{- range $tlsHost := $tls.hosts }}
      - {{ $tlsHost | quote }}
       {{- end }}
      secretName: {{ $tls.secretName }}
     {{- end }}
     {{ end }}

     

Values for the Chart

Examples

Deployment
deployment:
name: ratingsapi-deploy
namespace: aks-workshop-dev
labels:
  app: ratingsapi-deploy
selectorLabels:
  app: ratingsapi-pod    
replicas: 2
strategyType: RollingUpdate
maxSurge: 1
maxUnavailable: 1
nodeSelector:
  agentpool: aksapipool
containers:
- name: ratingsapi-app
  image: <acrName>.azurecr.io/ratings-api:v1.0.0
  imagePullPolicy: IfNotPresent
  readinessPort: 3000
  readinessPath: /healthz
  livenessPort: 3000
  livenessPath: /healthz
  memoryRequest: "64Mi"
  cpuRequest: "100m"
  memoryLimit: "256Mi"
  cpuLimit: "200m"
  containerPorts: [3000]    
  env:          
  - name: MONGODB_URI
    valueKey: MONGOCONNECTION
    valueSecret: aks-workshop-mongo-secret

 

Service
service:
name: ratingsapi-service
namespace: aks-workshop-dev
selector:
  app: ratingsapi-pod
type: ClusterIP
ports:
- protocol: TCP
  port: 80
  targetPort: 3000

 

RBAC
clusteradmin:
name: aks-workshop-cluster-admin-bindings
roleName: cluster-admin
subjects:
- name: <>
  kind: User

developer:
roleName: aks-workshop-developer-roles
roleNamespace: aks-workshop-dev
rules:
- apiGroups: ["", "apps", "networking.k8s.io"]
  resources: ["configmaps", "pods", "pods/exec", "pods/log", "deployments", "services", "events", "ingresses"]
  verbs: ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"]
bindingName: aks-workshop-developer-rb
bindingNamespace: aks-workshop-dev
subjects:
- name: <AzureAD_Group_Name>
  kind: Group

manager:
roleName: aks-workshop-manager-roles
roleNamespace: aks-workshop-dev
rules:
- apiGroups: ["", "apiextensions.k8s.io", "apps", "autoscaling", "batch", "events.k8s.io", "networking.k8s.io", "policy", "rbac.authorization.k8s.io", "scheduling.k8s.io"]
  resources: ["configmaps", "endpoints", "events", "limitranges", "persistentvolumeclaims", "pods", "resourcequotas", "serviceaccounts", "namespaces", "services", "customresourcedefinitions", "daemonsets", "deployments", "replicasets", "statefulsets", "horizontalpodautoscalers", "cronjobs", "jobs", "events", "ingresses", "networkpolicies", "poddisruptionbudgets", "rolebindings", "roles", "priorityclasses"]
  verbs: ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"]
- apiGroups: ["metrics.k8s.io"]
  resources: ["nodes", "pods"]
  verbs: ["get", "list"]
bindingName: aks-workshop-manager-rb
bindingNamespace: aks-workshop-dev
subjects:
- name: <AzureAD_Group_Name>
  kind: Group

 

Ingress
ingress:
name: aks-workshop-ingress
namespace: aks-workshop-dev
annotations:
  ingressClass: nginx
  proxyBodySize: "10m"
  enableCors: "true"
  rewriteTarget: /$1    
hosts:
- name: <dns-server>
  paths:    
  - path: /?(.*)
    pathType: Prefix
    service: ratingsweb-service
    port: 80

 

Install/Upgrade the Chart

helm install <chart-name> -n aks-workshop-dev ./<chart-name>/ -f ./<chart-name>/values-dev.yaml
helm upgrade <chart-name> -n aks-workshop-dev ./<chart-name>/ -f ./<chart-name>/values-dev.yaml

 

UnInstall the Chart

helm uninstall <chart-name>

 

Integration with Azure DevOps

Helm deployment Task in Azure DevOps

 

References

Updated Jan 26, 2022
Version 1.0
No CommentsBe the first to comment