generated from masterpointio/terraform-module-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.tf
376 lines (343 loc) · 17.1 KB
/
main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# This Terraform code automates the creation and management of Spacelift stacks based on the structure
# and configurations defined in the Git repository, default Stack values and additional input variables.
# It primarily relies on dynamic local expressions to generate configurations based on the
# input variables and Git structure so it can be consumed by Spacelift resources.
# This module can also manage the automation stack itself, but it should be bootstrapped manually.
#
# It handles the following:
#
# 1. Stack Configurations (see ## Stack Configurations)
# Reads the Spacelift stack configurations strictly based on the root modules structure in Git and file names.
# These are the configurations required to be set for a stack, e.g. project_root, terraform_workspace, root_module.
#
# 2. Common Stack configurations (see ## Common Stack configurations)
# Some configurations are equal across the whole root module, and can be set it on a root module level:
# * Space IDs: in the majority of cases all the workspaces in a root module belong to the same Spacelift space, so
# we allow setting a "global" space_id for all stacks on a root module level.
# * Autodeploy: if all the stacks in a root module should be autodeployed.
# * Administrative: if all the stacks in a root module are administrative, e.g stacks that manage Spacelift resources.
#
# 3. Labels (see ## Labels)
# Generates labels for the stacks based on administrative, dependency, and folder information.
#
# Syntax note:
# The local expression started with an underscore `_` is used to store intermediate values
# that are not directly used in the resource creation.
locals {
_multi_instance_structure = var.root_module_structure == "MultiInstance"
# Read all stack files following the associated root_module_structue convention:
# MultiInstance: root-module-name/stacks/*.yaml
# SingleInstance: root-module-name/stack.yaml
# Example:
# [
# MultiInstance:
# "../root-module-a/stacks/example.yaml",
# "../root-module-a/stacks/common.yaml",
# "../root-module-b/stacks/example.yaml",
# "../root-module-b/stacks/common.yaml",
# ] OR [
# SingleInstance:
# "../root-module-a/stack.yaml",
# "../root-module-b/stack.yaml",
# ]
# "../root-module-a/stacks/example.yaml",
# "../root-module-a/stacks/common.yaml",
# "../root-module-b/stacks/example.yaml",
# "../root-module-b/stacks/common.yaml",
# ] OR [
# "../root-module-a/stack.yaml",
# "../root-module-b/stack.yaml",
# ]
_multi_instance_stack_files = fileset("${path.root}/${var.root_modules_path}/*/stacks", "*.yaml")
_single_instance_stack_files = fileset("${path.root}/${var.root_modules_path}/*", "stack.yaml")
_all_stack_files = local._multi_instance_structure ? local._multi_instance_stack_files : local._single_instance_stack_files
# Extract the root module name from the stack file path
_all_root_modules = distinct([for file in local._all_stack_files : dirname(replace(replace(file, "../", ""), "stacks/", ""))])
# If all root modules are enabled, use all root modules, otherwise use only those given to us
enabled_root_modules = var.all_root_modules_enabled ? local._all_root_modules : var.enabled_root_modules
# Read and decode Stack YAML files from the root directory
# Example:
# MultiInstance: {
# "random-pet" = {
# "common.yaml" = {
# "stack_settings" = { ... }
# ...
# }
# "example.yaml" = {
# "stack_settings" = { ... }
# ...
# }
# }
# }
# SingleInstance: {
# "random-pet" = {
# "default" = { stack_settings = { ... }, ... }
# }
# }
_multi_instance_root_module_yaml_decoded = {
for module in local.enabled_root_modules : module => {
for yaml_file in fileset("${path.root}/${var.root_modules_path}/${module}/stacks", "*.yaml") :
yaml_file => yamldecode(file("${path.root}/${var.root_modules_path}/${module}/stacks/${yaml_file}"))
} if local._multi_instance_structure
}
_single_instance_root_module_yaml_decoded = {
for module in local.enabled_root_modules : module => {
"default" = yamldecode(file("${path.root}/${var.root_modules_path}/${module}/stack.yaml"))
} if !local._multi_instance_structure
}
_root_module_yaml_decoded = merge(local._multi_instance_root_module_yaml_decoded, local._single_instance_root_module_yaml_decoded)
## Common Stack configurations
# Retrieve common Stack configurations for each root module.
# SingleInstance root_module_structure does not support common configs today.
# Example:
# {
# "random-pet" = {
# "stack_settings" = {
# "description" = "This stack generates random pet names"
# "manage_state" = true
# }
# "tfvars" = {
# "enabled" = false
# }
# }
# }
_common_configs = {
for module, files in local._root_module_yaml_decoded : module => lookup(files, var.common_config_file, {})
}
# If we're SingleInstance, then default_tf_workspace_enabled is true. Otherwise, use given value.
_default_tf_workspace_enabled = local._multi_instance_structure ? var.default_tf_workspace_enabled : true
## Stack Configurations
# Merge all Stack configurations from the root modules into a single map, and filter out the common config.
# Example:
# {
# "random-pet-example" = {
# "project_root" = "examples/complete/components/random-pet"
# "root_module" = "random-pet"
# "stack_settings" = {
# "manage_state" = true
# }
# "terraform_workspace" = "example"
# "tfvars" = {
# "enabled" = true
# }
# }
# }
_root_module_stack_configs = merge([for module, files in local._root_module_yaml_decoded : {
for file, content in files :
local._multi_instance_structure ? "${module}-${trimsuffix(file, ".yaml")}" : module =>
merge(
{
# Use specified project_root, if not, build it using the root_modules_path and module name
"project_root" = try(content.stack_settings.project_root, replace(format("%s/%s", var.root_modules_path, module), "../", "")),
"root_module" = module,
# If default_tf_workspace_enabled is true, use "default" workspace, otherwise our file name is the workspace name
"terraform_workspace" = try(content.default_tf_workspace_enabled, local._default_tf_workspace_enabled) ? "default" : trimsuffix(file, ".yaml"),
# tfvars_file_name only pertains to MultiInstance, as SingleInstance expects consumers to use an auto.tfvars file.
# `yaml` is intentionally used here as we require Stack and `tfvars` config files to be named equally
"tfvars_file_name" = trimsuffix(file, ".yaml"),
},
content
) if file != var.common_config_file
}
]...)
# Get the configs for each stack, merged with the common configurations
# Example:
# {
# "random-pet-example" = {
# "project_root" = "examples/complete/components/random-pet"
# "root_module" = "random-pet"
# "stack_settings" = {
# "manage_state" = true
# }
# "terraform_workspace" = "example"
# "tfvars" = {
# "enabled" = false
# }
# }
# }
configs = {
for key, value in module.deep : key => value.merged
}
# Get the Stacks configs, this is just to improve code readability
# Example:
# {
# "random-pet-example" = {
# "manage_state" = true
# }
# }
stack_configs = {
for key, value in local.configs : key => value.stack_settings
}
# Get the list of all stack names
stacks = toset(keys(local.stack_configs))
## Labels
# Сreates a map of administrative labels for each stack that has the administrative property set to true.
# Example:
# {
# "spacelift-automation-mp-main" = [
# "administrative",
# ]
# "spacelift-policies-notify-tf-completed" = [
# "administrative",
# ]
# }
_administrative_labels = {
for stack, configs in local.stack_configs : stack => ["administrative"] if tobool(try(configs.administrative, false)) == true
}
# Creates a map of `depends-on` labels for each stack based on the root module level dependency configuration.
# Example:
# {
# "random-pet-example" = [
# "depends-on:spacelift-automation-default",
# ]
# }
_dependency_labels = {
for stack in local.stacks : stack => [
"depends-on:spacelift-automation-${terraform.workspace}"
]
}
# Creates a map of folder labels for each stack based on Git structure for a proper grouping stacks in Spacelift UI.
# https://docs.spacelift.io/concepts/stack/organizing-stacks#label-based-folders
# Example:
# {
# "random-pet-example" = [
# "folder:random-pet/example",
# ]
# }
_folder_labels = {
for stack in local.stacks : stack => [
local._multi_instance_structure ? "folder:${local.configs[stack].root_module}/${local.configs[stack].tfvars_file_name}" : "folder:${local.configs[stack].root_module}"
]
}
# Merge all the labels into a single map for each stack.
# Example:
# {
# "random-pet-example" = tolist([
# "folder:random-pet/example",
# "depends-on:spacelift-automation-default",
# ])
# }
labels = {
for stack in local.stacks :
stack => compact(flatten([
lookup(local._administrative_labels, stack, []),
lookup(local._folder_labels, stack, []),
lookup(local._dependency_labels, stack, []),
try(local.stack_configs[stack].labels, []),
]))
}
# Merge all before_init steps into a single map for each stack.
before_init = {
for stack in local.stacks : stack =>
# tfvars are implicitly enabled in MultiInstance, which means we include the tfvars copy command in before_init
# In SingleInstance, we expect the consumer to use an auto.tfvars file, so we don't include the tfvars copy command in before_init
try(local.configs[stack].tfvars.enabled, local._multi_instance_structure) ?
compact(concat(
var.before_init,
try(local.stack_configs[stack].before_init, []),
# This command is required for each stack.
# It copies the tfvars file from the stack's workspace to the root module's directory
# and renames it to `spacelift.auto.tfvars` to automatically load variable definitions for each run/task.
["cp tfvars/${local.configs[stack].tfvars_file_name}.tfvars spacelift.auto.tfvars"],
)) : compact(concat(
var.before_init,
try(local.stack_configs[stack].before_init, []),
))
}
}
# Perform deep merge for common configurations and stack configurations
module "deep" {
source = "cloudposse/config/yaml//modules/deepmerge"
version = "1.0.2"
for_each = local._root_module_stack_configs
# Stack configuration will take precedence and overwrite the conflicting value from the common configuration (if any)
maps = [local._common_configs[each.value.root_module], each.value]
# To support merging labels from common.yaml, we need lists to append instead of overwrite
append_list_enabled = true
}
resource "spacelift_stack" "default" {
for_each = local.stacks
administrative = coalesce(try(local.stack_configs[each.key].administrative, null), var.administrative)
additional_project_globs = try(local.stack_configs[each.key].additional_project_globs, var.additional_project_globs)
after_apply = compact(concat(try(local.stack_configs[each.key].after_apply, []), var.after_apply))
after_destroy = compact(concat(try(local.stack_configs[each.key].after_destroy, []), var.after_destroy))
after_init = compact(concat(try(local.stack_configs[each.key].after_init, []), var.after_init))
after_perform = compact(concat(try(local.stack_configs[each.key].after_perform, []), var.after_perform))
after_plan = compact(concat(try(local.stack_configs[each.key].after_plan, []), var.after_plan))
autodeploy = coalesce(try(local.stack_configs[each.key].autodeploy, null), var.autodeploy)
autoretry = try(local.stack_configs[each.key].autoretry, var.autoretry)
before_apply = compact(coalesce(try(local.stack_configs[each.key].before_apply, []), var.before_apply))
before_destroy = compact(coalesce(try(local.stack_configs[each.key].before_destroy, []), var.before_destroy))
before_init = local.before_init[each.key]
before_perform = compact(coalesce(try(local.stack_configs[each.key].before_perform, []), var.before_perform))
before_plan = compact(coalesce(try(local.stack_configs[each.key].before_plan, []), var.before_plan))
branch = try(local.stack_configs[each.key].branch, var.branch)
description = coalesce(try(local.stack_configs[each.key].description, null), var.description)
enable_local_preview = try(local.stack_configs[each.key].enable_local_preview, var.enable_local_preview)
enable_well_known_secret_masking = try(local.stack_configs[each.key].enable_well_known_secret_masking, var.enable_well_known_secret_masking)
github_action_deploy = try(local.stack_configs[each.key].github_action_deploy, var.github_action_deploy)
labels = local.labels[each.key]
manage_state = try(local.stack_configs[each.key].manage_state, var.manage_state)
name = each.key
project_root = local.configs[each.key].project_root
protect_from_deletion = try(local.stack_configs[each.key].protect_from_deletion, var.protect_from_deletion)
repository = try(local.stack_configs[each.key].repository, var.repository)
runner_image = try(local.stack_configs[each.key].runner_image, var.runner_image)
space_id = coalesce(try(local.stack_configs[each.key].space_id, null), var.space_id)
terraform_smart_sanitization = try(local.stack_configs[each.key].terraform_smart_sanitization, var.terraform_smart_sanitization)
terraform_version = try(local.stack_configs[each.key].terraform_version, var.terraform_version)
terraform_workflow_tool = var.terraform_workflow_tool
terraform_workspace = local.configs[each.key].terraform_workspace
worker_pool_id = try(local.stack_configs[each.key].worker_pool_id, var.worker_pool_id)
dynamic "github_enterprise" {
for_each = var.github_enterprise != null ? [var.github_enterprise] : []
content {
namespace = github_enterprise.value["namespace"]
id = github_enterprise.value["id"]
}
}
}
# The Spacelift Destructor is a feature designed to automatically clean up the resources no longer managed by our IaC.
# Don't toggle the creation/destruction of this resource with var.destructor_enabled,
# as it will delete all resources in the stack when toggled from 'true' to 'false'.
# Use the 'deactivated' attribute to disable the stack destructor functionality instead.
# https://github.com/spacelift-io/terraform-provider-spacelift/blob/master/spacelift/resource_stack_destructor.go
resource "spacelift_stack_destructor" "default" {
for_each = local.stacks
stack_id = spacelift_stack.default[each.key].id
deactivated = !try(local.stack_configs[each.key].destructor_enabled, var.destructor_enabled)
# `depends_on` should be used to make sure that all necessary resources (environment variables, roles, integrations, etc.)
# are still in place when the destruction run is executed.
# See https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/stack_destructor
depends_on = [
spacelift_drift_detection.default,
spacelift_aws_integration_attachment.default
]
}
resource "spacelift_aws_integration_attachment" "default" {
for_each = {
for stack, configs in local.stack_configs : stack => configs
if try(configs.aws_integration_enabled, var.aws_integration_enabled)
}
integration_id = try(local.stack_configs[each.key].aws_integration_id, var.aws_integration_id)
stack_id = spacelift_stack.default[each.key].id
read = var.aws_integration_attachment_read
write = var.aws_integration_attachment_write
}
resource "spacelift_drift_detection" "default" {
for_each = {
for stack, configs in local.stack_configs : stack => configs
if try(configs.drift_detection_enabled, var.drift_detection_enabled)
}
stack_id = spacelift_stack.default[each.key].id
ignore_state = try(local.stack_configs[each.key].drift_detection_ignore_state, var.drift_detection_ignore_state)
reconcile = try(local.stack_configs[each.key].drift_detection_reconcile, var.drift_detection_reconcile)
schedule = try(local.stack_configs[each.key].drift_detection_schedule, var.drift_detection_schedule)
timezone = try(local.stack_configs[each.key].drift_detection_timezone, var.drift_detection_timezone)
lifecycle {
precondition {
condition = alltrue([for schedule in try(local.stack_configs[each.key].drift_detection_schedule, var.drift_detection_schedule) : can(regex("^([0-9,\\-\\*]+\\s+){4}[0-9,\\-\\*]+$", schedule))])
error_message = "Invalid cron schedule format for drift detection"
}
}
}