Skip to content

[RFC] Terramate Scaffolding #2052

@soerenmartius

Description

@soerenmartius

Introduction

To enable non-expert users to provision complex cloud infrastructure with Infrastructure as Code in self-service, this proposal introduces a layer of abstraction and components called Terramate Scaffolding.

Motivation

We asked ourselves how to achieve a system where everybody could deploy cloud services.

Deploying cloud infrastructure is a complex and complicated task with many responsibilities and a deep understanding of the underlying cloud requirements. Non-expert users want to have a database and not learn the cloud. Nor do they want to learn the technologies used to provision cloud infrastructure.

Infrastructure as Code (IaC) makes it reliable and repeatable to deploy cloud services at scale (replacing manual approaches known as ClickOps). Using Infrastructure as Code tooling like Terraform or OpenTofu (TF) requires everybody to learn the tooling and deeply understand the configuration to be applied.

We want to introduce Terramate Scaffolding to address such issues, reduce complexity, reduce cognitive load and risk, and enable everyone to provide Cloud Services in a self-service approach using curated templates or blueprints provided by experts.

Terramate Scaffolding will not require the user to understand the technologies behind the simple requests that they actually have: "My application required a database and a place to run." Enable your developers to build applications, not design cloud infrastructure.

  • Platform teams (expert users) provide a simplified, golden path for development teams while preserving full control.
  • Developers (non-expert users) use those golden paths to scaffold new infrastructure in self-service.

While the long-term solution we have in mind is meant to be IaC agnostic, the following sections will focus on TF (Terraform and OpenTofu) as the IaC engine of choice.

Many DevOps and Platform teams use abstraction layers such as Terraform Modules, which are a way of bundling related infrastructure components into reusable building blocks with pre-configured best practices aligned with individual organizations' compliance requirements. Still, using TF Modules requires developers to know how to:

  • Manage the lifecycle of infrastructure
  • Write and maintain TF Code using HCL
  • Configure cloud infrastructure on AWS, Google Cloud and Microsoft Azure in a secure and sane manner
  • Understand where to manage IaC within a repository
  • Understand how to structure and manage IaC State

Such knowledge is typically only obtained by expert users, meaning most organizations' developers cannot deploy and manage infrastructure self-service using IaC.

Many of our customers have expressed a desire for a feature that allows Platform teams to provide non-expert users with an abstraction layer. This would enable them to scaffold IaC templates without dealing with the complexity mentioned above.

While Terramate CLI provides features to generate code from templates and orchestrate stacks, it currently lacks the capability to generate complete stacks that deploy the desired complex infrastructure while providing a simple interface and not requiring the user to know any details. This gap will now be closed by introducing Terramate Scaffolding, which uses Terramate Services and Terramate Bundles provided by experts and usable by non-expert users but is flexible enough to be extended when needed.

Proposal

Terramate Scaffolding will introduce new abstraction layers to provide the desired goal.

Abstraction Layers

  • Terramate Services provide a layer to the actual configuration of the IaC Tooling such as Terraform or OpenTofu Code (Resources, Modules, Data Sources, etc), Terramate Code Generation, or any combination.
  • Terramate Bundles provide a layer for provisioning Terramate Services into Stacks and defining the location within a code repository.

Using Terramate Services and Bundles will still result in the IaC Code being generated, which can then be automatically deployed or follow any review and deployment workflows implemented within the teams.

In order to support Terramate Scaffolding, additional services will be provided to improve the experience:

  • Terramate Config Store is a centralized service in Terramate Cloud used by teams to share configuration values among multiple repositories and stacks. The values can be scoped and maintained within the IaC lifecycle (with the upcoming Terramate provider), enabling authors of bundles and services to provide dynamic and actual presets when instantiating a bundle.
  • Terramate Service and Bundle Registry is a centralized service in Terramate Cloud used by teams to publish and share Terramate Bundles and Terramate Services.
  • Catalog is a centralized service in Terramate Cloud that provides a selective view of registry content (subset) for use to the actual teams. It is used to include specific versions of IaC that are used by different teams in the lifecycle. It supports authors in maintaining the lifecycle by allowing for easy deprecation, usage overviews and rolling out updates on a team-by-team basis.

