bt

command module
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2024 License: MPL-2.0 Imports: 15 Imported by: 0

README

= bt: Build Terraform

A no commitments Terraform/Tofu wrapper that provides build caching functionality.
It also makes working with workspaces a breeze.

== Features

* Run `terraform init`, `terraform providers lock`, `terraform plan`, `conftest` and `terraform apply` with a single command and with caching.

* Automatically inject init backend tfvars and plan tfvars based on config values.

* Automatically load tfvar files based on workspace name.

* Allow you to have multiple profiles in the config file so that you can have a different configs for multiple workspaces.
For example, one profile for dev and another for prod, or one profile for North America and another for China.
+
Each profile uses a separate `TF_DATA_DIR`.
This allows to work with multiple profiles pointing to different backends under the same directory without conflicts.

* Runs pre-apply checks for you.
If you define for example, `conftest` checks, it will run them for you before trying to apply.

* Track the archs you want to use when running terraform provider lock.

* Allow you to work on different workspaces in different terminals.
It automatically uses the `TF_WORKSPACE` environment variable rather than selecting the workspace.

* Allows you to define stacks of components and their dependencies and orchestrate their deployment (and destruction in the reverse order).

* Allows to define retries for stack components that fail due to race conditions.

* TODO: Allow option to skip component in destroy if the state shows no resources.

* TODO: Collect component exit codes and have a return code based on whether there were any changes in the stack or not.

* TODO: Add post apply tasks with the ability to run a different task for success or error.
For example, to check for the last change in the failing component and notify the owner, or to run a post apply test.

* TODO: Ensure logs for parallel component runs are printed in serial.
This one might be nice to combine with a progress indicator in the dag library.

* TODO: Figure out how to invalidate plan cache after apply when running in target mode.

* TODO: Recursive inspection of modified local sources of a local module within a local module.

* TODO: Run terraform providers lock after init. Add `init --lock` option.

* TODO: Add option to skip component in stack if ws doesn't exist.

* TODO: Add progress indicator x out of y > this can be written to the argo workflow progress file:
https://argo-workflows.readthedocs.io/en/latest/progress/

* TODO: Automatically handle when Terraform is unable to write the state to the backend.
+
----
│ Error: Failed to persist state to backend
│
│ The error shown above has prevented Terraform from writing the updated
│ state to the configured backend. To allow for recovery, the state has been
│ written to the file "errored.tfstate" in the current working directory.
│
│ Running "terraform apply" again at this point will create a forked state,
│ making it harder to recover.
│
│ To retry writing this state, use the following command:
│     terraform state push errored.tfstate
----

== Install

* Install using homebrew:
+
----
brew tap DavidGamba/dgtools https://github.com/DavidGamba/dgtools
brew install DavidGamba/dgtools/bt
----
+
[NOTE]
====
Completion is auto setup for bash.

For `zsh` completions, an additional step is required, add the following to your `.zshrc`:

[source, zsh]
----
export ZSHELL="true"
source "$(brew --prefix)/share/zsh/site-functions/dgtools.bt.zsh"
----
====
+
Upgrade with:
+
----
brew update
brew upgrade bt
----

* Install using go:
+
Install the binary into your `~/go/bin`:
+
----
go install github.com/DavidGamba/dgtools/bt@latest
----
+
Then setup the completion.
+
For bash:
+
----
complete -o default -C bt bt
----
+
For zsh:
+
[source, zsh]
----
export ZSHELL="true"
autoload -U +X compinit && compinit
autoload -U +X bashcompinit && bashcompinit
complete -o default -C bt bt
----

== Config file

The config file must be saved in a file named `.bt.cue`.
It will be searched from the current dir upwards.

Example:

.Config file .bt.cue
[source, cue]
----
package bt

config: {
	default_terraform_profile: "default"
	terraform_profile_env_var: "BT_TERRAFORM_PROFILE"
}
terraform_profile: {
	default: {
		binary_name: "terraform"
		init: {
			backend_config: ["backend.tfvars"]
		}
		plan: {
			var_file: ["~/auth.tfvars"]
		}
		workspaces: {
			enabled: true
			dir: "envs"
		}
		pre_apply_checks: {
			enabled: true
			commands: [
				{name: "conftest", command: ["conftest", "test", "$TERRAFORM_JSON_PLAN"]},
			]
		}
		platforms: ["darwin_amd64", "darwin_arm64", "linux_amd64", "linux_arm64"]
	}
}
----

See the link:./config/schema.cue[schema] for extra details.

== Usage Basics

. (optional) Run `bt terraform init` to initialize your config.

