Operations grimoire/Deploy with Terraform: Difference between revisions
Proofread |
|||
| (19 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
== | == Overview == | ||
In addition to Salt, Nasqueron manages Infrastructure as Code using Terraform and OpenTofu. We deploy from [[Complector]] using <code>/opt/salt/nasqueron-operations</code> as our local copy of rOPS, authoritative for both Salt and Terraform. | |||
It's important to work | We provide a <code>tf</code> wrapper that automatically runs OpenTofu or Terraform based on the directory and mounts the encrypted state directory on the fly. | ||
<div class="alert">It's important to work on Complector, so that all deployments use the shared Terraform state.</div> | |||
=== Paths === | |||
Terraform states are stored on a local backend. To protect sensitive infrastructure state (which often contains plaintext secrets and topology data) at rest, we use a split-architecture approach combining [[Operations grimoire/ZFS|ZFS]] and [[Operations grimoire/PEFS|PEFS]] (Private Encrypted File System). | |||
{| class="wikitable" | |||
|+ Paths for Terraform / OpenTofu deployment | |||
|- | |||
! Path | |||
! Type | |||
! Purpose | |||
! Filesystem Properties | |||
|- | |||
| <code>/opt/salt/nasqueron-operations/terraform/</code> | |||
| Standard Dir | |||
| Terraform/OpenTofu declarative code | |||
| Standard ZFS | |||
|- | |||
| <code>/opt/terraform.enc</code> | |||
| ZFS Dataset | |||
| Underlying encrypted block storage for PEFS | |||
| <code>compression=zstd</code> | |||
|- | |||
| <code>/opt/terraform/encrypted</code> | |||
| PEFS Mount | |||
| '''Encrypted state files.''' Mounted only during TF operations. | |||
| AES-256-XTS | |||
|- | |||
| <code>/opt/terraform/tf-data</code> | |||
| ZFS Dataset | |||
| '''Provider cache and modules files.''' <code>TF_DATA_DIR</code> target. | |||
| <code>compression=lz4</code>, <code>recordsize=128K</code> (optimized for Go binaries) | |||
|} | |||
=== Access control === | |||
* PEFS operations (mounting, adding keys) are executed as the <code>deploy</code> user. | |||
* The wrapper verifies that the invoking user belongs to the <code>ops</code> or <code>deploy</code> group before proceeding. | |||
* The PEFS passphrase is stored in HashiCorp Vault at <code>ops/infra/complector/terraform/pefs</code>. | |||
== Standard operations == | |||
The <code>tf</code> wrapper automatically mounts PEFS, fetches the decryption key from Vault, executes the command, and unmounts PEFS upon exit, even on failure or <code>Ctrl+C</code>. | |||
You can use it exactly like you would use terraform or tofu commands: | |||
<syntaxhighlight lang="bash"> | |||
$ cd /opt/salt/nasqueron-operations/terraform/ovh-ops-backups | |||
$ tf init | |||
$ tf plan | |||
$ tf apply | |||
</syntaxhighlight> | |||
== Specific deployment notes == | == Specific deployment notes == | ||
=== Maintenance of .tfstate files === | |||
For manual state surgery or moving files, use the explicit mount commands. These bypass the automatic unmount. | |||
<syntaxhighlight lang="bash"> | |||
# Mount PEFS and load keys for manual maintenance | |||
tf mount | |||
# Perform manual operations (e.g., moving state files) | |||
cd /opt/terraform/encrypted/tf-states | |||
# ... | |||
# Unmount when finished | |||
tf unmount | |||
</syntaxhighlight> | |||
=== Choose between opentofu or terraform === | |||
By default, tofu is used. | |||
The wrapper maintains an allowlist of projects that still require HashiCorp Terraform. | |||
The main blocker is <code>hashicorp/vault</code>, not compiled for FreeBSD on OpenTofu registry. | |||
To add or remove a project from this list, edit the <code>STILL_REQUIRE_TERRAFORM</code> variable in {{Ops file|roles/salt-primary/opentofu/files/tf.sh}}. | |||
=== New project configuration === | |||
Every Terraform project must explicitly configure its local backend to write state into the PEFS-encrypted mount. | |||
Add this to your project's <code>backend.tf</code> or <code>providers.tf</code>: | |||
<syntaxhighlight lang="hcl"> | |||
terraform { | |||
backend "local" { | |||
path = "/opt/terraform/encrypted/tf-states/<project-name>/terraform.tfstate" | |||
} | |||
} | |||
</syntaxhighlight> | |||
''Note: The provider cache path does not need to be configured in HCL. The wrapper automatically exports <code>TF_DATA_DIR=/opt/terraform/tf-data/<project-name></code>.'' | |||
=== Vault / OpenBao === | === Vault / OpenBao === | ||
==== General notes ==== | ==== General notes ==== | ||
;OpenTofu support | ;OpenTofu support | ||
As of 2026- | As of 2026-09-06, the Vault provider isn't compiled for FreeBSD. You need to use Terraform instead. | ||
;Vault | ;Vault | ||
| Line 22: | Line 115: | ||
$ cd /opt/salt/nasqueron-operations/terraform/openbao | $ cd /opt/salt/nasqueron-operations/terraform/openbao | ||
$ | $ tf init # install any newly required providers and modules | ||
$ | $ tf plan | ||
$ | $ tf apply | ||
</syntaxhighlight> | </syntaxhighlight> | ||
It could be interesting to maintain an alternative policy to admin for the Terraform provider, restricted to the current actions. Note benefits would be limited as those include policies management. | It could be interesting to maintain an alternative policy to admin for the Terraform provider, restricted to the current actions. Note that the benefits would be limited as those include policies management. | ||
==== Propagate secrets (DRP) ==== | ==== Propagate secrets (DRP) ==== | ||
| Line 37: | Line 130: | ||
;Full procedure | ;Full procedure | ||
Once the AppRole | Once the AppRole has been created in Vault, its credentials need to be provisioned to the relevant configuration files. | ||
<syntaxhighlight lang="shell"> | <syntaxhighlight lang="shell"> | ||
$ cd /opt/salt/nasqueron-operations/terraform/openbao | $ cd /opt/salt/nasqueron-operations/terraform/openbao | ||
$ | $ tf init # if you've a new entry requiring a module, it needs to be installed | ||
$ | $ tf plan | ||
$ | $ tf apply | ||
$ cd /opt/salt/nasqueron-operations | $ cd /opt/salt/nasqueron-operations | ||
| Line 51: | Line 144: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
<div class="alert">Important | <div class="alert">Important: Each time you reprovision the secrets, they will change. | ||
Don't forget to always apply this full procedure. | Don't forget to always apply this full procedure. | ||
</div> | </div> | ||
=== CARP OVH secrets usage === | ==== CARP OVH secrets usage ==== | ||
The OVH credentials used by the CARP failover script are stored in Vault at: | The OVH credentials used by the CARP failover script are stored in Vault at: | ||
| Line 62: | Line 155: | ||
'''apps/network/carp-hyper-001-switch''' | '''apps/network/carp-hyper-001-switch''' | ||
These credentials are accessed by router-002 and router-003 | These credentials are accessed by router-002 and router-003. They are then used by a script triggered by devd to update the OVH failover IP MAC address depending on the CARP state (MASTER/BACKUP). The script authenticates to Vault using AppRole (via secretsmith) to retrieve them. | ||
They are used by a script triggered by devd to update the OVH failover IP MAC address depending on the CARP state (MASTER/BACKUP). | |||
The AppRole credentials (role_id and secret_id) used by the | The AppRole credentials (role_id and secret_id) used by the script are stored in: | ||
'''ops/secrets/network/router/vault''' | '''ops/secrets/network/router/vault''' | ||
Vault secrets are provisioned using Terraform (openbao module), and must be propagated to the routers using Salt. | |||
The Vault secrets may need to be rotated, so we need a simple way to propagate | |||
updated credentials to routers. This is enabled by the mechanism | |||
introduced in D4026. | |||
=== ovh-ops-backups === | |||
This configuration is handled by Terraform. | |||
It requires full access to the public cloud project for backups, as the project is fully configured by these Terraform files. | |||
You'll need to get the application key, application secret, and consumer key and write them in your $HOME/.ovh.conf on Complector. | |||
You can also generate the file with <code>ovhcloud login</code>. | |||
== | == First Terraform configuration (DRP) == | ||
=== Initialize Terraform working directories === | |||
Terraform and OpenTofu projects are currently deployed from Complector. | |||
The states are encrypted, and a wrapper decrypts them for each operation. | |||
You need to restore the contents of /opt/terraform.enc from a backup, or if it is not available, initialize a new encrypted directory. | |||
Currently, the procedure is specific to FreeBSD, as it requires PEFS to encrypt directories. | |||
# On the salt primary server, deploy the full roles/salt-primary, or at least the roles/salt-primary/opentofu unit | |||
| | # Ensure pefs is active with <code>kldstat | grep pefs</code> | ||
# Ensure Vault is up, then <code>vault kv put ops/infra/complector/terraform/pefs password=$(openssl rand -hex 48)</code> | |||
# Run <code>initialize-pefs ops/infra/complector/terraform/pefs /opt/terraform.enc /opt/terraform/encrypted</code> | |||
The previous version of the Vault secret, if still available, belongs to the previous directory; keep it if you need to restore that directory. However, you need a new password when creating a fresh encrypted directory. | |||
== Troubleshoot == | == Troubleshoot == | ||
=== Error: Module not installed === | === Error: Module not installed === | ||
You need to run <code> | You need to run <code>tf init</code> to prepare for any new provider. | ||
<syntaxhighlight lang="shell"> | <syntaxhighlight lang="shell"> | ||
$ | $ tf plan | ||
│ Error: Module not installed | │ Error: Module not installed | ||
| Line 111: | Line 209: | ||
<syntaxhighlight lang="shell"> | <syntaxhighlight lang="shell"> | ||
$ | $ tf init | ||
Initializing the backend... | Initializing the backend... | ||
| Line 127: | Line 225: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
On OpenTofu, some | On OpenTofu, some providers are only compiled for Linux, not for FreeBSD. | ||
Switch to Terraform pending a solution to help the OpenTofu builds. | Switch to Terraform pending a solution to help the OpenTofu builds. | ||
To allow the wrapper to run <code>terraform</code> commands, add your project directory to the <code>STILL_REQUIRE_TERRAFORM</code> variable in {{Ops file|roles/salt-primary/opentofu/files/tf.sh}}. | |||
=== State rollback === | |||
When declaring new Terraform resources, it's possible to remove from the state an item, without actually destroying it at infrastructure level: | |||
$ tf state rm <state name> | |||
Removed ….… | |||
Successfully removed 1 resource instance(s). | |||
[[Category:Operations grimoire]] | [[Category:Operations grimoire]] | ||
[[Category:Terraform]] | [[Category:Terraform]] | ||
Latest revision as of 21:08, 6 September 2026
Overview
In addition to Salt, Nasqueron manages Infrastructure as Code using Terraform and OpenTofu. We deploy from Complector using /opt/salt/nasqueron-operations as our local copy of rOPS, authoritative for both Salt and Terraform.
We provide a tf wrapper that automatically runs OpenTofu or Terraform based on the directory and mounts the encrypted state directory on the fly.
Paths
Terraform states are stored on a local backend. To protect sensitive infrastructure state (which often contains plaintext secrets and topology data) at rest, we use a split-architecture approach combining ZFS and PEFS (Private Encrypted File System).
| Path | Type | Purpose | Filesystem Properties |
|---|---|---|---|
/opt/salt/nasqueron-operations/terraform/
|
Standard Dir | Terraform/OpenTofu declarative code | Standard ZFS |
/opt/terraform.enc
|
ZFS Dataset | Underlying encrypted block storage for PEFS | compression=zstd
|
/opt/terraform/encrypted
|
PEFS Mount | Encrypted state files. Mounted only during TF operations. | AES-256-XTS |
/opt/terraform/tf-data
|
ZFS Dataset | Provider cache and modules files. TF_DATA_DIR target.
|
compression=lz4, recordsize=128K (optimized for Go binaries)
|
Access control
- PEFS operations (mounting, adding keys) are executed as the
deployuser. - The wrapper verifies that the invoking user belongs to the
opsordeploygroup before proceeding. - The PEFS passphrase is stored in HashiCorp Vault at
ops/infra/complector/terraform/pefs.
Standard operations
The tf wrapper automatically mounts PEFS, fetches the decryption key from Vault, executes the command, and unmounts PEFS upon exit, even on failure or Ctrl+C.
You can use it exactly like you would use terraform or tofu commands:
$ cd /opt/salt/nasqueron-operations/terraform/ovh-ops-backups
$ tf init
$ tf plan
$ tf apply
Specific deployment notes
Maintenance of .tfstate files
For manual state surgery or moving files, use the explicit mount commands. These bypass the automatic unmount.
# Mount PEFS and load keys for manual maintenance
tf mount
# Perform manual operations (e.g., moving state files)
cd /opt/terraform/encrypted/tf-states
# ...
# Unmount when finished
tf unmount
Choose between opentofu or terraform
By default, tofu is used.
The wrapper maintains an allowlist of projects that still require HashiCorp Terraform.
The main blocker is hashicorp/vault, not compiled for FreeBSD on OpenTofu registry.
To add or remove a project from this list, edit the STILL_REQUIRE_TERRAFORM variable in rOPS: roles/salt-primary/opentofu/files/tf.sh.
New project configuration
Every Terraform project must explicitly configure its local backend to write state into the PEFS-encrypted mount.
Add this to your project's backend.tf or providers.tf:
terraform {
backend "local" {
path = "/opt/terraform/encrypted/tf-states/<project-name>/terraform.tfstate"
}
}
Note: The provider cache path does not need to be configured in HCL. The wrapper automatically exports TF_DATA_DIR=/opt/terraform/tf-data/<project-name>.
Vault / OpenBao
General notes
- OpenTofu support
As of 2026-09-06, the Vault provider isn't compiled for FreeBSD. You need to use Terraform instead.
- Vault
You need a Vault token to allow the provider to connect.
You also need to set VAULT_ADDR to https://172.27.27.7:8200 as Vault doesn't listen on 127.0.0.1
$ export VAULT_ADDR=https://172.27.27.7:8200
$ sudo /opt/salt/nasqueron-operations/utils/vault/issue-admin-token.py > ~/.vault-token
$ cd /opt/salt/nasqueron-operations/terraform/openbao
$ tf init # install any newly required providers and modules
$ tf plan
$ tf apply
It could be interesting to maintain an alternative policy to admin for the Terraform provider, restricted to the current actions. Note that the benefits would be limited as those include policies management.
Propagate secrets (DRP)
- No automatic secret rotation
Secrets rotation is disabled with a lifecycle management ignore_changes = [ secret_id, ].
To rotate a secret, it needs first to be destroyed from terraform state:
terraform destroy -target=module.viperserv_approle.vault_approle_auth_backend_role_secret_id.this
- Full procedure
Once the AppRole has been created in Vault, its credentials need to be provisioned to the relevant configuration files.
$ cd /opt/salt/nasqueron-operations/terraform/openbao
$ tf init # if you've a new entry requiring a module, it needs to be installed
$ tf plan
$ tf apply
$ cd /opt/salt/nasqueron-operations
$ salt windriver state.sls_id /usr/local/etc/secrets/rhyne-wyse.yaml roles/reports/rhyne-wyse/config
$ salt windriver state.sls_id /srv/viperserv/.credentials roles/viperserv/eggdrop/config
# Missing for router
Don't forget to always apply this full procedure.
CARP OVH secrets usage
The OVH credentials used by the CARP failover script are stored in Vault at:
apps/network/carp-hyper-001-switch
These credentials are accessed by router-002 and router-003. They are then used by a script triggered by devd to update the OVH failover IP MAC address depending on the CARP state (MASTER/BACKUP). The script authenticates to Vault using AppRole (via secretsmith) to retrieve them.
The AppRole credentials (role_id and secret_id) used by the script are stored in:
ops/secrets/network/router/vault
Vault secrets are provisioned using Terraform (openbao module), and must be propagated to the routers using Salt.
The Vault secrets may need to be rotated, so we need a simple way to propagate updated credentials to routers. This is enabled by the mechanism introduced in D4026.
ovh-ops-backups
This configuration is handled by Terraform.
It requires full access to the public cloud project for backups, as the project is fully configured by these Terraform files.
You'll need to get the application key, application secret, and consumer key and write them in your $HOME/.ovh.conf on Complector.
You can also generate the file with ovhcloud login.
First Terraform configuration (DRP)
Initialize Terraform working directories
Terraform and OpenTofu projects are currently deployed from Complector. The states are encrypted, and a wrapper decrypts them for each operation.
You need to restore the contents of /opt/terraform.enc from a backup, or if it is not available, initialize a new encrypted directory. Currently, the procedure is specific to FreeBSD, as it requires PEFS to encrypt directories.
- On the salt primary server, deploy the full roles/salt-primary, or at least the roles/salt-primary/opentofu unit
- Ensure pefs is active with
kldstat | grep pefs - Ensure Vault is up, then
vault kv put ops/infra/complector/terraform/pefs password=$(openssl rand -hex 48) - Run
initialize-pefs ops/infra/complector/terraform/pefs /opt/terraform.enc /opt/terraform/encrypted
The previous version of the Vault secret, if still available, belongs to the previous directory; keep it if you need to restore that directory. However, you need a new password when creating a fresh encrypted directory.
Troubleshoot
Error: Module not installed
You need to run tf init to prepare for any new provider.
$ tf plan
│ Error: Module not installed
│
│ on rhyne_wyse.tf line 23:
│ 23: module "rhyne_wyse_approle" {
│
│ This module is not yet installed. Run "tofu init" to install all modules required by this configuration.
Error: Incompatible provider version
$ tf init
Initializing the backend...
Initializing modules...
- rhyne_wyse_approle in modules/app_credentials
Initializing provider plugins...
- Finding hashicorp/vault versions matching "5.3.0"...
╷
│ Error: Incompatible provider version
│
│ Provider registry.opentofu.org/hashicorp/vault v5.3.0 does not have a package available for your current platform, freebsd_amd64.
│
│ Provider releases are separate from OpenTofu CLI releases, so not all providers are available for all platforms. Other versions of this provider may have different platforms supported.
On OpenTofu, some providers are only compiled for Linux, not for FreeBSD. Switch to Terraform pending a solution to help the OpenTofu builds.
To allow the wrapper to run terraform commands, add your project directory to the STILL_REQUIRE_TERRAFORM variable in rOPS: roles/salt-primary/opentofu/files/tf.sh.
State rollback
When declaring new Terraform resources, it's possible to remove from the state an item, without actually destroying it at infrastructure level:
$ tf state rm <state name> Removed ….… Successfully removed 1 resource instance(s).
