💾 Archived View for d.moonfire.us › blog › 2024 › 01 › 05 › teaching-nixos-about-opentofu captured on 2024-03-21 at 15:03:24. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2024-02-05)
-=-=-=-=-=-=-
In my endless quest to come up with a completely data-driven and reproducible environment, I decided to take a stab at a new automation tool: OpenTofu[1]. I've already gotten a good NixOS[2] setup, but I wanted to also be able to check in the setup for my instances (and to a smaller degree, my bare metal servers in my home lab) to expand on the functionality. It didn't hurt that work had settled on Terraform.
Previously, I had taken a stab at Pulumi[3] (during my wedding anniversary trip in 2022). It was fun and I liked the code but I ended up gutting it later. For some reason, it quickly ended up feeling like a chore to play with. At that point, I figured I would just do things manually. But then I saw the announcement that OpenTofu had forked from Terraform because of enshittification of licenses (develop with an open license, then switch to a more limited one once profits became important). That little thing set me off and I decided to try it out.
Since all my infrastructure code is in a Nix flake, to get started just required me to add to the shell's packages.
devShell.${system} = pkgs.mkShell { buildInputs = [ pkgs.just pkgs.opentofu pkgs.openstackclient ]; };
I also grabbed the OpenStack client because it made easier to find some of the nasty little identifiers I needed to import.
The way tofu works, it grabs all the `*.tf` files in the same directory. So inside my infrastructure flake, I have a `src/tofu` directory with configuration files that make sense to me:
All of them are picked up, merged together, and made into a single set of settings. I also use `tofu fmt` a lot since I like to normalize my files on every commit.
Fortunately, my hosting provider of choice is DreamHost[4]. They aren't the cheapest or the best, but they appear to be ethetical. Mostly, I stick with them because they went to the court to fight some overreaching gag orders[5].
5: https://techfreedom.org/victory-online-political-free-speech-dreamhost-case/
(I also tried DigitalOcean at the same time as Pulumi but dropped that also.)
OpenTofu (via Terraform plugins) does a wonderful job of supporting both OpenStack and DreamHost DNS to tie everything together.
terraform { required_version = ">= 0.14.0" required_providers { dreamhost = { source = "adamantal/dreamhost" version = "0.3.2" } openstack = { source = "terraform-provider-openstack/openstack" version = "~> 1.53.0" } } }
I added the `adamantal/dreamhost` plugin so I could also assign the DNS record directly from my script and have everything working.
Even though my infrastructure flake is in a private repository, I still encrypt all my secrets. I use SOPS for this, which means setting up the `.sops.yaml` file to encrypt and then decrypting/encrypting files using `just``:
decrypt: decrypt-clouds decrypt-secrets decrypt-clouds: if [ ! -f clouds.yaml ];then sops -d clouds.yaml.enc > clouds.yaml;fi encrypt-clouds: cp clouds.yaml clouds.yaml.enc sops -i -e clouds.yaml.enc decrypt-secrets: if [ ! -f 050-secrets.tf ];then sops -d 050-secrets.tf.enc > 050-secrets.tf;fi encrypt-secrets: cp 050-secrets.tf 050-secrets.tf.enc sops -i -e 050-secrets.tf.enc
I do the same with my `.env` file so I can get the information I need set up properly.
Here is a short segment for creating an instance on DreamHost (or most OpenStack providers).
resource "openstack_compute_instance_v2" "instance1" { provider = openstack.dreamhost name = "instance1" // It really isn't instance1, but just pretend key_pair = "keypair1" // This is my SSH key set up somewhere else flavor_name = "gp1.supersonic" // gp1.supersonic means I don't need a swap disk user_data = <<-EOT #cloud-config runcmd: - curl https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect | NIX_CHANNEL=nixos-unstable bash 2>&1 | tee /tmp/infect.log EOT # This sets up the boot device as / block_device { source_type = "image" uuid = "2b2c61c6-324c-47f4-88c1-9ae8a978ddfd" # Ubuntu boot_index = 0 delete_on_termination = true destination_type = "volume" multiattach = false volume_size = 80 } network { // Also configured somewhere else name = openstack_networking_network_v2.public.name } } resource "dreamhost_dns_record" "instance1" { record = "instance1.mfgames.com" value = openstack_compute_instance_v2.instance1.network[0].fixed_ip_v4 type = "A" }
For me, the part is really cool is that I can bake in the NixOS infect[6] script right in. To my surprise, it just ran the first time without errors (thought I had to wait about ten minutes after OpenTofu said it was done).
6: https://github.com/elitak/nixos-infect
All I had to do was either show the results:
tofu plan
Or apply the changes:
tofu apply
Actually, the first thing I did was import my existing instances into the system. This involves creating a `.tf` file with the same basic setup at the other instance (some fields can be skipped but I was still learning), then import with the ID from OpenStack. Of course, getting the IDs was the hard part. Fortunately, this is where the OpenStack client comes into play. I can use that to get the list of servers, figure out the ID, then import it into Tofu.
$ openstack --os-cloud dreamhost server list +--------------------------------------+-----------+--------+------------+--------------------------+----------------+ | ID | Name | Status | Networks | Image | Flavor | +--------------------------------------+-----------+--------+------------+--------------------------+----------------+ | 55f8ee35-31b2-4137-af1d-b7597d348271 | instance0 | ACTIVE | public=*** | N/A (booted from volume) | gp1.supersonic | | 1a38092b-bbc5-46bd-9092-0df979ca8fe4 | instance1 | ACTIVE | public=*** | N/A (booted from volume) | gp1.supersonic | $ tofu import openstack_compute_instance_v2.instance0 55f8ee35-31b2-4137-af1d-b7597d348271
Now, while this was great for setting up things, I also wanted to pull that data into my Nix infrastructure flake. Fortunately, OpenTofu has a way of exporting the data pulled from the cloud. To do that, I need to add an `output` stanza at the bottom of my `500-instance1.tf` file:
output "instance1_ipv4" { value = openstack_compute_instance_v2.instance1.network[0].fixed_ip_v4 }
Since I'm (recently) fond of using Just[7] for automation, I banged up a little stanza that automatically creates a `default.nix` file inside that directory every time I apply.
apply: decrypt format && export tofu apply format: tofu fmt plan: decrypt format tofu plan export: echo "inputs: {" > default.nix tofu output | sort | perl -ne 'chomp;s@_@.@g;print " $_;\n"' >> default.nix echo "}" >> default.nix
I had to use `_` in the output since dotted notation isn't accepted, but I use `perl` to convert those underscores into Nix-happy format.
inputs: { instance0.ipv4 = "1.2.3.4"; instance1.ipv4 = "2.3.4.5"; }
I use this to pull into my `networking.nix` which is used to drive things like configuring AdGuard, services like Maddy (for DeltaChat) and other services.
inputs: let tofu = import ./tofu/default.nix { }; in { instance0 = tofu.instance0; instance1 = tofu.instance1; instance2.ipv4 = "192.168.0.2"; }
From there, I have a single place to get all my IP addresses:
inputs: let ip = (import ../../../../networks.nix { }).instance0.ipv4; in { }
It isn't the best or most graceful way of doing things, but I'm pretty happy how everything turned out. I made a few mistakes along the way of setting up Gitea Actions and had to drop and rebuild my instance0. That was just a matter of renaming `500-instance0.tf`, applying to drop, and then name the file back. Then I had nice clean slate to push out a new closure.
OpenTofu is much nicer than Pulmui. It didn't insist on having a cloud to maintain state, the file is checked into Git instead. It has a declarative language instead of code, and since I really don't need a lot of that logical flow, it just works for me. Plus I was able to inject into my `just deploy` top-level script that pushes out changes to my home lab and all my instances in a single call.
Categories:
Tags:
Below are various useful links within this site and to related sites (not all have been converted over to Gemini).
https://d.moonfire.us/blog/2024/01/05/teaching-nixos-about-opentofu/