Create GCS bucket with versioning - terraform

I want to create GCS bucket with versioning.
I created sub-module.
resource "google_storage_bucket" "cloud_storage" {
project = "${var.project}"
name = "${var.storage_name}"
location = "${var.location}"
storage_class = "${var.storage_class}"
versioning = "${var.versioning}"
}
As per Terraform doc, I can pass versioning arguments to configure versioning.
I don't know what kind of data versioning argument accepts. I tried passing bool (true), map and list as follow.
map
variable "versioning" {
type = list
default = {
generation = true,
metageneration = true
}
}
List
variable "versioning" {
type = list
default = [
"generation",
"metageneration"
]
description = "Enable versioning on Bucket"
}
I tried this after reading this GCP Doc
Error
error I am getting it as below.
Error: Unsupported argument
on ../modules/storage/main.tf line 6, in resource "google_storage_bucket" "cloud_storage":
6: versioning = "${var.versioning}"
An argument named "versioning" is not expected here. Did you mean to define a
block of type "versioning"?
The module works fine, if I don't use versioning arguments. But, I want to create module which can configure versioning too.
Please let me know if I am going in wrong direction.
Any help would be appreciated.

The error message is indicating that the versioning argument is a block (not a map), thus including the '=' confuses Terraform.
Use:
resource "google_storage_bucket" "foo" {
...
versioning {
enabled = true
}
}
NOT
resource "google_storage_bucket" "foo" {
...
versioning = {
enabled = true
}
}

Related

how does terraform pass value of variable when it is not being referenced in main.tf

