Chapter 1. Introducing Helm

Helm is the package manager for Kubernetes. That is the way the Helm developers have described Helm since the very first commits to the Git repository. And that sentence is the topic of this chapter.

In this chapter, we will start with a conceptual look at the cloud native ecosystem, in which Kubernetes is a key technology. We will take a fresh look at what Kubernetes has to offer to set the stage for describing Helm.

Next, we will look at the problems Helm sets out to solve. In this section, we will look at the concept of package management and why we have modeled Helm this way. We will also visit some of the unique facets of installing packages into a cluster management tool like Kubernetes.

Finally, we will finish the chapter with a high-level look at Helm’s architecture, focusing on the concepts of charts, templates, and releases. By the end of the chapter, you will understand how Helm fits into the broader ecosystem of tools, and you will be familiar with the terminology and concepts we will be using throughout this book.

The Cloud Native Ecosystem

The emergence of cloud technologies has clearly changed the way the industry looks at hardware, system management, physical networking, and so on. Virtual machines replaced physical servers, storage services displaced talk of hard drives, and automation tools rose in prominence. This was perhaps an early change in the way the industry conceptualized the cloud. But as the strengths and weaknesses of this new approach became clearer, the practices of designing applications and services also began to shift.

Developers and operators began to question the practice of building large single-binary applications that executed on beefy hardware. They recognized the difficulty of sharing data across different applications while retaining data integrity. Distributed locking, storage, and caching became mainstream problems instead of points of academic interest. Large software packages were broken down into smaller discrete executables. And, as Kubernetes founder Brendan Burns often puts it, “distributed computing went from an advanced topic to Computer Science 101.”

The term cloud native captures this cognitive shift in what one might call our architectural view of the cloud. When we design our systems around the capabilities and constraints of the cloud, we are designing cloud native systems.

Containers and Microservices

At the very heart of cloud native computing is this philosophical perspective that smaller discrete standalone services are preferable to large monolithic services that do everything. Instead of writing a single large application that handles everything from generating the user interface to processing task queues to interacting with databases and caches, the cloud native approach is to write a series of smaller services, each relatively special purpose, and then join these services together to serve a higher-level purpose. In such a model, one service might be the sole user of a relational database. Services that wish to access the data will contact that service over (typically) a representational state transfer (REST) API. And, using JavaScript Object Notation (JSON) over HTTP, these other services will query and update data.

This breakdown allows developers to hide the low-level implementation and instead offer a set of features specific to the business logic of the broader application.

Microservices

Where once an application consisted of a single executable that did all of the work, cloud native applications are distributed applications. While separate programs each take responsibility for one or two discrete tasks, together these programs all form a single logical application.

With all this theory, a simple example may better explain how this works. Imagine an ecommerce website. We can think of several tasks that jointly comprise this sort of website. There is a product catalog, user accounts and shopping carts, a payment processor that handles the security-sensitive process of monetary transactions, and a frontend through which customers view items and select their purchases. There is also an administrative interface where the store owners manage inventory and fulfill orders.

Historically, applications like this were once built as one single program. The code responsible for each of these units of work was all compiled together into one large executable, which was then often run on a single large piece of hardware.

The cloud native approach to such an application, though, is to break this ecommerce application into multiple pieces. One handles payment transactions. Another tracks the product catalog. Yet another provides the administrative, and so on. These services then communicate with each other over the network using well-defined REST APIs.

Taken to an extreme, an application is broken down into its smallest constituent parts, and each part is a program. This is the microservice architecture. Standing at the opposite end of the spectrum of a monolithic application, a microservice is responsible for handling only one small part of the overall application’s processing.

The microservice concept has had an outsized influence on the evolution of cloud native computing. And nowhere is this more evident than in the emergence of container computing.

Containers

It is common to compare and contrast a container and a virtual machine. A virtual machine runs an entire operating system in an isolated environment on a host machine. A container, in contrast, has its own filesystem, but is executed in the same operating system kernel as the host.

But there is a second way of conceptualizing the container—one that may prove more beneficial for the present discussion. As its name suggests, a container provides a useful way of packaging up the runtime environment for a single program so that the executable is guaranteed to have all of its dependencies satisfied when it is moved from one host to another.

