Wedge: Taking ant’s typical site construction scenario as an example, after Kusion was introduced, the user-side configuration code was reduced to 5.5%, the four user-facing platforms were reduced by accessing a unified code base, and the delivery time was reduced from 2 days to 2 hours without any other exceptions…

Note: This article was shared at GIAC 2021. Please click below to download the PPT content

KCL Declarative Cloud Native Configuration Policy language

0. Hello GIAC

Hi, I’m from Ant. I’m glad to share KCL Configuration Policy Language with you in GIAC’s new Programming language paradigm section. KCL language is a SELF-developed DSL language for cloud native infrastructure configuration code in Ant’s internal Kusion solution. Currently, it has begun to be promoted in a small scope in some scenarios such as site construction scene.

Let’s look at the simple KCL code:

schema GIACInvitation[name: str]:
	Name:     str = name
	Topic:    str = "Share the theme"Company? :str = None
	Type:     str = "Sharing guest"
	Address:  str = "Shenzhen"

invitation = GIACInvitation("Name") {
	Topic:   "KCL Configuration Policy Language"
	Company: "Ant Group"
}
Copy the code

This example code starts by defining a GIacocsion structure with a schema: this structure has a name parameter of type STR and a set of attributes labeled with type and default values. An instance of GIACInvitation is then constructed using declarative syntax.

This example, while simple, contains KCL’s most important Schema language constructs. As you can see from the examples, KCL attempts to improve the writing and maintenance of configuration code through declarative syntax and static type checking. This is also the original intention of the design of KCL language, we hope to solve the problem of configuration coding in the cloud native domain through the mature technical theories in the programming field.

1. The birth background of KCL language

In the classic Linux/UNIX operating system, we interact with the kernel through Shell and various built-in tools, while managing higher-level apps through Shell scripts. It can be said that Shell language greatly simplifies the programming interface of the kernel, not only improves the usability of the operating system, but also simplifies the management, operation and maintenance of upper-layer APPS, and improves the production efficiency. Kubernetes, the de facto standard in container management, has become the Linux/UNIX of the cloud computing era. Unlike UNIX systems, Kubernetes currently lacks an interactive language and tools that conform to its declarative, open, and shared design philosophy.

1.1 Why design KCL language?

K8s has become the operating system for cloud computing, but it currently lacks a fully functional SHELL interface. At present, although there are many open source solutions, there is no mature solution like the Shell of UNIX, especially it cannot meet the requirements of large-scale engineering of the head Internet enterprise. There is a Gap between cloud native technology and enterprise landing, which is the problem to be solved by cloud native engineering, and also the starting point of DESIGNING KCL language.

1.2 Now is a good time

Cloud-native thinking is highly open and democratic, with the result that everything is configurable and everything configured is code. Everyone is equal before the configuration code, and each user can interact with the infrastructure by adjusting the configuration code. Therefore, writing and maintaining configuration is becoming a necessary skill and requirement for software engineers in the era of cloud computing. With the growing need for codification of cloud-native configurations, many leading Companies in Silicon Valley have experimented and validated this direction on a large scale, giving us a lot of lessons to learn.

Therefore, Ant’s Kusion project attempts to use KCL configuration policy language to simplify the design of access to cloud native technology facilities. Its design goal is not only to improve the openness and efficiency of ant infrastructure, but also to optimize the development process of sharing and collaboration. It is positioned as a Shell language for the cloud native era. Although it is still in the exploration and practice stage, we would like to share some ideas of the design and implementation of THE KCL language with you in this article, so as to contribute a little bit to the rapid arrival of cloud native.

1.3 The birth history of KCL

The initial research and design work of KCL language began in 2019. Release KCL-0.1 by March 2020, based on Python custom syntax and developed with tools such as Go versions of Grumpy and AntLR. Switching to Python in the second half of 2020 and speeding up development and iteration, the kCL-0.2.x release introduced a number of language features, added support for plugins, and supported the IDEA plug-in. In the first half of 2021, unified optimization and integration of language features will begin. The released kcl-0.3 will optimize the type system, integrate unit testing tools, optimize execution performance, and provide support for multi-language apis such as Go, while providing support for VSCode through LSP. In the second half of 2021, we will introduce static type checking, optimize performance, and improve documentation support for the language.

