# Skycfg and the case for writing Kubernetes configuration in Starlark

> Skycfg is a Stripe developed extension library for the Starlark language that lets a configuration file construct Protocol Buffer messages, which turns a Kubernetes or Envoy configuration into a typed program instead of a YAML document. It is still a pre-1.0 library that depends on go-protobuf internals, and that dependency is the main thing to understand before adopting it.

**stripe/skycfg** — Skycfg is an extension library for the Starlark language that adds support for constructing Protocol Buffer messages.

- Repository: https://github.com/stripe/skycfg
- Stars: 677 · Forks: 60
- Language: Go
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/stripe-skycfg

## A Starlark extension that emits Protobuf messages

Skycfg is an extension library for the Starlark language that adds support for constructing Protocol Buffer messages. It was developed by Stripe to simplify the configuration of Kubernetes services, Envoy routes, Terraform resources and other complex configuration data.

The problem it solves is not that YAML is unreadable. It is that YAML is untyped. A YAML file is a tree of strings, numbers and maps, and nothing in the file itself says that a field named replicas wants an integer or that a port field is supposed to be a number rather than a quoted string. The schema lives somewhere else, usually in the consuming application, and the mismatch surfaces at deploy time.

With Skycfg, the schema is the Protobuf definition, so the configuration file is a program that produces typed messages. That is the whole idea in one sentence, and the rest of the project is the machinery to make it pleasant.

The scope is narrower than the description first suggests. At present, only the Go implementation of Starlark is supported, so this is a Go library first, and a configuration language second. The repository is a Go module, github.com/stripe/skycfg, requiring Go 1.22, with go.starlark.net as the Starlark runtime and google.golang.org/protobuf as the message library. The licence is Apache 2.0.

The repository layout is more than a single library file. There is a go directory, an internal directory, a debugger directory, a build directory, a tools directory, an _examples directory and a docs directory, plus a reader.go, a skycfg.go and their test files at the root. The debugger and the _examples tree are where the interesting parts of the design show.

## The entry point is Load, and a config file is a main function

The entry point to Skycfg is the skycfg.Load() function, which reads a configuration file from local disk. The documentation notes that as the implementation stabilizes the public API surface is expected to expand so that Skycfg can be combined with other Starlark extensions, which tells you the current API is deliberately small.

A Skycfg file is a Starlark file that defines a main function returning a list of Protobuf messages. The Go side looks like this:

```go
package main

import (
    "context"
    "fmt"
    "github.com/stripe/skycfg"
    _ "github.com/golang/protobuf/ptypes/wrappers"
)

func main() {
    ctx := context.Background()
    config, err := skycfg.Load(ctx, "hello.sky")
    if err != nil { panic(err) }
    messages, err := config.Main(ctx)
    if err != nil { panic(err) }
    for _, msg := range messages {
        fmt.Printf("%s\n", msg.String())
    }
}
```

The config file itself is short, and the interesting part is the proto.package call, which binds the file to a Protobuf package so that the message constructors are available:

```python
# hello.sky
pb = proto.package("google.protobuf")

def main(ctx):
  return [pb.StringValue(value = "Hello, world!")]
```

The Go program then loads the file, calls Main, and prints each message. The developer workflow the documentation describes is to build a small test driver and see what comes back:

```bash
$ go get github.com/stripe/skycfg
$ go build -o test-skycfg
$ ./test-skycfg
value:"Hello, world!"
```

The loop is the point. Because a config file is a program, you can run it locally, print the messages it produces, and see the Protobuf text format before anything is applied to a cluster. The _examples directory makes the same point in more detail, with a repl example for interactive evaluation of a Skycfg file, a k8s example for basic Kubernetes integration, and a wasm example running Skycfg in the browser through WebAssembly.

## Type errors are caught in the config file, not in the cluster

The first argument the documentation makes for Skycfg over YAML is type safety. Protobuf has a statically typed data model, which means the type of every field is known while the configuration is being built. There is no risk of accidentally assigning a string to a number, a struct to a different struct, or forgetting to quote a YAML value.

The demonstration is a one character change:

```python
pb = proto.package("google.protobuf")

def main(ctx):
  return [pb.StringValue(value = 123)]
```

Which fails immediately rather than at apply time:

```bash
$ ./test-skycfg
panic: TypeError: value 123 (type `int') can't be assigned to type `string'.
```