This is a more philosophical approach, perhaps, because it imposes some non-technical restrictions on a container. For example, one could package a dozen different programs in a single container and execute them all at the same time. But containers, at least as they were designed by Docker, were intended as a vehicle for one top-level program.

Note

When we talk about programs here, we’re really thinking at a higher level of abstraction than “a binary.” Most Docker containers have at least a few executables that are there merely to assist the main program. But these executables are auxiliary to the primary function of the container. For example, a web server may require a few other local utilities for starting up or performing low-level tasks (Apache, for example, has tools for modules), but it is the web server itself that is the primary program.

Containers and microservices are, by design, a perfect match. Small discrete programs can be packaged, along with all their dependencies, into svelte containers. And those containers can be moved around from host to host. When executing a container, the host need not have all the tools required to execute the program because all of those tools are packaged within the container. The host merely must have the ability to run containers.

For example, if a program is built in Python 3, the host does not need to install Python, configure it, and then install all the libraries that the program requires. All of that is packaged in the container. When the host executes the container, the correct version of Python 3 and each required library is already stored in the container.

Taking this one step further, a host can freely execute containers with competing requirements. A containerized Python 2 program can run in the same host as a containerized Python 3 requirement, and the host’s administrators need not do any special work to configure these competing requirements!

These examples illustrate one of the features of the cloud native ecosystem: administrators, operators, and site reliability engineers (SREs) are no longer in the business of managing program dependencies. Instead, they are free to focus on a higher level of resource allocation. Rather than fretting over which versions of Python, Ruby, and Node are running on different servers, operators can focus on whether network, storage, and CPU resources are correctly allocated for these containerized workloads.

Running a program in complete isolation is sometimes useful. But more often, we want to expose some aspects of this container to the outside world. We want to give it access to storage. We want to allow it to answer network connections. And we want to inject tidbits of configuration into the container based on our present needs. All of these tasks (and more still) are provided by the container runtime. When a container declares that it has a service that is internally listening on port 8080, the container runtime may grant it access on the host port 8000. Thus, when the host gets a network request on port 8000, the container sees this as a request on its port 8080. Likewise, a host can mount a filesystem into the container, or set specific environment variables inside of the container. In this way, a container can participate in the broader environment around it—including not just other containers on that host, but remote services on the local network or even the internet.

Container images and registries

Container technology is a sophisticated and fascinating space in its own right. But for our purposes, we only need to understand a few more things about how containers work before be can proceed to the next layer of the cloud native stack.

As we discussed in the previous section, a container is a program together with its dependencies and environment. This whole thing can be packaged together into a portable representation called a container image (often just referred to as an image). Images are not packaged into one large binary; instead, they are packaged into discrete layers, each of which has its own unique identifier. When images are moved around, they are moved as a collection of layers, which provides a huge advantage. If one host has an image with five layers and another host needs the same image, it only needs to fetch the layers that it doesn’t already have. So if it has two of the five layers already, it only needs to fetch three layers to rebuild the entire container.

There is a crucial piece of technology that provides the ability to move container images around. An image registry is a specialized piece of storage technology that houses containers, making them available for hosts. A host can push a container image to a registry, which transfers the layers to the registry. And then another host can pull the image from the registry to the host’s environment, after which the host can execute the container.

The registry manages the layers. When one host requests an image, the registry lets the host know which layers compose that image. The host can then determine which layers (if any) are missing and subsequently download just those layers from the registry.

A registry uses up to three pieces of information to identify a particular image:

Name

An image name can range from simple to complex, depending on the registry that stores the image: nginx, servers/nginx, or example.com/servers/nginx.

Tag

The tag typically refers to the version of the software installed (v1.2.3), though tags are really just arbitrary strings. The tags latest and stable are often used to indicate “the most recent version” and “the most recent production-ready version,” respectively.

Digest

Sometimes it is important to pull a very specific version of an image. Since tags are mutable, there is no guarantee that at any given time a tag refers to exactly a specific version of the software. So registries support fetching images by digest, which is a SHA-256 or SHA-512 digest of the image’s layer information.

