Master Helm, Chart the Kubernetes Seas 🌊🧭🏴‍☠️
Tags: kubernetes, docker, programming, tutorial
Published: 2023-11-01 21:45:23

- [Using Helm Charts](#using-helm-charts)
- [App with K8s configuration files](#app-with-k8s-configuration-files)
  - [Application setup](#application-setup)
  - [Standard Kubernetes Configuration](#standard-kubernetes-configuration)
- [App with a Helm Chart](#app-with-a-helm-chart)

Kubernetes is an amazing technology for managing large scale distributed applications. Its YAML configuration files make it easy to tune an application to fit your needs but Kubernetes has a lot of different objects and therefor a lot of different configuration files to maintain. For example, a production application could have a Deployment, Service, Secret, Config Map and Persistent Volume Claim for each of its components such as a Web App, API, Database, bot, etc. This is a lot of configuration files to maintain and they would be difficult to package if you are building an application which users need to download and run in their own environment. This is where Helm Charts come in to the picture, they group all of the configuration files needed to run an application into a single package called a `Chart`. Helm Charts templatize the configuration files, allow you to manage them with a global configuration and make it easy to publish your entire infrastructure as a single package. Lets take a look at some examples of using and building helm charts.

> 💡 Refer to [Kubernetes Quick Start Guide ☁️⚡️🚀](https://dev.to/dpills/developers-guide-to-building-kubernetes-cloud-apps-7f1) for an in-depth Kubernetes tutorial
> 

## Using Helm Charts

Make sure you have [Minikube](https://minikube.sigs.k8s.io/docs/start/) setup and running and install [Helm](https://helm.sh/docs/intro/install/). After helm is installed we need to add a helm chart repository. [Artifact Hub](https://artifacthub.io/packages/search?kind=0) can be referenced to find helm repositories. A popular repository which offers production grade version of most of the popular open source applications is [Bitnami](https://bitnami.com/stacks/helm).

Add the Bitnami repository and run the `helm repo update` command to make sure we have the latest versions.

```bash
$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈
```

Search the configured helm repositories to look for applications we may want to use, in this case search for a Redis chart.

```bash
$ helm search repo redis
NAME                 	CHART VERSION	APP VERSION	DESCRIPTION
bitnami/redis        	18.2.0       	7.2.2      	Redis(R) is an open source, advanced key-value ...
bitnami/redis-cluster	9.0.13       	7.2.2      	Redis(R) is an open source, scalable, distribut...
```

> 💡  The full details of helm charts can be referenced in their associated [GitHub Repository](https://github.com/bitnami/charts/tree/main/bitnami/redis).
> 

Install the standard Redis chart.

```bash
$ helm install mydb bitnami/redis
NAME: mydb
LAST DEPLOYED: Mon Oct 30 07:43:01 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: redis
CHART VERSION: 18.2.0
APP VERSION: 7.2.2

** Please be patient while the chart is being deployed **

Redis&reg; can be accessed on the following DNS names from within your cluster:

    mydb-redis-master.default.svc.cluster.local for read/write operations (port 6379)
    mydb-redis-replicas.default.svc.cluster.local for read-only operations (port 6379)

To get your password run:

    export REDIS_PASSWORD=$(kubectl get secret --namespace default mydb-redis -o jsonpath="{.data.redis-password}" | base64 -d)

To connect to your Redis&reg; server:

1. Run a Redis&reg; pod that you can use as a client:

   kubectl run --namespace default redis-client --restart='Never'  --env REDIS_PASSWORD=$REDIS_PASSWORD  --image docker.io/bitnami/redis:7.2.2-debian-11-r0 --command -- sleep infinity

   Use the following command to attach to the pod:

   kubectl exec --tty -i redis-client \
   --namespace default -- bash

2. Connect using the Redis&reg; CLI:
   REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h mydb-redis-master
   REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h mydb-redis-replicas

To connect to your database from outside the cluster execute the following commands:

    kubectl port-forward --namespace default svc/mydb-redis-master 6379:6379 &
    REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h 127.0.0.1 -p 6379
```

Verify the helm chart is deployed and Redis is running on the Kubernetes cluster.

```bash
$ helm ls
NAME	NAMESPACE	REVISION	UPDATED                             	STATUS  	CHART       	APP VERSION
mydb	default  	1       	2023-10-30 07:43:01.730917 -0400 EDT	deployed	redis-18.2.0	7.2.2

$ kubectl get all
NAME                        READY   STATUS    RESTARTS   AGE
pod/mydb-redis-master-0     1/1     Running   0          4m4s
pod/mydb-redis-replicas-0   1/1     Running   0          4m4s
pod/mydb-redis-replicas-1   1/1     Running   0          3m23s
pod/mydb-redis-replicas-2   1/1     Running   0          3m1s

NAME                          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP    2d22h
service/mydb-redis-headless   ClusterIP   None            <none>        6379/TCP   4m4s
service/mydb-redis-master     ClusterIP   10.106.79.115   <none>        6379/TCP   4m4s
service/mydb-redis-replicas   ClusterIP   10.106.46.152   <none>        6379/TCP   4m4s

NAME                                   READY   AGE
statefulset.apps/mydb-redis-master     1/1     4m4s
statefulset.apps/mydb-redis-replicas   3/3     4m4s
```

Follow the post installation prompt and verify that Redis is working correctly.

```bash
$ export REDIS_PASSWORD=$(kubectl get secret --namespace default mydb-redis -o jsonpath="{.data.redis-password}" | base64 -d)
$ kubectl run --namespace default redis-client --restart='Never'  --env REDIS_PASSWORD=$REDIS_PASSWORD  --image docker.io/bitnami/redis:7.2.2-debian-11-r0 --command -- sleep infinity
pod/redis-client created

$ kubectl exec --tty -i redis-client --namespace default -- bash
I have no name!@redis-client:/$ REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h mydb-redis-master

mydb-redis-master:6379> SET key value
OK
mydb-redis-master:6379> GET key
"value"
```

Helm charts are a great way to run popular applications like Databases since the helm chart implements many of the best practices for you and allows you to just provide higher level tuning.

Delete the Redis helm chart.

```bash
$ helm delete mydb
release "mydb" uninstalled

$ helm ls
NAME	NAMESPACE	REVISION	UPDATED	STATUS	CHART	APP VERSION
```

Helm utilizes a `values.yaml` file for its configuration, here is the [Bitnami Redis values.yaml](https://github.com/bitnami/charts/blob/main/bitnami/redis/values.yaml) as a reference and a custom `values.yaml` file can be passed to the `helm install` command to override any of the values.

Create a custom `values.yaml` file to override the password and replica count.

> 📝 values.yaml
> 

```yaml
auth:
  password: myDbSecret123

replica:
  replicaCount: 4
```

Install Redis again but now pass in the updated configuration.

```bash
$ helm install mydb bitnami/redis -f values.yaml
NAME: mydb
LAST DEPLOYED: Tue Oct 31 06:57:24 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: redis
CHART VERSION: 18.2.0
APP VERSION: 7.2.2

** Please be patient while the chart is being deployed **

Redis&reg; can be accessed on the following DNS names from within your cluster:

    mydb-redis-master.default.svc.cluster.local for read/write operations (port 6379)
    mydb-redis-replicas.default.svc.cluster.local for read-only operations (port 6379)

To get your password run:

    export REDIS_PASSWORD=$(kubectl get secret --namespace default mydb-redis -o jsonpath="{.data.redis-password}" | base64 -d)

To connect to your Redis&reg; server:

1. Run a Redis&reg; pod that you can use as a client:

   kubectl run --namespace default redis-client --restart='Never'  --env REDIS_PASSWORD=$REDIS_PASSWORD  --image docker.io/bitnami/redis:7.2.2-debian-11-r0 --command -- sleep infinity

   Use the following command to attach to the pod:

   kubectl exec --tty -i redis-client \
   --namespace default -- bash

2. Connect using the Redis&reg; CLI:
   REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h mydb-redis-master
   REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h mydb-redis-replicas

To connect to your database from outside the cluster execute the following commands:

    kubectl port-forward --namespace default svc/mydb-redis-master 6379:6379 &
    REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h 127.0.0.1 -p 6379
```

Validate that the new configuration has been applied correctly, you should see the new password set in the secret and 4 replicas running.

```bash
$ helm ls
NAME	NAMESPACE	REVISION	UPDATED                             	STATUS  	CHART       	APP VERSION
mydb	default  	1       	2023-10-31 06:57:24.661346 -0400 EDT	deployed	redis-18.2.0	7.2.2

$ kubectl get all
NAME                        READY   STATUS    RESTARTS   AGE
pod/mydb-redis-master-0     1/1     Running   0          3m51s
pod/mydb-redis-replicas-0   1/1     Running   0          3m51s
pod/mydb-redis-replicas-1   1/1     Running   0          3m16s
pod/mydb-redis-replicas-2   1/1     Running   0          2m51s
pod/mydb-redis-replicas-3   1/1     Running   0          2m30s

NAME                          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP    3d21h
service/mydb-redis-headless   ClusterIP   None            <none>        6379/TCP   3m51s
service/mydb-redis-master     ClusterIP   10.102.17.165   <none>        6379/TCP   3m51s
service/mydb-redis-replicas   ClusterIP   10.111.189.75   <none>        6379/TCP   3m51s

NAME                                   READY   AGE
statefulset.apps/mydb-redis-master     1/1     3m51s
statefulset.apps/mydb-redis-replicas   4/4     3m51s

$ echo $(kubectl get secret --namespace default mydb-redis -o jsonpath="{.data.redis-password}" | base64 -d)
myDbSecret123

$ helm delete mydb
release "mydb" uninstalled
```

Helm also allows you to override values by passing them in with the `--set` flag which can be used to pass in sensitive values such as secrets. In production this method is used to set the secrets of a helm chart using environment variables within the CI/CD Pipeline.

> 💡 Refer to **[DevOps CI/CD Quick Start Guide with GitHub Actions 🛠️🐙⚡️](https://dev.to/dpills/devops-cicd-quick-start-guide-with-github-actions-3c2b)** for a Guide on creating CI/CD Pipelines
> 

Remove the password from the `values.yaml` file.

> 📝 values.yaml
> 

```yaml
replica:
  replicaCount: 4
```

Now pass in the password from the command line with the `--set` flag.

```bash
$ helm install mydb bitnami/redis --set auth.password=myDbSecret123 -f values.yaml
NAME: mydb
LAST DEPLOYED: Tue Oct 31 07:10:39 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: redis
CHART VERSION: 18.2.0
APP VERSION: 7.2.2

** Please be patient while the chart is being deployed **

Redis&reg; can be accessed on the following DNS names from within your cluster:

    mydb-redis-master.default.svc.cluster.local for read/write operations (port 6379)
    mydb-redis-replicas.default.svc.cluster.local for read-only operations (port 6379)

To get your password run:

    export REDIS_PASSWORD=$(kubectl get secret --namespace default mydb-redis -o jsonpath="{.data.redis-password}" | base64 -d)

To connect to your Redis&reg; server:

1. Run a Redis&reg; pod that you can use as a client:

   kubectl run --namespace default redis-client --restart='Never'  --env REDIS_PASSWORD=$REDIS_PASSWORD  --image docker.io/bitnami/redis:7.2.2-debian-11-r0 --command -- sleep infinity

   Use the following command to attach to the pod:

   kubectl exec --tty -i redis-client \
   --namespace default -- bash

2. Connect using the Redis&reg; CLI:
   REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h mydb-redis-master
   REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h mydb-redis-replicas

To connect to your database from outside the cluster execute the following commands:

    kubectl port-forward --namespace default svc/mydb-redis-master 6379:6379 &
    REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli -h 127.0.0.1 -p 6379
```

Verify the password has been set correctly then delete the chart.

```bash
$ echo $(kubectl get secret --namespace default mydb-redis -o jsonpath="{.data.redis-password}" | base64 -d)
myDbSecret123

$ helm delete mydb
release "mydb" uninstalled
```

Nice! This is the basics for using helm charts and there are charts available for nearly every popular application, this gives us a simple way to get production grade instances of them running. Now lets take a look at creating a Kubernetes application.

## App with K8s configuration files

> ⭐️ The complete source code referenced in this guide is available on GitHub
> <https://github.com/dpills/helm-quick-start>
> 

### Application setup

Create a small FastAPI app which will use a secret static token to authenticate the endpoint.

> 💡 Refer to **[FastAPI Production Setup Guide 🏁⚡️🚀](https://dev.to/dpills/fastapi-production-setup-guide-1hhh)** for a full Python FastAPI Guide
> 

Install the necessary dependencies with Poetry.

> 📝 pyproject.toml
> 

```python
[tool.poetry]
name = "helm-api"
version = "0.1.0"
description = ""
authors = ["dpills"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.104.1"
uvicorn = { extras = ["standard"], version = "^0.23.2" }
pydantic-settings = "^2.0.3"

[tool.poetry.group.dev.dependencies]
ruff = "^0.1.3"
black = "^23.10.1"
mypy = "^1.6.1"

[tool.black]
line-length = 88

[tool.ruff]
select = ["E", "F", "I"]
fixable = ["ALL"]
exclude = [".git", ".mypy_cache", ".ruff_cache"]
line-length = 88

[tool.mypy]
disallow_any_generics = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
strict_equality = true
disallow_untyped_decorators = false
ignore_missing_imports = true
implicit_reexport = true
plugins = "pydantic.mypy"

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
warn_untyped_fields = true

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
```

```bash
$ poetry install
Installing dependencies from lock file

No dependencies to install or update
```

Create the API code.

> 📝 main.py
> 

```python
import uvicorn
from fastapi import FastAPI, HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    static_token: str
    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

settings = Settings()
app = FastAPI()
security = HTTPBearer()

class Data(BaseModel):
    item: str

@app.get("/data", response_model=Data)
async def get_data(
    access_token: HTTPAuthorizationCredentials = Security(security),
) -> Data:
    """
    Get Data
    """
    if access_token.credentials != settings.static_token:
        raise HTTPException(status_code=401, detail="Unauthorized")

    return Data(item="helm")

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        log_level="debug",
        reload=True,
    )
```

Add a `.env` file for local testing.

> 📝 .env
> 

```bash
STATIC_TOKEN=MY_SECRET_TOKEN_123
```

Make sure the API is working as expected locally, it should only allow the API call to be made when the secret static token is sent in the Authorization Bearer token header.

```bash
$ poetry run python3 main.py
INFO:     Will watch for changes in these directories: ['/Users/dpills/articles/helm-quick-start/api']
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [13765] using WatchFiles
INFO:     Started server process [13771]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     127.0.0.1:51135 - "GET /data HTTP/1.1" 403 Forbidden
INFO:     127.0.0.1:51136 - "GET /data HTTP/1.1" 200 OK
```

```bash
$ curl -v http://localhost:8000/data
< HTTP/1.1 403 Forbidden
{"detail":"Not authenticated"}

$ curl -v http://localhost:8000/data -H 'Authorization: Bearer MY_SECRET_TOKEN_123'
< HTTP/1.1 200 OK
{"item":"helm"}
```

Now that we know it is working as expected, containerize it with a `Dockerfile`.

> 📝 Dockerfile
> 

```docker
FROM python:3.11-slim-bookworm as requirements-stage

RUN pip install poetry
COPY ./pyproject.toml ./poetry.lock /
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes --without=dev

FROM python:3.11-slim-bookworm

COPY --from=requirements-stage /requirements.txt /requirements.txt
COPY ./app /app

RUN python3 -m pip install --no-cache-dir --upgrade -r requirements.txt
EXPOSE 8000

ENTRYPOINT ["uvicorn", "--host", "0.0.0.0", "--port", "8000", "main:app"]
```

> 💡 Refer to **[Containers Demystified 🐳🤔](https://dev.to/dpills/containers-demystified-4c5e) for a Docker container guide**
> 

Build and push the container image to Docker Hub.

```bash
$ docker build . -t dpills/helm-api:v1.0.0 --push
[+] Building 40.7s (13/13) FINISHED                                               docker:desktop-linux
...
 => [requirements-stage 2/4] RUN pip install poetry                                              19.6s
 => [requirements-stage 3/4] COPY ./pyproject.toml ./poetry.lock /                                0.0s 
 => [requirements-stage 4/4] RUN poetry export -f requirements.txt --output requirements.txt --w  0.5s 
 => [stage-1 2/4] COPY --from=requirements-stage /requirements.txt /requirements.txt              0.0s 
 => [stage-1 3/4] COPY ./main.py /main.py                                                         0.0s 
 => [stage-1 4/4] RUN python3 -m pip install --no-cache-dir --upgrade -r requirements.txt         6.4s 
...
 => => naming to docker.io/dpills/helm-api:v1.0.0                                                 0.0s 
 => pushing dpills/helm-api:v1.0.0 with docker                                                   11.2s 
```

### Standard Kubernetes Configuration

Now that we have our application container image built, we need to create the Kubernetes configuration files to deploy it.

Create a secret for our `STATIC_TOKEN` environment variable. 

> 📝 k8s/secret.yaml
> 

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: helm-guide-api-secret
data:
  STATIC_TOKEN: TVlfU0VDUkVUX1RPS0VOXzEyMw==
```

> 💡 This can be generated from the `.env` file with `kubectl` . Secret files are just base64 encodings of the values.
> 
> 
> ```bash
> $ kubectl create secret generic helm-guide-api-secret --from-env-file=.env --dry-run=client -o yaml
> apiVersion: v1
> data:
>   STATIC_TOKEN: TVlfU0VDUkVUX1RPS0VOXzEyMw==
> kind: Secret
> metadata:
>   creationTimestamp: null
>   name: helm-guide-api-secret
> 
> $ echo TVlfU0VDUkVUX1RPS0VOXzEyMw== | base64 -d
> MY_SECRET_TOKEN_123
> ```
> 

Create a deployment with the new API container image and map the secret to the environment variables.

> 📝 k8s/deployment.yaml
> 

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: helm-guide-api
  name: helm-guide-api
spec:
  replicas: 1 # One Instance
  selector:
    matchLabels:
      app: helm-guide-api
  template:
    metadata:
      labels:
        app: helm-guide-api
    spec:
      containers:
        - name: helm-guide-api
          image: dpills/helm-api:v1.0.0 # API Image
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8000
              protocol: TCP
          envFrom:
            - secretRef:
                name: helm-guide-api-secret # Secret Mapping
          resources:
            limits:
              cpu: "1"
              memory: 1Gi
            requests:
              cpu: "1"
              memory: 1Gi
```

Finally, create a service to expose the API.

> 📝 k8s/service.yaml
> 

```yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: helm-guide-api
  name: helm-guide-api-svc
spec:
  ports:
    - name: http
      port: 8000
      protocol: TCP
      targetPort: 8000
  selector:
    app: helm-guide-api
```

Apply the Kubernetes configuration.

```bash
$ kubectl apply -f k8s
deployment.apps/helm-guide-api created
secret/helm-guide-api-secret created
service/helm-guide-api-svc created
```

Connect to the service with Minikube.

```bash
$ minikube service helm-guide-api-svc --url
😿  service default/helm-guide-api-svc has no node port
http://127.0.0.1:51476
❗  Because you are using a Docker driver on darwin, the terminal needs to be open to run it.
```

Validate it is working as expected with the exposed Minikube port from the last command.

```bash
$ curl -v http://localhost:51476/data
< HTTP/1.1 403 Forbidden
{"detail":"Not authenticated"}

$ curl -v http://localhost:51476/data -H 'Authorization: Bearer MY_SECRET_TOKEN_123'
< HTTP/1.1 200 OK
{"item":"helm"}
```

Awesome! this is great but if we look at the Kubernetes configuration files we can see that the root application name `helm-guide-api` needs to be constantly referenced throughout the configuration and there is a lot of boilerplate which will never change. The main items we will update often will be the image version and potentially replica count.  Also with following Infrastructure as Code (IaC) best practices and committing all of our configuration files, we are exposing our application secret. Being able to template these files and create variables for the portions that we need to change or keep secret is what Helm allows us to do.

## App with a Helm Chart

Create the new helm chart with the `helm create` command.  You can take a look at the default bootstrapped helm chart example created by this command to see a lot of the functionality which we will not fully cover in this article.

```bash
$ helm create infra
Creating infra
```

Update the `Chart.yaml` with a description and version.

> 📝 infra/Chart.yaml
> 

```yaml
apiVersion: v2
name: helm-guide-api
description: Helm API
type: application
version: 1.0.0
appVersion: "1.0.0"
```

Now delete all of the bootstrapped files in the `templates` folder and copy the `deployment.yaml` , `service.yaml` and `secret.yaml` configuration files into the `templates` folder.

The file structure should look like this.

![Helm Chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5o319fxc4hasj213taq.png)

Update the service to use the helm release name for the application name using the double curly brace helm templating syntax.

> 📝 infra/templates/service.yaml
> 

```yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: {{ .Release.Name }}
  name: {{ .Release.Name }}-svc
spec:
  ports:
    - name: http
      port: 8000
      protocol: TCP
      targetPort: 8000
  selector:
    app: {{ .Release.Name }}
```

For the deployment file we need to pass in the replica count and container image and as we saw in the start of this article the `values.yaml` file is used to pass in values. Update the `values.yaml` file with an API configuration defining these two values.

> 📝 infra/values.yaml
> 

```yaml
api:
  image: dpills/helm-api:v1.0.0
  replicas: 1
```

Update the deployment template to use the release name, api image and replica count.  For the replica count, cast the type to make sure it is an integer.

> 📝 infra/templates/deployment.yaml
> 

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: {{ .Release.Name }}
  name: {{ .Release.Name }}
spec:
  replicas: {{ int .Values.api.replicas }}
  selector:
    matchLabels:
      app: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}
    spec:
      containers:
        - name: {{ .Release.Name }}
          image: {{ .Values.api.image }}
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8000
              protocol: TCP
          envFrom:
            - secretRef:
                name: {{ .Release.Name }}-secret
          resources:
            limits:
              cpu: "1"
              memory: 1Gi
            requests:
              cpu: "1"
              memory: 1Gi
```

For the secret we need to pass in the static token and base64 encode it but we should not commit the secret to our source code. Add an entry will a `null` value to the `values.yaml` file which will be passed in with the helm install `--set` flag.

> 📝 infra/values.yaml
> 

```yaml
api:
  image: dpills/helm-api:v1.0.0
  replicas: 1
  static_token: null # Pass this in with --set static_token=XXX
```

Update the secret to to use the release name,  validate that a static token value has been passed in with the `required` keyword and base64 encode it by piping to the `b64enc` function.

> 📝 infra/templates/secret.yaml
> 

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: {{ .Release.Name }}-secret
data:
  STATIC_TOKEN: {{ required "A static token is required" .Values.api.static_token | b64enc }}
```

The `NOTES.txt` is what is printed out after installing the chart, update this to some simple text for this example.

> 📝 infra/templates/NOTES.txt
> 

```
Helm API Example!
```

This should now be fully setup, delete the standard Kubernetes configuration deployment if it is still running and run the application with the helm chart instead.

```bash
$ kubectl delete -f k8s
deployment.apps "helm-guide-api" deleted
secret "helm-guide-api-secret" deleted
service "helm-guide-api-svc" deleted

$ helm install helm-api ./infra --set api.static_token=MY_SECRET_TOKEN_123
NAME: helm-api
LAST DEPLOYED: Wed Nov  1 08:44:37 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Helm API Example!
```

Validate the application is working as expected.

```bash
$ helm ls
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                   APP VERSION
helm-api        default         1               2023-11-01 08:44:37.222599 -0400 EDT    deployed        helm-guide-api-1.0.0    1.0.0

$ kubectl get all
NAME                            READY   STATUS    RESTARTS   AGE
pod/helm-api-79877dcfc6-8rv47   1/1     Running   0          61s

NAME                   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/helm-api-svc   ClusterIP   10.109.109.197   <none>        8000/TCP   61s
service/kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP    4d23h

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/helm-api   1/1     1            1           61s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/helm-api-79877dcfc6   1         1         1       61s
```

```bash
$ minikube service helm-api-svc --url
😿  service default/helm-api-svc has no node port
http://127.0.0.1:51778
❗  Because you are using a Docker driver on darwin, the terminal needs to be open to run it.

$ curl -v http://localhost:51778/data
< HTTP/1.1 403 Forbidden
{"detail":"Not authenticated"}

$ curl -v http://localhost:51778/data -H 'Authorization: Bearer MY_SECRET_TOKEN_123'
< HTTP/1.1 200 OK
{"item":"helm"}
```

Congrats you just created a helm chart!  🎉  We can see that the release name that we passed in to the helm install command has been used for all of the object names.  The primary configuration file is greatly simplified with the `values.yaml` file and our secret configuration is committed to our source code but the sensitive value is no longer exposed. The [Helm Chart Documentation](https://helm.sh/docs/chart_template_guide/getting_started/) can be referenced for a full overview of the syntax and features. Utilizing helm charts to manage Kubernetes applications has been very useful for me and I hope this guide helps you build your next amazing application! 😊