I don't understand the logic of the following terraform code, and not sure, but I guess it might be me, but would appreciate some help with this.
So there's the following module https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/modules/definition/main.tf
resource azurerm_policy_definition def {
name = local.policy_name
display_name = local.display_name
description = local.description
policy_type = "Custom"
mode = var.policy_mode
management_group_id = var.management_group_id
metadata = jsonencode(local.metadata)
parameters = length(local.parameters) > 0 ? jsonencode(local.parameters) : null
policy_rule = jsonencode(local.policy_rule)
lifecycle {
create_before_destroy = true
}
timeouts {
read = "10m"
}
}
and https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/modules/definition/variables.tf
variable management_group_id {
type = string
description = "The management group scope at which the policy will be defined. Defaults to current Subscription if omitted. Changing this forces a new resource to be created."
default = null
}
variable policy_name {
type = string
description = "Name to be used for this policy, when using the module library this should correspond to the correct category folder under /policies/policy_category/policy_name. Changing this forces a new resource to be created."
default = ""
validation {
condition = length(var.policy_name) <= 64
error_message = "Definition names have a maximum 64 character limit, ensure this matches the filename within the local policies library."
}
}
variable display_name {
type = string
description = "Display Name to be used for this policy"
default = ""
validation {
condition = length(var.display_name) <= 128
error_message = "Definition display names have a maximum 128 character limit."
}
}
variable policy_description {
type = string
description = "Policy definition description"
default = ""
validation {
condition = length(var.policy_description) <= 512
error_message = "Definition descriptions have a maximum 512 character limit."
}
}
variable policy_mode {
type = string
description = "The policy mode that allows you to specify which resource types will be evaluated, defaults to All. Possible values are All and Indexed"
default = "All"
validation {
condition = var.policy_mode == "All" || var.policy_mode == "Indexed" || var.policy_mode == "Microsoft.Kubernetes.Data"
error_message = "Policy mode possible values are: All, Indexed or Microsoft.Kubernetes.Data (In Preview). Other modes are only allowed in built-in policy definitions, these include Microsoft.ContainerService.Data, Microsoft.CustomerLockbox.Data, Microsoft.DataCatalog.Data, Microsoft.KeyVault.Data, Microsoft.MachineLearningServices.Data, Microsoft.Network.Data and Microsoft.Synapse.Data"
}
}
variable policy_category {
type = string
description = "The category of the policy, when using the module library this should correspond to the correct category folder under /policies/var.policy_category"
default = null
}
variable policy_version {
type = string
description = "The version for this policy, if different from the one stored in the definition metadata, defaults to 1.0.0"
default = null
}
variable policy_rule {
type = any
description = "The policy rule for the policy definition. This is a JSON object representing the rule that contains an if and a then block. Omitting this assumes the rules are located in /policies/var.policy_category/var.policy_name.json"
default = null
}
variable policy_parameters {
type = any
description = "Parameters for the policy definition. This field is a JSON object that allows you to parameterise your policy definition. Omitting this assumes the parameters are located in /policies/var.policy_category/var.policy_name.json"
default = null
}
variable policy_metadata {
type = any
description = "The metadata for the policy definition. This is a JSON object representing additional metadata that should be stored with the policy definition. Omitting this will fallback to meta in the definition or merge var.policy_category and var.policy_version"
default = null
}
variable file_path {
type = any
description = "The filepath to the custom policy. Omitting this assumes the policy is located in the module library"
default = null
}
locals {
# import the custom policy object from a library or specified file path
policy_object = jsondecode(coalesce(try(
file(var.file_path),
file("${path.cwd}/policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.root}/policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.root}/../policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.module}/../../policies/${title(var.policy_category)}/${var.policy_name}.json")
)))
# fallbacks
title = title(replace(local.policy_name, "/-|_|\\s/", " "))
category = coalesce(var.policy_category, try((local.policy_object).properties.metadata.category, "General"))
version = coalesce(var.policy_version, try((local.policy_object).properties.metadata.version, "1.0.0"))
# use local library attributes if runtime inputs are omitted
policy_name = coalesce(var.policy_name, try((local.policy_object).name, null))
display_name = coalesce(var.display_name, try((local.policy_object).properties.displayName, local.title))
description = coalesce(var.policy_description, try((local.policy_object).properties.description, local.title))
metadata = coalesce(var.policy_metadata, try((local.policy_object).properties.metadata, merge({ category = local.category },{ version = local.version })))
parameters = coalesce(var.policy_parameters, try((local.policy_object).properties.parameters, null))
policy_rule = coalesce(var.policy_rule, try((local.policy_object).properties.policyRule, null))
# manually generate the definition Id to prevent "Invalid for_each argument" on set_assignment plan/apply
definition_id = var.management_group_id != null ? "${var.management_group_id}/providers/Microsoft.Authorization/policyDefinitions/${local.policy_name}" : azurerm_policy_definition.def.id
}
and an example how to use the module https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/examples/definitions.tf
module "deny_resource_types" {
source = "..//modules/definition"
policy_name = "deny_resource_types"
display_name = "Deny Azure Resource types"
policy_category = "General"
management_group_id = data.azurerm_management_group.org.id
}
From how I see it (might be wrrong) a variable can be used as a default value to the local in a Terraform script. So how is the value for the variable policy_name used when main.tf references local.policy_name instead of var.policy_name. The variable policy_name is also not having a default value.
What am I missing ?
Thank you !
EDIT:
Thank you, very clear explanation. I think I understand it better now. So basically, when I’m calling the definition module Terraform essentially load and process that module’s configuration files (including variables.tf). I was confused to see name = local.policy_name instead of for example mode = var.policy_mode. But the way I understand it now, is that when calling this module, I can set the value for the variable policy_name, which is then further processed inside the locals section, and result is what is actual provided to the name argument required by azurerm_policy_definition https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/policy_definition. Could you please confirm that my understanding is correct?
Thank you !
policy_name does have default value, but the value is empty.
default = ""
Empty value can be default value. Terraform expects module inputs only when there is no default attribute set in the input field.
OK, so there are two scenarios at play here.
When the policy_name isn't provided to the module, it takes on its default behaviour of empty string
When a value is provided to the policy_name, the locals section transforms that value and then uses it in the code as local.policy_name for the resources. L103
policy_name = coalesce(var.policy_name, try((local.policy_object).name, null))
If you look for the coalesce function, its purpose is to return the first element that is not null/empty.
Although, I don't see the point of that logic since if both cases are null it is supposed to return null. Could've used a simple condition instead.
I hope this clarifies things more.
Ps: an empty string (""), Terraform consider as a value.