Throughout this book, we will see images referenced using the three preceding pieces of information. The canonical format for combining these is name:tag@digest, where only name is required. Thus, example.com/servers/nginx:latest says “give me the tag latest for the image named example.com/servers/nginx.” And

example.com/my/app@sha256:
a428de44a9059feee59237a5881c2d2cffa93757d99026156e4ea544577ab7f3

says “give me example.com/my/app with the exact digest given here.”

While there is plenty more to learn about images and containers, we have enough knowledge now to move on to the next important topic: schedulers. And in that section, we’ll discover Kubernetes.

Schedules and Kubernetes

In the previous section we saw how containers encapsulate individual programs and their required environment. Containers can be executed locally on workstations or remotely on servers.

As developers began packaging their applications into containers and operators began using containers as an artifact for deployment, a new set of questions emerged. How do we best execute lots of containers? How do we best facilitate a microservice architecture where lots of containers need to work together? How do we judiciously share access to things like network attached storage, load balancers, and gateways? How do we manage injecting configuration information into lots of containers? And perhaps most importantly, how do we manage resources like memory, CPU, network bandwidth, and storage space?

Moving even one level beyond, people began asking (based on their experiences with virtual machines) how one might manage distributing containers across multiple hosts, spreading the load equitably while still judiciously using resources? Or, more simply, how do we run the fewest possible hosts while running as many containers as we need?

In 2015, the time was right: Docker containers were making inroads into the enterprise. And there was a clear need for a tool that could manage container scheduling and resource management across hosts. Multiple technologies landed on the scene: Mesos introduced Marathon; Docker created Swarm; Hashicorp released Nomad; and Google created an open source sibling to its internal Borg platform, and named this technology Kubernetes (the Greek word for a ship’s captain).

All of these projects were providing an implementation of a clustered container management system that could schedule containers and wire them up for hosting sophisticated microservice-like distributed applications.

Each of these schedulers had strengths and weaknesses. But Kubernetes introduced two concepts that set it apart from the crowd: declarative infrastructure and the reconciliation loop.

Declarative infrastructure

Consider the case of deploying a container. One might approach the process of deploying a container like this: I create the container. I open a port for it to listen on, and then I attach some storage at this particular place on the filesystem. Then I wait for everything to be initialized. Then I test it to see if the container is ready. Then I mark it as available.

In this approach, we are thinking procedurally by focusing on the process of setting up a container. But Kubernetes’ design is that we think declaratively. We tell the scheduler (Kubernetes) what our desired state is, and Kubernetes takes care of converting that declarative statement into its own internal procedures.

Installing a container on Kubernetes, then, is more a matter of saying, “I want this container running on this port with this amount of CPU and some storage mounted at this location on the filesystem.” Kubernetes works behind the scenes to wire everything up according to our declaration of what we want.

The reconciliation loop

How does Kubernetes work behind the scenes to do all of this? When we viewed things procedurally, there was a certain order of operations there. How does Kubernetes know the order? This is where the idea of the reconciliation loop comes into play.

In a reconciliation loop, the scheduler says “here is the user’s desired state. Here is the current state. They are not the same, so I will take steps to reconcile them.” The user wants storage for the container. Currently there is no storage attached. So Kubernetes creates a unit of storage and attaches it to the container. The container needs a public network address. None exists. So a new address is attached to the container. Different subsystems in Kubernetes work to fulfill their individual part of the user’s overall declaration of desired state.

Eventually, Kubernetes will either succeed in creating the user’s desired environment or will arrive at the conclusion that it cannot realize the user’s desires. Meanwhile, the user takes a passive role in observing the Kubernetes cluster and waiting for it to achieve success or mark the installation as failed.

From containers to pods, services, deployments, etc.

While concise, the preceding example is a little misleading. Kubernetes doesn’t necessarily treat the container as the unit of work. Instead, Kubernetes introduces a higher-level abstraction called a pod. A pod is an abstract envelope that describes a discrete unit of work. A pod describes not just a container, but one or more containers (as well as their configuration and requirements) that together perform one unit of work:

