Introducing crd2pulumi: Typed CustomResources for Kubernetes
Posted on
CustomResources in Kubernetes allow users to extend the API with their types. These types are defined using CustomResourceDefinitions (CRDs), which include an OpenAPI schema. This extensibility is quite useful but comes at the cost of complex YAML definitions. Our new crd2pulumi tool takes the pain out of managing CustomResources by generating types in the Pulumi-supported language of your choice!
Pulumi already supports the management of CRDs and their associated CustomResources using the apiextensions package. However, these SDK resources are untyped since every schema is, well, custom. While this is fine for simple CRDs, it quickly becomes unwieldy for real-world CRDs like cert-manager or Istio. These CRDs contain thousands of lines of complex YAML schemas, making it difficult to write CustomResources that adhere to those specs. Programming languages offer a better path forward. Instead of wrangling error-prone YAML definitions, using types in a programming language lets you use IDE type checking and autocomplete features!
Getting Started with crd2pulumi
Let’s test crd2pulumi
on the example CronTab CRD specified in the Kubernetes Documentation.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# name must match the spec fields below, and be in the form: <plural>.<group>
name: crontabs.stable.example.com
spec:
# group name to use for REST API: /apis/<group>/<version>
group: stable.example.com
# list of versions supported by this CustomResourceDefinition
versions:
- name: v1
# Each version can be enabled/disabled by Served flag.
served: true
# One and only one version must be marked as the storage version.
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
# either Namespaced or Cluster
scope: Namespaced
names:
# plural name to be used in the URL: /apis/<group>/<version>/<plural>
plural: crontabs
# singular name to be used as an alias on the CLI and for display
singular: crontab
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: CronTab
# shortNames allow shorter string to match your resource on the CLI
shortNames:
- ct
Copy this definition into a file called crontab.yaml
and then run crd2pulumi
to generate types for your language of
choice.
$ crd2pulumi --nodejsPath ./crontabs crontabs.yaml
import * as crontabs from "./crontabs"
import * as pulumi from "@pulumi/pulumi"
// Register the CronTab CRD.
const cronTabDefinition = new crontabs.stable.CronTabDefinition("my-crontab-definition")
// Instantiate a CronTab resource.
const myCronTab = new crontabs.stable.v1.CronTab("my-new-cron-object",
{
metadata: {
name: "my-new-cron-object",
},
spec: {
cronSpec: "* * * * */5",
image: "my-awesome-cron-image",
replicas: 3,
}
})
$ crd2pulumi --pythonPath ./crontabs crontabs.yaml
import pulumi_kubernetes as k8s
import crontabs.pulumi_crds as crontabs
# Register the CronTab CRD.
crontab_definition = k8s.yaml.ConfigFile("my-crontab-definition", file="crontabs.yaml")
# Instantiate a CronTab resource.
crontab_instance = crontabs.stable.v1.CronTab(
"my-new-cron-object",
metadata=k8s.meta.v1.ObjectMetaArgs(
name="my-new-cron-object"
),
spec=crontabs.stable.v1.CronTabSpecArgs(
cron_spec="* * * */5",
image="my-awesome-cron-image",
replicas=3,
)
)
$ crd2pulumi --dotnetPath ./crontabs crontabs.yaml
using Pulumi;
using Pulumi.Kubernetes.Yaml;
using Pulumi.Kubernetes.Types.Inputs.Meta.V1;
class MyStack : Stack
{
public MyStack()
{
// Register a CronTab CRD.
var cronTabDefinition = new Pulumi.Kubernetes.Yaml.ConfigFile("my-crontab-definition",
new ConfigFileArgs{
File = "crontabs.yaml"
}
);
// Instantiate a CronTab resource.
var cronTabInstance = new Pulumi.Crds.Stable.V1.CronTab("my-new-cron-object",
new Pulumi.Kubernetes.Types.Inputs.Stable.V1.CronTabArgs{
Metadata = new ObjectMetaArgs{
Name = "my-new-cron-object"
},
Spec = new Pulumi.Kubernetes.Types.Inputs.Stable.V1.CronTabSpecArgs{
CronSpec = "* * * * */5",
Image = "my-awesome-cron-image",
Replicas = 3
}
});
}
}
$ crd2pulumi --goPath ./crontabs crontab.yaml
package main
import (
crontabsv1 "crds-go-final/crontabs/stable/v1"
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v2/go/kubernetes/meta/v1"
"github.com/pulumi/pulumi-kubernetes/sdk/v2/go/kubernetes/yaml"
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Register the CronTab CRD.
_, err := yaml.NewConfigFile(ctx, "my-crontab-definition",
&yaml.ConfigFileArgs{
File: "crontabs.yaml",
},
)
if err != nil {
return err
}
// Instantiate a CronTab resource.
_, err = crontabsv1.NewCronTab(ctx, "my-new-cron-object", &crontabsv1.CronTabArgs{
Metadata: &metav1.ObjectMetaArgs{
Name: pulumi.String("my-new-cron-object"),
},
Spec: crontabsv1.CronTabSpecArgs{
CronSpec: pulumi.String("* * * * */5"),
Image: pulumi.String("my-awesome-cron-image"),
Replicas: pulumi.IntPtr(3),
},
})
if err != nil {
return err
}
return nil
})
}
As you can see, the v1.CronTab
object is strongly-typed. So if you try to set replicas to a string or add an
unsupported argument, your IDE will immediately warn you!
Cert Manager Example
Now let’s examine a real-world cert-manager example. In this case, the CRD is over 1200 lines
of YAML, but crd2pulumi
generates a nice interface so that we don’t have to worry about it. Here’s
what it looks like to create a Certificate
CustomResource using our new types.
$ crd2pulumi --nodejsPath ./certificates certificate.yaml
import * as certificates from "./certificates"
// Register the Certificate CRD.
new certificates.certmanager.CertificateDefinition("certificate");
// Instantiate a Certificate resource.
new certificates.certmanager.v1beta1.Certificate("example-cert", {
metadata: {
name: "example-com",
},
spec: {
secretName: "example-com-tls",
duration: "2160h",
renewBefore: "360h",
commonName: "example.com",
dnsNames: [
"example.com",
"www.example.com",
],
issuerRef: {
name: "ca-issuer",
kind: "Issuer",
}
}
});
$ crd2pulumi --pythonPath ./certificates certificate.yaml
import pulumi_kubernetes as k8s
import certmanager.pulumi_crds as certmanager
# Register the Certificate CRD.
_ = k8s.yaml.ConfigFile("my-certificate-definition", file="certificate.yaml")
# Instantiate a Certificate resource.
_ = certmanager.certmanager.v1beta1.Certificate(
"example-cert",
metadata=k8s.meta.v1.ObjectMetaArgs(
name="example-com"
),
spec=certmanager.certmanager.v1beta1.CertificateSpecArgs(
secret_name="example-com-tls",
duration="2160h",
renew_before="360h",
common_name="example.com",
dns_names=[
"example.com",
"www.example.com"
],
issuer_ref=certmanager.certmanager.v1beta1.CertificateSpecIssuerRefArgs(
name="ca-issuer",
kind="Issuer"
)
)
)
$ crd2pulumi --dotnetPath ./certificates certificate.yaml
using Pulumi;
using Pulumi.Kubernetes.Yaml;
using Pulumi.Kubernetes.Types.Inputs.Meta.V1;
class MyStack : Stack
{
public MyStack()
{
// Register a Certificate CRD.
var certificateDefinition = new Pulumi.Kubernetes.Yaml.ConfigFile("my-certificate-definition",
new ConfigFileArgs{
File = "certificate.yaml"
}
);
// Instantiate a Certificate resource.
var certificateInstance = new Pulumi.Crds.Certmanager.V1Beta1.Certificate("example-cert",
new Pulumi.Kubernetes.Types.Inputs.Certmanager.V1Beta1.CertificateArgs{
Metadata = new ObjectMetaArgs{
Name = "example-com"
},
Spec = new Pulumi.Kubernetes.Types.Inputs.Certmanager.V1Beta1.CertificateSpecArgs{
SecretName = "example-com-tls",
Duration = "2160h",
RenewBefore = "360h",
CommonName = "example.com",
DnsNames = {
"example.com",
"www.example.com"
},
IssuerRef = new Pulumi.Kubernetes.Types.Inputs.Certmanager.V1Beta1.CertificateSpecIssuerRefArgs{
Name = "ca-issuer",
Kind = "Issuer"
}
}
}
);
}
}
$ crd2pulumi --goPath ./certificates certificate.yaml
package main
import (
certv1b1 "crds-go-final/certificates/certmanager/v1beta1"
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v2/go/kubernetes/meta/v1"
"github.com/pulumi/pulumi-kubernetes/sdk/v2/go/kubernetes/yaml"
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Register the Certificate CRD.
_, err := yaml.NewConfigFile(ctx, "my-certificate-definition",
&yaml.ConfigFileArgs{
File: "certificate.yaml",
},
)
if err != nil {
return err
}
// Instantiate a Certificate resource.
_, err = certv1b1.NewCertificate(ctx, "example-cert", &certv1b1.CertificateArgs{
Metadata: &metav1.ObjectMetaArgs{
Name: pulumi.String("example-com"),
},
Spec: certv1b1.CertificateSpecArgs{
SecretName: pulumi.String("example-com-tls"),
Duration: pulumi.String("2160h"),
RenewBefore: pulumi.String("360h"),
CommonName: pulumi.String("example.com"),
DnsNames: pulumi.StringArray{
pulumi.String("example.com"),
pulumi.String("www.example.com"),
},
IssuerRef: certv1b1.CertificateSpecIssuerRefArgs{
Name: pulumi.String("ca-issuer"),
Kind: pulumi.String("Issuer"),
},
},
})
if err != nil {
return err
}
return nil
})
}
Kubernetes can be complex, but Pulumi gives you the tools you need to manage it successfully. With Pulumi superpowers at your fingertips, you can stop worrying about YAML indentation, and get back to solving the problems you care about!
Learn More
If you’d like to try crd2pulumi
today, head to the release page and download the appropriate binary for your
operating system.
If you’d like to learn about Pulumi and how to manage your infrastructure and Kubernetes through code, get started today. Pulumi is open source and free to use.
For further examples on how to use Pulumi to create Kubernetes clusters, or deploy workloads to a cluster, check out the rest of the Kubernetes tutorials.
As always, you can check out our code on GitHub, follow us on Twitter, subscribe to our YouTube channel, or join our Community Slack channel if you have any questions, need support, or just want to say hello.