This is the part that separates Skycfg from a template language. A template engine that renders a Kubernetes manifest will happily produce a manifest with a string where an integer belongs, because the template has no idea what the target schema is. Here the schema is the source of truth, the message constructor takes typed arguments, and the failure is at the moment you run the config file locally.

The cost of that guarantee is that a config file can only produce messages it knows about, and that the Protobuf definitions have to be available to the tool that loads the file. The blank import of the wrappers package in the Go example is a hint of that: message types are linked into the binary, and the import registers them.

The documentation is also careful in its framing, noting that the Python-like syntax of Skycfg might not seem like a win compared with bare YAML or TOML, and asking why you would want configuration files with all those quotes and colons and braces. The answer it builds towards is the next two features.

## Functions and modules turn a config file into an API

The second benefit is ordinary language features. As in standard Python you can define helper functions to reduce duplicated typing and share logic. The example defines a greet function that takes a language and returns a message:

```python
pb = proto.package("google.protobuf")

def greet(lang):
  greeting = {
    "en": "Hello, world!",
    "fr": "Bonjour, monde!",
  }[lang]
  return pb.StringValue(value = greeting)

def main(ctx):
  return [greet("en"), greet("fr")]
```

Third is modules, and this is the part with the longest reach. Starlark supports importing from other files, which lets you share code between configurations. By default the paths to load are resolved on the local filesystem, and you can also override the load() handler to support syntaxes such as Bazel-style target labels.

That combination is what makes the modules feature more than a convenience. With target label syntax, a config file can load shared code by logical path rather than by filesystem path, so the same repository can be consumed by different tools:

```python
load("//config/common/kubernetes.sky", "kubernetes")

def my_service(ctx):
  return kubernetes.pod(
      name = "my-namespace/my-service",
      containers = [
          kubernetes.container(name = "main", image = "index.docker.io/hello-world"),
      ]
  )
```

The documentation names the point directly: modules can protect service owners from complex Kubernetes logic. Combined with VCS hooks such as GitHub CODEOWNERS, you can use modules to provide an API surface for third-party tools deployed in your infrastructure, where the shared module carries the platform's rules and the service owner's file only carries its own parameters. The second module example loads a constants file and a versioned dashboard file and calls the same function for two clusters, one of them a canary.

The practical effect is a configuration repository that behaves like a small library, with code owners on shared logic rather than on every copy of a snippet.

## Context variables are the only dynamic behaviour

Skycfg supports limited dynamic behaviour through context variables, which let the Go caller pass arbitrary key and value pairs in the ctx parameter. The word limited is doing work, and it is a deliberate restriction rather than a missing feature.

On the Go side, the variables are attached with skycfg.WithVars:

```go
func main() {
    // ...
    messages, err := config.Main(ctx, skycfg.WithVars(starlark.StringDict{
      "revision": starlark.String("master/12345"),
    }))
```

And in the config file they are read from ctx.vars:

```python
pb = proto.package("google.protobuf")

def main(ctx):
  print("ctx.vars:", ctx.vars)
  return []
```

The output shows the value arriving as a dictionary:

```bash
$ ./test-skycfg
[hello.sky:4] ctx.vars: {"revision": "master/12345"}
```

So the only input a config file receives is its own ctx, and the only way to vary a file at evaluation time is through those variables. There is no reading of the environment, no file access, no network. That is what makes the whole file reviewable and reproducible, and it is the reason a config file can be linted and evaluated in CI without side effects.

The limitation is that everything dynamic has to be threaded through this one channel. If a config needs to differ per environment, per region or per deployment, the caller has to supply that as variables or the file has to load different modules. The examples show the pattern for the second case, with a versioned path per configuration set, which is a form of conditionality that lives in the file selection rather than in the file body.

## Bazel is official, the Go toolchain is not

The build story is stated plainly: Bazel is the officially supported build system for Skycfg, and the go toolchain is unofficially supported as well. To regenerate BUILD files the documentation gives one command:

```bash
$ bazel run //:gazelle
```

The Go route is three steps, and the first is installing the Protobuf code generator:

```bash
$ go install google.golang.org/protobuf/cmd/protoc-gen-go
$ go generate ./...
$ go test ./...
```

