Authoring Your Own Blueprints

Create cloud infrastructure templates tailored specifically for your company and teams

Blueprints are templated Terraform files that streamline creating similar configurations. They use tags as placeholders, enhancing reusability and customizability across diverse setups.

During resource creation, the blueprint is rendered into the final Terraform configuration with tags substituted by the values provided by a developer. Resourcely will open a PR containing the ready-to-apply, customized Terraform config.

Quickstart

The blueprint templating syntax consists of variable tags, section blocks, and other metadata.

Variable Tag: A placeholder {{ name }} that gets replaced with its respective value during rendering.

Section Block: A block of content enclosed in opening and closing section tags {{# name }}...{{/ name }}. During rendering, the content is repeated once for each element of the respective value (which must be a list).

Tag Parameters: Arguments to tags are specified using the pipe | character: {{ name | desc: "Name of project" | required: true }}.

Special Variable: {{ __guid }} is a special variable that generates a single unique value during rendering, useful for ensuring unique resource names across multiple renderings.

Frontmatter: The blueprint may start with a YAML frontmatter section, specifying both variable objects (as alternative means of declaring tag parameters) and constant objects (defining internal constant values).

Example

For a practical understanding of the Blueprint templating language, consider the following example that creates an AWS EC2 instance with a corresponding security group with arbitrary ingress and egress rules.

example.tft
---
constants:
  __name: "{{ name }}_{{ __guid }}"
variables:
  instance_type:
    desc: "Type of AWS instance"
    suggest: "t2.micro"
  ingress_rules.cidr_blocks:
    type: list(string)
    suggest: ["0.0.0.0/0"]
  egress_rules.cidr_blocks:
    type: list(string)
    suggest: ["0.0.0.0/0"]
---
resource "aws_instance" "{{ __name }}" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = {{ instance_type | order: 2 }}

  tags = {
    Name = {{ name | type: string | order: 1 }}
  }

  vpc_security_group_ids = [aws_security_group.{{ __name }}_sg.id]
}

resource "aws_security_group" "{{ __name }}_sg" {
  name        = {{ name }}
  description = "Security group for {{ name }}"

  {{# ingress_rules | desc: "List of ingress rules" | order: 3 }}
  ingress {
    from_port   = {{ ingress_rules.port_range.low  | default: 22 }}
    to_port     = {{ ingress_rules.port_range.high | default: 22 }}
    protocol    = {{ ingress_rules.protocol        | default: "tcp" }}
    cidr_blocks = {{ ingress_rules.cidr_blocks }}
  }
  {{/ ingress_rules }}

  {{# egress_rules | desc: "List of egress rules" | order: 4 }}
  egress {
    from_port   = {{ egress_rules.port_range.low  | default: 22 }}
    to_port     = {{ egress_rules.port_range.high | default: 22 }}
    protocol    = {{ egress_rules.protocol        | default: "tcp" }}
    cidr_blocks = {{ egress_rules.cidr_blocks }}
  }
  {{/ egress_rules }}
}

From this blueprint, the Resourcely Portal generates a form that makes it easy for a developer to supply the required values and render the resulting Terraform configuration.

Syntax

Variable Tags

Variable tags are inputs that are replaced by respective values during rendering. The syntax for variable tags is {{ name }}.

resource "aws_s3_bucket" "bucket" {
  bucket = "{{ bucket_name }}"
  acl    = "private"
}

Section Tags

Section tags enclose content that can be repeated during rendering. A {{# name }} tag opens a repeated section and a corresponding {{/ name }} closes it. Section tags can be nested to allow complex structures.

The section tag is given a list of values during rendering. The enclosed content is repeated for each value in the list. Nested tags can reference the current element by using the section name.

Section tags also have a special property called __index that can be used to access the index of the list being referenced. This can be helpful to create unique names inside a section tag.

{{# iam_users }}
resource "aws_iam_user" "user_{{ iam_users.__index }}" {
  name = "{{ iam_users.name }}"
  path = "/system/"
}
{{/ iam_users }}

Another example of section tags for AWS tagging functionality

tags = {
    {{# tags }}
      {{ tags.key }}   = {{ tags.value }}
    {{/ tags }}
  }

Dotted Syntax in Tag Names

Dotted syntax in tag names allows for accessing nested properties of an input object supplied for rendering. When values are passed into the blueprint for rendering, they form a structure that mirrors the structure of the tags in the blueprint.

This becomes even more useful when dealing with repeated sections. For instance, consider a repeated section {{# users }}...{{/ users }} that creates multiple users. Inside this section, you might have tags like {{ users.name }} and {{ users.email }}. Here, the users list will be an array of objects, with each object having properties name and email.

Tag Parameters

Tag parameters are additional metadata for each tag. They can be declared inline inside a tag using the pipe (|) character:

{{ instance_type | desc: "AWS instance type" | required: true }}

They can also be declared in the front matter.

---
variables:
  instance_type:
    desc: "AWS instance type"
    required: true
---
{{ instance_type }}

The following tag parameters are supported. All are optional.

type

Defines the type of the input variable, such as string, number, or boolean. It helps in validating the input and may affect how the input is rendered or processed.

The type parameter is usually automatically inferred, so does not need to be explicitly included. See Type Inference for details.

desc

Description of the variable. This will be shown in the Resourcely Portal to assist the user creating a resource from the blueprint.

. This allows you to create multi-line descriptions with formatting, linking, bullets, headings, and more.

Using Markdown in desc

To use markdown in the desc parameter, add | after desc:

Bold, italics, underlining, and code styling:

desc: |
  This variable defines the **region** where the resource will be created./
  Use regions closest to your users for better performance./
  **Important:** Avoid cross-region dependencies./
  __Example__: `us-central1`, `europe-west1`

Bullets and URL linking

desc: |
  The type of instance to be created.
  - Refer to the [GCP Machine Types Documentation](https://cloud.google.com/compute/docs/machine-types) for more details
  - Second bullet
  - Third bullet

Headings and numbered lists

desc: |
  ## Key Rotation Period
  ### This variable specifies the rotation period for the encryption key.
  
  1. Make sure you're adding a rotation period
  2. The rotation period should be in format Xd where X = number of days

required

A boolean that, when true, specifies that the input variable must be provided. If a required variable is missing, it will prevent the blueprint from being instantiated.

suggest

Specifies an initial value for the input variable. This value will be pre-populated when the create form loads, but the user can change it.

Currently complex suggestions (lists and objects) can only be specified in the frontmatter, not inline. Primitive defaults can be specified inline as well.

order

Controls the order in which input fields appear in the Resourcely Portal form.

group

Specifies a grouping for the input fields in the Resourcely Portal form.

global_value

Constrains the available inputs to only those referenced by a Global Value The value here should be the key of the Global Value.

Alternatively, provide the Global Value options as suggestions in a drop-down, but still allow the user to type in a different value. Use the syntax suggestions(key) for this behavior.

advanced

This will move this particular input to an "Advanced" section at the bottom of the Resourcely Portal Create form. This is usually used for optional inputs that require additional context only a power user will have.

links_to

Specifies that this input can link to resource properties of the given type. See 'Links' section for more detail.

prefer_mode

Specifies the prefer mode for an input in the create form, the supported mode is env.

{{ instance_type | prefer_mode: "env" }}

Type Inference

The type of most tags is inferred automatically from their context within the Terraform config. If a tag is used as the value for a Terraform attribute, Resourcely will use the expected type of that attribute. For instance, if a tag is used as the value for the bucket attribute of an aws_s3_bucket resource, the tags will be automatically inferred from the type of aws_s3_bucket.bucket.

However, there may be instances where automatic type inference is not possible or sufficient. In these cases, the type parameter can be explicitly specified. This is often necessary when the tag is used as an input to a Terraform module, as modules do not have a fixed structure that the templating engine can infer types from.

For instance, a tag with an explicit type parameter may look like

{{ module_input | type: string | desc: "Input for Terraform module" }}

Here, the type: string specification ensures that module_input will be processed as a string, even though its type cannot be inferred from the content.

Type Syntax

The type syntax is an extension of the Terraform type constraint syntax. In addition to the types supported by Terraform, you can reference the type of resource property directly.

{{ foo | type: resource.aws_s3_bucket_versioniong.versioning_configuration.status }}

or

{{ foo | type: data.aws_ami.filter }}

This makes it possible to easily give exact types for module inputs without having to duplicate the entire typing of the underlying resource properties.

These "reference" types can be nested inside of list or object types, just like any other type.

In Terraform, it is common for resources to reference other resources by name or id. This DRYs out the Terraform config, and (much more importantly) teaches Terraform how to create and destroy resources in the correct order.

In most cases, Resourcely will infer whether each input supports links, and if so, which resource properties it can link to. You can augment the inference for a given input by adding a links_to tag parameter. links_to must be followed by the name of a resource property or a complex type whose leaves are resource properties (see 'Advanced Linking,' below). Resourcely parses this value to determine the resource type and property path.

In the following example, {{ kms_key_id }} can link to the key_id property on any aws_kms_key resource. The tag can also link to data sources that match the given resource type and property.

{{ kms_key_id | links_to: resource.aws_kms_key.key_id }}

The create form user will see a list of resources (or data sources) of the given type that have the given property. Using the example above, this looks like:

The list includes resources that are currently being created, as well as already-checked-in resources. Currently, Resourcely doesn't support linking to repeating (count, for_each) resources, optional properties, or repeating properties (list/map items, HCL blocks). If one of these is important to you, let us know!

Just like how Resourcely can infer type, it can infer links_to in many cases. Here are our inference rules, with examples:

When you use a {{ tag }} as the value of a resource property that "usually" links to other resources, Resourcely will infer links_to . "Usually" reflects how Resourcely's knowledge of linkability is constantly evolving in response to feedback. If you think you found a missing linkability inference, let us know!

resource "aws_db_instance" "example" {
  // Resourcely knows that resource.aws_db_instance.kms_key_id usually links to
  // resource.aws_kms_key.id.
  kms_key_id = {{ kms_key_id }}
}

When you use a resource property as the type of a tag, and that property usually links to other resources, Resourcely will infer links_to.

module "my_module" {
  source = "./some/random/source"
  
  // Resourcely doesn't know about unknown_prop, but can infer links_to from the
  // explicitly specified 'type'
  unknown_prop = {{ kms_key_id | type: resource.aws_db_instance.kms_key_id }}
}

You can always explicitly specify links_to. Explicitly specified values augment any inferred values.

module "my_module" {
  source = "./some/random/source"
  
  unknown_prop = {{ kms_key_id | type: string | links_to: "resource.aws_kms_key.id" }}
}

Advanced Linking

There are three advanced topics associated with links_to:

  • Complex types

  • Linking to multiple types

  • Structural matching and type coercion

Complex types

When type is a complex type such as list(string), the structure of links_to must match the structure of type. You can always make a valid links_to parameter by copying the type parameter and replacing all occurrences of string, number, and bool with some resource property, like resource.aws_kms_key.key_id.

In the next example, {{ kms_key_ids }} is a list of strings, and each item can link to the key_id property on any aws_kms_key resource.

{{ kms_key_ids | type: list(string) | links_to: list(resource.aws_kms_key.key_id) }}

Linking to multiple types

Certain Terraform resources support polymorphic links, such as aws_lambda_function.dead_letter_config.target_arn. This property, for example, can contain the arn of an SNS topic or an SQS queue.

You can specify multiple types in links_to by separating them with |. Note that you must enclose the links_to value in quotes when | is present.

{{ dlq_arn | links_to: "resource.aws_sns_topic.arn|resource.aws_sqs_queue.arn" }}

If you to link to a resource but within a string with prefix and suffix you can do the following

output_data_config {
    s3_uri = "s3://{{ output_s3_uri | links_to: resource.aws_s3_bucket.id }}/data/"
}

Structural matching and type coercion

Terraform supports type coercion between strings, numbers, and booleans. Because of this, Resourcely checks for a structural match between type and links_to but does not otherwise check for type alignment. This is best explained by example.

// This is legal - Terraform will convert the number to a string
{{ tag | type: string | links_to: resource.example.number_prop }}

// This is also legal! For all Resourcely knows, you only use valid numbers as the
// values of example.string_prop
{{ tag | type: number | links_to: resource.example.string_prop }}

// This is legal - the object on the left matches the object on the right.
{{ tag | type: object({a=string}) | links_to: object({a=resource.example.string_prop}) }}

// This is illegal - the list on the left does not match the primitive on the right.
{{ tag | type: list(string) | links_to: resource.example.string_prop }}

// This is also illegal - the list on the left does not match the map on the right
{{ tag | type: list(string) | links_to: map(resource.example.string_prop) }}

Module input defaults

If your blueprint invokes a module, you can specify each module input's default value in the blueprint. Resourcely will omit that property during rendering if the current value is equal to the default value. This can lead to substantially shorter module blocks, especially for modules with a large number of optional properties.

You can specify both default and suggest on one variable. Resourcely will initialize the create form using the suggest value and omit the property if the user enters the default value. This is useful with 3rd-party modules: if you don't like their default, Resourcely can suggest your preferred default.

If only default is specified, Resourcely will automatically suggest the default value. This lets the create form user see what the value will be, even though it won't appear in the rendered Terraform.

---
variables:
  ingress_rules:
    type: list(string)
    default: ["10.0.0.0/8"]
  egress_rules:
    type: list(string)
    default: []
    suggest: ["10.0.0.0/8"]
---
module "example" {
  source = "example-source"
  
  // The create form will be pre-populated with one ingress rule: "10.0.0.0/8".
  // If the user leaves it as-is, Resourcely will omit the ingress_rules property.
  ingress_rules = {{ ingress_rules }}
  
  // Here, default != suggest. The create form will be pre-populated with one egress
  // rule: "10.0.0.0/8". If the user removes it, Resourcely will omit the property.
  egress_rules = {{ egress_rules }}
}

Constants and Special Variables

Variables beginning with double underscore (__) are not exposed in the Resourcely Portal form, so are considered to be internal values.

Constants can be defined in the constants section of the frontmatter. This is useful for avoiding repetition in your template.

There are two special variables whose values are automatically supplied during rendering.

__guid

__guid is a special constant that gets a unique value during rendering. This is useful for generating unique resource names across multiple renderings of the same blueprint.

__index

Inside of a repeated section, <name>.__index is a special variable that points to the current element's index in the <name> list.

Conventions

Common convention is to define a __name constant that incorporates the __guid variable. Use {{ __name }} as part of each Terraform block local name (resource, data source, module, etc.) to ensure that

  1. resources created together share a name, for easy identification.

  2. resource names will not conflict when the Blueprint is instantiated multiple times.

Front matter

The blueprint can include a YAML frontmatter section to define variables and constants.

Variables

The variables object is an alternative way to specify the various tag parameters for the corresponding tag:

variables:
   foo:
     desc: "my foo value"
     required: false
     group: Network
   bar:
     default: true

Groups

The groups object defines metadata for groups.

groups:
  Network:
    order: 2
    desc: "Configure the network for the compute instances"
  General:
    order: 1

Constants

The constants object defines variables with constant values that can be referenced in the blueprint. These constant values can themselves be simple templated strings.

---
constants:
   __name: "{{ name }}_{{ __guid }}"
---
resource "aws_s3_bucket" "{{ __name }}" {
   bucket = {{ bucket }}
}

Terraform Style Config

The terraform_style_config object is used to customize how the blueprint's Terraform will be rendered in a PR.

Only a single property is supported today:

  • block_names.allow_hyphens Controls whether hyphens - should be allowed in block names. Terraform only allows certain characters (including hyphens -) in block names. If the value of a {{ tag }} used in a block name contains illegal characters, Resourcely will replace them with underscores _ when rendering the blueprint. Many users prefer to standardize on underscores _ , not hyphens -, in block names. If this setting is false, hyphens - will be treated as illegal in block names and will be replaced by underscores _. Defaults to true.

Example:

The blueprint

---
terraform_style_config:
  block_names:
    allow_hyphens: false
---
module "{{ foo }}" {
   name = {{ foo }}
}

with inputs

foo = "a-value-with-hyphens-and$special$chars"

will render as

module "a_value_with_hyphens_and_special_chars" {
   name = "a-value-with-hyphens-and$special$chars"
}

Notice that the name property is still rendered verbatim — it includes the hyphens and special chars. Only the module block name is transformed to comply with Terraform's block name rules.

Groups

Top-level tags can be grouped using the group tag param. If a tag is in a group, its order param specifies its order within its group. Any tags without an explicit group are placed in the default General group.

Groups support their own metadata params that can be set in the groups frontmatter object.

groups:
  <group>:
    desc: ...
    order: ...

desc

Description of the variable. This will be shown in the Resourcely Portal to assist the user creating a resource from the blueprint.

order

Controls the order in which the groups appear in the Resourcely Portal form.

Global Values

Admins can create globally defined values that you can then use to restrict inputs to a Blueprint.

You can reference Global Values in Blueprints through either the Frontmatter definition, or inline.

The following example shows both cases. The dept tag parameter references the Global Values of department in the frontmatter, and the ami tag parameter references the Global Values of approved_amis inline.

---
variables:
  dept:
    global_value: department
---
resource "aws_instance" "instance" {
  ami = {{ ami | type: resource.aws_instance.instance.ami | global_value: approved_amis }}
  display_name = "{{ dept }}"
  // ...resource properties
}

Global Values can also be complex types (lists/structs). Since each Global Value has a schema for these complex types, you can dereference these types directly in the Blueprint.

In the following example, os references the global value os_family, which is a struct with keys ami and os_type.

---
variables:
  os: 
    global_value: os_family
    default: ubuntu
  dept:
    global_value: department
---
resource "aws_instance" "instance" {
  ami = {{ os.ami }}
  display_name = "{{ dept }}-{{ os.os_type }}"
  // ...more resource properties
}

resource "aws_licensemanager_association" "example" {
  count = {{ os.os_type }} == "windows" ? 1 : 0
  // ...windows licensing config
}

Global Value Group By

By default, the global value options are presented to the user in a selection dropdown. When there are hundreds of options, it may be difficult for the user to find the desired one.

group_by(...) only works for global values with OBJECT type.

Use the group_by modifier to specify a set of properties on each option object that can be used to filter the options available in the selection dropdown:

{{ variable | global_value, group_by(global_value_key, [propA, propB])

In the following example, the {{ ami }} variable uses the amis global value, filtered by distribution and virt_type.

Assume the amis global value has the following options:

amis global value options
key: amis
options:
  - key: ubuntu20_04_hvm
    value:
      distribution: ubuntu
      name: "ubuntu/images/hvm-ssd/ubuntu-*-20.04-amd64-server-*"
      virt_type: "hvm"
      owner: "099720109477"
  - key: ubuntu22_04_hvm
    value:
      distribution: ubuntu
      name: "ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"
      virt_type: "hvm"
      owner: "099720109477"
  - key: ubuntu20_04_pv
    value:
      distribution: ubuntu
      name: "ubuntu/images/hvm-ssd/ubuntu-*-20.04-amd64-server-*"
      virt_type: "pv"
      owner: "099720109477"
  - key: ubuntu22_04_pv
    value:
      distribution: ubuntu
      name: "ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"
      virt_type: "pv"
      owner: "099720109477"
  - key: debian11_hvm
    value:
      distribution: debian
      name: "debian-11-amd64-*"
      virt_type: "hvm"
      owner: "136693071363"
  - key:debian12_hvm
    value:
      distribution: debian
      name: "debian-12-amd64-*"
      virt_type: "hvm"
      owner: "136693071363"
  - key: debian11_pv
    value:
      distribution: debian
      name: "debian-11-amd64-*"
      virt_type: "pv"
      owner: "136693071363"
  - key: debian12_pv
    value:
      distribution: debian
      name: "debian-12-amd64-*"
      virt_type: "pv"
      owner: "136693071363"

The following blueprint uses the group_by modifier to display the distribution and virt_type filter dropdowns.

---
variables:
  ami:
    global_value: group_by(amis, [distribution, virt_type])
---
data "aws_ami" "{{ __guid }}" {
  most_recent = true
  
  filter {
        name   = "name"
        values = [{{ ami.name }}]
      }
      filter {
        name   = "virtualization-type"
        values = [{{ ami.virt_type }}]
      }

   owners = [{{ ami.owner }}]
}

resource "aws_instance" "{{ __guid }}" {
  ami = data.aws_ami.{{ __guid }}.id
}

Global Value Suggestions

Instead of restricting the inputs of a blueprint, global values can also be used to suggest possible values for the inputs. The values will be shown in a dropdown, but the user can also type in a different value.

suggestions(...) only works for string inputs.

To use a global value as a source of suggestions, wrap its key in the suggestions(...) modifier:

{{ variable | global_value: suggestions(global_value_key) }}

In the following example, the {{ name }} uses the departments global_value to suggest some values.

---
variables:
  name:
    global_value: suggestions(departments)
---
resource "aws_s3_bucket" "department_files" {
   bucket = "acme-{{ name }}"
}

Comments

Resourcely preserves most comments from the blueprint through to the rendered Terraform. In general, we preserve:

  • Any // whole-line comment (including # these and /* these */)

  • /* inline */ comments found within HCL function calls.

If we aren't preserving a comment that's important to you, please contact us.

Resourcely also parses special // resourcely:<key> comments. These contain metadata that is useful for Resourcely, but never useful for Terraform. Unless noted below, these comments will not be preserved in the rendered Terraform.

File placement comments

What do they do? These comments let your blueprint place top-level HCL resources in specific files when creating the pull request. Include these in your blueprint if your organization has per file naming conventions (e.g. storage.tf vs instance.tf).

File placement comments will override the default_file specified in resourcely.yaml.

Where do they go? Before any top-level HCL block (e.g. resource or data).

Example: // resourcely:file <filename.tf>

you can embed tags directly into file placement comments by incorporating them within the filename e.g // resourcely:file ec2-{{ name }}.tf

Module output comments

What do they do? These comments teach Resourcely about the presence (and type) of module outputs. When you create a blueprint by importing a module, Resourcely automatically generates these comments.

Where do they go? As whole-line comments within the module invocation block.

Example: // resourcely:output instance_arns list(aws_instance.arn)

Last updated