apiVersion: v1 1
kind: Pod
metadata:
    name: example-pod
spec:
    containers: 2
    - image: "nginx:latest"
      name: example-nginx
1

The first two lines define the Kubernetes kind (v1 Pod).

2

A pod can have one or more containers.

Most frequently, a pod only has one container. But sometimes they have containers that do some preconfiguration for the main container, exiting before the main container comes online. These are called init containers. Other times, there are containers that run alongside the main container and provide auxiliary services. These are called sidecar containers. These are all considered part of the same pod.

Note

In the preceding code, we have written a definition of a Kubernetes Pod resource. These definitions, when expressed as YAML or JSON, are referred to as manifests. A manifest can contain one or more Kubernetes resources (also called objects or resource definitions). Each resource is associated with one of the Kubernetes types, such as a Pod or Deployment. In this book, we typically use resource because the word object is overloaded: YAML defines the word object to mean a named key/value structure.

A Pod describes what configuration the container or containers need (such as network ports or filesystem mount points). Configuration information in Kubernetes may be stored in ConfigMaps or, for sensitive information, Secrets. And the Pod’s definition may then relate those ConfigMaps and Secrets to environment variables or files within each container. As Kubernetes sees those relationships, it will attempt to attach and configure the configuration data as described in the Pod definition:

apiVersion: v1 1
kind: ConfigMap
metadata:
    name: configuration-data
data: 2
    backgroundColor: blue
    title: Learning Helm
1

In this case, we have declared a v1 ConfigMap object.

2

Inside of data, we declare some arbitrary name/value pairs.

A Secret is structurally similar to a ConfigMap, except that the values in the data section must be Base64 encoded.

Pods are linked to configuration objects (like ConfigMap or Secret) using volumes. In this example, we take the previous Pod example and attach the Secret above:

apiVersion: v1
kind: Pod
metadata:
    name: example-pod
spec:
    volumes: 1
    - name: my-configuration
      configMap:
        name: configuration-data 2
    containers:
    - image: "nginx:latest"
      name: example-nginx
      env: 3
        - name: BACKGROUND_COLOR 4
          valueFrom:
            configMapKeyRef:
                name: configuration-data 5
                key: backgroundColor 6
1

The volumes section tells Kubernetes which storage sources this pod needs.

2

The name configuration-data is the name of our ConfigMap we created in the previous example.

3

The env section injects environment variables into the container.

4

The environment variable will be named BACKGROUND_COLOR inside of the container.

5

This is the name of the ConfigMap it will use. This map must be in volumes if we want to use it as a filesystem volume.

6

This is the name of the key inside the data section of the ConfigMap.

A pod is the “primitive” description of a runnable unit of work, with containers as part of that pod. But Kubernetes introduces higher-order concepts.

Consider a web application. We might not want to run just one instance of this web application. If we ran just one, and it failed, our site would go down. And if we wanted to upgrade it, we would have to figure out how to do so without taking down the whole site. Thus, Kubernetes introduced the concept of a Deployment. A Deployment describes an application as a collection of identical pods. The Deployment is composed of some top-level configuration data as well as a template for how to construct a replica pod.

With a Deployment, we can tell Kubernetes to create our app with a single pod. Then we can scale it up to five pods. And back down to three. We can attach a HorizontalPodAutoscaler (another Kubernetes type) and configure that to scale our pod based on resource usage. And when we upgrade the application, the Deployment can employ various strategies for incrementally upgrading individual pods without taking down our entire application:

apiVersion: apps/v1 1
kind: Deployment
metadata:
    name: example-deployment
    labels:
        app: my-deployment
spec:
    replicas: 3 2
    selector:
        matchLabels:
            app: my-deployment
    template: 3
        metadata:
            labels:
                app: my-deployment
        spec:
            containers:
            - image: "nginx:latest"
              name: example-nginx
1

This is an apps/v1 Deployment object.

2

Inside of the spec, we ask for three replicas of the following template.

3

The template specifies how each replica pod should look.

