Skip to content

Commit 00ea92d

Browse files
authored
[remote-state] Improve backend compatibility (#105)
1 parent 0f393cd commit 00ea92d

File tree

3 files changed

+83
-110
lines changed

3 files changed

+83
-110
lines changed

modules/remote-state/README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,58 @@
22

33
Terraform module that accepts a component and a stack name and returns remote state outputs for the component.
44

5-
The module supports `s3` and `remote` (Terraform Cloud) backends.
5+
The module supports all backends supported by Terraform and OpenTofu, plus the Atmos-specific `static` backend.
6+
7+
8+
### Errors
9+
10+
> [!NOTE]
11+
>
12+
> If you experience an error from the `terraform_remote_state` data source,
13+
> this is most likely not an error in this module, but rather an error in the
14+
> `remote_state` configuration in the referenced stack. This module performs
15+
> no validation on the remote state configuration, and only modifies the configuration
16+
> for the `remote` backend (to set the workspace name) and,
17+
> _only when `var.privileged` is set to `true`_, the `s3` configuration (to remove
18+
> settings for assuming a role). If `var.privileged` is left at the default value of `false`
19+
> and you are not using the `remote` backend, then this module will not modify the backend
20+
> configuration in any way.
21+
22+
### "Local" Backend
23+
24+
> [!IMPORTANT]
25+
>
26+
> If the local backend has a relative path, it will be resolved
27+
> relative to the current working directory, which is usually a root module
28+
> referencing the remote state. However, when the local backend is created,
29+
> the current working directory is the directory where the target root module
30+
> is defined. This can cause the lookup to fail if the source is not reachable
31+
> from the client directory as `../source`.
32+
33+
For example, if your directory structure looks like this:
34+
35+
```text
36+
project
37+
├── components
38+
│   ├── client
39+
│   │   └── main.tf
40+
│   └── complex
41+
│   └── source
42+
│   └── main.tf
43+
└── local-state
44+
└── complex
45+
└── source
46+
└── terraform.tfstate
47+
```
48+
49+
Terraform code in `project/components/complex/source` can create its local state
50+
file (`terraform.tfstate`) in the `local-state/complex/source`
51+
directory using `path = "../../../local-state/complex/source/terraform.tfstate"`.
52+
However, Terraform code in `project/components/client` that references the same
53+
local state using the same backend configuration will fail because the current
54+
working directory is `project/components/client` and the relative path will not
55+
resolve correctly.
56+
657

758
## Usage
859

Lines changed: 28 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,54 @@
11
locals {
2-
data_source_backends = ["local", "remote", "s3", "azurerm", "gcs"]
3-
is_data_source_backend = contains(local.data_source_backends, local.backend_type)
2+
custom_backends = ["none", "bypass", "static"]
3+
is_data_source_backend = !contains(local.custom_backends, local.backend_type)
44

55
remote_workspace = var.workspace != null ? var.workspace : local.workspace
66
ds_backend = local.is_data_source_backend ? local.backend_type : "none"
77
ds_workspace = local.ds_backend == "none" ? null : local.remote_workspace
88

9+
# The `privileged` flag is no longer used in the Cloud Posse reference architecture, but is maintained for compatibility.
10+
# This was and is only supported for the S3 backend.
11+
#
12+
# When the `privileged` flag is set to `true`, the user running Terraform is considered privileged and therefore
13+
# does not need to assume a different role to access the S3 backend.
14+
#
15+
# This is accomplished by removing any profile or role ARN settings from the configuration.
16+
s3_privileged_backend = { for k, v in local.backend : k => v if !contains(["profile", "role_arn", "assume_role", "assume_role_with_web_identity"], k) }
17+
18+
# Workaround for the fact that the 2 different backends can be different types,
19+
# but both results of a conditional must be the same type.
20+
s3_backend = {
21+
# normal, not privileged
22+
false = local.backend
23+
# privileged
24+
true = local.s3_privileged_backend
25+
}
26+
27+
# Customize certain configurations. Otherwise we will just use whatever was configured in the stack.
928
ds_configurations = {
1029
# If no valid configuration is found for the backend datasource, provide a dummy one.
1130
none = {
1231
path = "${path.module}/dummy-remote-state.json"
1332
}
1433

15-
# Note: If the local backend has a relative path, it will be resolved
16-
# relative to the current working directory, which is usually a root module
17-
# referencing the remote state. However, when the local backend is created,
18-
# the current working directory is the directory where the target root module
19-
# is defined. This will likely cause the lookup to fail unless the current
20-
# and target root module directories are in the same directory.
21-
#
22-
# Both path and workspace_dir are optional.
23-
local = local.ds_backend != "local" ? null : merge({},
24-
try(length(lookup(local.backend, "path", "")), 0) > 0 ? {
25-
path = lookup(local.backend, "path", "")
26-
} : {},
27-
try(length(lookup(local.backend, "workspace_dir", "")), 0) > 0 ? {
28-
workspace_dir = lookup(local.backend, "workspace_dir", "")
29-
} : {}
30-
)
31-
32-
remote = local.ds_backend != "remote" ? null : {
33-
organization = local.backend.organization
34-
34+
remote = merge(local.backend, {
3535
workspaces = {
3636
name = local.remote_workspace
3737
}
38-
}
39-
40-
s3 = local.ds_backend != "s3" ? null : {
41-
encrypt = local.backend.encrypt
42-
bucket = local.backend.bucket
43-
key = local.backend.key
44-
dynamodb_table = local.backend.dynamodb_table
45-
region = local.backend.region
46-
47-
# NOTE: component types
48-
# Privileged components are those that require elevated (root-level) permissions to provision and access their remote state.
49-
# For example: `tfstate-backend`, `account`, `account-map`, `account-settings`, `iam-primary`.
50-
# Privileged components are usually provisioned during cold-start (when we don't have any IAM roles provisioned yet) by using an admin user credentials.
51-
# To access the remote state of privileged components, the caller needs to have permissions to access the backend and the remote state without assuming roles.
52-
# Regular components, on the other hand, don't require root-level permissions and are provisioned and their remote state is accessed by assuming IAM roles (or using profiles).
53-
# For example: `vpc`, `eks`, `rds`
54-
55-
# NOTE: global `backend` config
56-
# The global `backend` config should be declared in a global YAML stack config file (e.g. `globals.yaml`)
57-
# where all stacks can import it and have access to it (note that the global `backend` config is organization-wide and will not change after cold-start).
58-
# The global `backend` config in the global config file should always have the `role_arn` or `profile` specified (added after the cold-start).
59-
60-
# NOTE: components `backend` config
61-
# The `backend` portion for each individual component should be declared in a catalog file (e.g. `stacks/catalog/<component>.yaml`)
62-
# along with all the default values for a component.
63-
# The `privileged` attribute should always be declared in the `backend` portion for each individual component in the catalog.
64-
# Top-level stacks where a component is provisioned import the component's catalog (the default values and the component's backend config portion) and can override the default values.
65-
66-
# NOTE: `cold-start`
67-
# During cold-start we don't have any IAM roles provisioned yet, so we use an admin user credentials to provision the privileged components.
68-
# The `privileged` attribute for the privileged components should be set to `true` in the components' catalog,
69-
# and the privileged components should be provisioned using an admin user credentials.
70-
71-
# NOTE: after `cold-start`
72-
# After the privileged components (including the primary IAM roles) are provisioned, we update the global `backend` config in the global config file
73-
# to add the IAM role or profile to access the backend (after this, the global `backend` config should never change).
74-
# For some privileged components we can change the `privileged` attribute in the YAML config from `true` to `false`
75-
# to allow the regular components to access their remote state (e.g. we set the `privileged` attribute to `false` in the `account-map` component
76-
# since we use `account-map` in almost all regular components.
77-
# For each regular component, set the `privileged` attribute to `false` in the components' portion of `backend` config (in `stacks/catalog/<component>.yaml`)
78-
79-
# Advantages:
80-
# The global `backend` config is specified just once in the global config file, IAM role or profile is added to it after the cold start,
81-
# and after that the global `backend` config never changed.
82-
# We can make a component privileged or not any time by just updating its `privileged` attribute in the component's catalog file.
83-
# We can change a component's `backend` portion any time without touching/affection the backend configs of all other components (e.g. when we add a new
84-
# component, we don't touch the `globals.yaml` file at all, and we don't update the component's `role_arn` and `profile` settings).
85-
86-
# Use the role to access the remote state if the component is not privileged and `role_arn` is specified
87-
role_arn = !coalesce(try(local.backend.privileged, null), var.privileged) && contains(keys(local.backend), "role_arn") ? local.backend.role_arn : null
88-
89-
# Use the profile to access the remote state if the component is not privileged and `profile` is specified
90-
profile = !coalesce(try(local.backend.privileged, null), var.privileged) && contains(keys(local.backend), "profile") ? local.backend.profile : null
91-
92-
workspace_key_prefix = local.workspace_key_prefix
93-
94-
# S3-compatible backend for Oracle
95-
# source: https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/terraformUsingObjectStore.htm#s3
96-
skip_region_validation = try(local.backend.skip_region_validation, null)
97-
skip_credentials_validation = try(local.backend.skip_credentials_validation, null)
98-
skip_requesting_account_id = try(local.backend.skip_requesting_account_id, null)
99-
use_path_style = try(local.backend.use_path_style, null)
100-
force_path_style = try(local.backend.force_path_style, null)
101-
skip_metadata_api_check = try(local.backend.skip_metadata_api_check, null)
102-
skip_s3_checksum = try(local.backend.skip_s3_checksum, null)
103-
endpoints = try(local.backend.endpoints, null)
104-
endpoint = try(local.backend.endpoint, null)
105-
}
106-
107-
azurerm = local.ds_backend != "azurerm" ? null : {
108-
resource_group_name = local.backend.resource_group_name
109-
storage_account_name = local.backend.storage_account_name
110-
container_name = local.backend.container_name
111-
key = local.backend.key
112-
}
113-
114-
gcs = local.ds_backend != "gcs" ? null : {
115-
bucket = local.backend.bucket
116-
prefix = local.backend.prefix
117-
}
38+
})
11839

40+
s3 = local.s3_backend[var.privileged]
11941
} # ds_configurations
12042

12143
}
12244

12345
data "terraform_remote_state" "data_source" {
12446
count = var.bypass ? 0 : 1
12547

48+
# Use a dummy local backend when the real backend is not supported by the data source
12649
backend = local.ds_backend == "none" ? "local" : local.ds_backend
12750
workspace = local.ds_workspace
128-
config = local.ds_configurations[local.ds_backend]
129-
defaults = var.defaults
51+
# If nothing needs to be customized, just use whatever was configured in the stack
52+
config = lookup(local.ds_configurations, local.ds_backend, local.backend)
53+
defaults = var.defaults
13054
}

modules/remote-state/main.tf

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ locals {
1717
config = try(yamldecode(data.utils_component_config.config[0].output), {})
1818

1919
remote_state_backend_type = try(local.config.remote_state_backend_type, "")
20-
backend_type = try(coalesce(local.remote_state_backend_type, local.config.backend_type), "")
20+
backend_type = try(coalesce(local.remote_state_backend_type, local.config.backend_type), "none")
2121

2222
# If `config.remote_state_backend` is not declared in YAML config, the default value will be an empty map `{}`
2323
backend_config_key = try(local.config.remote_state_backend, null) != null && try(length(local.config.remote_state_backend), 0) > 0 ? "remote_state_backend" : "backend"
@@ -30,12 +30,10 @@ locals {
3030

3131
backend = local.backend_configs[local.backend_config_key]
3232

33-
workspace = lookup(local.config, "workspace", "")
34-
workspace_key_prefix = lookup(local.backend, "workspace_key_prefix", null)
33+
workspace = lookup(local.config, "workspace", "")
34+
# workspace_key_prefix = lookup(local.backend, "workspace_key_prefix", null)
3535

3636
remote_states = {
37-
# s3 = data.terraform_remote_state.s3
38-
# remote = data.terraform_remote_state.remote
3937
data_source = try(data.terraform_remote_state.data_source[0].outputs, var.defaults)
4038
bypass = var.defaults
4139
static = local.backend

0 commit comments

Comments
 (0)