Watch our latest talk from KubeCon + CloudNativeCon EU 2024!
Carvel Logo

Blog Posts

Carvelizing Helm Chart

by Rohit Aggarwal — Feb 16, 2022

In this blog post we will first show you how to wrap and distribute Bitnami Nginx Helm chart as a Carvel package, and then install it on the Kubernetes cluster via PackageInstall CR (via kapp-controller).

Why should I choose Carvel

Kubernetes configuration takes many forms – plain YAML configurations, Helm charts, ytt templates, jsonnet templates, etc. Software running on Kubernetes lives in many different places, e.g. a Git repository, an archive over HTTP, a Helm repository.

Carvel is a collection of small sharp tools that breaks up the problem into smaller building blocks with clean boundaries. This approach facilitates interoperability with other tools, giving users the option to swap out pieces with tools of their choice. Carvel declarative APIs approach provides a familiar experience for Kubernetes users. This means that package management using Carvel is about declaring the desired state via a Package CR and having it be continuously reconciled to the desired state on a Kubernetes cluster.


Basic knowledge of imgpkg, kbld, kapp-controller, kapp

kbld: kbld incorporates image building and image pushing into your development and deployment workflows.

imgpkg: A tool to package, distribute and relocate your Kubernetes configuration and dependent OCI images as one OCI artifact: a bundle.

kapp: kapp is a cli used to deploy and view groups of Kubernetes resources as “application”.

kapp-controller: kapp-controller provides declarative APIs to create, customize, install, and update your Kubernetes applications into packages. It also continuously fetches and reconciles resources to converge into their desired state.

Installing Carvel Tools

We’ll be using Carvel tools throughout this tutorial, so first, we’ll install them using script.

$ wget -O- >

# Inspect before running...
$ sudo bash
# Install kapp-controller
$ kubectl apply -f

Authoring a Carvel Package

To create a package, we need to create two Custom Resources (CRs). We will go through step by step process of authoring an Nginx Helm chart:

1. Create PackageMetadata CR: Package Metadata contains high level information about the package. Multiple versions of a package share the same package metadata.

Save the below PackageMetadata CR to a file named pkgMetadata.yaml

kind: PackageMetadata
  # This will be the name of our package metadata
  displayName: "Bitnami Nginx Carvel Package"
  longDescription: "Proxifying Server"
  shortDescription: "Proxifying Server"
  - proxy-server
  providerName: VMWare
  - name: "Carvel"
  - name: "CarvelInd"
  - name: "Rohit Aggarwal"

2. Package Helm Chart: The Package CR is config including metadata and references to OCI artifacts that informs the package manager what software it holds and how to install itself onto a Kubernetes cluster.

Save the below Package CR to a file named 1.0.0.yaml

kind: Package
  # Must be of the form '<spec.refName>.<spec.version>' (Note the period)
  # The name of the PackageMetadata associated with this version
  # Must be a valid PackageMetadata name (see PackageMetadata CR for details)
  # Cannot be empty
  # Package version; Referenced by PackageInstall;
  # Must be valid semver (required)
  # Cannot be empty
  version: 1.0.0
  # Version release notes (optional; string)
    The initial release of the Nginx package by wrapping Helm Chart. Nginx Helm chart version is 9.5.4
  # valuesSchema can be used to show template values that
  # can be configured by users when a Package is installed.
  # These values should be specified in an OpenAPI schema format. (optional)
  # For Helm chart, we can either define the configurable values here or let it be. Even if we don't define them here, we can still customize them.
      - helmChart:
          name: nginx
          version: 9.5.4
            # From where to pull the Helm chart
      - helmTemplate: {}
      - kapp: {}

Note: This is one way of packaging Helm chart. Another way is to use the imgpkg bundle to store the Helm chart itself and then reference it.

3. Create Package Repository: A package repository is a collection of packages and their metadata. We will use the imgpkg bundle to create a Package Repository.

For this tutorial, we will run an unsecured local docker registry. In the real world please be safe and use appropriate security measures.

$ docker run -d -p 5000:5000 --restart=always --name registry registry:2

From the terminal, we can access this registry as localhost:5000 but within the cluster, we’ll need the IP Address. We will store the IP address in a variable:

$ export REPO_HOST="`ifconfig | grep inet | grep -E '\b10\.' | awk '{ print $2}'`:5000"

Confirm that REPO_HOST is set to <IP_ADDRESS:PORT>

$ echo $REPO_HOST

imgpkg has a pre-defined bundle structure which allows it to perform recursive image relocation.

Let’s start by creating the directories required by imgpkg:

$ mkdir -p nginx-bitnami-repo nginx-bitnami-repo/.imgpkg nginx-bitnami-repo/packages/

We can copy our CR YAMLs from the previous step to the proper package subdirectory.

$ cp 1.0.0.yaml nginx-bitnami-repo/packages/
$ cp pkgMetadata.yaml nginx-bitnami-repo/packages/

Now, let’s use kbld to record which package bundles are used:

$ kbld -f nginx-bitnami-repo/packages/ --imgpkg-lock-output nginx-bitnami-repo/.imgpkg/images.yml

We will push the bundle into the repository using imgpkg.

$ imgpkg push -b ${REPO_HOST}/packages/nginx-bitnami-repo:1.0.0 -f nginx-bitnami-repo
dir: .
dir: .imgpkg
file: .imgpkg/images.yml
dir: packages
dir: packages/
file: packages/
file: packages/
Pushed ''

We can verify by checking the Docker registry catalog:

$ curl ${REPO_HOST}/v2/_catalog

Consuming Carvel Helm Package

1. Install Package Repository: Before installing the package, we have to make it visible to kapp-controller by using a PackageRepository CR. A PackageRepository is a collection of packages that are available to install.

Save the below PackageRepository CR to a file named repo.yaml

kind: PackageRepository
  name: simple-package-repository
      image: ${REPO_HOST}/packages/nginx-bitnami-repo:1.0.0

Replace the ${REPO_HOST} in the repo.yaml file with the actual value you got above.

$ kapp deploy -a repo -f repo.yaml -y
Target cluster '' (nodes: minikube)


Namespace  Name                       Kind               Conds.  Age  Op      Op st.  Wait to    Rs  Ri
default    simple-package-repository  PackageRepository  -       -    create  -       reconcile  -   -

Op:      1 create, 0 delete, 0 update, 0 noop, 0 exists
Wait to: 1 reconcile, 0 delete, 0 noop

