From bcc22220ff5994ac31f02530969e95a3608343a8 Mon Sep 17 00:00:00 2001 From: Ben Doerr Date: Tue, 12 Dec 2023 10:58:12 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A9=20(backend):=20Allows=20omitting?= =?UTF-8?q?=20the=20backend=20user=20and=20role=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aws-iam-apply.tf | 16 ++++++--- aws-iam-backend.tf | 27 ++++++++++---- examples/only_apply/ctx.tf | 27 ++++++++++++++ examples/only_apply/infracost-usage.yml | 5 +++ examples/only_apply/main.tf | 28 +++++++++++++++ outputs.tf | 32 ++++++++--------- test/examples_only_apply_test.go | 47 +++++++++++++++++++++++++ trivy.yaml | 2 ++ variables.tf | 45 +++++++++++++++++------ 9 files changed, 192 insertions(+), 37 deletions(-) create mode 100644 examples/only_apply/ctx.tf create mode 100644 examples/only_apply/infracost-usage.yml create mode 100644 examples/only_apply/main.tf create mode 100644 test/examples_only_apply_test.go diff --git a/aws-iam-apply.tf b/aws-iam-apply.tf index b3e8b8d..7ae6149 100644 --- a/aws-iam-apply.tf +++ b/aws-iam-apply.tf @@ -38,13 +38,21 @@ data "aws_iam_policy_document" "apply_assume_role" { } dynamic "statement" { - for_each = range(length(coalesce(var.apply_role.extra_principals, []))) + for_each = range(length(coalesce(var.apply_role.extra_assume_statements, []))) content { sid = replace("${module.label_apply.id}-${statement.key + 1}", "-", "") - actions = ["sts:AssumeRole"] + actions = var.apply_role.extra_assume_statements[statement.key].actions principals { - type = var.apply_role.extra_principals[statement.key].type - identifiers = var.apply_role.extra_principals[statement.key].identifiers + type = var.apply_role.extra_assume_statements[statement.key].principals.type + identifiers = var.apply_role.extra_assume_statements[statement.key].principals.identifiers + } + dynamic "condition" { + for_each = range(length(coalesce(var.apply_role.extra_assume_statements[statement.key].conditions, []))) + content { + test = var.apply_role.extra_assume_statements[statement.key].conditions[condition.key].test + variable = var.apply_role.extra_assume_statements[statement.key].conditions[condition.key].variable + values = var.apply_role.extra_assume_statements[statement.key].conditions[condition.key].values + } } } } diff --git a/aws-iam-backend.tf b/aws-iam-backend.tf index e701771..a02c32e 100644 --- a/aws-iam-backend.tf +++ b/aws-iam-backend.tf @@ -20,7 +20,7 @@ resource "aws_iam_access_key" "backend" { } data "aws_iam_user" "backend" { - count = var.backend_user.create ? 0 : 1 + count = var.backend_user.create ? 0 : (var.backend_user.name != null ? 1 : 0) user_name = var.backend_user.name } @@ -134,13 +134,21 @@ data "aws_iam_policy_document" "backend_assume_role" { } dynamic "statement" { - for_each = range(length(coalesce(var.backend_role.extra_principals, []))) + for_each = range(length(coalesce(var.backend_role.extra_assume_statements, []))) content { sid = replace("${module.label_backend[0].id}-${statement.key + 1}", "-", "") - actions = ["sts:AssumeRole"] + actions = var.backend_role.extra_assume_statements[statement.key].actions principals { - type = var.backend_role.extra_principals[statement.key].type - identifiers = var.backend_role.extra_principals[statement.key].identifiers + type = var.backend_role.extra_assume_statements[statement.key].principals.type + identifiers = var.backend_role.extra_assume_statements[statement.key].principals.identifiers + } + dynamic "condition" { + for_each = range(length(coalesce(var.backend_role.extra_assume_statements[statement.key].conditions, []))) + content { + test = var.backend_role.extra_assume_statements[statement.key].conditions[condition.key].test + variable = var.backend_role.extra_assume_statements[statement.key].conditions[condition.key].variable + values = var.backend_role.extra_assume_statements[statement.key].conditions[condition.key].values + } } } } @@ -153,17 +161,24 @@ resource "aws_iam_role" "backend" { assume_role_policy = data.aws_iam_policy_document.backend_assume_role[0].json } +locals { + attach_dyndb = var.backend_role.create && (var.backend_role.dynamodb_policy.create || var.backend_role.dynamodb_policy.policy_arn != null) + attach_s3 = var.backend_role.create && (var.backend_role.s3_policy.create || var.backend_role.s3_policy.policy_arn != null) +} + data "aws_iam_role" "backend" { - count = var.backend_role.create ? 0 : 1 + count = var.backend_role.create ? 0 : (local.attach_dyndb || local.attach_s3 ? 1 : 0) name = var.backend_role.arn } resource "aws_iam_role_policy_attachment" "backend_dynamodb" { + count = local.attach_dyndb ? 1 : 0 role = var.backend_role.create ? aws_iam_role.backend[0].id : data.aws_iam_role.backend[0].id policy_arn = var.backend_role.dynamodb_policy.create ? aws_iam_policy.backend_dynamodb_rw[0].arn : var.backend_role.dynamodb_policy.policy_arn } resource "aws_iam_role_policy_attachment" "backend_s3" { + count = local.attach_s3 ? 1 : 0 role = var.backend_role.create ? aws_iam_role.backend[0].id : data.aws_iam_role.backend[0].id policy_arn = var.backend_role.s3_policy.create ? aws_iam_policy.backend_s3_rw[0].arn : var.backend_role.s3_policy.policy_arn } diff --git a/examples/only_apply/ctx.tf b/examples/only_apply/ctx.tf new file mode 100644 index 0000000..68611f8 --- /dev/null +++ b/examples/only_apply/ctx.tf @@ -0,0 +1,27 @@ +terraform { + required_version = ">= 0.13" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = "us-east-1" +} + +variable "namespace" { + type = string +} + +module "context" { + source = "bendoerr-terraform-modules/context/null" + version = "0.4.1" + namespace = var.namespace + environment = "example" + role = "tfuser" + region = "us-east-1" + project = "simple" +} diff --git a/examples/only_apply/infracost-usage.yml b/examples/only_apply/infracost-usage.yml new file mode 100644 index 0000000..d6ea368 --- /dev/null +++ b/examples/only_apply/infracost-usage.yml @@ -0,0 +1,5 @@ +# You can use this file to define resource usage estimates for Infracost to use when calculating +# the cost of usage-based resource, such as AWS S3 or Lambda. +# `infracost breakdown --usage-file infracost-usage.yml [other flags]` +# See https://infracost.io/usage-file/ for docs +version: 0.1 diff --git a/examples/only_apply/main.tf b/examples/only_apply/main.tf new file mode 100644 index 0000000..7e3174f --- /dev/null +++ b/examples/only_apply/main.tf @@ -0,0 +1,28 @@ +module "tfuser" { + source = "../.." + context = module.context.shared + + apply_user = { + create = true, + pgp_key = "keybase:bendoerr" + } + + apply_role = { + create = true + budgets = true + dynamodb = true + ec2_account = true + ec2_networking = true + ec2_tags = true + ecs = true + efs = true + iam = true + kms = true + lambda = true + logs = true + route53 = true + s3 = true + sns = true + ssm_params = true + } +} diff --git a/outputs.tf b/outputs.tf index e1e6525..69c06f9 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,63 +1,63 @@ output "backend_user_arn" { - value = var.backend_user.create ? aws_iam_user.backend[0].arn : "" + value = try(aws_iam_user.backend[0].arn, data.aws_iam_user.backend[0].arn, null) } output "backend_user_name" { - value = var.backend_user.create ? aws_iam_user.backend[0].name : "" + value = try(aws_iam_user.backend[0].id, data.aws_iam_user.backend[0].user_name, null) } output "backend_user_unique_id" { - value = var.backend_user.create ? aws_iam_user.backend[0].unique_id : "" + value = try(aws_iam_user.backend[0].unique_id, data.aws_iam_user.backend[0].id, null) } output "backend_user_access_key_id" { - value = var.backend_user.create ? aws_iam_access_key.backend[0].id : "" + value = var.backend_user.create ? aws_iam_access_key.backend[0].id : null } output "backend_user_access_key_encrypted_secret" { - value = var.backend_user.create ? aws_iam_access_key.backend[0].encrypted_secret : "" + value = var.backend_user.create ? aws_iam_access_key.backend[0].encrypted_secret : null } output "backend_role_arn" { - value = var.backend_role.create ? aws_iam_role.backend[0].arn : data.aws_iam_role.backend[0].arn + value = try(aws_iam_role.backend[0].arn, data.aws_iam_role.backend[0].arn, null) } output "backend_role_name" { - value = var.backend_role.create ? aws_iam_role.backend[0].name : data.aws_iam_role.backend[0].name + value = try(aws_iam_role.backend[0].name, data.aws_iam_role.backend[0].name, null) } output "backend_dynamodb_rw_policy_arn" { - value = var.backend_role.dynamodb_policy.create ? aws_iam_policy.backend_s3_rw[0].arn : var.backend_role.s3_policy.policy_arn + value = try(aws_iam_policy.backend_dynamodb_rw[0].arn, var.backend_role.dynamodb_policy.policy_arn) } output "backend_s3_rw_policy_arn" { - value = var.backend_role.s3_policy.create ? aws_iam_policy.backend_s3_rw[0].arn : var.backend_role.s3_policy.policy_arn + value = try(aws_iam_policy.backend_s3_rw[0].arn, var.backend_role.s3_policy.policy_arn) } output "apply_user_arn" { - value = var.apply_user.create ? aws_iam_user.apply[0].arn : "" + value = try(aws_iam_user.apply[0].arn, data.aws_iam_user.apply[0].arn, null) } output "apply_user_name" { - value = var.apply_user.create ? aws_iam_user.apply[0].name : "" + value = try(aws_iam_user.apply[0].name, data.aws_iam_user.apply[0].user_name, null) } output "apply_user_unique_id" { - value = var.apply_user.create ? aws_iam_user.apply[0].unique_id : "" + value = try(aws_iam_user.apply[0].unique_id, data.aws_iam_user.apply[0].id, null) } output "apply_user_access_key_id" { - value = var.apply_user.create ? aws_iam_access_key.apply[0].id : "" + value = var.apply_user.create ? aws_iam_access_key.apply[0].id : null } output "apply_user_access_key_encrypted_secret" { - value = var.apply_user.create ? aws_iam_access_key.apply[0].encrypted_secret : "" + value = var.apply_user.create ? aws_iam_access_key.apply[0].encrypted_secret : null } output "apply_role_arn" { - value = var.apply_role.create ? aws_iam_role.apply[0].arn : data.aws_iam_role.apply[0].arn + value = try(aws_iam_role.apply[0].arn, data.aws_iam_role.apply[0].arn, null) } output "apply_role_name" { - value = var.apply_role.create ? aws_iam_role.apply[0].name : data.aws_iam_role.apply[0].name + value = try(aws_iam_role.apply[0].name, data.aws_iam_role.apply[0].name, null) } diff --git a/test/examples_only_apply_test.go b/test/examples_only_apply_test.go new file mode 100644 index 0000000..a162c1f --- /dev/null +++ b/test/examples_only_apply_test.go @@ -0,0 +1,47 @@ +package test_test + +import ( + "context" + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/gruntwork-io/terratest/modules/terraform" + test_structure "github.com/gruntwork-io/terratest/modules/test-structure" +) + +func TestOnlyApply(t *testing.T) { + rootFolder := "../" + terraformFolderRelativeToRoot := "examples/only_apply" + + tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) + + rndns := strings.ToLower(random.UniqueId()) + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: tempTestFolder, + Upgrade: true, + Vars: map[string]interface{}{ + "namespace": rndns, + }, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + + // AWS Session + _, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion("us-east-1"), + ) + + if err != nil { + t.Fatal(err) + } +} diff --git a/trivy.yaml b/trivy.yaml index 2fa7410..767e73b 100644 --- a/trivy.yaml +++ b/trivy.yaml @@ -4,3 +4,5 @@ scan: - ./.infracost - ./examples/simple/.terraform - ./examples/simple/.infracost + - ./examples/only_apply/.terraform + - ./examples/only_apply/.infracost diff --git a/variables.tf b/variables.tf index 8031702..2445854 100644 --- a/variables.tf +++ b/variables.tf @@ -23,6 +23,10 @@ variable "backend_user" { force_destroy = optional(bool) # opt pgp_key = optional(string) # req if create is true or invalid }) + default = { + create = false + } + nullable = false # TODO Validation @@ -33,26 +37,37 @@ variable "backend_role" { create = bool arn = optional(string) # opt, if create is false - extra_principals = optional(list(object({ - type = string - identifiers = list(string) + extra_assume_statements = optional(list(object({ + actions = list(string) + principals = object({ + type = string + identifiers = list(string) + }) + conditions = optional(list(object({ + test = string + variable = string + values = list(string) + }))) }))) - dynamodb_policy = object({ + dynamodb_policy = optional(object({ create = bool policy_arn = optional(string) # req, if create is false or invalid table_arn = optional(string) # req, if create is true or invalid kms_key = optional(string) # opt, if create is true or invalid - }) + }), { create = false }) - s3_policy = object({ + s3_policy = optional(object({ create = bool policy_arn = optional(string) # req, if create is false or invalid bucket_arn = optional(string) # req, if create is true or invalid kms_key = optional(string) # opt, if create is true or invalid - }) - + }), { create = false }) }) + default = { + create = false + } + nullable = false # TODO Validation } @@ -71,9 +86,17 @@ variable "apply_role" { create = bool arn = optional(string) # req, if create is false - extra_principals = optional(list(object({ - type = string - identifiers = list(string) + extra_assume_statements = optional(list(object({ + actions = list(string) + principals = object({ + type = string + identifiers = list(string) + }) + conditions = optional(list(object({ + test = string + variable = string + values = list(string) + }))) }))) budgets = optional(bool, false)