Using dynamic values for Kubernetes namespace labels - terraform

I am managing my on-prem Kubernetes cluster namespaces with Terraform and want to include some custom labels/annotations on them. This is to make auditing easier and also we have mutating webhooks that rely on labels/annotations.
I am trying to do something like this (pseudo code)
resource "kubernetes_namespace" "namespaces" {
for_each = {for k, v in var.namespaces: k => v}
metadata {
name = each.value.name
annotations = {
"linkerd.io/inject" = each.value.linkerd
{{loop over each.value.custom_annotations}}
}
labels = {
"apps.kubernetes.io/app" = each.value.app
"k8s.domain.co/managed-by" = each.value.managed
"k8s.domain.co/owner" = each.value.owner
{{loop over each.value.custom.labels}}
}
}
}
I have my var.namespaces variable constructed like
description = "List of namespaces controlled by Terraform"
type = list(object({
name = string
linkerd = string
app = string
owner = string
managed = string
custom_annotations = list(object({
label = string
value = string
}))
custom_labels = list(object({
label = string
value = string
}))
}))
I am trying to end up with
namespaces = [
{
name = foo
...
custom_annotations = {
label = "myannotation"
value = "myvalue"
custom_labels = {
label = "mylabel"
value = "myvalue"
}]
resource "kubernetes_namespace" "namespaces" {
for_each = {for k, v in var.namespaces: k => v}
metadata {
name = each.value.name
annotations = {
"linkerd.io/inject" = each.value.linkerd
myannotation = myvalue
}
labels = {
"apps.kubernetes.io/app" = each.value.app
"k8s.domain.co/managed-by" = each.value.managed
"k8s.domain.co/owner" = each.value.owner
mylabel = myvalue
}
}
}
I have a feeling some mix of locals and dynamic blocks would be the solution but I can't seem to pin them together in a way that works
Any advice please?

I managed to get this almost working for myself without using locals or dynamic blocks. However I can't include the default labels and annotations
resource "kubernetes_namespace" "namespaces" {
for_each = { for k, v in var.namespaces: k => v} //loop over the namespaces
metadata {
name = each.value.name
annotations = {
for annotation in each.value.custom_annotations: annotation.label => annotation.value
}
labels = {
for label in each.value.custom_labels: label.label => label.value
}
}
}
With this input
namespaces = [
{
name = "metallb-system"
linkerd = "enabled"
app = "metallb"
owner = "KPE"
managed = "Terraform"
custom_annotations = []
custom_labels = [{label="foo.io/bar", value="foobar"}, {label="bar.io/foo", value="barfoo"}]
},
{ name = "test-ns"
linkerd = "enabled"
app = "myapp"
owner = "Me"
managed = "Terraform"
custom_annotations = [{label="foo.io/annotation", value="test"}]
custom_labels = [{label="app.io/label", value="value"}]
}
]
It gives me this output
Changes to Outputs:
+ namespaces = {
+ 0 = {
+ id = "metallb-system"
+ metadata = [
+ {
+ annotations = {}
+ generate_name = ""
+ generation = 0
+ labels = {
+ "bar.io/foo" = "barfoo"
+ "foo.io/bar" = "foobar"
}
+ name = "metallb-system"
+ resource_version = "410142"
+ uid = "02d6b1e1-707a-49cf-9a2d-3f28c9ce1e5a"
},
]
+ timeouts = null
}
+ 1 = {
+ id = (known after apply)
+ metadata = [
+ {
+ annotations = {
+ "foo.io/annotation" = "test"
}
+ generate_name = null
+ generation = (known after apply)
+ labels = {
+ "app.io/label" = "value"
}
+ name = "test-ns"
+ resource_version = (known after apply)
+ uid = (known after apply)
},
]
+ timeouts = null
}
}

I found a way to add default labels and annotations using setunion
locals {
default_annotations = [{label = "foo", value = "bar"}]
default_labels = [{label = "terraform", value = true}]
}
resource "kubernetes_namespace" "namespaces" {
for_each = { for k, v in var.namespaces: k => v} //loop over the namespaces
metadata {
name = each.value.name
annotations = {
for annotation in setunion(each.value.custom_annotations, local.default_annotations) : annotation.label => annotation.value
}
labels = {
for label in setunion(each.value.custom_labels, local.default_labels) : label.label => label.value
}
}
}
I know this doesn't exactly solve your use case, as you are wanting to read the value from your list of namesapces, however I do think it is one step closer!

Related

Terraform resource with for_each depending on a boolean variable

Given the following map and its implementation:
variable "tunnel_service_maps" {
default = {}
type = map(object({
target_service = string
create_service_token = bool
}))
tunnel_service_maps = {
tunnel1 = {
target_service = "http://tunnel1"
create_service_token = true
}
tunnel2 = {
target_service = "http://tunnel2"
create_service_token = false
}
tunnel3 = {
target_service = "http://tunnel3"
create_service_token = true
}
}
I want to create the following resource exclusively if create_service_token == true:
resource "example_resource" "example" {
for_each = var.tunnel_service_maps # <- row to change
name = "allow-service-token-${each.value.target_service}"
}
The expected results will be 2 example resources created
That should be easy to achieve:
resource "example_resource" "example" {
for_each = { for k, v in var.tunnel_service_maps : k => v if v.create_service_token }
name = "allow-service-token-${each.value.target_service}"
}

Create VPS in GCP via Terraform module using count

getting stuck with problem.
Need a terraform expert help. I want to create VPS in GCP with count using module. How to correct create and attach google_compute_address and google_compute_disk to each VPS with different names
Any help, please
Module code:
resource "google_compute_instance" "vps" {
count = var.server_count
name = var.server_count > 1 ? "${var.server_name}-${count.index}" : var.server_name
description = var.server_description
machine_type = var.server_type
zone = var.server_datacenter
deletion_protection = var.server_delete_protection
labels = var.server_labels
metadata = var.server_metadata
tags = var.server_tags
boot_disk {
auto_delete = false
initialize_params {
size = var.boot_volume_size
type = var.boot_volume_type
image = var.boot_volume_image
labels = var.boot_volume_labels
}
}
dynamic "attached_disk" {
for_each = var.volumes
content {
source = attached_disk.value["volume_name"]
}
}
dynamic "network_interface" {
for_each = var.server_network
content {
subnetwork = network_interface.value["subnetwork_name"]
network_ip = network_interface.value["subnetwork_ip"]
dynamic "access_config" {
for_each = network_interface.value.nat_ip ? [1] : []
content {
nat_ip = google_compute_address.static_ip.address
}
}
}
}
}
resource "google_compute_disk" "volume" {
for_each = var.volumes
name = each.value["volume_name"]
type = each.value["volume_type"]
size = each.value["volume_size"]
zone = var.server_datacenter
labels = each.value["volume_labels"]
}
resource "google_compute_address" "static_ip" {
count = var.server_count
name = var.server_count > 1 ? "${var.server_name}-${count.index}" : var.server_name
region = var.server_region
}
Using example:
module "vps-test" {
source = "../module"
credentials_file = "../../../../main/vault/prod/.tf/terraform-bb-prod-ground.json"
server_count = 2
server_name = "example-vps"
server_description = "simple vps for module testing"
server_type = "e2-small"
server_region = "europe-west4"
server_datacenter = "europe-west4-c"
server_labels = {
project = "terraform"
environment = "test"
}
server_metadata = {
groups = "parent_group.child_group"
}
boot_volume_image = "debian-cloud/debian-11"
boot_volume_size = 30
boot_volume_labels = {
environment = "production"
project = "v3"
type = "system"
}
server_tags = ["postgres", "production", "disable-gce-firewall"]
server_delete_protection = true
server_network = {
common_network = {
subnetwork_name = "${data.terraform_remote_state.network.outputs.subnetwork_vpc_production_common_name}"
subnetwork_ip = ""
nat_ip = true
} # },
# custom_network = {
# subnetwork_name = (data.terraform_remote_state.network.outputs.subnetwork_vpc_production_k8s_name)
# subnetwork_ip = ""
# nat_ip = false
# }
}
volumes = {
volume_data1 = {
volume_name = "v3-postgres-saga-import-test-storage"
volume_size = "40"
volume_type = "pd-ssd"
volume_labels = {
environment = "production"
project = "v3"
type = "storage"
}
},
volume_data2 = {
volume_name = "volume-vpstest2"
volume_size = "20"
volume_type = "pd-ssd"
volume_labels = {
environment = "production"
project = "v2"
type = "storage"
}
}
}
}
Now error is: Because google_compute_address.static_ip has "count" set, its attributes must be accessed on specific instances And i know, error with same disk name will come

Terraform modules forces to define all the blocks defined in resources, is there any way to make dynamic import from resources

I am trying to create aws codepipeline using resources in TF. here is my resources section in m,y TF.
resource "aws_codepipeline" "codepipeline" {
name = var.name
role_arn = var.role_arn
artifact_store {
location = var.location
type = var.type
}
stage {
name = var.stage1_name
action {
name = var.action1_name
category = var.source_category
owner = var.source_owner
provider = var.source_provider
version = var.source_version
output_artifacts = var.source_output_artifacts
configuration = {
ConnectionArn = var.connection_arn
FullRepositoryId = var.full_repository_id
BranchName = var.branch_name
OutputArtifactFormat = var.output_artifact_format
}
}
}
stage {
name = var.stage2_name
action {
name = var.action2_name
category = var.build_category
owner = var.build_owner
provider = var.build_provider
input_artifacts = var.input_artifacts
output_artifacts = var.build_output_artifacts
version = var.build_version
configuration = {
ProjectName = var.project_name
EnvironmentVariables = var.environment_variables /*jsonencode(
[
{
name = var.environment_name
type = var.environment_type
value = var.environment_value
}
]
) */
}
}
}
}
In my TF modules section, creating codepipeline by calling the resources given above. my modules code is
module "codepipeline_notification" {
source = "../../modules/codepipeline"
name = var.codepipeline_lambda_notification_name
role_arn = aws_iam_role.cp_lambda_deploy_role.arn #var.codepipeline_lambda_notification_role_arn
location = module.s3_codepipeline_artifact.s3_bucket_account_id #var.codepipeline_lambda_notification_location
type = var.codepipeline_lambda_notification_type
stage1_name = var.codepipeline_lambda_notification_stage1_name
action1_name = var.codepipeline_lambda_notification_action1_name
source_category = var.codepipeline_lambda_notification_source_category
source_owner = var.codepipeline_lambda_notification_source_owner
source_provider = var.codepipeline_lambda_notification_source_provider
source_version = var.codepipeline_lambda_notification_source_version
source_output_artifacts = var.codepipeline_lambda_notification_source_output_artifacts
full_repository_id = var.codepipeline_lambda_notification_full_repository_id
branch_name = var.codepipeline_lambda_notification_branch_name
output_artifact_format = var.codepipeline_lambda_notification_output_artifact_format
environment_variables = jsonencode(
[
{
name = var.codepipeline_lambda_notification_environment_name
type = var.codepipeline_lambda_notification_environment_type
value = var.codepipeline_lambda_notification_environment_value
}
]
)
build_output_artifacts = var.codepipeline_lambda_notification_build_output_artifacts
connection_arn = module.codestarconnections.arn
stage2_name = var.codepipeline_lambda_notification_stage2_name
action2_name = var.codepipeline_lambda_notification_action2_name
build_category = var.codepipeline_lambda_notification_build_category
build_owner = var.codepipeline_lambda_notification_build_owner
build_provider = var.codepipeline_lambda_notification_build_provider
build_version = var.codepipeline_lambda_notification_build_version
input_artifacts = var.codepipeline_lambda_notification_input_artifacts
project_name = module.codebuild_notification.name
}
with this approach, I am trying to create 4 pipelines where one pipeline has only 2 stages and other 2 pipeline has 3 stages, If I define 3 stages in resources then Terraform forces the modules to create 3 stages in all pipelines where I need onyl two stages. Is there any way in terraform to define in resources and use the resource in modules based on condition
Not sure if you ever got an answer to your question, but yes, there is a way. It's called Dynamic Pipeline. I have a repository that walks you through the usage of the dynamic pipeline. In short, you treat the resource like a dynamic resource using each statement and passing in the configuration as a map.
The module looks like this:
resource "aws_codepipeline" "codepipeline" {
for_each = var.code_pipeline
name = "${local.name_prefix}-${var.AppName}"
role_arn = each.value["code_pipeline_role_arn"]
tags = {
Pipeline_Key = each.key
}
artifact_store {
type = lookup(each.value, "artifact_store", null) == null ? "" : lookup(each.value.artifact_store, "type", "S3")
location = lookup(each.value, "artifact_store", null) == null ? null : lookup(each.value.artifact_store, "artifact_bucket", null)
}
dynamic "stage" {
for_each = lookup(each.value, "stages", {})
iterator = stage
content {
name = lookup(stage.value, "name")
dynamic "action" {
for_each = lookup(stage.value, "actions", {}) //[stage.key]
iterator = action
content {
name = action.value["name"]
category = action.value["category"]
owner = action.value["owner"]
provider = action.value["provider"]
version = action.value["version"]
run_order = action.value["run_order"]
input_artifacts = lookup(action.value, "input_artifacts", null)
output_artifacts = lookup(action.value, "output_artifacts", null)
configuration = action.value["configuration"]
namespace = lookup(action.value, "namespace", null)
}
}
}
}
}
Executing Module
module "code_pipeline" {
source = "../module-aws-codepipeline" #using module locally
#source = "your-github-repository/aws-codepipeline" #using github repository
AppName = "My_new_pipeline"
code_pipeline = local.code_pipeline
}
Sample locals.tf with pipeline variable
locals {
/*
DECLARE enviornment variables. Note each Action does not require environment variables
*/
action_second_stage_variables = [
{
name = "PIPELINE_EXECUTION_ID"
type = "PLAINTEXT"
value = "#{codepipeline.PipelineExecutionId}"
},
{
name = "NamespaceVariable"
type = "PLAINTEXT"
value = "some_value"
},
]
action_third_stage_variables = [
{
name = "PL_VARIABLE_1"
type = "PLAINTEXT"
value = "VALUE1"
},
{
name = "PL_VARIABLE 2"
type = "PLAINTEXT"
value = "VALUE2"
},
{
name = "PL_VARIABLE_3"
type = "PLAINTEXT"
value = "VAUE3"
},
{
name = "PL_VARIABLE_4"
type = "PLAINTEXT"
value = "#{BLD.NamespaceVariable}"
},
]
/*
BUILD YOUR STAGES
*/
code_pipeline = {
codepipeline-configs = {
code_pipeline_role_arn = "arn:aws:iam::aws_account_name:role/role_name"
artifact_store = {
type = "S3"
artifact_bucket = "your-aws-bucket-name"
}
stages = {
stage_1 = {
name = "Download"
actions = {
action_1 = {
run_order = 1
category = "Source"
name = "First_Stage"
owner = "AWS"
provider = "CodeCommit"
version = "1"
output_artifacts = ["download_ouput"]
configuration = {
RepositoryName = "Codecommit_target_repo"
BranchName = "main"
PollForSourceChanges = true
OutputArtifactFormat = "CODE_ZIP"
}
}
}
}
stage_2 = {
name = "Build"
actions = {
action_1 = {
run_order = 2
category = "Build"
name = "Second_Stage"
owner = "AWS"
provider = "CodeBuild"
version = "1"
namespace = "BLD"
input_artifacts = ["Download_ouput"]
output_artifacts = ["build_outputs"]
configuration = {
ProjectName = "codebuild_project_name_for_second_stage"
EnvironmentVariables = jsonencode(local.action_second_stage_variables)
}
}
}
}
stage_3 = {
name = "Validation"
actions = {
action_1 = {
run_order = 1
name = "Third_Stage"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = "1"
input_artifacts = ["build_outputs"]
output_artifacts = ["validation_outputs"]
configuration = {
ProjectName = "codebuild_project_name_for_third_stage"
EnvironmentVariables = jsonencode(local.action_third_stage_variables)
}
}
}
}
}
}
}
}
The full use of the module can be found in this GitHub repository. In your case, you could pass in multiple resources to create various pipelines in one module with unique and custom stages and actions. I hope this helps.

Access variable in nested map with for_each

I have local variable:
locals {
bucket = {
firstBucket = {
sse = true
lifecycle_rules = [
{
id = "firstBucket"
enabled = true
expiration = {
days = 7
}
}
]
}
secondBucket = {
sse = false
lifecycle_rules = [
{
id = "secondBucket"
enabled = true
expiration = {
days = 7
}
}
]
}
}
}
I want first bucket to be encrypted (sse=true) and the second one should be encrypted (sse=false)
Then I try to create two s3 buckets using module. I want to use sse field defined in a local variable to set security options:
module "gitlab_bucket" {
for_each = local.bucket
/* some stuff */
server_side_encryption_configuration = lookup(each.value, "sse", null) ? var.security_cofig : {}
}
But it returns error The given key does not identify an element in this collection value
The syntax seems okay, but the default value(when sse attribute is missing) will have to be a boolean value (either true or false, so can't be null) for conditional expression.
I tested the below code in terraform 13.5, and it gave the expected result.
locals {
bucket = {
firstBucket = {
sse = true
lifecycle_rules = [
{
id = "firstBucket"
enabled = true
expiration = {
days = 7
}
}
]
}
secondBucket = {
#sse = false
lifecycle_rules = [
{
id = "secondBucket"
enabled = true
expiration = {
days = 7
}
}
]
}
}
}
resource "random_pet" "example" {
for_each = local.bucket
keepers = {
sse = lookup(each.value, "sse", false) ? jsonencode({x = "yes"}) : jsonencode({})
}
}
Below was the plan result:
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# random_pet.example["firstBucket"] will be created
+ resource "random_pet" "example" {
+ id = (known after apply)
+ keepers = {
+ "sse" = jsonencode(
{
+ x = "yes"
}
)
}
+ length = 2
+ separator = "-"
}
# random_pet.example["secondBucket"] will be created
+ resource "random_pet" "example" {
+ id = (known after apply)
+ keepers = {
+ "sse" = jsonencode({})
}
+ length = 2
+ separator = "-"
}
Plan: 2 to add, 0 to change, 0 to destroy.

alternative way, since for_each and count cannot be in same resource

I've config that need to use count and for_each at the same time.
here's the config block
variable "all_zone" {
type = list(any)
default = ["asia-southeast1-a", "asia-southeast1-b", "asia-southeast1-c"]
}
variable "backends" {
description = "Map backend indices to list of backend maps."
type = map(object({
neg_name = string
}))
}
data "google_compute_network_endpoint_group" "get_neg" {
for_each = var.backends
count = length(var.all_zone)
zone = var.all_zone[count.index]
name = lookup(each.value, "neg_name")
}
resource "google_compute_backend_service" "default" {
. . .
dynamic "backend" {
for_each = [for b in data.google_compute_network_endpoint_group.get_neg[*].id : b]
content {
group = backend.value
}
}
}
is there anyway to do this?
Update: here's sample var.backends
backends = {
default = {
neg_name = 'name-1'
}
}
Update: Thanks for #marcin for the solution. but I've another problem to acessing this data.
Before I use helper_map
data "google_compute_network_endpoint_group" "get_neg" {
count = length(var.all_zone)
zone = var.all_zone[count.index]
name = 'name-1'
}
and here structure off the output data:
neg = [
+ {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-a/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-a"
},
+ {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-b/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-b"
},
+ {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-c/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-c"
},
]
here's I accessing the data
for_each = [for b in data.google_compute_network_endpoint_group.get_neg[*].id : b]
after use map_helper
neg = [
+ {
+ default-asia-southeast1-a = {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-a/networkEndpointGroups/k8s1-e051d246-default-gclb-poc-8080-ef51ff1c"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-a"
}
+ default-asia-southeast1-b = {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-b/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-b"
}
+ default-asia-southeast1-c = {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-c/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-c"
}
},
]
and how i access this id of data.
I'm not sure exactly what you want to achieve with your data structures, but one way to overcome your issue, would be to create local helper variable, which would be the combination of your backends and all_zone. For example:
variable "backends" {
description = "Map backend indices to list of backend maps."
type = map(object({
neg_name = string
}))
default = {
default = {
neg_name = "name-1"
}
}
}
variable "all_zone" {
type = list(any)
default = ["asia-southeast1-a", "asia-southeast1-b", "asia-southeast1-c"]
}
locals {
helper_map = merge([
for backend_key, backend_value in var.backends:
{
for zone in var.all_zone:
"${backend_key}-${zone}" => {
backend_value = backend_value.neg_name
zone = zone
}
}
]...)
}
which gives:
{
"default-asia-southeast1-a" = {
"backend_value" = "name-1"
"zone" = "asia-southeast1-a"
}
"default-asia-southeast1-b" = {
"backend_value" = "name-1"
"zone" = "asia-southeast1-b"
}
"default-asia-southeast1-c" = {
"backend_value" = "name-1"
"zone" = "asia-southeast1-c"
}
}
Then, you can easily iterate over this join structure (example only):
data "google_compute_network_endpoint_group" "get_neg" {
for_each = local.helper_map
zone = each.value.zone
name = each.value.backend_value
}
The above will probably require further adjustment to match your data structures, but the general idea of using local helper variable remains same.

Resources