When it comes to attaching a Kubernetes application to other things on the network, Kubernetes provides Service definitions. A Service is a persistent network resource (sort of like a static IP) that persists even if the pod or pods attached to it go away. In this way, Kubernetes Pods can come and go while the network layer can continue to route traffic to the same Service endpoint. While a Service is an abstract Kubernetes concept, behind the scenes it may be implemented as anything from a routing rule to an external load balancer:

apiVersion: v1 1
kind: Service
metadata:
  name: example-service
spec:
  selector:
    app: my-deployment 2
  ports:
    - protocol: TCP 3
      port: 80
      targetPort: 8080
1

The kind is v1 Service.

2

This Service will route to pods with the app: my-deployment label.

3

TCP traffic to port 80 of this Service will be routed to port 8080 on the pods that match the app: my-deployment label.

The Service described will route traffic to the Deployment we created earlier.

We’ve introduced a few of the many Kubernetes types. There are dozens more that we could cover, but the most frequently used by far are Pod, Deployment, ConfigMap, Secret, and Service. In the next chapter we will begin working with these concepts more directly. But for now, armed with some generic information, we can introduce Helm.

Helm’s Goals

Up to this point, we have focused on the broader cloud native ecosystem and on Kubernetes’ role within that ecosystem. In this section, we will change focus to Helm.

In the previous section, we saw several distinct Kubernetes resources: A Pod, a ConfigMap, a Deployment, and a Service. Each of these performs some discrete role. But an application typically requires more than one of these.

For example, the WordPress CMS system can be run inside of Kubernetes. But typically it would need at least a Deployment (for the WordPress server), a ConfigMap for configuration and probably a Secret (to keep passwords), a few Service objects, a StatefulSet running a database, and a few role-based access control (RBAC) rules. Already, a Kubernetes description of a basic WordPress site would span thousands of lines of YAML. At the very core of Helm is this idea that all of those objects can be packaged to be installed, updated, and deleted together.

When we wrote Helm, we had three main goals:

  1. Make it easy to go from “zero to Kubernetes”

  2. Provide a package management system like operating systems have

  3. Emphasize security and configurability for deploying applications to Kubernetes

We will look at each of these three goals, and then take a look at one other aspect of Helm’s usage: its participation in the life cycle management story.

From Zero to Kubernetes

The Helm project started in 2015, a few months before the inaugural KubeCon. Kubernetes was difficult to set up, often requiring new users to compile the Kubernetes source code and then use some shell scripts to get Kubernetes running. And once the cluster was up, new users were expected to write YAML (as we did in previous sections) from scratch. There were few basic examples and no production-ready examples.

We wanted to invert the learning cycle: instead of requiring users to start with basic examples and try to construct their own applications, we wanted to provide users with ready-made production-ready examples. Users could install those examples, see them in action, and then learn how Kubernetes worked.

That was, and still is to this day, our first priority with Helm: make it easier to get going with Kubernetes. In our view, a new Helm user with an existing Kubernetes cluster should be able to go from download to an installed application in five minutes or less.

But Helm isn’t just a learning tool. It is a package manager.

Package Management

Kubernetes is like an operating system. At its foundation, an operating system provides an environment for executing programs. It provides the tools necessary to store, execute, and monitor the life cycle of a program.

Instead of executing programs, it executes containers. But similar to an operating system, it provides the tools necessary to store, execute, and monitor those containers.

Most operating systems are supported by a package manager. The job of the package manager is to make it easy to find, install, upgrade, and delete the programs on an operating system. Package managers provide semantics for bundling programs into installable applications, and they provide a scheme for storing and retrieving packages, as well as installing and managing them.

As we envisioned Kubernetes as an operating system, we quickly saw the need for a Kubernetes package manager. From the first commit to the Helm source code repository, we have consistently applied the package management metaphor to Helm:

  • Helm provides package repositories and search capabilities to find what Kubernetes applications are available.

  • Helm has the familiar install, upgrade, and delete commands.

  • Helm defines a method for configuring packages prior to installing them.

  • Additionally, Helm has tools for seeing what is already installed and how it is configured.