Creating a dynamic secret variable block within Terraform for Cloud Run

I'm trying to create the following block dynamically based on a list of strings
env {
name = "SECRET_ENV_VAR"
value_from {
secret_key_ref {
name = google_secret_manager_secret.secret.secret_id
key = "1"
}
}
}
Based off documentation: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_service#example-usage---cloud-run-service-secret-environment-variables
I would like to dynamically add Secrets, and have defined the following dynamic block:
dynamic "env" {
for_each = toset(var.secrets)
content {
name = each.value
value_from {
secret_key_ref {
name = each.value
key = "1"
}
}
}
}
Where secrets is a variable of type list(string)
However, this throws an error: Blocks of type "value_from" are not expected here.
I'm not sure what I'm missing, or where I have incorrectly specified the value_from block.
Could someone point me in the right direction for fixing this up?
UPDATE;
I have also tried to implement this variable as a map, as per the suggestion in the comments on this post. (https://www.terraform.io/docs/language/expressions/dynamic-blocks.html#multi-level-nested-block-structures)
dynamic "env" {
for_each = var.secrets
content {
name = each.key
dynamic "value_from" {
for_each = env.value.name
secret_key_ref {
name = value_from.value.name
key = value_from.value.version
}
}
}
}
However, this also gives the same error. Blocks of type "value_from" are not expected here.
In this example, the secrets variable is defined as a list(any) with this value:
secrets = [
{
name = "SECRET"
version = "1"
}
]
You have to upgrade your gcp provider. Support for secrets in google_cloud_run_service was added in v3.67.0. Current version is v4.1.0, which means that you must be using very old gcp provider.
In the end, I solved this by changing the variable type to a map(any):
secrets = {
"SECRET" = "1"
}
This allowed me to create the "dynamic" env block, without needing to implement the nested dynamic block.

Terraform: Optional fields in Resource

In terraform I'm trying to create some firewall rules, which normally don't have logging enabled - to accomplish this, I must not have the log_config field created. However, I have a variable firewall_logging, which if true, should add this field and the options I wish it to contain.
I don't think using a dynamic here is quite the right thing to do but it's possible it is and I've misunderstood how to generate it, instead I came up with the following:
resource "google_compute_firewall" "this" {
name = var.name
project = var.project
network = var.network
source_ranges = var.source_ranges
source_tags = var.source_tags
target_tags = var.target_tags
priority = var.priority
direction = var.direction
allow {
protocol = lower(var.protocol)
ports = var.ports
}
## If log_config is defined, this enables logging. By not defining it, we are disabling logging.
var.firewall_logging == true ? log_config { metadata = var.log_metadata } : null
I was hoping that the variable would be evaluated, and if true, the log_config section is added to the resource but I get an error that a argument or block definition is required.
It should work using dynamic blocks. For instance:
dynamic "log_config" {
for_each = var.firewall_logging == true ? [true] : []
content {
metadata = var.log_metadata
}
}

terraform; why is metadata referenced via a list indexing syntax?

Example: https://www.terraform.io/docs/providers/kubernetes/r/service_account.html
We see this:
resource "kubernetes_service_account" "example" {
metadata {
name = "terraform-example"
}
secret {
name = "${kubernetes_secret.example.metadata.0.name}"
}
}
Metadata is not a list, why does the secret/name value reference metadata using .0?
The provider has defined the metadata block type as being represented internally as a list of objects. This is common in today's Terraform providers because Terraform versions prior to Terraform v0.12 required a provider to choose between only lists and sets as the backing data type for nested block types.
Terraform v0.12 and later do support having a singleton block represented as a single object, but providers that predated the Terraform v0.12 release tend to still use the list representation for backward-compatibility. (Most providers are, at the time of writing, still compatible with both Terraform v0.11 and v0.12 in their new releases.)
The new kubernetes provider (still under development at the time I'm writing this) was built specifically for Terraform v0.12 and later, so it is able to more directly reflect the underlying Kubernetes schema, including treating metadata as a single object. The announcement post about the new provider version includes this example:
resource "kubernetes_manifest" "example_crd" {
provider = kubernetes-alpha
manifest = {
apiVersion = "apiextensions.k8s.io/v1"
kind = "CustomResourceDefinition"
metadata = {
name = "testcrds.hashicorp.com"
labels = {
app = "test"
}
}
spec = {
group = "hashicorp.com"
names = {
kind = "TestCrd"
plural = "testcrds"
}
scope = "Namespaced"
versions = [
{
name = "v1"
served = true
storage = true
schema = {
openAPIV3Schema = {
type = "object"
properties = {
data = {
type = "string"
}
refs = {
type = "number"
}
}
}
}
}
]
}
}
}
The metadata name for this new provider can be accessed in a more intuitive way, because metadata is a single object:
kubernetes_manifest.example_crd.manifest.metadata.name

Iterate Through Map of Maps in Terraform 0.12

I need to build a list of templatefile's like this:
templatefile("${path.module}/assets/files_eth0.nmconnection.yaml", {
interface-name = "eth0",
addresses = element(values(var.virtual_machines), count.index),
gateway = element(var.gateway, count.index % length(var.gateway)),
dns = join(";", var.dns_servers),
dns-search = var.domain,
}),
templatefile("${path.module}/assets/files_etc_hostname.yaml", {
hostname = element(keys(var.virtual_machines), count.index),
}),
by iterating over a map of maps like the following:
variable templatefiles {
default = {
"files_eth0.nmconnection.yaml" = {
"interface-name" = "eth0",
"addresses" = "element(values(var.virtual_machines), count.index)",
"gateway" = "element(var.gateway, count.index % length(var.gateway))",
"dns" = "join(";", var.dns_servers)",
"dns-search" = "var.domain",
},
"files_etc_hostname.yaml" = {
"hostname" = "host1"
}
}
}
I've done something similar with a list of files:
file("${path.module}/assets/files_90-disable-console-logs.yaml"),
file("${path.module}/assets/files_90-disable-auto-updates.yaml"),
...but would like to expand this to templatefiles (above).
Here's the code I've done for the list of files:
main.tf
variable files {
default = [
"files_90-disable-auto-updates.yaml",
"files_90-disable-console-logs.yaml",
]
}
output "snippets" {
value = flatten(module.ingition_snippets.files)
}
modules/main.tf
variable files {}
resource "null_resource" "files" {
for_each = toset(var.files)
triggers = {
snippet = file("${path.module}/assets/${each.value}")
}
}
output "files" {
value = [for s in null_resource.files: s.triggers.*.snippet]
}
Appreciate any help!
Both of these use-cases can be met without using any resource blocks at all, because the necessary features are built in to the Terraform language.
Here is a shorter way to write the example with static files:
variable "files" {
type = set(string)
}
output "files" {
value = tomap({
for fn in var.files : fn => file("${path.module}/assets/${fn}")
})
}
The above would produce a map from filenames to file contents, so the calling module can more easily access the individual file contents.
We can adapt that for templatefile like this:
variable "template_files" {
# We can't write down a type constraint for this case
# because each file might have a different set of
# template variables, but our later code will expect
# this to be a mapping type, like the default value
# you shared in your comment, and will fail if not.
type = any
}
output "files" {
value = tomap({
for fn, vars in var.template_files : fn => templatefile("${path.module}/assets/${fn}", vars)
})
}
Again, the result will be a map from filename to the result of rendering the template with the given variables.
If your goal is to build a module for rendering templates from a source directory to publish somewhere, you might find the module hashicorp/dir/template useful. It combines fileset, file, and templatefile in a way that is hopefully convenient for static website publishing and similar use-cases. (At the time I write this the module is transitioning from being in my personal GitHub account to being in the HashiCorp organization, so if you look at it soon after you may see some turbulence as the docs get updated, etc.)

Resources