Carvel Logo

Getting started

Overview

Configuration authors looking for examples of how to use functions and variables, modules, data values schema, or a custom library, will see concrete examples in this guide. Language reference introduces concepts of basic syntax like ytt directives and ytt annotations definitions (ie:#@). See the ytt playground ‘getting started’ section for additional examples.

Variable and function reuse

A foundational concept in ytt is using Starlark code to create variables or functions. Inside a YAML file, prefix Starlark code with a ytt annotation #@ (including a space afterwards) to use it inline.

Starlark variables

In the code block below there are duplicated values for name: frontend, and other values that we may want to modify often.

#! config.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-deployment
  namespace: default
  labels:
    app.kubernetes.io/version: 0.1.0
    app.kubernetes.io/name: frontend
spec:
  selector:
    matchLabels:
      app: frontend
  replicas: 1
  template:
    spec:
      containers:
        - name: frontend
          image: index.docker.io/k14s/image@sha256:6ab29951e0207fde6760f6db227f218f20e875f45b22e8ca0ee06c0c8cab32cd
---
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
  labels:
    app.kubernetes.io/version: 0.1.0
    app.kubernetes.io/name: frontend
spec:
  type: ClusterIP
  ports:
    - port: 80

(This is a ytt comment #!, use these instead of YAML comments # which are discouraged to ensure that all comments are intentional. Comments will be consumed during execution.)

Using Starlark’s Python-like syntax, extract these values into Starlark variables. All the code defined here can be used in the same file.

#! config.yml

#@ name = "frontend"
#@ namespace = "default"
#@ version = "0.1.0"
#@ replicas = 1

apiVersion: apps/v1
kind: Deployment
metadata:
  name: #@ name + "-deployment"
  namespace: #@ namespace
  labels:
    app.kubernetes.io/version: #@ version
    app.kubernetes.io/name: #@ name
spec:
  selector:
    matchLabels:
      app: #@ name
  replicas: #@ replicas
  template:
    spec:
      containers:
        - name: #@ name
          image: index.docker.io/k14s/image@sha256:6ab29951e0207fde6760f6db227f218f20e875f45b22e8ca0ee06c0c8cab32cd
---
apiVersion: v1
kind: Service
metadata:
  name: #@ name + "-service"
  labels:
    app.kubernetes.io/version: #@ version
    app.kubernetes.io/name: #@ name
spec:
  type: ClusterIP
  ports:
    - port: 80

Execute this template by running ytt -f config.yml.

The result is identical to our original template, and now we can be sure all our repeated values will be consistent and easier to modify.

Functions

Functions provide a way to extract common code into a separate fragment or code snippet.

There are two ways to define a function in ytt: as a Starlark function; as a YAML fragment function.

Starlark functions make use of a return statement. Because of this they can be great for returning a value that must be transformed in some way.

YAML fragment functions differ in that they are YAML structure wrapped in a Starlark function definition. Everything inside the function will be the return value. They can be great when needing to return nested YAML structure, or key and value pairs.

Going back to the previous solution, we can see each labels key is duplicated YAML, like app.kubernetes.io/version: #@ version. There is also some duplicated string manipulation in the metadata.name key.

#! config.yml

#@ name = "frontend"
#@ namespace = "default"
#@ version = "0.1.0"
#@ replicas = 1

apiVersion: apps/v1
kind: Deployment
metadata:
  name: #@ name + "-deployment"
  namespace: #@ namespace
  labels:
    app.kubernetes.io/version: #@ version
    app.kubernetes.io/name: #@ name
spec:
  selector:
    matchLabels:
      app: #@ name
  replicas: #@ replicas
  template:
    spec:
      containers:
        - name: #@ name
          image: index.docker.io/k14s/image@sha256:6ab29951e0207fde6760f6db227f218f20e875f45b22e8ca0ee06c0c8cab32cd
---
apiVersion: v1
kind: Service
metadata:
  name: #@ name + "-service"
  labels:
    app.kubernetes.io/version: #@ version
    app.kubernetes.io/name: #@ name
spec:
  type: ClusterIP
  ports:
    - port: 80

Move the duplicated labels keys into a YAML fragment function, and move name formatting into a Starlark function.

#! config.yml

#@ name = "frontend"
#@ namespace = "default"
#@ version = "0.1.0"
#@ replicas = 1

#! Starlark function
#@ def fmt(name, type):
#@   return "{}-{}".format(name, type)
#@ end

#! YAML fragment function 
#@ def labels(name, version):
app.kubernetes.io/version: #@ version
app.kubernetes.io/name: #@ name
#@ end

apiVersion: apps/v1
kind: Deployment
metadata:
  name: #@ fmt(name, "deployment")
  namespace: #@ namespace
  labels: #@ labels(name, version)
spec:
  selector:
    matchLabels:
      app: #@ name
  replicas: #@ replicas
  template:
    spec:
      containers:
        - name: #@ name
          image: index.docker.io/k14s/image@sha256:6ab29951e0207fde6760f6db227f218f20e875f45b22e8ca0ee06c0c8cab32cd
---
apiVersion: v1
kind: Service
metadata:
  name: #@ fmt(name, "service")
  labels: #@ labels(name, version)
spec:
  type: ClusterIP
  ports:
    - port: 80

Execute this template by running ytt -f config.yml. Again, the result is identical to our original template, and we can be sure all our repeated sections of YAML will be consistent.


Externalize a value with data values schema

Use Data values schema to externalize a configuration value. When externalizing a value, you can also set a default value for it, and provide implicit type validation. Data values schema are used by configuration authors to declare a data value as an input to templates by naming it in a schema file. It can then be used in templates, and configuration consumers can modify it by providing a separate Data Value file.

Building on the previous solution, name, namespace, version, and replicas are values we want as data values input, so that we can easily change them for different applications or environments.

#! schema.yml
#@data/values-schema
---
name: "frontend"     #! ensures that any value for 'frontend' must be a string
namespace: "default" #! ensures that any value for 'default' must be a string
version: "0.1.0"     #! ensures that any value for 'version' must be a string
replicas: 1          #! ensures that any value for 'replicas' must be a int
#! config.yml

#@ load("@ytt:data", "data")

#@ def fmt(name, type):
#@   return "{}-{}".format(name, type)
#@ end

#@ def labels(name, version):
app.kubernetes.io/version: #@ version
app.kubernetes.io/name: #@ name
#@ end

apiVersion: apps/v1
kind: Deployment
metadata:
  name: #@ fmt(data.values.name, "deployment")
  namespace: #@ data.values.namespace
  labels: #@ labels(data.values.name, data.values.version)
spec:
  selector:
    matchLabels:
      app: #@ data.values.name
  replicas: #@ data.values.replicas
  template:
    spec:
      containers:
        - name:  #@ data.values.name
          image: index.docker.io/k14s/image@sha256:6ab29951e0207fde6760f6db227f218f20e875f45b22e8ca0ee06c0c8cab32cd
---
apiVersion: v1
kind: Service
metadata:
  name: #@ fmt(data.values.name, "service")
  labels: #@ labels(data.values.name, data.values.version)
spec:
  type: ClusterIP
  ports:
    - port: 80

Execute ytt via ytt -f config.yml -f schema.yml. The result is identical to our original template.

Extract code into modules

Modules contain code in a file that can be imported and used in templates.

Starlark modules have the .star extension.

YAML modules have the .lib.yml extension.

These two files are imported identically. The main difference is that Starlark modules contain only Starlark code, and YAML modules are YAML structures with Starlark code contained in #@ annotations, just like the code we have seen thus far.

Starlark module

Following the last solution, move the fmt() function to a separate Starlark file.

#! format.star

def fmt(name, type):
  return "{}-{}".format(name, type)
end

Import the module by loading it #@ load("format.star", "fmt").

YAML module

Move the labels() function to a separate YAML file.

#! labels.lib.yml

#@ def labels(name, version):
app.kubernetes.io/version: #@ version
app.kubernetes.io/name: #@ name
#@ end

Import the module by loading it #@ load("labels.lib.yml", "labels").

The load function takes a module file path, and secondly the name of the function or variable to export from the module. For multiple symbols, use a comma separated list of strings. If your module has many symbols that are usually all exported together, consider putting them in a struct, and load that struct.

#! config.yml

#@ load("@ytt:data", "data")
#@ load("labels.lib.yml", "labels")
#@ load("format.star", "fmt")

apiVersion: apps/v1
kind: Deployment
metadata:
  name: #@ fmt(data.values.name, "deployment")
  namespace: #@ data.values.namespace
  labels: #@ labels(data.values.name, data.values.version)
spec:
  selector:
    matchLabels:
      app: #@ data.values.name
  replicas: #@ data.values.replicas
  template:
    spec:
      containers:
        - name:  #@ data.values.name
          image: index.docker.io/k14s/image@sha256:6ab29951e0207fde6760f6db227f218f20e875f45b22e8ca0ee06c0c8cab32cd
---
apiVersion: v1
kind: Service
metadata:
  name: #@ fmt(data.values.name, "service")
  labels: #@ labels(data.values.name, data.values.version)
spec:
  type: ClusterIP
  ports:
    - port: 80
#@data/values-schema
---
name: "frontend"
namespace: "default"
version: "0.1.0"
replicas: 1
$ tree .
.
├── config.yml
├── schema.yml
├── format.star
└── labels.lib.yml

Execute ytt via ytt -f . to include all files in this directory.

Extract functionality into custom library

You can extract a whole set of input files (i.e. templates, overlays, data values, etc.) into a “Library” in the _ytt_lib/ folder. A library can be thought of as a separate self-contained ytt invocation. Libraries are not automatically included in ytt output. They must be programmatically imported using the library module, configured, evaluated, and inserted into a template that is part of the output.

Uses of a custom library

Libraries are helpful when

  • importing 3rd party configuration into one combined piece of configuration. Such as from a division of responsibility or shared configuration.
    • For example, having a library that provides helpful functions that needs to be used across multiple teams. Authors may use a tool to ensure these stay in sync.
  • A template is needed by two distinct applications like frontend and backend.
  • There is a need to update one application with an evaluated value from the other. Playground example here.

All the previous section’s files have moved to _ytt_lib/resources:

config/
$ tree .
├── _ytt_lib/
│   └── resources/
│       ├── config.yml
│       ├── schema.yml
│       ├── labels.lib.yml
│       └── format.star
├── config.yml
└── values.yml

In this example we want the resources from the library for a frontend application, and also for a backend application. We will use this library to create both of these.

Focusing on only the two top level files, config.yml, and values.yml, we import the custom library, provide it data values for a frontend and separately for a backend, and use #@ template.replace() to insert it inline so it shows in the output.

#! config.yml
#@ load("@ytt:data", "data")
#@ load("@ytt:library", "library")
#@ load("@ytt:template", "template")

#@ resources_lib = library.get("resources")
#@ backend = resources_lib.with_data_values(data.values.backend)
#@ frontend = resources_lib.with_data_values(data.values.frontend)

--- #@ template.replace(backend.eval())
--- #@ template.replace(frontend.eval())

Provide the values that we pass to the library.

#@data/values-schema
---
frontend:
  name: "frontend"
  namespace: "dev"
  replicas: 1
  version: "0.5.0"
backend:
  name: "backend"
  namespace: "dev"
  replicas: 1
  version: "0.2.0"

Run ytt with . to include all files in this directory.

$ ytt -f .

The result is the similar to our original template with resources configured for two different applications.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-deployment
  namespace: dev
  labels:
    app.kubernetes.io/version: 0.2.0
    app.kubernetes.io/name: backend
spec:
  selector:
    matchLabels:
      app: backend
  replicas: 1
  template:
    spec:
      containers:
        - name: backend
          image: index.docker.io/k14s/image@sha256:6ab29951e0207fde6760f6db227f218f20e875f45b22e8ca0ee06c0c8cab32cd
---
apiVersion: v1
kind: Service
metadata:
  name: backend-service
  labels:
    app.kubernetes.io/version: 0.2.0
    app.kubernetes.io/name: backend
spec:
  type: ClusterIP
  ports:
    - port: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-deployment
  namespace: dev
  labels:
    app.kubernetes.io/version: 0.5.0
    app.kubernetes.io/name: frontend
spec:
  selector:
    matchLabels:
      app: frontend
  replicas: 1
  template:
    spec:
      containers:
        - name: frontend
          image: index.docker.io/k14s/image@sha256:6ab29951e0207fde6760f6db227f218f20e875f45b22e8ca0ee06c0c8cab32cd
---
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
  labels:
    app.kubernetes.io/version: 0.5.0
    app.kubernetes.io/name: frontend
spec:
  type: ClusterIP
  ports:
    - port: 80

(Help improve our docs: edit this page on GitHub)