Ten years ago, I co-authored a blog post about application interfaces. The lack of well defined, discoverable application interfaces impedes automation and management of applications, and obstructs configuration validation and generation in particular.

Since then, for user-facing interfaces, specifications like Swagger/OpenAPI have matured and evolved. Now there are agent-facing interfaces that support interface discovery like MCP, as well.

Unfortunately, for the most part, the management-oriented surfaces that the post was about have not received the same amount of investment and standardization.

One exception to that is OpenTelemetry, which I helped bring into the CNCF. OpenTelemetry is now widely supported by observability tools.

Partly thanks to Kubernetes, service discovery is largely through DNS still.

Infrastructure from Code tools automate infrastructure provisioning, but I’m not aware of any interface specifications coming out of that space.

Developer-oriented specs like score.dev and Docker Compose describe how to run an application in a specific configuration, not the interfaces it exposes.

Application configuration continues to be very fragmented, with many formats: INI, TOML, YAML, JSON, etc. Some applications, such as Grafana, even consume multiple formats. Also typically, there are no schemas published to specify valid configuration fields and values. This is especially true for environment variables read by an application. Hopefully that information is at least documented, but such rigor is hard to maintain for internal applications. In many cases it’s an afterthought. Often the only way to validate configuration is to run the application.

One low-hanging fruit of publishing a schema for application configuration is that it becomes easier to validate it. For example, ConfigHub supports a vet-jsonschema function that can use JSON Schema to validate application configuration in any supported format: JSON, YAML, INI, TOML, Properties, and even Env. For our worker component, I used a Go struct to specify all supported environment variables, including default values and description text, and then generated a JSON schema from that, which is published in the worker’s installer package.

