Templates
CUE (Configure Unify Execute) is a powerful language for defining, generating, and validating data. Its templating capabilities make it particularly useful for managing configurations across different environments and reducing boilerplate code.
Unifying Types, Values, and Policies
CUE's strength lies in its ability to unify types, values, and constraints (policies) into a single, coherent model. This unification allows for:
- Type Checking: CUE ensures that values conform to their defined types.
- Default Values: You can specify default values that can be overridden when needed.
- Constraints: CUE allows you to define policies or constraints that configurations must adhere to.
Here's a simple example demonstrating this unification:
#ServerConfig: {
port: int | *8080 // Type: int, Default: 8080
maxConn: int & >=0 & <=1000 // Type: int, Constraint: between 0 and 1000
debug: bool | *false // Type: bool, Default: false
}
// A concrete instance
myServer: #ServerConfig & {
port: 9000
maxConn: 500
}
→ View the full example on CUE Playground
In this example, #ServerConfig
defines types, default values, and constraints. The myServer
instance inherits these properties and overrides some values.
Configurations for Different Environments
CUE excels at managing configurations for different environments. You can define a base configuration and then extend or modify it for specific environments. Here's an example:
#BaseConfig: {
region: string | *"us-west-1"
instanceType: string | *"t3.micro"
replicas: int | *2
db: {
user: string | *"admin"
password: string | *"password"
host: string | *"localhost"
port: int | *5432
}
}
ProdConfig: #BaseConfig & {
region: "us-east-1" // Production uses a different region
replicas: 5 // More replicas in production
instanceType: "t3.mini"
db: {
host: "prod-db.company.com" // Production database
password: "secure_prod_pass"
user: string | *"admin"
port: 8080
}
}
DemoConfig: #BaseConfig & {
instanceType: "t3.nano" // Minimal instance type for demo
replicas: 1 // Only 1 replica for demo
db: {
host: "demo-db.company.com"
password: "demo_pass"
}
}
This approach allows you to maintain a single source of truth (#BaseConfig
) while easily creating environment-specific configurations.
→ View the full example on CUE Playground
Separating Configs from Scripting Logic
CUE allows you to separate configuration definitions from the scripting logic that uses them. This separation enhances debugging and tracing of configurations. Let's look at an example based on the provided deployment.cue
and how it's used in install_app.tf.cue
:
First, the configuration definition in deployment.cue
:
package defs
flaskDynamoDBDeployment: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: "flask-dynamodb-app"
}
spec: {
replicas: 2
selector: {
matchLabels: {
app: "flask-dynamodb-app"
}
}
template: {
metadata: {
labels: {
app: "flask-dynamodb-app"
}
}
spec: {
containers: [{
name: "flask-dynamodb-app"
image: "\(common.container_repo)"
ports: [{
containerPort: 5000
}]
env: [
{
name: "AWS_ACCESS_KEY_ID"
valueFrom: {
secretKeyRef: {
name: "aws-secret"
key: "aws-access-key-id"
}
}
},
{
name: "AWS_SECRET_ACCESS_KEY"
valueFrom: {
secretKeyRef: {
name: "aws-secret"
key: "aws-secret-access-key"
}
}
},
{
name: "AWS_DEFAULT_REGION"
value: "us-west-2"
},
{
name: "DYNAMODB_TABLE_NAME"
value: string | *null @var(table_name)
}
]
}]
}
}
}
}
Now, let's see how this configuration is used in the flow defined in install_app.tf.cue
:
package main
import (
"mantis.io/flask-dynamodb-app/defs"
)
deploy_flask_dynamodb: {
@flow(deploy_flask_dynamodb)
// ... other tasks ...
deploy_flask_app: {
@task(mantis.core.K8s)
dep: [create_dynamodb_table]
config: defs.flaskDynamoDBDeployment
}
// ... other tasks ...
}
This separation of configuration and execution logic offers several benefits:
-
Reusability: The Kubernetes Deployment configuration (
flaskDynamoDBDeployment
) is defined separately and can be reused across different flows or tasks without duplication. -
Simplified Debugging: Configuration issues can be isolated and debugged independently of the execution logic.
-
Clear Tracing: It's easy to trace where and how configurations are used in the broader system. In this case, we can see that
flaskDynamoDBDeployment
is used in thedeploy_flask_app
task of thedeploy_flask_dynamodb
flow. -
Separation of Concerns: The
deployment.cue
file focuses solely on defining the structure of the Kubernetes Deployment, whileinstall_app.tf.cue
handles the execution flow and task ordering. -
Flexibility: You can easily swap out configurations by changing the reference in the flow, without altering the flow's structure. For example, you could create different deployment configurations for different environments and use them in the same flow structure.
By structuring your Mantis projects this way, you create a clear separation between the "what" (configurations) and the "how" (execution flows). This separation enhances maintainability, reusability, and clarity in your infrastructure-as-code projects.
Reducing Boilerplate for Terraform and Kubernetes Manifests
CUE significantly reduces boilerplate in Terraform and Kubernetes manifests through its powerful templating and inheritance capabilities. Here's how:
-
Default Values: CUE allows you to define default values, reducing the need to specify common configurations repeatedly.
-
Inheritance: You can create base configurations and extend them for specific use cases, avoiding duplication.
-
Constraints: CUE's built-in constraint system ensures valid configurations without verbose validation code.
-
Templating: CUE's string interpolation and reference system allow for dynamic value generation.
Here's a simplified example showing how CUE can reduce boilerplate in a Kubernetes manifest:
#BaseDeployment: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: string
spec: {
replicas: int | *1
selector: matchLabels: app: metadata.name
template: {
metadata: labels: app: metadata.name
spec: containers: [{
name: metadata.name
image: string
ports: [{containerPort: int}]
}]
}
}
}
MyApp: #BaseDeployment & {
metadata: name: "my-app"
spec: {
replicas: 3
template: spec: containers: [{
image: "my-repo/my-app:v1"
ports: [{containerPort: 8080}]
}]
}
}
This CUE configuration generates a complete Kubernetes Deployment manifest with minimal boilerplate, leveraging defaults and inheritance.
By utilizing these CUE features, you can create more maintainable, type-safe, and concise configuration management systems for your infrastructure and applications.