Application Configuration Management with Pulumi

by Puja Abbassi on Nov 20, 2020

Application configuration management with Pulumi image thumbnail

As this series on application configuration management in Kubernetes has progressed, the predominant message that has emerged is the community's desire for a more flexible, expressive means for templating configuration. We've already seen that this has resulted in solutions involving the Jsonnet data templating language; Ksonnet and its self-proclaimed successor, Tanka, are examples. But, what if you could get the same experience using a programming language that you're already familiar with? This is the principal tenet behind the next solution under consideration in this series, Pulumi.


What is Pulumi?


Pulumi is an 'infrastructure as code' solution, that is technology and platform agnostic. For our purposes, that is to say, it’s not just targeted on Kubernetes and application configuration management. For example, Pulumi provides infrastructure resources on each of the three major public cloud platforms (AWS, Google Cloud Platform, and Azure), and many more besides. In fact, Pulumi is general-purpose enough in its approach to provision resources at many different levels of the stack. And, of course, this includes Kubernetes, which it considers a core target environment.


Language Use


You might already be thinking that there are already perfectly good, tried, and tested solutions for provisioning infrastructure using code as configuration. Terraform is the obvious standout example here. And, when it comes to handling configuration for Kubernetes environments, we've got Helm, Kustomize, and so on.


So, what does Pulumi bring to the table that these popular alternatives don't?


Putting it simply and succinctly, Pulumi implements infrastructure as code using traditional programming languages. The argument is that developers are already familiar with a language or languages, and should consider the need to learn yet another Domain-Specific Language (DSL), an unnecessary overhead. In addition, DSLs and templating languages tend to be less expressive, or very unwieldy when trying to define programming concepts like loops, conditionals, and abstraction.

So, Pulumi's approach is to have infrastructure configuration defined using primitives coded into API packages for different programming languages. It supports JavaScript, TypeScript, Python, and Golang, with JVM-based languages planned for the future. Using the APIs, the user defines their infrastructure configuration in the language of their choice, and then provisions and manages the infrastructure using the Pulumi CLI. For Kubernetes resources, such as Deployments, Services, ServiceAccounts, and so on, we'd use the primitives defined in Pulumi's Kubernetes API.

How_Security_Evolves_with_your_Kubernetes_Footprint

Let's have a look and see how this works for application configuration management in Kubernetes.


The Mechanics


Much like Kubernetes itself, Pulumi operates using the notion of desired state expression and performs reconciliation between the actual and desired state using its deployment engine. This model relies on storing state, and Pulumi does this using 'backends'. It provides its own cloud-based backend for storing state, Pulumi Console, or if preferred, backends can be self-hosted locally or remotely.

Because Pulumi supports defining configuration in multiple different languages, it needs a mechanism for abstracting the specifics of the language environment in its control loop. It uses the 'language host' abstraction to do this. The language host runs the code that you have written (using an executor specific to the language in question) and registers any infrastructure resources defined within the code with the deployment engine.

The deployment engine consults its view of the current state of the world and works out what actions need to be carried out to achieve the desired state. These actions may require the creation, amendment, or deletion of resources. Of course, Pulumi itself can't perform the required actions directly on the infrastructure resources. Instead, it uses the APIs exposed by the infrastructure providers to get the configuration applied. And, in order to consume those infrastructure APIs, Pulumi uses 'resource providers', which are comprised of a resource plugin (a binary) and an SDK for the resources associated with the target environment. The following schematic shows how these components fit together.


Given the earlier comment regarding Pulumi's similarity to Terraform, one interesting point of note is that some of the Pulumi resource providers are generated directly from the equivalent Terraform providers, using the Pulumi Terraform Bridge.


Pulumi and Kubernetes


The fact that Pulumi's Kubernetes provider ranks alongside its AWS, GCP, and Azure providers as the set of 'core' providers illustrates that Pulumi is partly aimed at solving the application configuration management problem. Provisioning resources in Kubernetes has equal rank to provisioning resources on AWS, for example. Let's dig down a little bit to see how this works.


Workflow Components


Like a lot of the other tools we've considered, Pulumi works with a directory structure. A directory that contains a file called 'Pulumi.yaml' is considered a Pulumi project, which is analogous to a GitHub repo. As a minimum, the 'Pulumi.yaml' file contains the name of the project, the language runtime, and any associated configuration details.

