Programming Language over Data language #
When it comes to configuration languages, the end goal is usually to use a
language that provides human-readable data structures. The most modern language
for this purpose is YAML, from a syntax point of view. It’s easy to work with in
terms of data type structure and readability. However, when it comes to
templating, those features are not enough. Usually, composition, inheritance,
type information, and validation of data structures are needed to.
After choosing YAML, most people have to come up with their own tooling and
patterns to create templates. The original motivation for using templates is to
facilitate shared configuration. This is not just a matter of a single Boolean
value for scaling the number of instances or a simple true/false statement for
enabling/disabling something. It often requires structures with multiple levels
and shapes.
To illustrate the complexity of configuration, let’s look at an example from
Kubernetes.
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: myimage
resources:
limits:
memory: "64Mi"
cpu: "500m"
requests:
memory: "32Mi"
cpu: "250m"
- name: anothercontainer
image: ubuntu
resources:
limits:
memory: "32Mi"
We can see that the containers
can be a list, with each entry having
resources
, with multiple nobs and dials. There are user experience issues with
the above:
- Are the values for
resources
required or optional?
Via the kubectl
CLI, we can ask the server for verification the values are
correct.
Can we set the same value for memory
in all properties?
Can we set all the containers
to the same properties, but just change one
for mycontainer
?
With YAML anchors, and :
We can use them for a value:
memory: &memory "32Mi"
another_memory: *memory
Ability to override the value :
limits: &limits
memory: "32Mi"
cpu: "500m"
# ... some lines far away ...
limits:
<<: *limits
memory: "64Mi"
Can we use resources
to all containers
that are configured for this
cluster? Not just this file.
Can we use this is a base template and received semantic updates without
interference?
Can we use conditionals for parts of the configuration?
Yes and no. There is tooling that has been created to do this, such as helm
charts. It is industry standard and clunky.
As a result, the pattern of needing to come up with these templates using YAML
just keeps happening. We need to accept alternatives such as Lua, Python, or
JavaScript/Typescript, which have the required features to help us.
To demonstrate how Typescript can be used to rewrite the previous configuration,
let’s take a look at the following code:
const pod = {
apiVersion: "v1",
kind: "Pod",
metadata: {
name: "mypod",
},
spec: {
containers: [
{
name: "mycontainer",
image: "myimage",
resources: {
limits: {
memory: "64Mi",
cpu: "500m",
},
requests: {
memory: "32Mi",
cpu: "250m",
},
},
},
{
name: "anothercontainer",
image: "ubuntu",
resources: {
limits: {
memory: "32Mi",
},
},
},
],
},
};
In this TypeScript code, we have defined a type for Pod
and Container
to
ensure that the values provided are optional, required, and typed correctly. By
doing so, we get the benefits of type safety and can catch errors early in the
development process.
interface Pod {
apiVersion: string;
kind: string;
metadata: {
name: string;
};
spec: {
containers: Container[];
};
}
interface Container {
name: string;
image: string;
resources: {
limits: {
memory: string;
cpu?: string;
};
requests?: {
memory: string;
cpu?: string;
};
};
}
Additionally, we could use npm
to inherit a versioned definition of the Pod
and Container
types and override what we need to. This allows us to reuse
common configuration patterns across projects and maintain versioning control.
There could be a type for Pod
that is always configured correctly for the
apiVersion
.
Using TypeScript for configuration provides type discoverability, allowing for
quick and easy identification of available fields and expected types. This
feature is especially helpful when working in an editor like Visual Studio Code,
which provides IntelliSense for writing configuration files. With IntelliSense,
you can get suggestions and auto-completion for configuration fields, providing
a pleasant experience for the programmer.
In addition to the benefits mentioned above, it’s also possible to output the
structure of a TypeScript configuration into a YAML file. While this adds an
extra layer of abstraction, it can be managed effectively for projects that
cannot change their default configuration language.
This isn’t an argument for the exclusive use of JavaScript/TypeScript, but
rather that it offers similar functionality to popular configuration languages
like YAML, Jsonnet, CUE,
HCL, YTT,
Toml, JSON, and
more. While the actual configuration may not change significantly, using
JavaScript/TypeScript can add built-in functionality. As a personal statement, I
would caution against trying to create a configuration language, as it may end
up being a poorly implemented programming language.
YAML may lack the required features for complex configurations, leading to the
use of templates that can add complexity. TypeScript offers explicit typing,
versioning control, and the ability to reuse common configuration patterns,
making it a strong contender for configuration languages. By choosing the right
language and implementing best practices, you can ensure your configuration is
well-structured, maintainable, and scalable for your application’s success.