by skunxicat

Bootstrapping Cloudless Infrastructure

Every system starts somewhere.

cloudless-infra is that starting point — a streamlined Terraform foundation designed for rapid prototyping with rigor. It sets your naming, environment, and context once, then shares that discipline across every module.

It’s not a framework.
It’s not an opinionated meta-module.
It’s the first block — the solid foundation every build starts from.

cloudless-infra helps you get started fast, letting you build immediately while staying minimal and foundational. It gives you just enough structure to move quickly, without adding anything you don’t need.

What it does

  • Provides a standard context (namespace, stage, region, etc.)
  • Applies consistent tagging via CloudPosse null-label
  • Defines shared locals for region, environment, and naming
  • Exposes reusable outputs like context, tags, and label
  • Acts as a “root include” for every other module

And just as important:

  • It doesn’t create AWS resources by itself
  • It doesn’t lock you to a structure
  • It doesn’t depend on any external tooling

It’s zero-opinion infrastructure glue.


Context

I kept finding myself bootstrapping new Terraform root modules in exactly the same way — defining labels, locals, and AWS context. Every new project began with this familiar pattern:

# main.tf
module "label" {
  source  = "cloudposse/label/null"
  version = "0.25.0"

  namespace = local.namespace
  name      = local.name
}

data "aws_caller_identity" "current" {}

locals {
  profile    = var.profile
  region     = var.region
  identity   = data.aws_caller_identity.current
  account_id = local.identity.account_id
  name       = var.name
  namespace  = var.namespace
  id         = module.label.id
  # ... 
}
# variables.tf
variable "namespace" {
  type = string
  default = null
}

variable "profile" {
  type = string
  default = null
}
# ... 

# outputs.tf
output env {
    value = {
        profile = local.profile
        region = local.region
        namespace = module.label.namespace
        name = module.label.name
        id = module.label.id
        account_id = local.account_id
    }
}

# terraform.tf
provider "aws" {
  region = local.region
}

terraform {
  required_version = "~> 1.12.2"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.5.0"
    }
  }
}

My environment setup always looked like this — minimal but consistent:

AWS_PROFILE=ql4b
AWS_REGION=us-east-1
NAMESPACE=cloudless
NAME=project

TF_VAR_name=${NAME}
TF_VAR_namespace=${NAMESPACE}
TF_VAR_region=${AWS_REGION}
TF_VAR_profile=${AWS_PROFILE}

TERRAFORM_VERSION="v1.13.3"
TERRAFORM_BIN="/usr/local/bin/terraform-$TERRAFORM_VERSION"

This setup is intentionally minimal — a single workspace, a single AWS account — but it’s how I start almost every prototype.
When I want to test a pattern or an idea, I don’t want to lose time rewriting boilerplate. I want to move fast, but without losing the discipline that keeps things consistent.

That’s why cloudless-infra exists: a one-command bootstrap that gets me from empty folder to ready-to-deploy in seconds.

Avoiding repeated boilerplate setup is only half the battle. Just as important is making sure that every project, environment, and resource follows the same naming and tagging conventions—without having to think about it each time. That’s where consistent use of tools like null-label comes in.

Naming consistency

When you want to stay consistent across projects, the challenge isn’t naming — it’s naming every time.

With the CloudPosse null-label module, you don’t have to reinvent your naming logic for each new stack.

You simply choose the context variables that make sense — namespace, name, stage, tenant, and so on — and the module handles the hierarchy for you.

The result: predictable, uniform resource names and tags across every environment, without thinking twice about it.

Managing Environmen & Terraform Versions

Cloudless uses a minimal Bash-based approach for handling Terraform versions, leveraging a simple source command and a lightweight wrapper script. By defining TERRAFORM_VERSION and TERRAFORM_BIN in the .env file and loading them with the activate script, you can easily pin, switch, and upgrade Terraform versions on a per-project basis without relying on complex dependency managers.

This approach keeps version management straightforward and transparent, allowing you to maintain consistent Terraform versions across your projects with minimal overhead.

The tf wrapper:

ROOT_PATH=$(realpath $(dirname "$0"))
INFRA_PATH="$ROOT_PATH/infra"

"$TERRAFORM_BIN" -chdir=$INFRA_PATH \
  "$@"

The activate file:

set -a
# shellcheck source=/dev/null
source  .env && \
PATH="$(pwd):$PATH"
set +a

Once the variables are set in the .env, simply

source activate

And you session shell is using the context you have setup.

tf init 
tf apply --auto-approve

Bootstrap your workspace with online cloudless-infra

Everything described before — environment setup, version management, activation, and wrapper scripts — will be automatically prepared by cloudless-infra when you bootstrap your project.

Run the following commands to initialize your project and handle all the setup automatically:

mkdir -p myapp && cd myapp
    
curl -sL https://raw.githubusercontent.com/ql4b/cloudless-infra/main/bootstrap | bash

This initializes your project and creates:

  • an .env file for environment variables
  • an activate script for loading them
  • and an infra/ folder containing a bootstrapped Terraform module — ready to initialize your remote backend and manage state consistently.

asciinema

Configure your environment

Once the bootstrap process completes, open the newly created .env file in your project root. This file defines your default environment context — values like namespace, stage, and region — along with AWS profile bindings that Terraform can automatically use.

Example:

# cloudless-infra
AWS_PROFILE=ql4b
AWS_REGION=us-east-1
NAMESPACE=cloudless
NAME=sample

TF_VAR_name=${NAME}
TF_VAR_namespace=${NAMESPACE}
TF_VAR_region=${AWS_REGION}
TF_VAR_profile=${AWS_PROFILE}

TERRAFORM_VERSION="v1.12.2"
TERRAFORM_BIN="/usr/local/bin/terraform-$TERRAFORM_VERSION"

Update these values to match your setup.
The activate script will automatically load them whenever you source it, ensuring your Terraform environment stays consistent across sessions.

Once your .env file is ready, activate your environment.

Activating Your Environment

Before running Terraform, load your environment variables:

source activate

The activate script reads your .env file and exports its variables into the current shell session, creating a lightweight, context-aware environment that matches the conventions defined in cloudless-infra. It also updates your prompt with the active context so you can visually confirm which workspace and credentials you’re using.

The scaffold includes a tiny tf wrapper script so you can pin and consistently invoke Terraform across projects.

  • It reads TERRAFORM_BIN from your .env (for example /usr/local/bin/terraform-v1.12.2).
  • If TERRAFORM_BIN is not set or missing, it falls back to the first terraform found on your PATH.
  • It simply forwards any arguments to Terraform (tf plan, tf apply, etc.), so you don’t need to remember which binary/version to use in each repo.

Because the bootstrap adds the project directory to your PATH, you can simply run tf instead of ./tf from anywhere inside the repository.

Check it works:

source activate     # ensures TERRAFORM_BIN is exported
tf version

You should see the pinned Terraform version.

Initialize & Apply

From the folder that contains your Terraform configuration:

Initialize providers, modules and apply changes

tf init -upgrade
tf apply -auto-approve
env = {
  "account_id" = "703177223665"
  "id" = "cloudless-myapp"
  "name" = "myapp"
  "namespace" = "cloudless"
  "profile" = "ql4b"
  "region" = "us-east-1"
}

Demo

asciinema

GitHub repository

Bootstrap your next project with cloudless-infra github.com/ql4b/cloudless-infra