Skip to content
Author with AI

Boilerplate Templates

Gruntwork Boilerplate generates files and folders from templates. Runbooks takes two things from it: the boilerplate.yml schema that <Inputs> and <Template> build their forms from, and the Go template syntax that <Template>, <TemplateInline>, <Command>, and <Check> render.

This page covers the parts of Boilerplate that runbook authors use. The Boilerplate repo has the complete documentation.

Boilerplate is a template engine, similar to Jinja, Nunjucks, or Cookiecutter.

Boilerplate is built for DevOps and infrastructure work:

  • As a CLI, it prompts for the variables defined in boilerplate.yml, or takes them from command-line options so it can run in automated tests.
  • Templates are Go templates, with conditionals, loops, and helpers for loading another file, running a shell command, and including partial templates.
  • A template can depend on other templates and include them conditionally on variable values.
  • Variables are typed and can have validations.
  • Hooks run arbitrary scripts.
  • It is a standalone binary for macOS, Linux, and Windows.

Say you want to generate a README for new projects. Create a template folder:

  • Directorymy-template/
    • boilerplate.yml
    • README.md

boilerplate.yml defines the variables:

variables:
- name: ProjectName
type: string
description: Name of the project
- name: Author
type: string
description: Who is the author?
default: Anonymous

README.md is the template file:

# {{ .ProjectName }}
Created by {{ .Author }}.

Run Boilerplate on the command line:

Terminal window
boilerplate \
--template-url ./my-template \
--output-folder ./output \
--var ProjectName="My Cool App" \
--var Author="Jane Doe"

The result is output/README.md:

# My Cool App
Created by Jane Doe.

Every template directory needs a boilerplate.yml file that defines its variables. Runbooks renders a form from it.

variables:
- name: ProjectName
type: string
description: What would you like to call your project?
default: my-project
validations:
- required
- name: Environment
type: enum
description: Which environment is this for?
options:
- dev
- staging
- prod
default: dev

This generates a form with a text input for ProjectName and a dropdown for Environment.

Each variable supports these properties:

Property Required Description
name Yes Variable name (used in templates as .Name)
type No Data type: string, int, float, bool, enum, list, map (defaults to string)
description No Help text shown in the form
default No Default value
options For enum List of allowed values for dropdowns
validations No Validation rules
sensitive No For a string, masks the field like a password input, with a button to show the value

A default can use other variables, with the same template syntax as your template files:

variables:
- name: ProjectName
type: string
default: acme
- name: Environment
type: enum
options: [dev, prod]
default: dev
- name: BucketName
type: string
default: "{{ .ProjectName }}-state"
- name: InstanceSize
type: string
default: '{{ if eq .Environment "prod" }}large{{ else }}small{{ end }}'
- name: CatalogRepositories
type: list
default:
- github.com/gruntwork-io/terraform-aws-service-catalog
- "github.com/{{ .ProjectName }}/modules"
- name: DefaultTags
type: map
default:
"{{ .ProjectName }}:Team": DevOps

Runbooks fills these in, using whatever the other fields hold at that moment, in two places: when it renders the body of a <Template> or <TemplateInline> block, and when a <Command> or <Check> script reads the variable through inputsId.

Other block props that use the variable through {{ .inputs.X }}, like <GitClone>’s prefilledUrl or <TemplateInline>’s outputPath, get the expression as written, not the value it resolves to. Give those props a variable with a plain default.

The form doesn’t show the {{ }} syntax. A linked default shows the value it comes to, with a link icon, and follows the fields it uses as you edit them. Here BucketName shows acme-state, and InstanceSize shows small until you pick prod. In a <Template>, a default can also use the values the block gets through inputsId.

When the form can’t work out the value yet, it shows a token for each variable the default uses instead, named the way that variable’s field is labelled: a Project Name token followed by -state. A default that does more than insert a variable shows one token that says what it’s based on, like Based on Environment. If the default uses a block output that hasn’t been produced yet, its token is yellow; hover over it to see which block has to run first. If no block on the page has the id the default names, the value can never be filled in: the token is red, the field shows an error, the block is red and won’t generate until you replace the value, and the error is counted in the runbook’s error summary.

A sensitive field, and any default built from a sensitive variable or output, always shows tokens, never the value.

