Skip to content

Latest commit

 

History

History
233 lines (193 loc) · 10.4 KB

template_dev.md

File metadata and controls

233 lines (193 loc) · 10.4 KB

Terraform Engine Template Development Guide

This document provides some tips and guidances for Terraform Engine template developers.

Config and Syntax

  1. Terraform Engine uses go templating under the hood.

  2. Use $ to escape $ in the Terraform Engine templates. (example)

  3. Use dot (.) to obtain mandatory map fields, e.g. {{.name}}.

  4. Use get function to obtain optional map fields, e.g. {{get . "exists" false}}. The last argument is the default value to return when the field cannot be found in the map.

  5. Use range function to iterate over a list, e.g.

    {{range $_, $env := .envs -}}
    template "env" {
      recipe_path = "./env.hcl"
      output_path = "./{{$env}}"
      data = {
        env = "{{$env}}"
      }
    }
    {{end}}
  6. Check out more available default functions here and custom functions here.

  7. When Terraform Engine generates Terraform configs from templates, if the resource_name field is not specified for a resource (example), then the underlying Terraform module or Terraform resource in the generated Terraform configs will be automatically named from the resource's unique identifier (bucket's name, network's name, group's id, etc) (example). All non-alphanumeric characters will be converted to _, such as -, ., and @.

  8. Members in iam_members component do not support referencing calculated values/attributes from other resources due to an underlying module limitation. A common example is referencing the email of a service account created in the same deployment in an iam_members resource. To work around this limitation, referencing the service account via $${google_service_account.myserviceaccount.account_id}@myproject.iam.gserviceaccount.com instead of $${google_service_account.myserviceaccount.email} as the account_id is not a calculated value. (example)

  9. When writing raw Terraform configs, use for_each to group similar resources.

  10. For large block of raw Terraform configs, instead of using the terraform_addons.raw_config block with in a template block, consider using a separate template block to generate a seperate .tf file from a template file, e.g. Do not name the .tf file main.tf as it will conflict with the default main.tf file generated by the Terraform Engine.

    template "terraform_deployment" {
      component_path = "./foo.tf.tmpl"
      output_path = "./foo.tf"
      data = {
        ...
      }
    }
  11. data maps can be specified either inside or outside a template block.

    In both cases, values in the data maps from an upper level template are passed down to its child template and made available. There is no need to repeat data values in child templates unless you would like to override them. For example, all data values specified in the top level template here are passed down and made available to its child template root.hcl as well as transitive child templates foundation.hcl and team.hcl.

    However, in the two cases, the data maps' value overriding precedence is different, which follows the 3 rules below:

    1. Values specified in the data maps inside the template block take higher precedence over values spcified in the data maps outside the template block.
    2. For data maps inside the template block, values specified in child templates take higher precedence over values specified in parent templates.
    3. For data maps outside the template block, values specified in parent templates take higher precedence over values specified in child templates.

    Consider the following example scenario:

    # main.hcl
    data = {
      bigquery_location = "A"
    }
    
    template "root" {
      recipe_path = "./modules/root.hcl"
      data = {
        bigquery_location = "B"
      }
    }
    # modules/root.hcl
    data = {
      bigquery_location = "C"
    }
    
    # The actual template that consumes bigquery_location.
    template "bigquery" {
      recipe_path = "./bigquery.hcl"
      data = {
        bigquery_location   = "D"
      }
    }
    # modules/bigquery.hcl
    resource "google_bigquery_dataset" "dataset" {
      dataset_id                  = "example_dataset"
      location                    = "{{.bigquery_location}}"
    }

    The 4 locations specified will have the following precedence, from high to low: D > B > A > C. And the final value for bigquery_location will be D. To explain in detail:

    1. From rule #1, B and D take higher precedence over A and C. So (B, D) > (A, C).
    2. From rule #2, D takes higher precedence over B. So D > B > (A, C).
    3. From rule #3, A takes higher precedence over C. So D > B > A > C.
  12. Custom schemas with additional variable pattern restrictions can be added to templates to perform custom validation.

Deployment and Resource Dependencies

  1. Deployment order of subfolders in the output is defined by the managed_dirs list in the cicd template (example). CICD jobs will iterate over the list and do plan or apply according to the type of Cloud Build trigger.

    Note that the tf-plan job could fail if one subfolder requires another subfolder to be fully deployed first. For example, deployment of subfolder B has a data dependency on the deployment of subfolder A, and in this case, terraform plan in the subfolder B will fail until subfolder A is fully deployed.

  2. Resource dependency is done by using Terraform's implicit dependency mechanism.

    Note that Terraform Engine automatically converts all non-alphanumeric characters to _ when naming the underlying Terraform modules or Terraform resources, so make sure to do that conversion when referencing them. In the example below, the service account's ID is compute-runner, but the underlying Terraform resource will be named as compute_runner. So when referencing this service account in an iam_member resource, use google_service_account.compute_runner with the _.

    template "example" {
      ...
      data = {
        resources = {
          service_accounts = [{
            account_id   = "compute-runner"
          }]
          iam_members = {
            "roles/storage.objectViewer" = [
              "serviceAccount:$${google_service_account.compute_runner.account_id}@my-project.iam.gserviceaccount.com",
            ]
          }
        }
      }
    }

Formatting

  1. terraform fmt is run by default as part of the tfengine command execution.

  2. There is no good formatting tools for .hcl files. You can use use the hclfmt command from terragrunt tool but it does not always work.

  3. {{- ...}} and {{... -}} can be used to remove empty lines before or after the current block in the generated configs.

Terraform Engine Execution

  1. When the Terraform Engine template is invalid and the terraform fmt step run as part of the tfengine command will fail, so the output won't get copied over to the output directory. To help debug, pass --format=false to the tfengine command and then see the broken file, fix it and then re-run the tfengine command with --format=true.

  2. By default, if you generated a file and then updated your Terraform Engine template to remove the file, tfengine won't remove it from the output directory. To delete those unmanaged files, use --delete_unmanaged_files in the tfengine command.