. Run `bt terraform build` to generate a plan.
If `init` wasn't run, it will run `init` once and cache the run so further calls won't run `init` again.

. Run `bt terraform build --lock` to ensure that `terraform providers lock` has run after `init` with the list of archs provided in the config file.

. Run `bt terraform build --ic` to generate a plan again even when it detects there are no file changes.

. Run `bt terraform build --show` to view the generated plan.

. Run `bt terraform build --apply` to apply the generated plan.

=== Caching Internals

After running `bt terraform init` it will save a `.tf.init` file.

After running `bt terraform build` it will save a `.tf.plan` or `.tf.plan-<workspace>` file.
It will check the time stamp of the `.tf.init` file and if it is newer than the `.tf.plan` file, a new plan needs to be generated.
It will also compare the `.tf.plan` file against any file changes in the current dir or any of the module dirs to determine if a new plan needs to be generated.

If `pre_apply_checks` are enabled, it will run the checks specified by passing the rendered json plan to the command.
For example, `conftest` policy checks.

After running `terraform apply` it will save a `.tf.apply` or `.tf.apply-<workspace>` file.
It will use that file and compare it to the `.tf.plan` time stamp to determine if the apply has already been made.

=== Backend Config / Var File helpers

Given the config setting for `backend_config` for init and `var_file` for plan, it will automatically include those files to the command.

For example, running `bt terraform init` with the example config file will be the same as running:

----
terraform init -backend-config backend.tfvars
----

In the same way, running `bt terraform build` with the example config file will be the same as running:

----
terraform plan -out .tf.plan -var-file ~/auth.tfvars
----

Finally, running `bt terraform build --apply` with the example config file will be the same as running:

----
terraform apply -input .tf.plan
----

== Workspaces helpers

Setting workspaces to `enabled: true` in the config file will enable the workspace helpers.
What the helpers do is to assume any `.tfvars` or `.tfvars.json` file in the `dir` folder is a workspace.

If a workspace has been selected, bt will automatically include the `<dir>/<workspace>.tfvars` or `<dir>/<workspace>.tfvars.json` file to the command.

If a workspace hasn't been selected, passing the `--ws` option will select the workspace by exporting the `TF_WORKSPACE` environment variable and will add the corresponging `<dir>/<workspace>.tfvars` or `<dir>/<workspace>.tfvars.json` file to the command.

For example, running `bt terraform build --ws=dev` with the example config file will be the same as running:

----
export TF_WORKSPACE=dev
terraform plan -out .tf.plan -var-file ~/auth.tfvars -var-file envs/dev.tfvars
----

And then running `bt terraform build --ws=dev --apply`:

----
export TF_WORKSPACE=dev
terraform apply -input .tf.plan
----

IMPORTANT: Because `bt` uses the `TF_WORKSPACE` environment variable rather than selecting the workspace,
it is possible to work with multiple workspaces at the same time on different terminals.

When using `bt terraform workspace-select default` bt will automatically delete the `.terraform/environment` file to ensure we can use the `TF_WORKSPACE` environment variable safely.

== Pre Apply Checks

When using `bt terraform build`, pre apply checks get run automatically after a plan if they are enabled.

Pre apply check commands get the following Env vars exported:

* `CONFIG_ROOT`: The dir of the config file.
* `TERRAFORM_JSON_PLAN`: The path to the rendered json plan.
* `TERRAFORM_TXT_PLAN`: The path to the rendered txt plan.
* `TF_WORKSPACE`: The current workspace or "default".
* `BT_COMPONENT`: The current component name if running in stack mode or the basename of the current directory.

If pre-apply checks are enabled in the config file, they can be disabled for the current run using the `--no-checks` option.

To run only the checks, use `bt terraform checks`, combine it with the `--ws` option to run the checks against the last generated plan for the given workspace.

== Profiles

Multiple terraform config profiles can be defined.
By default, the `default` profile is used.
The default profile can be overridden with `config.default_terraform_profile` in the config file.

To use a different profile, use the `--profile` option or export the `BT_TERRAFORM_PROFILE` environment variable.
The environment variable name itself can also be overridden to read an existing one in the environment.
For example, set `config.terraform_profile_env_var` to `AWS_PROFILE` and name your terraform profiles the same way you name your AWS profiles.

Each additional profile will have its own `TF_DATA_DIR` and the terraform data will be saved under `.terraform-<profile>/`.
The `config.default_terraform_profile` will still use the default `.terraform/` dir.
This allows to work with multiple profiles pointing to different backends under the same workspace directory without conflicts.