The repository carries both toolchains side by side, with BUILD.bazel, MODULE.bazel and MODULE.bazel.lock at the top level alongside go.mod and go.sum, plus a .bazelversion file that pins the Bazel version. The tools directory and the build directory are part of that setup.

For a Go developer who just wants to try Skycfg, the go route is the one to take, since the go get and go build commands in the getting started section are the shortest path to a running program. For a team already standardised on Bazel, the official support matters, because the Protobuf generation step has to be reproducible across machines and the two toolchains do not necessarily agree on generated file layout.

The contribution rules are also specific. Small simple changes can go straight to a pull request, while larger changes should start in the issue tracker so they can be checked against the roadmap. Changes to the Starlark language itself, such as new primitive types or syntax, do not belong here at all: those are applied to the starlark-go repository instead.

## The go-protobuf dependency is the real stability question

The stability section is the most important paragraph in the documentation, and it deserves to be quoted rather than paraphrased. Skycfg depends on internal details of the go-protobuf generated code, and as such may need to be updated to work with future versions of go-protobuf. The project says it will release Skycfg v1.0 after all dependencies on go-protobuf implementation details have been fixed, which will be after the api-v2 branch lands in a stable release of go-protobuf.

So the 1.0 release is gated on an upstream change, not on Skycfg's own roadmap. In practice that means a go-protobuf major version bump can force a Skycfg update, and a project that pins its dependencies tightly will feel that.

The mitigation offered is naming. Existing public APIs are expected to be stable even before the v1.0 release, and symbols that will change before v1.0 are hidden from the public docs and named with an Unstable prefix. That is a reasonable convention, and it gives you a way to tell at a glance which parts of the API you should not build a long lived dependency on.

The release history shows where things stand. Recent tags are v0.3.1 on 2025-06-23, v0.3.0 on the same day with the note that it adds the debugger, and v0.4.0 on 2025-11-18. The repository is not archived and the last push was on 2026-08-14, so the work continues between releases.

There is one concrete compatibility break already recorded. Skycfg dropped support for gogo/protobuf, an alternative Protobuf Go code generator, when it upgraded to the go-protobuf v2 API, and the documentation points to Skycfg v0.1.0 for anyone who still needs gogo support. That is a small, honest account of what changing a Protobuf library costs, and it is a fair proxy for what a future go-protobuf change might cost.

## Conclusion

Skycfg fits a team that configures Kubernetes, Envoy or Terraform at scale and has outgrown YAML, and that wants type checking and shared modules without giving up a reviewable source file. It is the wrong choice for a small deployment, where a YAML file plus kubectl apply is simpler and the dependency risk is not worth taking. Before adopting it, read the stability note about go-protobuf internals, check the Unstable prefixed symbols for anything you plan to call, and note that only the Go implementation of Starlark is supported.

## FAQ

### What is Skycfg and who developed it?

Skycfg is an extension library for the Starlark language that adds support for constructing Protocol Buffer messages. It was developed by Stripe to simplify the configuration of Kubernetes services, Envoy routes, Terraform resources and other complex configuration data.

### How do I get started with Skycfg?

The entry point is the skycfg.Load() function, which reads a configuration file from local disk, and the file defines a main function returning a list of Protobuf messages. The documentation shows go get github.com/stripe/skycfg, go build -o test-skycfg, then running the driver to print the messages it produced.

### Does Skycfg support Starlark implementations other than Go?

No. The README states that at present only the Go implementation of Starlark is supported.

### Is Skycfg stable enough to depend on?

It is pre-1.0 and depends on internal details of the go-protobuf generated code, so it may need updating for future go-protobuf versions. The project expects v1.0 after those dependencies are fixed, and marks symbols that will change with an Unstable prefix.

### Does Skycfg still support gogo/protobuf?

No. Skycfg dropped gogo/protobuf support when it upgraded to the go-protobuf v2 API, and the documentation points to Skycfg v0.1.0 for gogo support.

## Sources

- [Issues](https://github.com/stripe/skycfg/issues)
- [License: Apache-2.0](https://github.com/stripe/skycfg/blob/trunk/LICENSE)
- [README](https://github.com/stripe/skycfg/blob/trunk/README.md)
- [Releases](https://github.com/stripe/skycfg/releases)
- [stripe/skycfg on GitHub](https://github.com/stripe/skycfg)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/stripe-skycfg
