Write Kubernetes manifests faster and with less frustration
- Repeatable - A Kanyon Manifest will generate the same manifest given the same input every time
- Parameterized - Kanyon manifests allow the writer to inject values from the configuration directly into the manifest, removing the need for a find-and-replace step to inject parameters
- Stateless - Kanyon manifests simply render to a YAML file, which allows the user to use kubectl to apply the manifest and leverage the same patching and validation rules
- Easy-to-use - Kanyon manifests shouldn't require knowledge of magic strings like Kind names, APIVersion values, or other similar tribal knowledge. Where a value is static for a resource, it should be a readonly property so that the user doesn't need to duplicate knowledge and keystrokes
Kanyon uses Manifest objects to define what Kubernetes API resources should be emitted. Manifest objects are simply collections of IManifestObject, and can be inherited from and composed with to build the final manifest that Kanyon will serialize. The least-abstracted manifests resemble their YAML counterparts, making translation from YAML simple and frictionless. All of the Kubernetes core resources are implemented in the Kanyon.Kubernetes library.
Kanyon manifests can be defined in either a C# Script (.csx) file that returns a Manifest object or a compiled DLL with a type that inherits from Manifest. Here is an example of a simple manifest written in a C# Script
#r "nuget: Kanyon.Kubernetes, 2.0.0"
using Kanyon.Kubernetes.Core.V1;
using Kanyon.Kubernetes.Apps.V1;
new Kanyon.Core.Manifest() {
new Deployment() {
metadata = new ObjectMeta {
name = "helloworld"
},
spec = new DeploymentSpec {
template = new PodTemplateSpec {
spec = new PodSpec {
containers = new List<Container> {
new Container {
image = "nginx",
name = "hello",
ports = new List<ContainerPort> {
new ContainerPort { containerPort = 80 }
}
}
},
restartPolicy = "OnFailure"
}
}
}
},
new Service {
metadata = new ObjectMeta {
name = "helloworld",
},
spec = new ServiceSpec {
type = "ClusterIP",
selector = new { app = "helloworld" }
}
}
}
Manifests can be used to build abstractions to minimize repitition of patterns. For example, a HttpServiceManifest class can be created that abstracts the creation of the deployment and needed services.
public class HttpServiceManifest : Manifest
{
public HttpServiceManifest(string name, string image, string host, Dictionary<string, string> envVars, Dictionary<string, string> labels)
{
Add(new Deployment() {
metadata = new ObjectMeta {
labels = labels,
name = name
},
spec = new DeploymentSpec() {
selector = new LabelSelector {
matchLabels = labels
},
template = new PodTemplateSpec() {
metadata = new ObjectMeta() {
labels = labels
},
spec = new PodSpec() {
containers = new List<Container>() {
new Container() {
name = "api",
image = image,
ports = new List<ContainerPort> {
new ContainerPort { containerPort = 80, name = "http" }
},
env = envVars.Select(env => new EnvVar { name = env.Key, value = env.Value }).ToList()
}
},
}
}
}
});
Add(new Service() {
metadata = new ObjectMeta {
labels = labels,
name = name
},
spec = new ServiceSpec() {
type = "ClusterIP",
ports = new int[] { 80 }.Select(port => new ServicePort { port = 80 }).ToList(),
selector = labels
}
});
Add(new Ingress() {
metadata = new ObjectMeta {
labels = labels,
name = name,
annotations = new Dictionary<string, string> {
{ "kubernetes.io/ingress.class", "nginx" }
}
},
spec = new IngressSpec() {
rules = new List<IngressRule> {
new IngressRule {
host = host,
http = new HTTPIngressRuleValue() {
paths = new List<HTTPIngressPath>() {
new HTTPIngressPath() {
path = "/",
backend = new IngressBackend{serviceName = name, servicePort = "http" }
}
}
}
}
}
}
});
}
}
The Manifest object also exposes APIs to receive configuration values and expose them in the manifest. Each Manifest object will have the ConfigureItems method invoked before it is serialized. This method accepts a Dictionary<string, string>
as a parameter. This dictionary is populated with the environment variables available to the Kanyon process, as well as any arguments provided on the command line.
class CDManifest : Manifest {
public override void ConfigureItems(Dictionary<string, string> configuration) {
Add(
new Deployment {
metadata = new ObjectMeta() {
name = "helloworld"
},
spec = new DeploymentSpec() {
template = new PodTemplateSpec() {
spec = new PodSpec() {
containers = new List<Container>() {
new Container() {
image = $"busybox:{configuration["BuildNumber"]}",
name = "hello",
command = new List<string> { "sh", "-c", "echo \"Hello, Kubernetes!\" && sleep 3600" }
}
},
restartPolicy = "OnFailure"
}
}
}
});
}
}
new CDManifest()
Full examples can be found in the examples directory.
Kanyon is packaged as a .Net Core global tool to make it easy to install. Simply run
dotnet tool install --global kanyon
This will install the latest version from NuGet and add it into your path.
Kanyon writes the YAML directly to stdout to make it easy to pipe the output to file or to kubectl. To generate a manifest, run
kanyon -f your-manifest-filename.csx
and the manifest will be emitted to the console. To apply this manifest directly, you can run
kanyon -f your-manifest-filename.csx | kubectl apply -f -
Configuration values can also be provided over the command line. Values must be provided using the key=value
format. To pass a BuildNumber variable into a Kanyon manifest, you can run
kanyon -f your-manifest-filename.csx -c BuildNumber=2.0.0
Compiled manifests and policy sets can also be loaded directly from a URL, rather than a local file path. In order to load manifests or policy sets from a URL, the KANYON_ALLOWUNSAFECODE
environment variable must be set to true
. Then, run
kanyon -f https://your.domain.name/manifest.dll -c BuildNumber=2.0.0
- YAML is very painful to make changes to because it isn't type-safe, relies on whitespace for structure, and doesn't provide any validation around parameterization
- Helm provides automation around parameters, but inherits all of the pain with YAML and adds more pain with the template language obscuring the actual manifest definition
- Pulumi makes the editing experience much more friendly, but requires you to set well-known things that can be included in the abstraction (like API versions, etc). It's also unclear that we could
- CDK8S handles the editing experience well, but it relies on a scope that cross-cuts through the entire stack, and also seems to have the same leaky abstractions as Pulumi. Parameterization is also unclear.
The script manifests must return a Manifest object as the last instruction. If a semicolon is placed at the end of the line, it is treated as a full statement and nothing is returned.
You can debug compiled manifests using Visual Studio with some setup work. In the .csproj file, add the following snippet:
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish --no-build" />
</Target>
Then, in your project's launchsettings.json file, add the following profile:
"Kanyon": {
"commandName": "Executable",
"executablePath": "kanyon",
"commandLineArgs": "-f publish\\$(AssemblyName).dll -w"
}
After these two changes have been added, set the manifest project as the startup project and click Debug. The debugger will then launch the Kanyon shell with the published output loaded.