2. KCL design principles

Based on the ant practice classical operations middle precipitation experience for many years and the advantages and disadvantages of all kinds of questions to think about, Kusion project on how to make full use of the dividends of cloud native technology, build an open, transparent, declarative, can be carried out in cooperation with the operational system of the exploration and thinking, put forward and practice based on the infrastructure cloud native code of collaborative development model. KCL language is a declarative configuration programming language designed by Kusion project to solve cloud native collaborative development. Simplicity, stability, efficiency and engineering are the design concepts of KCL language design.

2.1 Simplicity is king

Simplicity not only reduces the cost of learning and communicating, but also reduces the risk of code breaking. From THE KISS principle of UNIX to the Less is More design philosophy of the Go language, a simple and easy-to-use interface has always been a goal of successful products. Also based on the principle of simplicity, KCL retained only the necessary elements in reference to modern programming languages, while providing basic flexibility in writing configuration definitions through automatic type derivation, the introduction of limited control flows, and schemas. Cutting language features has always been an important goal of KCL design efforts.

2.1.1 Declarative syntax

Declarative programming is a programming paradigm alongside imperative programming. Declarative programming only tells you what you want, and the execution engine is responsible for executing the process. Declarative programming is easier to use, reduces the complexity and side effects of imperative assembly, keeps the configuration code legible, and the complex execution logic is already supported by the Kubernetes system. The KCL language supports declarative syntax by simplifying the syntax for instantiating schema structures, reducing the complexity of command procedural programming by providing only a small number of statements. Around the syntax associated with schema and configuration, KCL wants each configuration requirement to be written as uniformly as possible, making the configuration code as uniform as possible.

For example, schema, the core structure of KCL’s declarative syntax, can be instantiated declaratively:

schema Name:
    firstName: str
    lastName: str

schema Person:
    name: Name = {
        firstName: "John"
        lastName: "default"
    }

JohnDoe = Person {
    name.lastName: "Doe"
}
Copy the code

First, a Name structure is defined through the schema, which contains two mandatory attributes of string type. Then reuse the Name type in Person to declare a Name property, and give the Name property a default value to make it easier for users to use. Finally, when defining the JohnDoe configuration definition, you only need to fill in the name. LastName attribute parameter, and all other attributes will take the default parameters.

For some standard business applications, the reusable model is encapsulated as KCL Schema, which provides the simplest configuration interface for front-end users. Based on the ant internal Konfig such as after the sofa. SofaAppConfiguration simply add a small amount of configuration parameters can customize an App

appConfiguration = sofa.SofaAppConfiguration {
    resource: resource.Resource {
        cpu: "4"
        memory: "8Gi"
        disk: "50Gi"
    }
    overQuota: True
}
Copy the code

Using declarative syntax to describe the necessary parameters (using default configurations for everything else) can greatly simplify the configuration code for ordinary users.

2.1.2 Sequence-independent syntax

Instead of imperative programming, KCL favors a declarative syntax that is better suited to configuration definitions. Taking Fibonacci numbers as an example, the definition of a set of declarative expressions can be regarded as a system of equations. The writing order of the equations does not affect the solution of the system in essence, and the process of calculating attribute dependence and “solving” is completed by KCL interpreter, so as to avoid a lot of imperative assembly process and order judgment code.

schema Fib:
    n1: int = n - 1
    n2: int = n1 - 1
    n: int
    value: int

    if n <= 1:
        value = 1
    elif n == 2:
        value = 1
    else:
        value = (Fib {n: n1}).value + (Fib {n: n2}).value

fib8 = (Fib {n: 8}).value  # 21
Copy the code

In the code, the members n, N1, and n2 of Fib are dependent, but not in the order in which they are written. The KCL engine automatically calculates the correct order of execution based on the dependencies in the declarative code, and alerts you to abnormal status such as circular references.

