Skip to content

Terraform Conventions

Nullstone enforces a set of conventions in Terraform that makes modules composable, reusable, and portable. When creating Nullstone modules, following the conventions outlined in this document is essential.

Terraform Provider

Nullstone modules use standard Terraform and don't require you to learn any new language or syntax. To make module development easier, use the Nullstone Terraform Provider. This provider injects valuable context and information about the current workspace.

When developing locally, use the Nullstone CLI to configure your workspace. The nullstone workspace select command will automatically configure your selected workspace and the Nullstone provider.

Automated Backend

Typically, when using Terraform, you need to define a Terraform block configuring where your state files are stored. Nullstone automatically configures the state file backend to ensure that modules are portable and reusable. Do *not create the backend stanza in your modules.

TIP

If you want to use a self-hosted state backend in Nullstone, upvote Bring your own state backend on our roadmap.

When developing locally, the nullstone workspaces select command will generate __backend__.tf containing something like the following snippet. Do not commit this file into source control.

hcl
terraform {
  backend "remote" {
    hostname     = "api.nullstone.io"
    organization = "<my-organization>"

  workspaces {
    name = "<workspace-id>"
  }
}
terraform {
  backend "remote" {
    hostname     = "api.nullstone.io"
    organization = "<my-organization>"

  workspaces {
    name = "<workspace-id>"
  }
}

Structured Workspaces

Typically, when using Terraform, a single workspace is uniquely identified by the name of the workspace. Instead, Nullstone identifies using structured workspaces to provide project-based permissions, ensure unique resource names in cloud providers, automate resource tagging, and enable preview environments.

The following illustrates the hierarchy of these workspaces in Nullstone.

└── organization
    └── stack
        └── block
            └── environment
└── organization
    └── stack
        └── block
            └── environment

Stacks create a namespace for isolating workloads and permissions and duplication into environments. Each Block in a Stack points to a specific Terraform module and creates a logical piece of infrastructure. (See Categories for more information.)

This structured workspace metadata is available through the Nullstone Terraform provider.

Dependency Injection

Typically, when using Terraform, you can make use of terraform_remote_state to retrieve necessary information from dependent infrastructure in other Terraform workspaces. There are several challenges with Terraform that prohibit modules from being portable and reusable.

  1. You have to know the target workspace name when building a module, or you have to inject the workspace name through a variable.
  2. There are no protections from selecting an incorrect workspace.
  3. If the dependency has not been provisioned yet, Terraform gives an obtuse error message.
  4. It's not very obvious how to create an "optional" dependency.
  5. There is no way to define a dynamic number of dependencies.

To resolve this, Nullstone introduces a Terraform data source called ns_connection as a replacement for terraform_remote_state. This data source provides a mechanism for injecting dependencies based on a contract. However, instead of pointing directly to a named workspace, you define a contract that a dependency follows. See Contracts to learn more.

When provisioning a workspace, Nullstone:

  1. Ensures all dependencies are provisioned.
  2. Provides a mechanism for attaching "optional" dependencies.
  3. Provides an application-specific mechanism for attaching any number of dependencies.

Here is an example of a module that needs the vpc_id of its host network.

hcl
data "ns_connection" "network" {
  name     = "network"
  contract = "network/aws/vpc"
}

locals {
  vpc_id = data.ns_connection.outputs.vpc_id
}
data "ns_connection" "network" {
  name     = "network"
  contract = "network/aws/vpc"
}

locals {
  vpc_id = data.ns_connection.outputs.vpc_id
}