Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/andrewbaxter/terrars
Terraform in Rust
https://github.com/andrewbaxter/terrars
Last synced: about 21 hours ago
JSON representation
Terraform in Rust
- Host: GitHub
- URL: https://github.com/andrewbaxter/terrars
- Owner: andrewbaxter
- License: isc
- Created: 2022-11-29T17:40:33.000Z (about 2 years ago)
- Default Branch: master
- Last Pushed: 2024-07-21T19:51:53.000Z (5 months ago)
- Last Synced: 2024-11-30T07:37:01.102Z (12 days ago)
- Language: Rust
- Size: 444 KB
- Stars: 97
- Watchers: 4
- Forks: 4
- Open Issues: 2
-
Metadata Files:
- Readme: readme.md
- License: license.txt
Awesome Lists containing this project
- awesome-tf - terrars - Terrars is a tool for building Terraform stacks in Rust. This is an alternative to the CDK. (Tools / Community providers)
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 typesWhy 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.