2.2.3 Merging configurations with the same name

As the entire business and development maintenance team becomes complex, writing and maintaining configuration code becomes complicated: The same configuration parameter may be scattered in multiple modules of multiple teams. In addition, a complete application configuration can take effect only by combining the same and different configuration parameters scattered in different places. The same configuration parameter may conflict due to the modification of different teams. Manually synchronizing these namesake configurations and merging different configurations can be a major challenge.

For example, the application configuration model in Konfig library is divided into base configuration and stack configuration of each environment, which requires the program to be merged into one application configuration according to a certain merge policy when running, which is equivalent to the requirement that the front-end configuration of the large library can be automatically merged, that is, it can be separated and merged for many times, and then instantiate to generate the corresponding unique front-end configuration. With the capabilities of THE KCL language and Konfig best practices, you can simplify configuration writing by automatically merging baseline and environment configurations. For example, for the standard SOFA application OPSFree, its baseline configuration and environment configuration are maintained separately, and the final configuration is merged and checked by platform tools. The KCL language achieves the design goal of simplifying collaborative team development by automatically merging namesake configurations.

For example, the base configuration collects the general configuration:

appConfiguration = sofa.SofaAppConfiguration {
    mainContainer: container.Main {
        readinessProbe: probe_tpl.defaultSofaReadinessProbe
    }
    resource: res_tpl.medium
    releaseStrategy: "percent"
}
Copy the code

The pre-generated environment is then fine-tuned for certain parameters based on the base configuration:

appConfiguration = sofa.SofaAppConfiguration {
    resource: resource.Resource {
        cpu: "4"
        memory: "8Gi"
        disk: "50Gi"
    }
    overQuota: True
}
Copy the code

The merged Pre configuration is actually a copy of the SofaAppConfiguration configuration (equivalent to the following code, the environment configuration takes precedence over the baseline configuration by default)

appConfiguration = sofa.SofaAppConfiguration {
    mainContainer: container.Main {
        readinessProbe: probe_tpl.defaultSofaReadinessProbe
    }
    resource: resource.Resource {
        cpu: "4"
        memory: "8Gi"
        disk: "50Gi"
    }
    overQuota: True
    releaseStrategy: "percent"
}
Copy the code

The current namonymous configuration, while valid only for the application’s main package configuration, has brought observable benefits.

2.2 Stability overrides everything

The more basic the component, the higher the stability requirement, and the more reused the stability, the better the benefit. Because stability is a necessary requirement in the infrastructure world, it requires not only logical correctness, but also reduced probability of error.

2.2.1 Static typing and strong immutability

Many configuration languages use the dynamic check type at run time. The biggest drawback of dynamic typing is that you can only check the type of the property being executed, which is very bad for finding type errors early in the development phase. Static typing not only analyzes most type errors in advance, but also reduces the performance cost of dynamic type checking at the backend runtime.

In addition to static typing, KCL also disallows certain important properties from being modified with the final keyword. Static typing, combined with the strong immutability of attributes, can provide greater stability for the configuration code. For example, the apiVersion information in CafeDeployment is a constant configuration parameter, and final provides guarantee for such configuration:

schema CafeDeployment:
    final apiVersion: str = "apps.cafe.cloud.alipay.com/v1alpha1"
    final kind: str = 123  # Type error

schema ContainerPort:
    containerPort: int = 8080
    protocol: "TCP" | "UDP" | "SCTP" = "TCP"
    ext? : str = None
Copy the code

The apiVersion and kind properties in the code are final protected from modification. But kind implies an error because of the initial value of the attribute type, which can be easily detected and corrected during development by static type checking.

2.2.2 Runtime type and logical check verification

KCL’s Schema is not just a typed structure, it can also be used to check the amount of untyped JSON and YAML data at run time. In addition, the check block of the schema can be written to check the semantics, which will be automatically checked when the schema is instantiated at run time. At the same time, schema-based inheritance and mixins can generate check rules with multiple associations.

Here’s an example of a common use of check:

