Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/andrewbaxter/terrars

Terraform in Rust
https://github.com/andrewbaxter/terrars

Last synced: about 21 hours ago
JSON representation

Terraform in Rust

Awesome Lists containing this project

README

        

Terrars is a tool for building Terraform stacks in Rust. This is an alternative to the [CDK](https://developer.hashicorp.com/terraform/cdktf).

**See a working example** in [helloworld](helloworld).

**Current status**: Usable, but may have some rough edges and missing features. I may continue to tweak things to improve ergonomics.

Why use this or the CDK instead of raw Terraform?

- All stacks eventually get complicated to the point you need a full programming language to generate them. CDK and this aren't particularly more verbose than raw Terraform, so I'd always use them from the start.
- Reuse constants and datastructures from your code (json structures, environment variables, endpoints and reverse routing functions) when defining your infrastructure
- Autocompletion, edit-time verification via types

Why use this instead of the CDK?

- It's Rust, which you're already using
- More static type safety - the CDK ignores types in a number of situations and munges required and optional parameters together
- Pre-generated bindings
- No complicated type hierarchy with scopes, inheritance, etc.
- Fewer layers - `cdk` requires `terraform`, a `cdk` CLI, Javascript tools, Javascript package directories, and depending on what language you use that language itself as well. CDK generation requires a `json spec -> typescript -> generated javascript -> final language` translation process. `terrars` only requires `terraform` both during generation and runtime and goes directly from the JSON spec to Rust.

Why _not_ use this instead of the CDK?

- You need to create your own workflow. You can create a simple build.rs file, but if you want a more complex wrapper you need to write it yourself.

# Pre-generated bindings

## Infrastructure

- [hashicorp/aws](https://github.com/andrewbaxter/terrars-hashicorp-aws)
- [hashicorp/google](https://github.com/andrewbaxter/terrars-hashicorp-google)
- [hectorj/googlesiteverification](https://github.com/andrewbaxter/terrars-hectorj-googlesiteverification)
- [digitalocean/digitalocean](https://github.com/andrewbaxter/terrars-digitalocean-digitalocean)
- [andrewbaxter/fly](https://github.com/andrewbaxter/terrars-andrewbaxter-fly)

## Other services

- [cloudflare/cloudflare](https://github.com/andrewbaxter/terrars-cloudflare-cloudflare)
- [integrations/github](https://github.com/andrewbaxter/terrars-integrations-github)
- [andrewbaxter/stripe](https://github.com/andrewbaxter/terrars-andrewbaxter-stripe)
- [backblaze/b2](https://github.com/andrewbaxter/terrars-backblaze-b2)
- [kreuzwerker/docker](https://github.com/andrewbaxter/terrars-kreuzwerker-docker)
- [dnsimple/dnsimple](https://github.com/andrewbaxter/terrars-dnsimple-dnsimple)

## Local, software

- [andrewbaxter/dinker](https://github.com/andrewbaxter/terrars-andrewbaxter-dinker) - Lightweight Docker image building
- [andrewbaxter/localrun](https://github.com/andrewbaxter/terrars-andrewbaxter-localrun) - External build scripts
- [hashicorp/random](https://github.com/andrewbaxter/terrars-hashicorp-random)
- [hashicorp/local](https://github.com/andrewbaxter/terrars-hashicorp-local)
- [hashicorp/tls](https://github.com/andrewbaxter/terrars-hashicorp-tls)

# Getting started

**Note**: There's a full, working example in [helloworld](helloworld).

1. Add `terrars` and pre-generated bindings such as [terrars-andrewbaxter-stripe](https://github.com/andrewbaxter/terrars-andrewbaxter-stripe) or else generate your own (see [Generation](#generation) below) to your project. Enable the features you want to use in the bindings.

2. Develop your code (ex: `build.rs`)

Create a `Stack` and set up providers:

```rust
let mut stack = &mut BuildStack{}.build();
BuildProviderStripe {
token: STRIPE_TOKEN,
}.build(stack);
```

The first provider instance for a provider type will be used by default for that provider's resources, so you don't need to bind it.

Then create resources:

```rust
let my_product = BuildProduct {
name: "My Product".into(),
}.build(stack);
let my_price = BuildPrice {
...
}.build(stack);
my_price.set_product(my_product.id());
...
```

Finally, write the stack out:

```rust
fs::write("mystack.tf.json", &stack.serialize("state.json")?)?;
```

3. Call `terraform` as usual in the directory you generated `mystack.tf.json` in

(`Stack` also has methods `run()` and `get_output()` to call `terraform` for you. You must have `terraform` in your path.)

# Generating bindings

While there are premade crates for some providers, you can generate code for new providers locally using `terrars-generate`.

1. Install the generate cli with `cargo install terrars`

2. Create a config file.
As an example, to use `hashicorp/aws`, create a json file (ex: `terrars_aws.json`) with the specification of what you want to generate:

```json
{
"provider": "hashicorp/aws",
"version": "4.48.0",
"include": [
"cognito_user_pool",
"cognito_user_pool_client",
"cognito_user_pool_domain",
"cognito_user_pool_ui_customization",
"route53_zone",
"route53_record",
"aws_acm_certificate",
"aws_acm_certificate_validation"
],
"dest": "src/bin/mydeploy/tfschema/aws"
}
```

`tfschema/aws` must be an otherwise unused directory - it will be wiped when you genenerate the code. If `include` is missing or empty, this will generate everything (alternatively, you can use `exclude` to blacklist resources/datasources). Resources and datasources don't include the provider prefix (`aws_` in this example). Datasources start with `data_`.

3. Make sure you have `terraform` in your `PATH`. Run `cargo install terrars`, then `terrars-generate terrars_aws.json`.

4. The first time you do this, create a `src/bin/mydeploy/tfschema/mod.rs` file with this contents to root the generated provider:

```
pub mod aws;
```

# General usage

## Definitions

There are `Build*` structs containing required parameters and a `build` method for most schema items (resources, stack, variables, outputs, etc). The `build` method registers the item in the `Stack` if applicable. Optional parameters can be set on the value returned from `build`.

## Expressions

Background: In Terraform, all fields regardless of type can be assigned a string template expression for values computed during stack application. Since all strings can potentially be templates, non-template strings must be escaped to avoid accidental interpolation.

How `terrars` handles it: When defining resources and calling methods, `String` and `&str` will be treated as non-template strings and appropriately escaped. To avoid the escaping, you can produce a `PrimExpr` object via `stack.str_expr` (to produce an expr that evaluates to a string) or `stack.expr` for other expression types. To produce the expression body you can use `format!()` as usual, but **note** - you must call `.raw()` on any `PrimExpr`s you use in the new expression to avoid double-antiescaping issues.

If Terraform gives you an error about something with the text `_TERRARS_SENTINEL*` it means you probably missed a `.raw()` call on that value (some expression was double-antiescaped).

As a rule of thumb

1. Converting from `expression` to `string`/`field` is OK. The expression gets turned into a sentinel value and interpolated during writing the template
2. Converting from `string`/`field` _with no sentinel values_ (literals, etc) to `expression` is OK.
3. Converting `string`/`field` _containing sentinel values_ -> `expression` is BAD. The sentinel replacement will happen twice and you'll have broken data. This can only happen if you convert an expression into a string and then back, so shouldn't happen often.

## For-each

Lists, sets, and record references have a `.map` method which takes care of all the different "for" methods in Terraform. Specifically

- Call `.map` and define a resource: does resource-level for-each (per Terraform limitations, this cannot be done on lists derived from other resources so has very limited use, you should probably just use a for loop)
- Call `.map` and define a block element: does block-level for-each
- Call `.map` and return an attribute reference: produces an attribute `for` expression

`.map` always produces a list reference, but this can be assgned to set fields as well. `.map_rec` is similar to `.map` but results in a record.

## Vecs and maps of primitives

There's two helper macros for generating vecs and maps of primitive values:

- `primvec![v, ...]` - creates a vec of primitive values, converting each value into a primitive if it is not. Use like `primvec!["stringone", "stringtwo"]` (easier than `vec!["stringone".into(), "stringtwo".into()]`).
- `primmap!{"k" = v, ...}` - creates a map of strings to primitive values, converting each value into a primitive if it is not. Same as above, performs automatic conversion.

# How it works

Terraform provides a method to output provider schemas as json. This tool uses that schema to generate structures that would output matching json Terraform stack files.

## Expressions/template strings/interpolation/escaping

Take as an example:

```rust
format!("{}{}", my_expr, verbatim_string))
```

This code would somehow need to escape the pattern and `verbatim_string`, while leaving `my_expr` unescaped, and the result would need to be treated as an "expression" to prevent escaping if it's used again in another `format!` or something. This applies to not just `format!` but serde serialization (json), other methods.

For now Terrars uses a simple (somewhat dirty) hack to avoid this. All expressions are put into a replacement table, and a sentinel string (ex: `_TERRARS_SENTINEL_99_`) is used instead. During final stack json serialization, the strings are escaped and then the original expression text is substituted back in , replacing the sentinel text.

This way, all normal string formatting methods should retain the expected expressions.

# Current limitations and warnings

- Not all Terraform features have been implemented

The only one I'm aware of missing at the moment is resource Provisioning.

- `ignore_changes` takes strings rather than an enum

- No variable or output static type checking

I'd like to add a derive macro for generating variables/outputs automatically from a structure at some point.

- Non-local deployment methods

I think this is easy, but I haven't looked into it yet.

# The name

I originally called this `terrarust` but then I realized it sounded like terrorist so I decided to play it safe and chopped out the `u` `t` which stands for unreal tournament.