As we've been discussing throughout this series, in the Kubernetes world there is a common requirement to use largely the same configuration for different environments, but with marginal changes. This is almost always required for deploying applications to dev, test, staging, and production environments, for example. Pulumi supports this need using its stack concept. A separate file (which uses the stack name) is created within the project structure. A file called 'Pulumi.staging.yaml' will get created for a stack called 'staging', and so on. Stacks hold the configuration for each of the separate environments that need to be configured for a given project.


Defining Kubernetes Resources


If we were using Go for a Kubernetes-related Pulumi project, a snippet of code for defining a Service object might look something like this:

package main
 
<snip>
        frontend, err := corev1.NewService(ctx, appName, &corev1.ServiceArgs{
            Metadata: meta,
            Spec: &corev1.ServiceSpecArgs{
                Type: pulumi.String("LoadBalancer"),
                Ports: &corev1.ServicePortArray{
                    &corev1.ServicePortArgs{
                        Port:       pulumi.Int(80),
                        TargetPort: pulumi.Int(80),
                        Protocol:   pulumi.String("TCP"),
                    },
                },
                Selector: appLabels,
            },
        })
<snip>


The code calls a function defined in Pulumi's SDK for Kubernetes for rendering Service resources and supplies field values using strings, integers, and pre-populated variables. If you read the article on Tanka in this series, this is not unlike Tanka's approach, but with the code written in Go rather than Jsonnet. Remember, Pulumi also offers support for JavaScript, Python, and other alternatives as well. If you're comfortable with any of these languages, and with the Kubernetes APIs, programming this infrastructure should be relatively straightforward.

But, how do we go about turning this definition into reality?


Pulumi CLI


Pulumi offers a comprehensive CLI for working with projects and stacks, including the means for scaffolding new projects using templates. But, in order to get Pulumi's deployment engine to reconcile the desired state expressed in your code, it's just a simple case of issuing the command 'pulumi up'.

$ pulumi up
Previewing update (staging):
     Type                           Name                Plan       
 +   pulumi:pulumi:Stack            frontend-staging    create     
 +   ├─ kubernetes:apps:Deployment  frontend            create     
 +   └─ kubernetes:core:Service     frontend            create     
 
Resources:
    + 3 to create
 
Do you want to perform this update? yes
Updating (staging):
     Type                           Name                Status      
 +   pulumi:pulumi:Stack            frontend-staging    created     
 +   ├─ kubernetes:apps:Deployment  frontend            created     
 +   └─ kubernetes:core:Service     frontend            created     
 
Outputs:
    ip: "10.104.251.213"
 
Resources:
    + 3 created
 
Duration: 26s
 
Permalink: file:///home/puja/.pulumi/stacks/staging.json


Pulumi works out which resources need creating, amending, or deleting by comparing the configuration supplied with that it has stored and invokes the resource provider plugin(s) to effect the changes.


Conclusion


The big question, then, is should you use Pulumi for application configuration management in Kubernetes? Obviously, this is a rhetorical question, because different considerations will concern different audiences. Some will find Pulumi's approach compelling, others not so.

For those looking for a more general approach to configuration management, a solution that can be used to provision infrastructure as well as other layers of the stack (including Kubernetes), might provide that compelling reason. It means different teams can use the same approach to configuring their respective components of the stack. We might also use Pulumi to coordinate hybrid deployments consisting of multiple components. For example, an AWS RDS instance along with its client application, with Pulumi providing the client with the credentials necessary for accessing the instance. But, Pulumi is up against some well-respected, tried-and-tested technology in the shape of Terraform and other vendor-specific tools, like Cloud Formation. Ripping out, or even migrating from, these configuration environments could represent a sizeable cost to organizations, and present a prohibitive barrier.

If, however, you're a developer looking to define the deployment configuration of your cloud-native applications for Kubernetes, having the ability to use your preferred programming language will be a very attractive proposition. Except that, at the moment at least, the range of languages supported by Pulumi is fairly limited. If you like the idea of Pulumi, but your language choice isn't covered, you can always get stuck in and contribute to the effort.

There's a lot to Pulumi that we haven't had time to discuss (such as the domain-specific crosswalk best practices), so it's definitely worth taking some time to dig a bit deeper. And, whilst Pulumi's language agnostic approach to configuration management is quite novel, it's not the only kid on the block. Focusing specifically on the Kubernetes domain, the AWS open source project CDK for Kubernetes (or cdk8s for short) recently saw the light of day and seeks to achieve much the same thing as Pulumi.