schema sample:
    foo: str
    bar: int
    fooList: [str]

    check:
        bar > 0 # minimum, also support the exclusive case
        bar < 100."message" # maximum, also support the exclusive case
        len(fooList) > 0 # min length, also support exclusive case
        len(fooList) < 100 # max length, also support exclusive case
        regex.match(foo, "^The.*Foo$") # regex match
        isunique(fooList) # unique
        bar in [range(100)] # range
        bar in [2.4.6.8] # enum
        multiplyof(bar, 2) # multipleOf
Copy the code

Each statement in check consists of an expression that produces a bool and an optional error message. (Each normal bool expression is actually a simplification of an assert statement.) Logical validation of property values at run time is possible with built-in syntax and functions.

2.2.3 Built-in test support

Unit testing is an effective way to improve code quality. KCL provides a flexible unit testing framework based on the existing Schema syntax structure with a built-in KCL-test command (with the testing package, you can specify command-line arguments of par value types).

Built-in testing tools

schema TestPerson:
    a = Person{}
    assert a.name == 'kcl'

schema TestPerson_age:
    a = Person{}
    assert a.age == 1
Copy the code

The kCL-test command not only executes unit tests, it also counts the time of each test execution, and you can select the specified tests to execute through the regular expression argument. In addition, kCL-test./… Unit tests of subdirectories can be recursively executed, and both integration and Plugin tests are supported.

2.3 Efficiency is the eternal pursuit

KCL code not only simplifies programming with a declarative style, but also provides an efficient development experience with module support, mixin features, built-in Lint and FMT tools, and IDE plug-ins.

Good syntax in 2.3.1 Schema

Schema is the core syntax structure in which KCL writes configurators, and almost every feature is designed to improve performance for a specific business scenario. For example, when defining and instantiating deep-nested configuration parameters, you can directly specify the path definition and initialization of properties.

schema A:
    a: b: c: int
    a: b: d: str = 'abc'

A {
    a.b.c: 5
}
Copy the code

Also for security, fields that default to non-empty for each property are automatically checked upon instantiation.

A schema is not just a stand-alone configuration object with typed annotations. We can extend existing schemas by inheritance:

schema Person:
    firstName: str
    lastName: str

# schema Scholar inherits schema Person
schema Scholar(Person):
    fullName: str = firstName + '_' + lastName
    subject: str

JohnDoe = Scholar {
    firstName: "John",
    lastName: "Doe",
    subject: "CS"
}
Copy the code

In the code, Scholar inherits from Person and then extends some properties. As a subclass, Scholar can directly access attribute information such as firstName defined in the parent class.

Inheritance is the basic method of code reuse in OOP programming, but there is also the technical problem of diamond inheritance caused by multiple inheritance. The KCL language deliberately simplifies the syntax of inheritance, keeping only the syntax of single inheritance. At the same time, the same code snippet can be mixed and reused through the mixin feature. For different capability matching, we write it through mixin mechanism and “mix” it into different structures by means of mixin declaration.

For example, you can add new attributes or logic to a schema (including the check block) by mixing in Person FullnameMixin:

schema FullnameProtocol:
    firstName : str = "default"
    lastName : str

mixin FullnameMixin for FullnameProtocol:
    fullName : str = "${firstName} ${lastName}"

schema relax Person:
    mixin [FullnameMixin]
    firstName : str = "default"
    lastName : str
Copy the code

With the language ability of KCL, students on the platform side can extend the structure through single inheritance, define the dependency relationship and value content of the attributes in the structure through mixin mechanism, complete declarative structure definition through the ordering independent writing method of the structure, and also support common functions such as logical judgment and default value.

2.3.2 Doc, FMT, Lint, and peripheral LSP tools

Although the code is the core part of the programming domain, the documentation and tools associated with the code are also highly relevant to the efficiency of programming. The policy design philosophy is not limited to the language itself, but also includes documentation, code formatting tools, code style evaluation tools, IDE support, and more. KCL supports the production of documentation directly from configuration code through KCL-DOC. Automated documentation not only reduces the cost of manual maintenance, but also reduces the cost of learning and communication. Kcl-fmt makes it easy to format all the code in the current directory (including nested subdirectories) into a single style, while the same format also reduces the cost of communication and code review. Kcl-lint, on the other hand, evaluates KCL code in parallel with some built-in risk monitoring strategies, making it easier for users to optimize the style of the code based on the results.