=== Providers lock using Platforms list

Use `bt terraform providers lock` to generate a lock file using all the os archs in the `platforms` list for a given profile.

== Stacks: A different take

Hashicorp recently https://www.hashicorp.com/blog/terraform-stacks-explained[introduced their solution] for deploying stacks of resources.

A stack is a collection of components that need to be deployed together to form a logical unit.

Instead of having a massive state file that contains all resources, you can split them into multiple smaller components.
This split provides numerous benefits that I won't get into here, however,
these components require an orchestration layer to deploy them together and in the correct order.

bt provides a separate config file for defining stacks: `bt-stacks.cue`

=== Features

* The stack is composed of multiple different components.

* Each component can be deployed to a different workspace but in general,
they should have a consistent naming convention so that the workspace name can be auto-resolved from the stack name.

* A stack can have multiple instances of the same component, that is, multiple workspaces of one component.

* The stack definition allows for conditionally added components.
Some regions or environments might not require certain components.

* The stack config file defines 2 different constructs.
One is the component definition where the component and its dependencies are defined.
The other is the stack definition, where the workspaces that compose a given stack and its variables are defined.

* Because component dependencies are tracked, stack builds run in parallel when possible.

* Components can have variables defined in the stack config file, since these variables are passed after the workspace var files they have higher precedence and allow for stack specific overrides.

* Components can define retries when they fail due to race conditions.

=== Stack config file

.bt-stacks.cue
[source, cue]
----
package bt_stacks

// Define the list of components
component: "networking": {}
component: "kubernetes": {
	depends_on: ["networking"]
}
component: "node_groups": {
	depends_on: ["kubernetes"]
}
component: "addons": {
	depends_on: ["kubernetes"]
}
component: "dns": {
	depends_on: ["kubernetes"]
	retries: 3
}
component: "dev-rbac": {
	path: "dev-rbac/terraform"
	depends_on: ["kubernetes", "addons"]
}

// Create component groupings with additional variable definitions
_standard_cluster: {
	"networking": component["networking"] & {
		variables: [
			{name: "subnet_size", value: "/28"},
		]
	}
	"kubernetes": component["kubernetes"]
	"node_groups": component["node_groups"]
	"addons": component["addons"]
	"dns": component["dns"] & {
		variables: [
			{name: "api_endpoint", value: "api.example.com"},
		]
	}
}

// Create a stack with a list of components
stack: "dev-us-west-2": {
	id: string
	components: [
		for k, v in _standard_cluster {
			[// switch
				if k == "networking" {
					v & {
						workspaces: [
							"\(id)-k8s",
						]
					}
				},
				if k == "node_groups" {
					v & {
						workspaces: [
							"\(id)a",
							"\(id)b",
							"\(id)c",
						]
					}
				},
				v & {
					workspaces: [id]
				},
			][0]
		},
		// Custom component that only applies to this stack
		component["dev-rbac"] & {
			workspaces: [id]
		}
	]
}

stack: "prod-us-west-2": {
	id: string
	components: [
		for k, v in _standard_cluster {
			[// switch
				if k == "networking" {
					v & {
						workspaces: [
							"\(id)-k8s",
						]
					}
				},
				if k == "node_groups" {
					v & {
						workspaces: [
							"\(id)a",
							"\(id)b",
							"\(id)c",
						]
					}
				},
				v & {
					workspaces: [id]
				},
			][0]
		}
	]
}
----

See the link:./stack/config/schema.cue[stack schema] for extra details.

=== Usage

==== Config

Quickly inspect the config file:

----
bt stack config
----

==== Graph

----
bt stack graph --id=dev-us-west-2 -T png
----

image::https://github.com/DavidGamba/screenshots/blob/master/dgtools/bt/stack-dev-us-west-2.png[]

----
bt stack graph --id=prod-us-west-2 -T png
----

image::https://github.com/DavidGamba/screenshots/blob/master/dgtools/bt/stack-prod-us-west-2.png[]

==== Build

Run all plans in parallel:

----
bt stack build --id=dev-us-west-2
----

Run all plans in serial:

----
bt stack build --id=dev-us-west-2 --serial
----

Review/Show the plan output for all components:

----
bt stack build --id=dev-us-west-2 --show --serial
----

Apply the changes:

----
bt stack build --id=dev-us-west-2 --apply
----

Destroy (pass both `--destroy` and `--reverse` to destroy in reverse order):

----
bt stack build --id=dev-us-west-2 --reverse --destroy
----

Apply the destroy:

----
bt stack build --id=dev-us-west-2 --reverse --destroy --apply
----

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL