Parameterizing Project Configuration with ytt
by Garrett Cheadle — Apr 13, 2022
If you’ve spent time learning ytt
, you might know how extremely powerful it is, but if you are new to ytt
, using it as your templating engine can be a daunting experience.
This blog post will cover how you can convert a simple application’s configuration into a parameterized and templated configuration with ytt
.
What is a Configuration File? ¶
When using software out of the box, it will usually come with a set of default settings. These settings can be changed, and you can often save your own custom configuration in yaml. So, configuration files refer to all the configuration settings set by a group of files, often yaml files.
ytt
is a tool that can add logic around these configuration files. As an application grows, the configuration grows, values get repeated, and it becomes harder to decipher what is being set. ytt
can help by turning your configuration files into parameterized templates that separate the important values being set, allowing you to ignore verbose software specific configuration.
Example Configuration ¶
Let’s begin by taking a look a single snippet from a YAML configuration file for a “Backstage” application (Github repo):
#! config.yaml
app:
title: Backstage Example App
baseUrl: http://localhost:3000
support:
url: https://github.com/backstage/backstage/issues
items:
- title: Issues
icon: github
links:
- url: https://github.com/backstage/backstage/issues
title: GitHub Issues
- title: Discord Chatroom
icon: chat
links:
- url: https://discord.gg/MUpMjP2
title: '#backstage'
organization:
name: My Company
techdocs:
builder: local
generator:
runIn: docker
publisher:
type: local
lighthouse:
baseUrl: http://localhost:3003
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters: []
kafka:
clientId: backstage
clusters:
- name: cluster
brokers:
- http://localhost:9092
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow:
- Component
- API
- Resource
- Group
- User
- Template
- System
- Domain
- Location
locations:
- type: file
target: ../catalog-model/examples/all-components.yaml
- type: file
target: ../../plugins/github-actions/examples/sample.yaml
- type: file
target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml
- type: file
target: ../catalog-model/examples/all-apis.yaml
- type: file
target: ../catalog-model/examples/all-resources.yaml
- type: file
target: ../catalog-model/examples/all-systems.yaml
- type: file
target: ../catalog-model/examples/all-domains.yaml
- type: file
target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml
- type: file
target: ../catalog-model/examples/acme-corp.yaml
- type: file
target: ../../cypress/e2e-fixture.catalog.info.yaml
auth:
environment: development
providers:
google:
development:
clientId: ${AUTH_GOOGLE_CLIENT_ID}
clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL}
gitlab:
development:
clientId: ${AUTH_GITLAB_CLIENT_ID}
clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
okta:
development:
clientId: ${AUTH_OKTA_CLIENT_ID}
clientSecret: ${AUTH_OKTA_CLIENT_SECRET}
audience: ${AUTH_OKTA_AUDIENCE}
oauth2:
development:
clientId: ${AUTH_OAUTH2_CLIENT_ID}
clientSecret: ${AUTH_OAUTH2_CLIENT_SECRET}
tokenUrl: ${AUTH_OAUTH2_TOKEN_URL}
oidc:
development:
metadataUrl: ${AUTH_OIDC_METADATA_URL}
clientId: ${AUTH_OIDC_CLIENT_ID}
clientSecret: ${AUTH_OIDC_CLIENT_SECRET}
tokenUrl: ${AUTH_OIDC_TOKEN_URL}
tokenSignedResponseAlg: ${AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG}
scope: ${AUTH_OIDC_SCOPE}
prompt: ${AUTH_OIDC_PROMPT}
auth0:
development:
clientId: ${AUTH_AUTH0_CLIENT_ID}
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
domain: ${AUTH_AUTH0_DOMAIN}
microsoft:
development:
clientId: ${AUTH_MICROSOFT_CLIENT_ID}
clientSecret: ${AUTH_MICROSOFT_CLIENT_SECRET}
tenantId: ${AUTH_MICROSOFT_TENANT_ID}
onelogin:
development:
clientId: ${AUTH_ONELOGIN_CLIENT_ID}
clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET}
issuer: ${AUTH_ONELOGIN_ISSUER}
bitbucket:
development:
clientId: ${AUTH_BITBUCKET_CLIENT_ID}
clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET}
atlassian:
development:
clientId: ${AUTH_ATLASSIAN_CLIENT_ID}
clientSecret: ${AUTH_ATLASSIAN_CLIENT_SECRET}
scopes: ${AUTH_ATLASSIAN_SCOPES}
homepage:
clocks:
- label: UTC
timezone: UTC
- label: NYC
timezone: America/New_York
- label: STO
timezone: Europe/Stockholm
- label: TYO
timezone: Asia/Tokyo
This YAML is long, repetitive, and tedious to adjust during the development process.
Applying ytt
Templating ¶
ytt
is a powerful tool. You can to annotate your YAML with annotations that are interpreted as code. ytt
annotations begin with #@
, and they allow you to escape into a pythonic language, called starlark, within your YAML templates.
This allows you to write code inline with your yaml! We can use loops, conditionals, functions, and much more to organize our YAML into configurable pieces. Let’s show some examples using the YAML above:
- The map contained in
auth.providers
has a repetitive structure, we can write a function to help the construction of these environment variables references.
#@ def create_auth_env_var(domain, varName):
#@ return 'AUTH_' + domain.upper() + '_' + varName.replace(' ', '_').upper()
#@ end
- There are also several URLs that follow the format:
localhost:port
. We can extract the creation of these URLs into a function call, making them easy to change in the future.
#@ def build_local_URL(port):
#@ return "http://localhost:" + str(port)
#@ end
- At the bottom of the configuration file, there is a list of time zones. We can store this information in a map, and then use a for-loop to template the list:
#@ timezones = {'UTC':'UTC', 'NYC':'America/New_York', 'STO':'Europe/Stockholm', 'TYO':'Asia/Tokyo'}
#@ for k in timezones:
- label: #@ k
timezone: #@ timezones[k]
#@ end
Including these functions and for-loops in our configuration file allows for easier and more targeted changes.
Parameterizing with ytt
¶
One of the major benefits when using ytt
is the ability to separate boilerplate configuration from the configuration values that are actually being set.
We can use the ytt
data values feature to separate non boilerplate configuration:
#! dataValues.yaml
#@data/values
---
catalogLocations:
- ../catalog-model/examples/all-components.yaml
- ../../plugins/github-actions/examples/sample.yaml
- ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml
- ../catalog-model/examples/all-apis.yaml
- ../catalog-model/examples/all-resources.yaml
- ../catalog-model/examples/all-systems.yaml
- ../catalog-model/examples/all-domains.yaml
- ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml
- ../catalog-model/examples/acme-corp.yaml
- ../../cypress/e2e-fixture.catalog.info.yaml
providersEnvironment: development
providers:
google: ['client Id', 'client Secret']
github: ['client Id', 'client Secret', 'enterprise Instance Url']
gitlab: ['client Id', 'client Secret']
okta: ['client Id', 'client Secret', 'audience']
oauth2: ['client Id', 'client Secret', 'token Url']
oidc: ['client Id', 'client Secret', 'metadata Url', 'token Url', 'token Signed Response Alg', 'scope', 'prompt']
auth0: ['client Id', 'client Secret', 'domain']
microsoft: ['client Id', 'client Secret', 'tenant Id']
onelogin: ['client Id', 'client Secret', 'issuer']
bitbucket: ['client Id', 'client Secret']
atlassian: ['client Id', 'client Secret', 'scopes']
By extracting this configuration into a separate ytt
data values file, we have parameterized our configuration.
Now, instead of finding the correct place to edit in the large config file, we can simply open the data values file to make changes to providers
, and catalogLocations
.
Also, we easily can find which environment variables each provider needs for authentication, since the variables are now listed next to the corresponding provider.
Next, the configuration file needs to utilize the parametrization that we set up with data values.
This is done by loading the data values into the configuration file (#@ load("@ytt:data", "data")
), and adding logic that uses these values:
#! config.yaml
#@ load("@ytt:data", "data")
#@ def build_local_URL(port):
#@ return "http://localhost:" + str(port)
#@ end
app:
title: Backstage Example App
baseUrl: #@ build_local_URL(3000)
support:
url: https://github.com/backstage/backstage/issues
items:
- title: Issues
icon: github
links:
- url: https://github.com/backstage/backstage/issues
title: GitHub Issues
- title: Discord Chatroom
icon: chat
links:
- url: https://discord.gg/MUpMjP2
title: '#backstage'
organization:
name: My Company
techdocs:
builder: local
generator:
runIn: docker
publisher:
type: local
lighthouse:
baseUrl: #@ build_local_URL(3003)
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters: []
kafka:
clientId: backstage
clusters:
- name: cluster
brokers:
- #@ build_local_URL(9092)
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow:
- Component
- API
- Resource
- Group
- User
- Template
- System
- Domain
- Location
locations:
#@ for c in data.values.catalogLocations:
- type: file
target: #@ c
#@ end
#@ def create_auth_env_var(name, variable):
#@ return '${AUTH_' + name.upper() + '_' + variable.replace(' ', '_').upper() + "}"
#@ end
auth:
environment: #@ data.values.providersEnvironment
providers:
#@ for/end pr in data.values.providers:
#@yaml/text-templated-strings
(@= pr @):
#@yaml/text-templated-strings
(@= data.values.providersEnvironment @):
#@ for/end val in data.values.providers[pr]:
(@= val.replace(' ','') @): #@ create_auth_env_var(pr, val)
homepage:
clocks:
#@ timezones = {'UTC':'UTC', 'NYC':'America/New_York', 'STO':'Europe/Stockholm', 'TYO':'Asia/Tokyo'}
#@ for k in timezones:
- label: #@ k
timezone: #@ timezones[k]
#@ end
We can now use ytt
to template the dataValues.yaml
and config.yaml
files into a single configuration YAML file.
The command ytt -f config.yaml -f dataValues.yaml
outputs the original YAML configuration from before. Take a look at these ytt
templates in the playground.
As you continue to work with this application, it will be easier to go into the dataValues.yaml
file and change the parameterized values.
To ensure that our edits to the Data Values are made correctly, we could add a Data Values Schema file.
A Data Values Schema file declares a Data Value’s name, default value, and type.
To learn more about the power of ytt
, you can see documentation about how to modularize with ytt.
Join the Carvel Community ¶
We are excited to hear from you and learn with you! Here are several ways you can get involved:
- Join Carvel’s slack channel, #carvel in Kubernetes workspace, and connect with over 1000+ Carvel users.
- Find us on GitHub. Suggest how we can improve the project, the docs, or share any other feedback.
- Attend our Community Meetings! Check out the Community page for full details on how to attend.
We look forward to hearing from you and hope you join us in building a strong packaging and distribution story for applications on Kubernetes!