Skip to content

Commit 535528f

Browse files
authored
Add documentation for OpenTofu remote state (#784)
* wip: add TF remote state docs * wip s3 remote state * improve gitlab backend configuration * automate s3 creds * make s3 buckets clearer * fix linting * try to allow same headings at different levels in markdown * fix tf lint errors * fix prettier errors
1 parent 06857df commit 535528f

File tree

5 files changed

+259
-0
lines changed

5 files changed

+259
-0
lines changed

.markdownlint.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"no-duplicate-heading": {
3+
"siblings_only": true
4+
}
5+
}

docs/opentofu-remote-state.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# OpenTofu remote state
2+
3+
OpenTofu supports a number of [remote state backends](https://opentofu.org/docs/language/state/remote/)
4+
which can be used to persist state independently of where a deployment is run.
5+
This allows deployments to be made from anywhere that can access the state
6+
without corrupting or conflicting with any existing resources from previous
7+
deployments.
8+
9+
Using remote state is therefore strongly recommended for environments which
10+
should only be instantiated once, e.g. `production` and `staging`.
11+
12+
This page provides guidance for configuring remote states using backends
13+
commonly available on OpenStack deployments.
14+
15+
> [!IMPORTANT]
16+
> In the below replace `$ENV` with the relevant environment name.
17+
18+
## GitLab
19+
20+
GitLab can be used with the [http backend](https://opentofu.org/docs/language/settings/backends/http/)
21+
to store separate states for each environment within the GitLab project.
22+
Access is protected by GitLab access tokens, which in the approach below are
23+
persisted to local files. Therefore each repository checkout will need to
24+
authenticate separately, using either a separate token or a shared token from
25+
some external secret store.
26+
27+
The below is based on the [official docs](https://docs.gitlab.com/user/infrastructure/iac/terraform_state/)
28+
but includes some missing details and is modified for common appliance workflows.
29+
30+
### Initial setup
31+
32+
1. Create the backend file:
33+
34+
```shell
35+
cp environments/site/tofu/example-backends/gitlab.tf environments/$ENV/tofu
36+
```
37+
38+
2. Modify `environments/$ENV/tofu/gitlab.tf` to set the default for the
39+
project ID. This can be found by clicking the 3-dot menu at the top right of
40+
the GitLab project page.
41+
42+
```terraform
43+
# environments/$ENV/tofu/backend.tf:
44+
terraform {
45+
backend "http" {}
46+
}
47+
```
48+
49+
3. Commit it.
50+
51+
4. Follow the per-checkout steps below.
52+
53+
### Per-checkout configuration
54+
55+
1. Create an access token in the GitLab UI, using either:
56+
57+
a. If project access tokens are available, create one via
58+
Project > Settings > Access tokens.
59+
The token must have `Maintainer` role and `api` scope.
60+
61+
b. Otherwise create a personal access token via
62+
User profile > Preferences > Access tokens.
63+
The token must have `api` scope.
64+
65+
Copy the generated secret and set an environment variable:
66+
67+
```shell
68+
export TF_VAR_gitlab_access_token=$secret
69+
```
70+
71+
2. If using a personal access token, set the GitLab username as an environment variable:
72+
73+
```shell
74+
export TF_VAR_gitlab_username=$your_username
75+
```
76+
77+
3. With the environment activated, initialise OpenTofu.
78+
79+
If no local state exists run:
80+
81+
```shell
82+
cd environments/$ENV/tofu/
83+
tofu init
84+
```
85+
86+
otherwise append `-migrate-state` to the `init` command to attempt to copy
87+
local state to the new backend.
88+
89+
OpenTofu is now configured to use GitLab to store state for this environment.
90+
91+
Repeat for each environment needing remote state.
92+
93+
> [!CAUTION]
94+
> The GitLab credentials are [persisted](https://opentofu.org/docs/language/settings/backends/configuration/#credentials-and-sensitive-data)
95+
> into a file `environments/$ENV/tofu/.terraform/terraform.tfstate` and any
96+
> plan files. These should therefore not be committed.
97+
98+
### Token expiry
99+
100+
If the project token expires repeat the per-checkout configuration, but using
101+
`opentofu init -reconfigure` instead.
102+
103+
## S3
104+
105+
For clouds with S3-compatible object storage (e.g. Ceph with [radosgw](https://docs.ceph.com/en/latest/radosgw/))
106+
the S3 backend can be used. This approach uses a bucket per environment and
107+
derives credentials from OpenStack credentials, meaning no backend-specific
108+
per-checkout configuration is required.
109+
110+
### Initial setup
111+
112+
1. Create an S3 bucket with a name `${cluster_name}-${environment_name}-tfstate`
113+
where:
114+
115+
- `CLUSTER_NAME` is defined in `environments/$ENV/tofu/main.tf`
116+
- `$ENVIRONMENT_NAME` is the name of the environment directory
117+
118+
e.g.
119+
120+
```shell
121+
openstack container create research-staging-tfstate
122+
```
123+
124+
2. Create `ec2` credentials:
125+
126+
```shell
127+
openstack ec2 credentials create
128+
```
129+
130+
and make a note of the `access` field returned.
131+
132+
3. Create the backend file:
133+
134+
```shell
135+
cp environments/site/tofu/example-backends/s3.tf environments/$ENV/tofu
136+
```
137+
138+
4. Modify `environments/$ENV/tofu/s3.tf` to set the default for `s3_backend_endpoint`.
139+
This is the radosgw address. If not known it can be determined by creating a
140+
public bucket, and then getting the URL using
141+
Project > Containers > (your public bucket) > Link
142+
which provides a URL of the form `https://$ENDPOINT/swift/...`.
143+
144+
5. Add the following to `environments/$ENV/activate`:
145+
146+
```bash
147+
export AWS_ACCESS_KEY_ID=$EC2_CREDENTIALS_ACCESS
148+
export AWS_SECRET_ACCESS_KEY=$(openstack ec2 credentials show $AWS_ACCESS_KEY_ID -f value -c secret)
149+
```
150+
151+
replacing `$EC2_CREDENTIALS_ACCESS` with the `access` field of the created
152+
credentials.
153+
154+
This avoids these credentials being persisted in local files.
155+
156+
6. Copy the lines above into your shell to set them for your current shell.
157+
158+
7. With the environment activated, initialise OpenTofu.
159+
160+
If no local state exists run:
161+
162+
```shell
163+
cd environments/$ENV/tofu/
164+
tofu init
165+
```
166+
167+
otherwise append `-migrate-state` to the `init` command to attempt to copy
168+
local state to the new backend.
169+
170+
8. If this fails, try setting `use_path_style = true` in `environments/$ENV/tofu/s3.tf`.
171+
172+
9. Once it works, commit `environments/$ENV/tofu/s3.tf` and `environments/$ENV/activate`.
173+
174+
OpenTofu is now configured to use the cloud's S3-compatible storage to store
175+
state for this environment.
176+
177+
Repeat for each environment needing remote state.
178+
179+
For more configuration options, see the OpenTofu [s3 backend docs](https://opentofu.org/docs/language/settings/backends/s3/).
180+
181+
### Per-checkout configuration
182+
183+
The ec2 credentials will automatically be loaded when activating the environment.
184+
For a new checkout simply initialise OpenTofu as normal as described in step 7 above.

docs/production.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ The value chosen should be the highest value demonstrated during testing. Note
316316
that any time spent blocked due to this parallelism limit does not count
317317
against the (un-overridable) internal OpenTofu timeout of 30 minutes
318318

319+
Consider configuring [OpenTofu remote state](./opentofu-remote-state.md) for any
320+
environments which should be unique, e.g. production and staging.
321+
319322
## Configure appliance
320323

321324
### Production configuration to consider
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
variable "gitlab_username" {
2+
type = string
3+
description = <<-EOF
4+
Username of actual GitLab user, for personal access token only.
5+
Default uses bot account name, for project access token.
6+
EOF
7+
default = null
8+
}
9+
10+
variable "gitlab_access_token" {
11+
type = string
12+
description = <<-EOF
13+
GitLab Project or Personal access token.
14+
Must have Maintainer role (for Project token) and API scope
15+
EOF
16+
}
17+
18+
variable "gitlab_project_id" {
19+
type = string
20+
description = "GitLab project ID - click 3-dot menu at the top right of project page"
21+
#default = # add here
22+
}
23+
24+
locals {
25+
gitlab_username = coalesce(var.gitlab_username, "project_${var.gitlab_project_id}_bot")
26+
gitlab_state_name = basename(var.environment_root)
27+
gitlab_state_address = "https://gitlab.com/api/v4/projects/${var.gitlab_project_id}/terraform/state/${local.gitlab_state_name}"
28+
}
29+
30+
# tflint-ignore: terraform_required_version
31+
terraform {
32+
backend "http" {
33+
address = local.gitlab_state_address
34+
lock_address = "${local.gitlab_state_address}/lock"
35+
unlock_address = "${local.gitlab_state_address}/lock"
36+
username = local.gitlab_username
37+
password = var.gitlab_access_token
38+
lock_method = "POST"
39+
unlock_method = "DELETE"
40+
retry_wait_min = 5
41+
}
42+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
variable "s3_backend_endpoint" {
2+
type = string
3+
description = "radosgw address without protocol or path e.g. leafcloud.store"
4+
#default = # add here
5+
}
6+
7+
# tflint-ignore: terraform_required_version
8+
terraform {
9+
backend "s3" {
10+
endpoint = var.s3_backend_endpoint
11+
bucket = "${var.cluster_name}-${basename(var.environment_root)}-tfstate"
12+
key = "environment.tfstate"
13+
14+
# Reginon is required but not used in radosgw:
15+
region = "dummy"
16+
skip_region_validation = true
17+
18+
# Normally STS is not configured in radosgw:
19+
skip_credentials_validation = true
20+
21+
# Enable path-style S3 URLs (https://<HOST>/<BUCKET> instead of https://<BUCKET>.<HOST>)
22+
# may or may not be required depending on radosgw configuration
23+
use_path_style = true
24+
}
25+
}

0 commit comments

Comments
 (0)