The following section details the specific components and their UX. Further RFCs will be provided to define all supporting services in detail.

Goal Requirements

  • Developers can scaffold new infrastructure with a single command, e.g., terramate scaffold create [BUNDLE] or via a UI being asked for minimal configuration details, abstracting away all complexity and providing compliance out of the box.
  • Code is generated in the background and checked into a git repository for auditability.
  • Deployments are done via GitOps using Pull Requests flows. PRs should optionally be automatically approved without requiring reviews.
  • Platform teams can easily transform existing infrastructure into bundles, stacks and services.
  • Platform teams can easily import and reuse existing abstraction layers, such as Terraform modules.
  • A migration is not required as scaffolding can live side-by-side with legacy Terramate or legacy Terraform/OpenTofu/Terragrunt configuration.

Design Decisions

Providing scaffolding capabilities in your organization will follow Terramate principles such as:

  • Using Scaffolding will not require any migration effort (living side-by-side with legacy setups)
  • Using Scaffolding provides full flexibility (dropping any additional code at any time)
  • Scaffolding will always result in actual runnable and native IaC configurations (easy debugging and local executions)
  • Scaffolding will support any IaC Tooling as an engine (Terraform, OpenTofu, Terragrunt, etc.)
  • Scaffolding will seamlessly integrate with existing GitOps workflows and run in the CI/CD of your choice
  • Scaffolding will seamlessly integrate with Terramate Cloud to enhance the end-to-end experience

Bundles

Bundles are collections of Terramate Stacks and Terramate Services within those stacks. They allow users to create multiple stacks simultaneously using a simple UI that hides the complexity.

How to use Bundles

Scaffold from bundle

# run `terramate scaffold create [BUNDLE]`

> terramate scaffold create

> Please choose a bundle to scaffold
  - terramate-aws-s3-bucket - Creates a SOC2 compliant S3 bucket
  - terramate-aws-rds       - Creates a RDS Database
  - ...
  < "terramate-aws-s3-bucket"
  
> Please choose an environment to deploy this bucket in
  - [dev] Development
  - [stg] Staging (Pre-Production)
  - [prd] Production
  < "dev"
  
> Please choose a name for the bucket
  < "example-bucket"

Results

# creates (after asking for inputs)
# a file in the specified bundle.path location
# -> "/stacks/dev/s3/example-bucket/bundle.tm.hcl" in the example case
bundle {
  id  = "terramate-aws-s3-bucket"
  ref = "v1"
    
  inputs {
     name        = "example-bucket"
     environment = "dev"

     # This can be overwritten manually by changing the file
     # It was not prompted for instantiation to keep the end user from
     # making a bad decision
     force_destroy = false
  }
}

# On `terramate generate` creates 
# - all related stacks, services, code etc.
# On change, everything gets reconfigured as defined in the bundle.

How to define Bundles

bundle.tm.hcl

# file: /path/to/bundles/terramate-aws-s3-bucket/bundle.tm.hcl

bundle {
  # when calling a bundle reference it by this id
  id  = "terramate-aws-s3-bucket"

  # (used when prompting for a bundle to use)
  description = "Creates a SOC2 compliant S3 bucket"
  
  # the path used to instantiate the bundle in when using `terramate scaffold create`
  path = "/stacks/${bundle.input.environment}/s3/${bundle.input.name}/bundle.tm.hcl"

  # all stacks will get those labels on generation  
  labels = {
    environment = terramate.bundle.input.environment
  }
}

#
# input "variable" {
#   type           = tbd
#   description    = "short description"
#   nullok         = true/false
#   available      = true/false (can be used as input)
#           example: terramate.component.input.name == "something"
#   prompt         = "gimme a value for xyz" # use this as prompt instead of name
#   # prompt         = null # do not prompt needs to provide a default or allow null
#   default        = tbd # (can use terramate.repository|organization.variable)
#   allowed_values = tbd
#   validation {}  # tdb
# }
#