12:35:17PM: ---- applying 1 changes [0/1 done] ----
12:35:17PM: create packagerepository/simple-package-repository ( namespace: default
12:35:17PM: ---- waiting on 1 changes [0/1 done] ----
12:35:17PM: ongoing: reconcile packagerepository/simple-package-repository ( namespace: default
12:35:17PM:  ^ Waiting for generation 1 to be observed
12:35:18PM: ok: reconcile packagerepository/simple-package-repository ( namespace: default
12:35:18PM: ---- applying complete [1/1 done] ----
12:35:18PM: ---- waiting complete [1/1 done] ----


After deploying, wait for the PackageRepository description to become Reconcile succeeded.

$ kubectl get packagerepository
NAME                        AGE   DESCRIPTION
simple-package-repository   40s   Reconcile succeeded

Now, we can see the list of package metadata’s available.

$ kubectl get packagemetadatas
NAME                       DISPLAY NAME                   CATEGORIES     SHORT DESCRIPTION   AGE   Bitnami Nginx Carvel Package   proxy-server   Proxifying Server   56s

2. List Packages: We can see the list of packages and their versions available for installation.

$ kubectl get packages
NAME                             PACKAGEMETADATA NAME       VERSION   AGE   1.0.0     1m29s

As we can see, our published Nginx Helm package is available for us to install.

3. Create Service Account: To install the above package, we need to create default-ns-sa service account that gives PackageInstall CR privileges to create resources in the default namespace.

$ kapp deploy -a default-ns-rbac -f -y
Target cluster '' (nodes: minikube)


Namespace  Name                     Kind            Conds.  Age  Op      Op st.  Wait to    Rs  Ri
default    default-ns-role          Role            -       -    create  -       reconcile  -   -
^          default-ns-role-binding  RoleBinding     -       -    create  -       reconcile  -   -
^          default-ns-sa            ServiceAccount  -       -    create  -       reconcile  -   -

Op:      3 create, 0 delete, 0 update, 0 noop, 0 exists
Wait to: 3 reconcile, 0 delete, 0 noop

12:37:16PM: ---- applying 2 changes [0/3 done] ----
12:37:16PM: create role/default-ns-role ( namespace: default
12:37:16PM: create serviceaccount/default-ns-sa (v1) namespace: default
12:37:16PM: ---- waiting on 2 changes [0/3 done] ----
12:37:16PM: ok: reconcile serviceaccount/default-ns-sa (v1) namespace: default
12:37:16PM: ok: reconcile role/default-ns-role ( namespace: default
12:37:16PM: ---- applying 1 changes [2/3 done] ----
12:37:17PM: create rolebinding/default-ns-role-binding ( namespace: default
12:37:17PM: ---- waiting on 1 changes [2/3 done] ----
12:37:17PM: ok: reconcile rolebinding/default-ns-role-binding ( namespace: default
12:37:17PM: ---- applying complete [3/3 done] ----
12:37:17PM: ---- waiting complete [3/3 done] ----


4. Install the Package: To install a Carvel Package, we need to create PackageInstall Kubernetes resource. A Package Install will install the Nginx Helm chart and its underlying resources on a Kubernetes cluster. A PackageInstall references a Package. Thus, we can create the PackageInstall yaml from the Package CR.

In this example, we will provide our custom values via secret. There are other ways we can provide the values like configMap etc.

NOTE: If you are using minikube, for Nginx service to be in ACTIVE state, start minikube tunnel in another window as services of LoadBalancer types do not come up otherwise in minikube.

Save the below PackageInstall CR to a file named pkgInstall.yaml

kind: PackageInstall
  name: nginx-pkg
  serviceAccountName: default-ns-sa
      constraints: 1.0.0
  - secretRef:
      name: nginx-values

apiVersion: v1
kind: Secret
  name: nginx-values
  values.yml: |
      pullPolicy: Always
    serverBlock: |-
      server {
        location / {
          return 200 "Response from Custom Server";
$ kapp deploy -a pkg-demo -f pkgInstall.yaml -y
Target cluster '' (nodes: minikube)


Namespace  Name          Kind            Conds.  Age  Op      Op st.  Wait to    Rs  Ri
default    nginx-pkg     PackageInstall  -       -    create  -       reconcile  -   -
^          nginx-values  Secret          -       -    create  -       reconcile  -   -

Op:      2 create, 0 delete, 0 update, 0 noop, 0 exists
Wait to: 2 reconcile, 0 delete, 0 noop

12:38:14PM: ---- applying 1 changes [0/2 done] ----
12:38:14PM: create secret/nginx-values (v1) namespace: default
12:38:14PM: ---- waiting on 1 changes [0/2 done] ----
12:38:14PM: ok: reconcile secret/nginx-values (v1) namespace: default
12:38:14PM: ---- applying 1 changes [1/2 done] ----
12:38:14PM: create packageinstall/nginx-pkg ( namespace: default
12:38:14PM: ---- waiting on 1 changes [1/2 done] ----
12:38:14PM: ongoing: reconcile packageinstall/nginx-pkg ( namespace: default
12:38:14PM:  ^ Waiting for generation 1 to be observed
12:38:15PM: ongoing: reconcile packageinstall/nginx-pkg ( namespace: default
12:38:15PM:  ^ Reconciling
12:39:15PM: ---- waiting on 1 changes [1/2 done] ----
12:39:16PM: ongoing: reconcile packageinstall/nginx-pkg ( namespace: default
12:39:16PM:  ^ Reconciling
12:39:31PM: ok: reconcile packageinstall/nginx-pkg ( namespace: default
12:39:31PM: ---- applying complete [2/2 done] ----
12:39:31PM: ---- waiting complete [2/2 done] ----


After the deploy has finished, kapp-controller will have installed the package in the cluster. We can verify this by doing the kapp inspect and checking the pods to see that we have a workload pod running.

$ kapp inspect -a pkg-demo
Target cluster '' (nodes: minikube)

Resources in app 'pkg-demo'

Namespace  Name          Kind            Owner  Conds.  Rs  Ri  Age
default    nginx-pkg     PackageInstall  kapp   1/1 t   ok  -   7m
^          nginx-values  Secret          kapp   -       ok  -   7m

Rs: Reconcile state
Ri: Reconcile information

2 resources

$ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
nginx-pkg-6db5c6978d-tt8k4   1/1     Running   0          8m

Once the pod is ready, you can use kubectl’s port forwarding to verify the customized response used in the workload.

$ kubectl port-forward service/nginx-pkg 3000:80 &

Now if we make a request against our service, we can see that server response is Response from Custom Server

$ curl localhost:3000
Response from Custom Server


You have successfully wrapped, distributed, and installed an existing Helm chart as a Carvel package.

Join the Carvel Community

We are excited to hear from you and learn with you! Here are several ways you can get involved:

  • Join Carvel’s slack channel, #carvel in Kubernetes workspace, and connect with over 1000+ Carvel users.
  • Find us on GitHub. Suggest how we can improve the project, the docs, or share any other feedback.
  • Attend our Community Meetings! Check out the Community page for full details on how to attend.

We look forward to hearing from you and hope you join us in building a strong packaging and distribution story for applications on Kubernetes!