In a text field:

  • Hover over the value to see its expression.
  • Click it to edit the expression. When you leave the field, a value that is still an expression is shown as linked again.
  • Click the X to clear it and type your own value.

The template-defaults demo runbook uses each kind.

Text input field.

- name: BucketName
type: string
description: Name for the S3 bucket
default: my-bucket

Numeric input field.

- name: InstanceCount
type: int
description: Number of instances to deploy
default: 3

Numeric input field that accepts decimals.

- name: ScalingThreshold
type: float
description: CPU utilization that triggers scaling
default: 0.75

Checkbox toggle. Without a default, the checkbox starts unchecked, which is the value false.

- name: EnableLogging
type: bool
description: Enable CloudWatch logging?
default: true

Dropdown select from predefined options. Without a default, the dropdown starts empty and the variable has no value until the user picks one. With the required validation, the form asks for a pick before it can be submitted. Boilerplate itself needs a value for every variable without a default, so give an optional enum a default ("" works if "" is one of the options).

- name: Region
type: enum
description: AWS region for deployment
options:
- us-east-1
- us-west-2
- eu-west-1
default: us-east-1

List of values. Users can add and remove items.

- name: AllowedIPs
type: list
description: IP addresses to allow
default: []

Key-value pairs. Users can add and remove entries.

- name: Tags
type: map
description: Resource tags
default: {}

For structured values, see x-schema.

Add validation rules to a variable:

- name: ProjectName
type: string
validations:
- required
- name: ContactEmail
type: string
validations:
- email
- name: WebsiteURL
type: string
validations:
- url
- name: Identifier
type: string
validations:
- alphanumeric
- name: CountryCode
type: string
validations:
- countrycode2
- name: Version
type: string
validations:
- semver
- name: ManyValidations
type: string
validations:
- "required"
- "email"

Use the object form with type and message:

variables:
- name: Email
type: string
validations:
- type: required
message: Email is required
- type: email
message: Must be a valid email address
Validation Description
required Field cannot be empty
email Must be a valid email address
url Must be a valid URL
alpha Only letters allowed (no numbers or special characters)
digit Only digits allowed (0-9)
alphanumeric Only letters and numbers allowed
countrycode2 Must be a valid two-letter country code (ISO 3166-1 alpha-2)
semver Must be a valid semantic version (e.g., 1.0.0, 2.1.3-beta)
length(min, max) Value must be between min and max characters long
regex("pattern") Value must match the given regular expression pattern
- name: ProjectSlug
type: string
validations:
- regex("^[a-z][a-z0-9-]+$")
- name: Description
type: string
validations:
- length(0, 256)

Template files use Go template syntax. Reference a variable from boilerplate.yml with dot notation. In a runbook, the same variable is also available as {{ .inputs.BucketName }}, as Inputs and outputs describes.

resource "aws_s3_bucket" "main" {
bucket = "{{ .BucketName }}-{{ .Environment }}"
}
{{- if .EnableLogging }}
resource "aws_cloudwatch_log_group" "main" {
name = "/app/{{ .ProjectName }}"
}
{{- end }}
instance_type = "{{ if eq .Environment "prod" }}t3.large{{ else }}t3.micro{{ end }}"
{{- range .AllowedIPs }}
- {{ . }}
{{- end }}
tags = {
{{- range $key, $value := .Tags }}
{{ $key }} = "{{ $value }}"
{{- end }}
}

Use - to trim whitespace around template directives:

  • {{- trims whitespace before
  • -}} trims whitespace after
{{- if .Description }}
description = "{{ .Description }}"
{{- end }}

Boilerplate includes helper functions for common transformations:

# String case transformations
{{ .ProjectName | snakeCase }} # my_project
{{ .ProjectName | camelCase }} # myProject
{{ .ProjectName | pascalCase }} # MyProject
{{ .ProjectName | kebabCase }} # my-project
{{ .ProjectName | upper }} # MY-PROJECT
{{ .ProjectName | lower }} # my-project
# String checks
{{ hasPrefix "prod" .Environment }} # true if starts with "prod"
{{ hasSuffix "-dev" .Name }} # true if ends with "-dev"

File and folder names can use template syntax:

  • Directorytemplates/
    • boilerplate.yml
    • {{ .ProjectName }}.tf
    • Directory{{ .Environment }}/
      • config.yaml