{
  "properties": {
...
    "CONFIGHUB_URL": {
      "description": "Base URL (scheme and host) of the ConfigHub API.",
      "type": "string"
    },
    "CONFIGHUB_WORKER_FUNCTIONS": {
      "description": "Comma-separated list of additional worker function names to register (e.g. \"vet-kyverno-server,vet-opa-gatekeeper\").",
      "type": "string"
    },
    "CONFIGHUB_WORKER_HTTP_SERVER_PORT": {
      "description": "Port for the worker's local HTTP server (Prometheus metrics, pprof, /internal/ok and /internal/ready). When unset, the HTTP server is not started.",
      "type": "string"
    },
...

Many server applications also still take configuration via command-line flags, though I don’t think it is a very good way to configure servers in Kubernetes. One reason is that it’s harder for tools to determine what the configuration is compared to other mechanisms. For instance, in general, it’s impossible to determine whether a value following a flag is that flag’s value or just a positional argument. Supporting multiple ways to configure the same property, such as by flag and by environment variable, makes it even harder. Also, in Kubernetes, command-line arguments are a positional array, so Kubernetes tools can’t merge new flags into the list in a standard way.

Our worker uses cobra.dev as its command framework, which supports generating help text as YAML, so we can at least produce a spec for its supported commands and flags.

In addition to application configuration, there are a number of runtime facets that are important to managing applications, such as exposed ports, probe endpoints, and parts of the filesystem written. For this, I created a runtime spec format:

type RuntimeSpec struct {
 Ports  []Port  `yaml:"Ports,omitempty"`
 Paths  []Path  `yaml:"Paths,omitempty"`
 Probes []Probe `yaml:"Probes,omitempty"`
}

// Port describes a network port the binary listens on.
type Port struct {
 // Name is a stable identifier for this port. Probes reference it by
 // name. It is also a good candidate for containers[].ports[].name.
 Name string `yaml:"Name"`
 // Description is a human-readable summary of what is served on this port.
 Description string `yaml:"Description,omitempty"`
 // Default is the default port number used when no Source resolves.
 // Zero (or unset) means there is no default and the port is bound
 // only when one of Sources resolves.
 Default int `yaml:"Default,omitempty"`
 // Protocol is "TCP" (default) or "UDP".
 Protocol string `yaml:"Protocol,omitempty"`
 // Sources, if non-empty, lists locations from which the port number
 // may be overridden. Evaluated in priority order; the first hit wins.
 Sources []ValueSource `yaml:"Sources,omitempty"`
}

...

The worker’s spec is:

Ports:
  - Name: http
    Description: Local HTTP server. Bound only when CONFIGHUB_WORKER_HTTP_SERVER_PORT is set. Serves /internal/metrics (Prometheus), /internal/pprof/* (pprof), /internal/routes (route listing), /internal/ok (liveness), and /internal/ready (readiness).
    Protocol: TCP
    Sources:
      - Type: Env
        EnvVar: CONFIGHUB_WORKER_HTTP_SERVER_PORT
Paths:
  - Name: tmp
    Description: Scratch space for schema caching and transient work. Required to be writable; safe to back with emptyDir.
    Path: /tmp
    Access: ReadWrite
    Persistence: Ephemeral
Probes:
  - Name: liveness
    Type: Liveness
    HTTPGet:
      Path: /internal/ok
      PortName: http
  - Name: readiness
    Type: Readiness
    HTTPGet:
      Path: /internal/ready
      PortName: http

After creating this spec, I discovered Pacto for defining a runtime contract. It looks similar in spirit and is worth checking out. However, it appears to have some concrete values mixed in, whereas I was aiming to decouple development decisions, baked into the application, from operational choices, specified in concrete configuration, similar to score.dev.

Besides configuration validation, of what use are these such interface specifications?

Instead of writing an enormous template that contains every supported configuration property, environment variable, and/or command-line flag, one can provide the specs to an AI agent to generate a valid configuration. At this point, that’s likely to yield a better experience than generating a large GUI or TUI form, for example. I’ll write another post that compares the experience of doing that with other approaches.

The agent could try to read all of the application source code, but that would consume more tokens and, depending on how well organized and how large or small the codebase is, could omit some properties or make other mistakes. If the application doesn’t have straightforward ways of generating such specs currently, I might use an AI agent to generate one. For instance, I generated a JSON Schema for Grafana LDAP configuration from its documentation.

...
      "servers": {
        "type": "array",
        "minItems": 1,
        "description": "List of LDAP servers to authenticate against",
        "items": {
          "type": "object",
          "required": ["host", "bind_dn", "search_filter", "search_base_dns", "attributes"],
          "properties": {
            "host": {
              "type": "string",
              "description": "LDAP server hostname or IP address. Can specify multiple hosts separated by space",
              "minLength": 1,
              "examples": ["ldap.example.com", "ldap1.example.com ldap2.example.com"]
            },
            "port": {
              "type": "integer",
              "description": "LDAP server port. Default 389 for non-SSL, 636 for SSL",
              "minimum": 1,
              "maximum": 65535,
              "default": 389
            },
            "use_ssl": {
              "type": "boolean",
              "description": "Use LDAPS (LDAP over SSL/TLS) for connections",
              "default": false
            },
...

Once a configuration has been generated, when it needs to be subsequently modified, it should be just changed directly, as I advocated in my installer post.

The runtime spec could be passed to a function that deterministically adds the corresponding settings to a Kubernetes Deployment, StatefulSet, or DaemonSet configuration. If you think such a function would be useful, let me know.

Because it’s so easy to generate boilerplate code using AI, one could even generate a tool to manage an application. That would not be an abstraction in the same sense as a template, but would be a view over the underlying configuration.

Now that AI agents are being used to automate all kinds of tasks, they need sufficient context to perform the tasks correctly. In the absence of any information, LLMs will still just guess. For example, I’ve observed that Claude Code will just try to execute hallucinated commands rather than use --help, read documentation, ask me for clarification, or even load relevant AI agent skills. In order to correctly run and configure server applications, application interface specifications can provide the necessary context.

Do you generate or maintain specifications for your applications’ interfaces? Do you have some other way of validating your application’s configuration other than just executing the application? Do you use AI agents to generate and/or update your applications’ configurations? How often do they hallucinate? What did you do to prevent it?

Feel free to email us at hello@confighub.com, or send me a message on LinkedIn, X/Twitter, or Bluesky.

You could also try out ConfigHub, which is now in preview.

If you found this interesting, you may be interested in other posts in my Kubernetes series.