We initially modeled Helm after Homebrew (a package manager for macOS) and Apt (the package manager for Debian). But as Helm has matured, we have sought to learn from as many different package managers as we can.

There are some differences between typical operating systems and Kubernetes. One of them is that Kubernetes supports running many instances of the same application. While I may only install the database MariaDB once on my workstation, a Kubernetes cluster could be running tens, hundreds, or even thousands of MariaDB installations—each with a different configuration or even a different version.

Another notion that is rare in typical operating systems, but is central to Kubernetes, is the idea of a namespace. In Kubernetes, a namespace is an arbitrary grouping mechanism that defines a boundary between the things inside the namespace and the things outside. There are many different ways to organize resources with namespaces, but oftentimes they are used as a fixture to which security is attached. For example, perhaps only specific users can access resources inside of a namespace.

These are just a few ways that Kubernetes differs from traditional operating systems. These and other differences have presented challenges in the design of Helm. We have had to build Helm to take advantage of these differences, but without giving up on our package management metaphor.

For example, the Helm installation command requires not only the name of the package, but also a user-supplied name by which the installed version of that package will be referenced. In the next chapter, we’ll see examples of this.

Likewise, operations in Helm are namespace-sensitive. One can install the same application into two different namespaces, and Helm provides tools to manage these different instances of the application.

In the end, though, Helm remains firmly in the package management class of tools.

Security, Reusability, and Configurability

Our third goal with Helm was to focus on three “must haves” for managing applications in a cluster:

  1. Security

  2. Reusability

  3. Configurability

In short, we wanted to make Helm aware enough about these principles that Helm users can have confidence in the packages they use. A user should be able to verify that a package came from a trustworthy source (and was not tampered with), reuse the same package multiple times, and configure the package to fit their needs.

Whereas Helm’s developers have direct control over the previous two design goals, this one is unique: Helm can only provide the right tools for package authors and hope that these creators choose to realize these three “must haves.”

Security

Security is a broad category. In this context, though, we are referring to the idea that when a user examines a package, the user has the ability to verify certain things about the package:

  • The package comes from a trusted source.

  • The network connection over which the package is pulled is secured.

  • The package has not been tampered with.

  • The package can be easily inspected so the user can see what it does.

  • The user can see what configuration the package has, and see how different inputs impact the output of a package.

Throughout this book, and especially in Chapter 6, we will cover security in more detail. But these five capabilities are things we believe we have provided with Helm.

Helm provides a provenance feature to establish verification about a package’s origin, author, and integrity. Helm supports Secure Sockets Layer/Transport Layer Security (SSL/TLS) for securely sending data across the network. And Helm provides dry-run, template, and linting commands to examine packages and their possible permutations.

Reusability

A virtue of package management is its ability to install the same thing repeatedly and predictably. With Helm, this idea is extended slightly: we may want to even install the same thing (repeatedly and predictably) into the same cluster or even same namespace in a cluster.

Helm charts are the key to reusability. A chart provides a pattern for producing the same Kubernetes manifests. But charts also allow users to provide additional configuration (which we will talk about in the next chapter). So Helm provides patterns for storing configuration so that the combination of a chart plus its configuration can even be done repeatedly.

In this way, Helm encourages Kubernetes users to package their YAML into charts so that these descriptions can be reused.

In the Linux world, each Linux distribution has its own package manager and repositories. This is not the case in the Kubernetes world. Helm was constructed so that all Kubernetes distributions could share the same package manager, and (with very, very few exceptions) the same packages as well. When there are differences between two different Kubernetes distributions, charts can accommodate this using templates (discussed more thoroughly in Chapter 5) coupled with configuration.

Configurability

Helm provides patterns for taking a Helm chart and then supplying some additional configuration. For example, I might install a website with Helm, but want to set (at installation time) the name of that website. Helm provides tools to configure packages at installation time, and to reconfigure installations during upgrades. But a word of caution is in order.

Helm is a package manager. Another class of software handles configuration management. This class of software, typified by Puppet, Ansible, and Chef, focuses on how a given piece of software (often packaged) is specifically configured for its host environment. Its responsibility is to manage configuration changes over time.