2.4 Engineering solutions

For any language to be practical in engineering, it requires not only good design, but also complete solutions for general scenarios such as upgrade, extension, and integration.

2.4.1 Multi-dimensional interfaces

The KCL language design provides maximum flexibility by providing nearly equivalent functional interfaces at different levels for the average user (THE KCL command line), the KCL language customizer (Go-API, Python-API), the KCL library extender (Plugin), and the IDE developer (LSP service).

2.4.2 Configuring the DB for thousands of users

KCL is a configuration oriented programming language, and the core of configuration is structured data. Therefore, we can think of the complete KCL code as a kind of configuration database. By using the override/ -o command to query and update KCL configuration parameters and the corresponding configuration path, you can query, temporarily modify, and save attributes.

By using codified configuration as the only source of DB, you can not only integrate with the mature query and analysis methods in the DB domain, but also adjust the logical structure of the configuration code through the configuration code perspective. Especially in the practice of automatic operation and maintenance, the PullRequest of configuration code modification automatically generated by the program can facilitate the introduction of developers to conduct code review, and well achieve man-machine operation and maintenance through different interfaces.

2.4.3 Smooth version upgrade

As the business and code evolve, the apis for related modules decay. The KCL language is designed to ensure smooth API upgrades and transitions through strict dependency version management, combined with the language’s built-in syntax and checking tools, and code integration testing and review processes to improve code security. KCL uses the @Deprecated feature to give early warning of code corruption, allow a window of time for users to upgrade over time, and even force synchronous upgrades with errors until the API is completely corrupted.

For example, in an upgrade where the name property is replaced by a fullName, you can use the @deprecated feature:

schema Person:
    @deprecated(version="1.1.0", reason="use fullName instead", strict=True)
    name: str.# Omitted contents

person = Person {
    # report an error on configing a deprecated attribute
    name: "name"
}
Copy the code

When instantiating Person, the name property initializer will receive an error message in a timely manner.

2.4.4 Built-in module, KCL module and plug-in module

KCL is a configuration-oriented programming language that provides engineered extension capabilities through built-in modules, KCL modules, and plug-in modules.

Builtin functions (such as len to calculate the length of a list, typeof to get a value, etc.) are not imported into user code, and there are built-in methods for basic types such as strings (such as converting the case of a string, etc.). For more complex generic work, flag libraries are provided, such as importing the Math library to use the related math functions, and importing the Regex library to use the regular expression library. Code for KCL can also be organized into modules, such as the Konfig library, which abstracts infrastructure and various standard applications into modules for use by upper-level users. In addition, Python can also be used to develop plug-ins for KCL through Plugin mechanism. For example, currently meta plug-ins can query configuration information of the center through the network, and app-Context plug-ins can be used to obtain the context information of the current application, thus simplifying code writing.

3. The implementation principle of KCL language

3.1 Overall Architecture

KCL, while a language dedicated to cloud native configuration and policy definition, maintains a similar implementation architecture to most procedural and functional programming languages, and its overall internal architecture is composed of a classic compiler “three-step” architecture. Here is the architecture diagram for the KCL implementation:

There are mainly the following key modules:

  • The KCL source code is analyzed to produce an AST (abstract syntax tree).
  • Compiler: The Compiler iterates the AST several times, performs semantic checks on the AST (such as type checking, invalid code checking) and optimizes the AST (incorporating constant expressions, etc.) to produce bytecode that the VIRTUAL machine can execute.
  • Virtual Machine (VM) : Executes the bytecode generated by the Compiler, computes the corresponding configuration results, and serializes the configuration results as YAML/JSON for output.

