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:
    suggest: ["0.0.0.0/0"]
  egress_rules.cidr_blocks:
    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 }}

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.

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.

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.

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.

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

The create form user will see a list of resources 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!

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" }}

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 foo.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:
    default: ["10.0.0.0/8"]
  egress_rules:
    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 }}
}

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 preset os_family, which is a struct with keys ami and os_type.

---
variables:
  os: 
    preset: os_family
    default: ubuntu
  dept:
    preset: 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
}

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>

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