Helm was not designed to be a configuration management tool, though there is at least some overlap between package management and configuration management.

Package management is typically confined to implementing three verbs: install, upgrade, and delete. Configuration management is a higher-order concept that focuses on managing an application or applications over time. This is sometimes called “day-two ops.”

While Helm did not set out to be a configuration management tool, it is sometimes used as one. Organizations rely upon Helm not just to install, upgrade, and delete, but also to track changes over time, to track configuration, and to determine whether an application as a whole is running. Helm can be stretched this way, but if you want a strong configuration management solution, you may want to leverage other tools in the Helm ecosystem. Many tools like Helmfile, Flux, and Reckoner have filled in details in the larger configuration management story.

Note

The Helm community has created a wealth of tools that interoperate with or augment Helm. The Helm project maintains a list of those tools in the official documentation.

One of the common themes you will notice in Helm charts is that configuration options are often set up so that you can take the same chart and release a minimal version of it into your development environment, or (with different configuration options) a sophisticated version into your production environment.

Helm’s Architecture

In the final section of this chapter, we will briefly turn to the high-level architecture of Helm. As well as rounding out the conceptual discussion of cloud native Kubernetes applications and package management, this section paves the way for Chapter 2, where we will dive into using Helm.

Kubernetes Resources

We have had a look at several kinds of Kubernetes resources. We saw a couple of Pod definitions, a ConfigMap, a Deployment, and a Service. There are dozens more provided by Kubernetes. You can even use custom resource definitions (CRDs) for defining your own custom resource types. The main Kubernetes documentation provides both accessible guides and detailed API documentation on each kind.

Throughout this book, we will use many different Kubernetes resource types. While we discuss them in context, you may find it beneficial to skim through the main Kubernetes document as you run across new resource definitions.

As we discussed earlier, resource definitions are declarative. You, the user, describe for Kubernetes the desired state of a resource. For example, you can read the Pod definition we created earlier in the chapter as a statement that, “I want Kubernetes to make me a Pod that has these features.” It is up to Kubernetes to figure out how to configure and run a pod according to your specification.

All Kubernetes resource definitions share a common subset of elements. The following manifest uses a Deployment to illustrate the main structural elements of a resource definition:

apiVersion: apps/v1 1
kind: Deployment 2
metadata: 3
    name: example-deployment 4
    labels: 5
        some-name: some-value
    annotations: 6
        some-name: some-value
# resource-specific YAML
1

The API family and version for this resource.

2

The kind of resource. Combined with apiVersion, we get the “resource type”.

3

The metadata section contains top-level data about the resource.

4

A name is required for almost every resource type.

5

Labels are used to give Kubernetes query-able “handles” to your resources.

6

Annotations provide a way for authors to attach their own keys and values to a resource.

Of particular note, a resource type in Kubernetes is composed of three pieces of information:

API group (or family)

Several base resource types like Pod and ConfigMap omit this name.

API version

Expressed as a v, followed by a major version and an optional stability marker. For example, v1 is a stable “version 1,” while v1alpha indicates an unstable “version 1 alpha 1.”

Resource kind

The (capitalized) name of the specific resource within the API group.

Note

While a full resource type name is something like apps/v1 Deployment or v1 Pod (for core types), Kubernetes users will often omit the group and version when talking or writing about well-known types. For example, in this book we simply write Deployment instead of apps/v1 Deployment. Fully qualified names are used when specifying an exact version or when discussing a resource type defined in a CRD.

Thus, apps/v1 Deployment indicates that the API group “apps” has a “version 1” (stable) resource kind called “Deployment.”

Kubernetes supports two main formats for declaring the resources you want: JSON and YAML. Strictly speaking, YAML is a superset of JSON. All JSON documents are valid YAML, but YAML adds a number of additional features.

In this book, we stick to the YAML format. We find it easier to read and write, and almost all Helm users choose YAML over JSON. However, should your preferences differ, both Kubernetes and Helm support plain JSON.

