# 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.