The advantage of having a three-part architecture is that you can combine the front end for the KCL source language and the back end for the target machine, and this approach to creating a compiler combination can greatly reduce the effort. For example, the current KCL bytecode definition and back-end virtual machine are self-developed implementation. KCL virtual machine is mainly used for computing and generating configuration results and serializing them to YAML/JSON for output. If you encounter other scenarios where KCL is used specifically, such as in a browser, you can easily port KCL to the browser by rewriting a wASM-friendly back end, but the syntax and semantics of KCL itself do not require any changes, and the compiler front-end code does not require any changes.

3.2 Communication principles between Go and Python

To better unlock the power of KCL to configure policy languages and automate product integrations all over the top (such as the well-known compiler backend LLVM, which has a well-designed API that allows developers to quickly build their own programming languages), KCLVM currently provides apis for Both Python and Go, enabling users to quickly build language peripheral tools using the corresponding apis. Language automation query and modification tools can improve the automation capability of the language, and further build service capability based on this. Help more users build their own cloud-native configuration code applications or fast access infrastructure.

The main component of KCLVM is implemented in Python code, and many cloud native applications are built with Go programs, so in order to better meet the demands of cloud native application users. Firstly, KCLVM built the communication media of Go program and Python program based on CGo and CPython. Based on this, the RPC call from Python function to Go function was designed, and the call parameters were stored in JSON form. Enables smooth transition of the power of the KCLVM-Python compiler into the Go code, which manipulates the KCL code with a Go one-line import call.

Supplement: In the process of servitization practice, the scheme of calling Python based on CGO also encountered some problems: First, Go+CGO+Python led to cross compilation difficulties, which challenged the automated testing and packaging of ACI; Secondly, Python after CGO does not support multi-language and multi-threaded concurrency, so it cannot take advantage of the performance of multi-core. Finally, even if the Python virtual machine is compiled into the Go program through CGO, the Python standard library and third-party libraries still need to be installed.

3.3 Principles of Cooperative Configuration

Once you have a configuration language that is easy to use and stable, another problem is how to use configuration codification to improve collaboration. Based on this, KCL configuration can be divided into user side configuration and platform side configuration, and the final configuration content is jointly determined by the configuration content of each user side and platform side. Therefore, there are two aspects of coordination problems:

  • The configuration on the platform is coordinated with that on the user side
  • Coordination between user-side configurations

In order to solve the above problems, KCL proposed abstract models such as sequence-independent syntax and namesake configuration merge to meet different collaborative configuration scenarios.

In the example above, the KCL code is first compiled into two graphs (a directed acyclic graph for direct references and dependencies of different user configurations), corresponding to the declaration code inside the structure and the declaration code used by the structure. The compilation process can be broken down into three simple steps

  • The platform-side structure is defined first and the internal declaration code diagram of the structure is formed
  • Second, declare and merge the different user side configuration code diagrams
  • Finally, the results of user side configuration code diagram are substituted into the platform side structure internal declaration code diagram to obtain the complete configuration diagram definition.

With this simple calculation process, most of the substitution operations can be done at compile time and the final solution can be obtained with only a few calculations at run time. While still in the process of compiling combined figure can perform type checking and the value of inspection, type checking is done is the difference between generalization, partial order is bounded on the value of the variable (check a meets established type or subtype of given type), the value of inspection is to be specialized, take partial order certainly (such as incorporating two dictionary is a dictionary).

4. Look to the future

KCL language is still in a stage of rapid development, and some applications have started to be used. We hope that the KCL language will provide more capability for Kusion technology stack and play an active role in the evolution of operations, trusted, cloud-native architecture. We also provide flexible extension and integration solutions for specific non-standard applications. For example, we are considering how to support the WebAssembly platform on the back end to support more integration solutions.

At the right time, we hope to open all the code of KCL and contribute to the rapid implementation of cloud native coding.

Thank you very much.

  • Ant Group ten thousand scale K8s cluster etCD high availability construction road

  • We built a distributed registry

  • Open a new chapter of cloud native MOSN – merging the Envoy and GoLang ecology

  • MOSN subproject Layotto: Opens a new chapter of service Grid + Application runtime

For more articles, please scan the code to follow the “financial level distributed Architecture” public account