Earlier, we introduced the term manifest. A manifest is just a Kubernetes resource serialized to either its JSON or YAML format. It would be fair to call our earlier Pod, ConfigMap, Deployment, and Service examples each a Kubernetes manifest, since they are resources expressed in YAML.

Charts

We have already talked about Helm packages in this chapter. In Helm’s vocabulary, a package is called a chart. The name is a play on the nautical nature of Kubernetes (which means “ship’s captain” in Greek) and Helm (which is the steering mechanism of a ship). A chart plots the way a Kubernetes application should be installed.

A chart is a set of files and directories that adhere to the chart specification for describing the resources to be installed into Kubernetes. Chapter 4 explains the chart structure in detail, but there are a few high-level concepts we will introduce here.

A chart contains a file called Chart.yaml that describes the chart. It has information about the chart version, the name and description of the chart, and who authored the chart.

A chart contains templates as well. These are Kubernetes manifests (like we saw earlier in this chapter) that are potentially annotated with templating directives. We will cover these in detail in Chapter 5.

A chart may also contain a values.yaml file that provides default configuration. This file contains parameters that you can override during installation and upgrade.

These are the basic things you will find in a Helm chart, though there are others that we will cover in Chapter 4. When you see a Helm chart, though, it may be presented in either unpacked or packed form.

An unpacked Helm chart is just a directory. Inside, it will have a Chart.yaml, a values.yaml, a templates/ directory, and perhaps other things as well. A packed Helm chart contains the same information as an unpacked one, but it is tarred and gzipped into a single file.

An unpacked chart is represented by a directory with the name of the chart. For example, the chart named mychart will be unpacked into a directory named mychart/. In contrast, a packed chart has the name and version of the chart, as well as the tgz suffix: mychart-1.2.3.tgz.

Charts are stored in chart repositories, which we will cover in Chapter 7. Helm knows how to download and install charts from repositories.

Resources, Installations, and Releases

To tie together the terminology introduced in this section, when a Helm chart is installed into Kubernetes, this is what happens:

  1. Helm reads the chart (downloading if necessary).

  2. It sends the values into the templates, generating Kubernetes manifests.

  3. The manifests are sent to Kubernetes.

  4. Kubernetes creates the requested resources inside of the cluster.

When a Helm chart is installed, Helm will generate as many resource definitions as it needs. Some may create one or two, others may create hundreds. When Kubernetes receives these definitions, it will create resources for them.

A Helm chart may have many resource definitions. Kubernetes sees each of these as a discrete thing. But in Helm’s view all of the resources defined by a chart are related. For example, my WordPress application may have a Deployment, a ConfigMap, a Service, and so on. But they are all part of one chart. And when I install them, they are all part of the same installation. The same chart can be installed more than once (with a different name each time). Thus, I may have multiple installations of the same chart, just as I might have multiple resources of the same Kubernetes resource type.

And this brings us to one final term. Once we install our WordPress chart, we have an installation of that chart. Then we upgrade that chart using helm upgrade. Now, that installation has two releases. A new release of an installation is created each time we use Helm to modify the installation.

A release is created when we install a new version of WordPress. But a release is also created when we merely change the configuration of an installation, or when we rollback an installation. This is an important feature of Helm that we will see again in Chapter 7.

A Brief Note About Helm 2

Those familiar with Helm 2 may notice certain concepts missing from this book. There is no mention of Tiller or gRPC. These things were removed from Helm 3, which is the subject of the present book. Also, this version of the book focuses on version 2 Helm charts. As confusing as it is, the Helm chart version increments separately from the Helm version. So Helm v2 used Helm Charts v1, and Helm v3 uses Helm Charts v2. These differ in a few important ways from version 1 Helm Charts—most notably in the way dependencies are declared. Helm 2 and Helm Charts v1 are considered deprecated.

Conclusion

The material here should prepare you for the coming chapters. But we hope it also provided insight into why we built Helm the way we did. Helm is only successful if it makes Kubernetes more usable both for the first-time users and for the long-time operations teams and SREs that use Helm day to day. The remainder of this book is dedicated to explaining (with lots of examples) how to get the most out of Helm—and how to do so securely and idiomatically.

Get Learning Helm now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.