input "environment" {
  type           = string
  # description    = "The environment to deploy the S3 Bucket in" 
  prompt         = "Please chose an environment to deploy this bucket in"

  # As type is string we will prompt user to the values and 
  # provide the related key in terramate.bundle.input.environment
  allowed_values = {
    dev = "Development"
    stg = "Staging (Pre Production)"
    prd = "Production"
  }
}

input "name" {
  type        = string
  description = "The name of the S3 Bucket"
  prompt      = "Please chose a name for the bucket" 
}

input "force_destroy" {
  type        = boolean
  description = "A boolean that indicates all objects should be deleted from the bucket 
so that the bucket can be destroyed without error. These objects are not
 recoverable."
  default = false
  # allow to overwrite manually but do not prompt the user for it - use default instead.
  prompt  = null
}

stack.tm.hcl

# file: /path/to/bundles/terramate-aws-s3-bucket/stack.tm.hcl
#
# Bundles can optionally configure multiple stacks at once: 
# - file: /path/to/bundles/terramate-aws-s3-bucket/stack-a/stack.tm.hcl
# - file: /path/to/bundles/terramate-aws-s3-bucket/stack-b/stack.tm.hcl

stack "bucket" {  
  name           = "s3-bucket(${terramate.bundle.input.name})[${terramate.bundle.input.environment}]"  
  description = "The ${terramate.bundle.input.name} AWS S3 Bucket (SOC2 compliant) in environment ${terramate.bundle.input.environment}"
  labels = {
    service          = "S3"
    environment = terramate.bundle.input.environment
  }

  # The concept of sharing stacks within a bundle is out of scope of this document and will be provided in an additional RFC

  # mode defines the way to handle multiple bundle instantiations that all define the same stack
  # available modes
  # - conflicting => instantiating the bundle will fail when the same stack is created twice
  # - shared      => instantiating the bundle will only create the stack when it is not already existing
  #                  destroying the bundle will only trigger a destruction of the stack when no other bundle is using it
  #                  removing the bundle will only remove the stack when no other bundle is using it
  # example use case: define a load balancer to be used between multiple instantiations of the same bundle
  mode = conflicting # default
  
  # overwrite the filesystem path of the stack
  # - using path allows the use variables within the path
  # - nested stacks will be nested under the resulting path too
  path = "." # default honor the filesystem
}

service.tm.hcl

# file: /path/to/bundles/terramate-aws-s3-bucket/service_s3.tm.hcl
#
service "s3" {  
  id  = "terramate-aws-s3-bucket-soc2"  
  ref = "v1"
  
  inputs {
    # The end-user can select a name and has limited control over other values.
    name = terramate.bundle.input.name
   
    # In the bundle we enforce control of the object ownership
    control_object_ownership = true
    object_ownership         = "ObjectWriter"
  
    force_destroy = terramate.bundle.input.force_destroy
    
    # ACL and versioning are enforced within the service we use already 
    # and are not available as inputs here for the end-user.
  }
}

Populate bundle to the Terramate Cloud Registry

> terramate bundle push
  terramate-aws-s3-bucket v1 is now available in your Terramate Cloud Organization
  https://cloud.terramate.io/o/terramate/registry/bundles/terramate-aws-s3-bucket/v1

Service

A collection of Terramate Code Generation Configuration or plain IaC to create Terraform or a-like configurations. The purpose of Services is to provide reusable configurations that can be used among one or multiple TM Bundles and Terramate Stacks.

How to use the Service

# example from above within the bundle

# Services can also be instantiated within existing Terramate Stacks (out of the scope of this document)
# Services can be scaffolded from TF Services to support maintainers converting TF Modules to Terramate Modules (out of scope of this document)