With ProjectName: "vpc" and Environment: "prod", this generates:

  • vpc.tf
  • Directoryprod/
    • config.yaml

Runbooks reads extra YAML properties, prefixed with x-, that change how the form renders. The Boilerplate CLI ignores them.

x-section groups fields under a named heading. This form renders two sections, Basic Settings and Advanced Settings:

variables:
- name: FunctionName
type: string
x-section: Basic Settings
- name: Runtime
type: enum
options: [python3.12, nodejs20.x]
x-section: Basic Settings
- name: MemorySize
type: int
default: 128
x-section: Advanced Settings
- name: Timeout
type: int
default: 30
x-section: Advanced Settings

Variables without x-section appear in an unnamed section at the top.

Lists the allowed values for a list variable. The form then renders a multi-select picker. The Boilerplate CLI doesn’t check the values against the list.

- name: Regions
type: list
description: Regions to deploy to
x-options:
- us-east-1
- us-west-2
- eu-west-1

A map variable collects key-value pairs where each value is a string. When each key needs several values, such as an email address, account ID, and name for each AWS account, give the map an x-schema. Runbooks then renders a structured form for each entry:

- name: AWSAccounts
type: map
description: AWS account configuration
x-schema:
email: string
name: string
id: string

Each map entry renders with those three fields.

Sets the label for each entry of a schema-based map:

- name: AWSAccounts
type: map
description: AWS account configuration
x-schema-instance-label: AWS Account Name
x-schema:
email: string
environment: string
id: string

You can run the Boilerplate CLI directly to test templates during development.

Terminal window
# macOS
brew install gruntwork-io/tap/boilerplate
# Or download from GitHub releases
# https://github.com/gruntwork-io/boilerplate/releases
Terminal window
boilerplate \
--template-url ./templates/vpc \
--output-folder ./output \
--var VpcName="my-vpc" \
--var Environment="dev"

Without --var flags, Boilerplate prompts for values interactively:

Terminal window
boilerplate \
--template-url ./templates/vpc \
--output-folder ./output

Use --non-interactive with a vars file for CI/CD:

Terminal window
boilerplate \
--template-url ./templates/vpc \
--output-folder ./output \
--var-file vars.yml \
--non-interactive

A template directory for a Lambda function:

  • Directorytemplates/
    • Directorylambda/
      • boilerplate.yml
      • main.tf
      • variables.tf
      • outputs.tf

boilerplate.yml:

variables:
- name: FunctionName
type: string
description: Name for the Lambda function
validations:
- required
x-section: Basic Settings
- name: Runtime
type: enum
description: Lambda runtime
options:
- python3.12
- nodejs20.x
default: python3.12
x-section: Basic Settings
- name: MemorySize
type: int
description: Memory in MB (128-10240)
default: 128
x-section: Advanced Settings
- name: EnableLogging
type: bool
description: Create CloudWatch log group?
default: true
x-section: Advanced Settings
- name: Tags
type: map
description: Resource tags
default: {}
x-section: Advanced Settings

main.tf:

resource "aws_lambda_function" "main" {
function_name = "{{ .FunctionName }}"
runtime = "{{ .Runtime }}"
memory_size = {{ .MemorySize }}
handler = "index.handler"
role = aws_iam_role.lambda.arn
tags = {
{{- range $key, $value := .Tags }}
{{ $key }} = "{{ $value }}"
{{- end }}
}
}
{{- if .EnableLogging }}
resource "aws_cloudwatch_log_group" "lambda" {
name = "/aws/lambda/{{ .FunctionName }}"
retention_in_days = 14
}
{{- end }}

Reference a template directory from a runbook with <Template>:

# Deploy a Lambda Function
Configure your Lambda function below:
<Template id="lambda-config" path="templates/lambda" />
After generating, review the files in the file tree on the right.

For a template that doesn’t need its own directory, pair <Inputs> with <TemplateInline>:

<Inputs id="config">
```yaml
variables:
- name: BucketName
type: string
```
</Inputs>
<TemplateInline inputsId="config" outputPath="bucket.tf">
```hcl
resource "aws_s3_bucket" "main" {
bucket = "{{ .BucketName }}"
}
```
</TemplateInline>