# file: /path/to/bundles/terramate-aws-s3-bucket/service_s3.tm.hcl
#
service "s3" {  
  id  = "terramate-aws-s3-bucket-soc2"  
  ref = "v1"
  
  inputs {
    # the end-user can select a name and has limit control over other values.
    name = terramate.bundle.input.name
   
    # in the bundle we enforce control of the object ownership
    control_object_ownership = true
    object_ownership         = "ObjectWriter"
    
    # acl and versioning are already enforced within the service we use
    # and are not available as inputs here for the end-user.
  }
}

How to configure Services

# file: /path/to/services/terramate-aws-s3-bucket-soc2/service.tm.hcl

service {
  id                 = "terramate-aws-s3-bucket-soc2"
  description = "Create a SOC-2 compliant S3 bucket"
}

input "name" {
  type             = string
  description  = "The name of the S3 Bucket"
  prompt         = "Please chose a name for the bucket" 
}

input "control_object_ownership" {
  type            = boolean
  description = "Whether to manage S3 Bucket Ownership Controls on this bucket."
  prompt        = "Do you want to manage S3 Bucket Ownership?"
  default         = false
} 

input "force_destroy" {
  type             = boolean
  description = "A boolean that indicates all objects should be deleted from the bucket 
so that the bucket can be destroyed without error. These objects are not
 recoverable."
  default = false
  # allow to overwrite manually but do not prompt the user for it - use default instead.
  prompt  = null
}

input "object_onwership" {
  type            = string
  description = "Object ownership"
  prompt        = "What Type of Ownership Control should the bucket provide?"
  default         = "BucketOwnerEnforced"
  allowed_values = {
    BucketOwnerEnforced  = "ACLs are disabled, and the bucket owner automatically owns and has full control over every object in the bucket."
    BucketOwnerPreferred = "Objects uploaded to the bucket change ownership to the bucket owner if the objects are uploaded with the bucket-owner-full-control canned ACL."
    ObjectWriter         = "The uploading account will own the object if the object is uploaded with the bucket-owner-full-control canned ACL."
  ]
  # only prompt for this value when we have object ownership enabled
  available = terramate.service.input.control_object_ownership == true # yes, we know! ;)  '== true' is not required ;)
}

# can later be used to share outputs between services defined within a stack.
output "bucket_arn" {
  value = module.bucket.bucket_arn
}
# file: /path/to/services/terramate-aws-s3-bucket-soc2/main.tf.tmgen

# using tmgen for simplicity, 
# generate_hcl, generate_file and all terramate features can be used too

service "bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "4.4.0"

  bucket = terramare.service.input.name

  # enforce SOC-2 settings
  acl = "private"
  versioning = {
    enabled = true
  }

  # allowed inputs (subset of available inputs to keep the example simple)

  force_destroy = terramare.service.input.force_destroy

  control_object_ownership = terramare.service.input.control_object_ownership
  object_ownership         = terramare.service.input.object_ownership
}

Appendix A: The Resulting Generated Code

From the example, once terramate generate is run after running the terramate scaffold, the following code will be generated:

# file: "/stacks/dev/s3/example-bucket/bundle.tm.hcl"

# GENERATED BY RUNNING THE `terramate scaffold create`

bundle {
  id  = "terramate-aws-s3-bucket"
  ref = "v1"
    
  inputs {
     name        = "example-bucket"
     environment = "dev"

     # This can be overwritten manually by changing the file
     # It was not prompted for instantiation to keep the end-user from
     # making a bad decision ;)
     force_destroy = false
  }
}

# On `terramate generate` creates 
# - all related stacks, services, code etc.
# On change every things gets reconfigured as defined in the bundle.
# file: "/stacks/dev/s3/example-bucket/stack.tm.hcl"

# INITIALLY GENERATED BY RUNNING THE `terramate scaffold create`
# UPDATED BY RUNNING `terramate generate`

stack {
  # The ID is generated when bundle code gets generated
  id          = "4a902baa-12ef-44f7-8db6-b811d52fb454"

  # all (input) variables are replaced by the actual values
  name        = "s3-bucket(example-bucket)[dev]"  
  description = "The example-bucket AWS S3 Bucket (SOC2 compliant) in environment dev"
  labels = {
    service     = "S3"
    environment = "dev"
  }
}
# file: "/stacks/dev/s3/example-bucket/s3_main.tf"

# INITIALLY GENERATED BY RUNNING THE `terramate scaffold create`
# UPDATED BY RUNNING `terramate generate`

service "bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "4.4.0"

  bucket = "example-bucket"

  acl = "private"
  versioning = {
    enabled = true
  }

  force_destroy = false
  
  control_object_ownership = true
  ownership                            = "ObjectWriter"
}

Appendix B: FAQ

What IaC Technologies are supported?

We will limit the initial support to the TF (Terraform/OpenTofu) Universe but plan to support other IaC Technologies in the future, which will be prioritized by demand.

Can I share data between Services within a stack?

Yes, data sharing can be easily done between TM Services within a TM Stack. Terramate will replace the actual references where needed.

Service outputs can be defined as output {} blocks within TM Services and referenced by other Services within the same stack via terramate.stack.service.{label}.{ouput} e.g. terramate.stack.service.s3.bucket_arn in the examples above.

Note: We will publish additions to the scaffolding soon. For the sake of simplicity, we kept the initial RFC small.

Can I share data between stacks within a bundle?

Yes, data sharing between stacks will be possible. This requires extra code to be generated and uses the Terramate Config Store and the Terramate Terraform Provider to keep the state of stack outputs.

Note: We will publish additions to the scaffolding soon. For the sake of simplicity, we kept the initial RFC small.

Can I use Terramate Functions?

Yes, Terramate Functions will be available in most contexts. We are not planning to limit the use.

Can I use Terramate Globals?

Yes, Terramate Globals will be supported. It is not recommended to make your TM Services depend on Globals because this will make TM Services dependent on the environment in which they are created. Still, we identified valid use cases for bundles to share local data between multiple bundles automatically.

Can I deploy a TM Service into an existing Terramate Stack not created by a bundle?

Yes, we plan to support this use case to target intermediate and expert users. The Bundle API Terramate provides is targeted to non-expert users (while totally viable also for experts of course).

Can TM Services contain plain TF Code?

Yes, you can also drop plain terraform code into TM Services, which will be cloned into a generated stack. We recommend following the .tmgen pattern as the additional set of features can be very helpful in making the generated code more context-aware.

Will local (non-registry) bundles and TM Services be supported?

Yes, to ensure developing new bundles and services will be easy and similar to the TF development workflows, we will support to call modules local to the repository.

How can Terramate support me in creating Terramate Services from TF Modules?

We are planning to support scaffolding Terramate Services from plain TF Modules to allow an easy migration.

Note: We will publish additions to the scaffolding soon. For the sake of simplicity, we kept the initial RFC small.

Can I reconfigure a bundle?

Yes, reconfiguration will be a core feature. Some input changes will lead to the recreation of resources within the cloud. Maintainers of a service or bundle can define what inputs can be reconfigured via a user flow and what inputs are only available at creation time (initial instantiation of a bundle or service). (you can constantly reconfigure a bundle or service by editing the generated bundle {} or service {} blocks with an IDE of your choice.

Can I integrate Terramate Scaffolding into my developer Portal?

Yes, we are planning to provide plugins for common developer portals but also encourage people to create them in the way they like them to be.

Is there a reference architecture or public service catalog available for bundles?

We will provide examples and reference architecture and plan to populate a service catalog with a set of curated services. But the actual idea behind this concept is that teams create their own service catalog that matches their requirements and compliance regulations of the organization they work for.

Will there be support in Terramate Cloud for Bundles?

Yes, we believe that scaffolding is the future way of provisioning IaC on scale for large teams. Terramate Cloud will provide the registry, service catalog and config store and will play a key-role in the maintenance of the lifecycle of bundles and services. It will help to create, publish, deprecate and unpublish services assisting the maintainers to keep track of the actual usage of their services. Rolling out new versions should be very easy for teams and maintainers.

I am not using a developer portal. Will there be a web-based UI to scaffold directly from the Service Catalog?

Yes, in the future Terramate Cloud can be used to create new infrastructure with just a few clicks. Our Scaffolding solution will still generate IaC in the back, but we can automate creating pull requests, automatically merging them and deploying them - in any CI/CD with any VCS.

At the beginning, Terramate CLI will provide the user with intelligent prompting of input values, providing an easy-to-use CLI-based UI and a great UX.

Why Bundles and Services on top of TF Modules?

Services provide a specialized use-case of more generic TF Modules. A generic TF Module can provide TF Code for the creation of any type of S3 bucket, while a Terramate Service can provide an API to create a specialized use of the generic services: TF State Buckets, Log Buckets, and Buckets for public assets. A service can also provide a wider use case by combining multiple TF Modules to additionally provide a Load Balancer in front of a Bucket.

This provides benefits on multiple sides:

  • A (maybe more inexperienced) User of the service has a simplified interface and just needs to provide the configuration for this use case using simpler documentation with a limited amount of configuration options.
  • The Maintainer (expert user) of TF Modules, does not need to provide specialized versions of the service.
  • An intermediate User can provide new services without understanding the details of TF implementations.

This is available as of today in multiple flavors and often called Terramate Code Generation, TF Blueprints, TF Templates, etc.

Bundles, combining multiple such services into deployable stacks, have been missing so far.

Similar benefits arise for bundles as with services. Still, the final result is an Interface that is simple enough that unexperienced users who have never used TF before can create infrastructure using IaC technologies where all decisions of how to size a stack and how to split infrastructure into stacks are already provided by the bundle creators and maintainers.

In addition, bundle maintainers do not need to know any TF as they use services that abstract away the details of the IaC used.

Scenarios like migration from Terraform to OpenTofu, updating providers, and updating IaC technologies are completely transparent to the end-user and do not affect any interfaces known to them.

How can I handle Stacks that are required by multiple other stacks, e.g. a Load-balancer shared between all my services?

Some stacks are required by multiple other stacks. They usually are created first and then used by various other stacks that depend on them. A good example is a LoadBlancer, where backend services are added over time or removed while the load-balancer resource/service is shared between all of them.

Terramate Scaffolding will be able to deploy the load-balancer stack with the first deployed service bundle and then be referenced by subsequent service bundle instantiations. When destroying or removing a service again, the load-balancer stack will be kept alive and only be removed with the very last service.

How are Services destroyed?

No special action is required here. Services are just a part of the stack, by simply removing it from the stack, the underlying code will not be created any more and thus removed. This will trigger a destruction as the underlying TF will plan for all resources not found in the code to be destroyed.

How are Bundles destroyed?

Destruction is part of the orchestration. Terramate CLI will introduce destroy triggers very soon to support the orchestration of stack destruction. This is out of the scope of this document.

How is the TF backend configured?

The TF Backend Configuration is not part of a bundle and will not change using bundles. The backend configuration is supposed to be globally configured using an inherited generate_hcl block at the root of the repository.

How are providers configured?

WIP - We will allow to define version constraints on the service level and export them to the Stacks where a generate_hcl logic can pick them up to create actual provider configurations for each stack.

How else can a cloud resource be shared between bundles/stacks/services?

Each service can provide code to be generated when referencing a service. The code will be generated in the context of the instantiated service but created in the consuming stack.

Example: A VPC service will tag a VPC with, e.g., the bundle ID and provide a data source generation template filtering for the tag. A consuming stack does not need to know about the implementation details, naming etc. and can just use a generated data.resource_type.label.vpc_id reference to access the VPC ID.

The same can be achieved by storing VPC IDs within the config store, as explained in the document.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions