diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl index 984384f01b..79a3acb315 100644 --- a/enos/enos-modules.hcl +++ b/enos/enos-modules.hcl @@ -1,17 +1,16 @@ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 -module "az_finder" { - source = "./modules/az_finder" +module "aws_az_finder" { + source = "./modules/aws_az_finder" } module "bats_deps" { source = "./modules/bats_deps" } -module "boundary" { - source = "app.terraform.io/hashicorp-qti/aws-boundary/enos" - version = ">= 0.6.2" +module "aws_boundary" { + source = "./modules/aws_boundary" project_name = "qti-enos-boundary" environment = var.environment @@ -27,8 +26,8 @@ module "boundary" { ssh_aws_keypair = var.aws_ssh_keypair_name } -module "worker" { - source = "./modules/worker" +module "aws_worker" { + source = "./modules/aws_worker" common_tags = { "Project" : "Enos", @@ -40,8 +39,8 @@ module "worker" { ssh_aws_keypair = var.aws_ssh_keypair_name } -module "bucket" { - source = "./modules/bucket" +module "aws_bucket" { + source = "./modules/aws_bucket" } module "build_crt" { @@ -67,8 +66,8 @@ module "generate_aws_host_tag_vars" { source = "./modules/generate_aws_host_tag_vars" } -module "iam_setup" { - source = "./modules/iam_setup" +module "aws_iam_setup" { + source = "./modules/aws_iam_setup" } module "aws_vpc" { @@ -95,8 +94,8 @@ module "map2list" { source = "./modules/map2list" } -module "target" { - source = "./modules/target" +module "aws_target" { + source = "./modules/aws_target" target_count = var.target_count project_name = "qti-enos-boundary" diff --git a/enos/enos-scenario-e2e-aws-base-with-vault.hcl b/enos/enos-scenario-e2e-aws-base-with-vault.hcl index 87c82817d2..cd92eb1d2f 100644 --- a/enos/enos-scenario-e2e-aws-base-with-vault.hcl +++ b/enos/enos-scenario-e2e-aws-base-with-vault.hcl @@ -30,7 +30,7 @@ scenario "e2e_aws_base_with_vault" { } step "find_azs" { - module = module.az_finder + module = module.aws_az_finder variables { instance_type = [ @@ -75,7 +75,7 @@ scenario "e2e_aws_base_with_vault" { } step "create_boundary_cluster" { - module = module.boundary + module = module.aws_boundary depends_on = [ step.create_base_infra, step.create_db_password, @@ -94,6 +94,7 @@ scenario "e2e_aws_base_with_vault" { local_artifact_path = step.build_boundary.artifact_path ubuntu_ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] vpc_id = step.create_base_infra.vpc_id + vpc_tag_module = step.create_base_infra.vpc_tag_module worker_count = var.worker_count worker_instance_type = var.worker_instance_type } @@ -122,7 +123,7 @@ scenario "e2e_aws_base_with_vault" { } step "create_target" { - module = module.target + module = module.aws_target depends_on = [step.create_base_infra] variables { diff --git a/enos/enos-scenario-e2e-aws-base.hcl b/enos/enos-scenario-e2e-aws-base.hcl index cab62b2604..07821d0852 100644 --- a/enos/enos-scenario-e2e-aws-base.hcl +++ b/enos/enos-scenario-e2e-aws-base.hcl @@ -30,7 +30,7 @@ scenario "e2e_aws_base" { } step "find_azs" { - module = module.az_finder + module = module.aws_az_finder variables { instance_type = [ @@ -75,7 +75,7 @@ scenario "e2e_aws_base" { } step "create_boundary_cluster" { - module = module.boundary + module = module.aws_boundary depends_on = [ step.create_base_infra, step.create_db_password, @@ -94,13 +94,14 @@ scenario "e2e_aws_base" { local_artifact_path = step.build_boundary.artifact_path ubuntu_ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] vpc_id = step.create_base_infra.vpc_id + vpc_tag_module = step.create_base_infra.vpc_tag_module worker_count = var.worker_count worker_instance_type = var.worker_instance_type } } step "create_target" { - module = module.target + module = module.aws_target depends_on = [step.create_base_infra] variables { diff --git a/enos/enos-scenario-e2e-aws.hcl b/enos/enos-scenario-e2e-aws.hcl index 6d6020d3b4..3a5ee4e9bb 100644 --- a/enos/enos-scenario-e2e-aws.hcl +++ b/enos/enos-scenario-e2e-aws.hcl @@ -31,7 +31,7 @@ scenario "e2e_aws" { } step "find_azs" { - module = module.az_finder + module = module.aws_az_finder variables { instance_type = [ @@ -76,7 +76,7 @@ scenario "e2e_aws" { } step "create_boundary_cluster" { - module = module.boundary + module = module.aws_boundary depends_on = [ step.create_base_infra, step.create_db_password, @@ -95,6 +95,7 @@ scenario "e2e_aws" { local_artifact_path = step.build_boundary.artifact_path ubuntu_ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] vpc_id = step.create_base_infra.vpc_id + vpc_tag_module = step.create_base_infra.vpc_tag_module worker_count = var.worker_count worker_instance_type = var.worker_instance_type } @@ -115,7 +116,7 @@ scenario "e2e_aws" { } step "create_targets_with_tag1" { - module = module.target + module = module.aws_target depends_on = [step.create_base_infra] variables { @@ -145,7 +146,7 @@ scenario "e2e_aws" { } step "create_targets_with_tag2" { - module = module.target + module = module.aws_target depends_on = [step.create_base_infra] variables { @@ -168,7 +169,7 @@ scenario "e2e_aws" { } step "iam_setup" { - module = module.iam_setup + module = module.aws_iam_setup depends_on = [ step.create_base_infra, step.create_test_id @@ -185,7 +186,7 @@ scenario "e2e_aws" { } step "create_isolated_worker" { - module = module.worker + module = module.aws_worker depends_on = [step.create_boundary_cluster] variables { vpc_id = step.create_base_infra.vpc_id @@ -205,7 +206,7 @@ scenario "e2e_aws" { } step "create_isolated_target" { - module = module.target + module = module.aws_target depends_on = [ step.create_base_infra, step.create_isolated_worker diff --git a/enos/enos-scenario-e2e-database.hcl b/enos/enos-scenario-e2e-database.hcl index 1328916f96..41ef856a3f 100644 --- a/enos/enos-scenario-e2e-database.hcl +++ b/enos/enos-scenario-e2e-database.hcl @@ -31,7 +31,7 @@ scenario "e2e_database" { } step "find_azs" { - module = module.az_finder + module = module.aws_az_finder variables { instance_type = [ @@ -75,7 +75,7 @@ scenario "e2e_database" { } step "create_targets_with_tag" { - module = module.target + module = module.aws_target depends_on = [step.create_base_infra] variables { @@ -98,7 +98,7 @@ scenario "e2e_database" { } step "iam_setup" { - module = module.iam_setup + module = module.aws_iam_setup depends_on = [ step.create_base_infra, step.create_test_id diff --git a/enos/enos-scenario-e2e-ui.hcl b/enos/enos-scenario-e2e-ui.hcl index fe562910a5..20d04d4830 100644 --- a/enos/enos-scenario-e2e-ui.hcl +++ b/enos/enos-scenario-e2e-ui.hcl @@ -31,7 +31,7 @@ scenario "e2e_ui" { } step "find_azs" { - module = module.az_finder + module = module.aws_az_finder variables { instance_type = [ @@ -76,7 +76,7 @@ scenario "e2e_ui" { } step "create_boundary_cluster" { - module = module.boundary + module = module.aws_boundary depends_on = [ step.create_base_infra, step.create_db_password, @@ -95,6 +95,7 @@ scenario "e2e_ui" { local_artifact_path = step.build_boundary.artifact_path ubuntu_ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] vpc_id = step.create_base_infra.vpc_id + vpc_tag_module = step.create_base_infra.vpc_tag_module worker_count = var.worker_count worker_instance_type = var.worker_instance_type } @@ -137,7 +138,7 @@ scenario "e2e_ui" { } step "create_targets_with_tag1" { - module = module.target + module = module.aws_target depends_on = [step.create_base_infra] variables { @@ -167,7 +168,7 @@ scenario "e2e_ui" { } step "create_targets_with_tag2" { - module = module.target + module = module.aws_target depends_on = [step.create_base_infra] variables { @@ -190,7 +191,7 @@ scenario "e2e_ui" { } step "iam_setup" { - module = module.iam_setup + module = module.aws_iam_setup depends_on = [ step.create_base_infra, step.create_test_id diff --git a/enos/modules/az_finder/main.tf b/enos/modules/aws_az_finder/main.tf similarity index 100% rename from enos/modules/az_finder/main.tf rename to enos/modules/aws_az_finder/main.tf diff --git a/enos/modules/aws_boundary/alb.tf b/enos/modules/aws_boundary/alb.tf new file mode 100644 index 0000000000..335e8b81cc --- /dev/null +++ b/enos/modules/aws_boundary/alb.tf @@ -0,0 +1,52 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +resource "aws_alb" "boundary_alb" { + name = "boundary-alb-${random_string.cluster_id.result}" + depends_on = [aws_instance.controller] + security_groups = [aws_security_group.boundary_alb_sg.id] + subnets = data.aws_subnets.infra.ids + tags = merge(local.common_tags, + { + Name = "boundary-alb-${random_string.cluster_id.result}" + } + ) +} + +resource "aws_alb_target_group" "boundary_tg" { + name = "boundary-tg-${random_string.cluster_id.result}" + port = var.listener_api_port + protocol = "HTTP" + vpc_id = var.vpc_id + + health_check { + path = var.healthcheck_path + port = var.listener_ops_port + interval = 5 + timeout = 2 + healthy_threshold = 2 + } + tags = merge(local.common_tags, + { + Name = "boundary-tg-${random_string.cluster_id.result}" + }, + ) +} + +resource "aws_lb_target_group_attachment" "boundary" { + for_each = toset([for idx in range(var.controller_count) : tostring(idx)]) + + target_group_arn = aws_alb_target_group.boundary_tg.arn + target_id = aws_instance.controller[each.value].id + port = var.listener_api_port +} + +resource "aws_alb_listener" "boundary" { + load_balancer_arn = aws_alb.boundary_alb.arn + port = var.alb_listener_api_port + protocol = "HTTP" + default_action { + target_group_arn = aws_alb_target_group.boundary_tg.arn + type = "forward" + } +} diff --git a/enos/modules/aws_boundary/boundary-instances.tf b/enos/modules/aws_boundary/boundary-instances.tf new file mode 100644 index 0000000000..6c0f8974ef --- /dev/null +++ b/enos/modules/aws_boundary/boundary-instances.tf @@ -0,0 +1,188 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +resource "aws_instance" "controller" { + count = var.controller_count + ami = var.ubuntu_ami_id + instance_type = var.controller_instance_type + vpc_security_group_ids = [ + aws_security_group.boundary_sg.id, + aws_security_group.boundary_aux_sg.id, + ] + subnet_id = tolist(data.aws_subnets.infra.ids)[count.index % length(data.aws_subnets.infra.ids)] + key_name = var.ssh_aws_keypair + iam_instance_profile = aws_iam_instance_profile.boundary_profile.name + monitoring = var.controller_monitoring + + root_block_device { + iops = var.controller_ebs_iops + volume_size = var.controller_ebs_size + volume_type = var.controller_ebs_type + throughput = var.controller_ebs_throughput + tags = local.common_tags + } + + tags = merge(local.common_tags, + { + Name = "${local.name_prefix}-boundary-controller-${count.index}" + Type = local.boundary_cluster_tag, + }, + ) +} + +resource "aws_instance" "worker" { + count = var.worker_count + ami = var.ubuntu_ami_id + instance_type = var.worker_instance_type + vpc_security_group_ids = [aws_security_group.boundary_sg.id] + subnet_id = tolist(data.aws_subnets.infra.ids)[count.index % length(data.aws_subnets.infra.ids)] + key_name = var.ssh_aws_keypair + iam_instance_profile = aws_iam_instance_profile.boundary_profile.name + monitoring = var.worker_monitoring + + root_block_device { + iops = var.worker_ebs_iops + volume_size = var.worker_ebs_size + volume_type = var.worker_ebs_type + throughput = var.worker_ebs_throughput + tags = local.common_tags + } + + tags = merge(local.common_tags, + { + Name = "${local.name_prefix}-boundary-worker-${count.index}", + Type = local.boundary_cluster_tag, + }, + ) +} + +resource "enos_bundle_install" "controller" { + depends_on = [aws_instance.controller] + for_each = toset([for idx in range(var.controller_count) : tostring(idx)]) + + destination = var.boundary_install_dir + artifactory = var.boundary_artifactory_release + path = var.local_artifact_path + release = var.boundary_release == null ? var.boundary_release : merge(var.boundary_release, { product = "boundary", edition = "oss" }) + + transport = { + ssh = { + host = aws_instance.controller[tonumber(each.value)].public_ip + } + } +} + +resource "enos_file" "controller_config" { + depends_on = [enos_bundle_install.controller] + destination = "/etc/boundary/boundary.hcl" + content = templatefile("${path.module}/${var.controller_config_file_path}", { + id = each.value + dbuser = var.db_user, + dbpass = var.db_pass, + dbhost = var.db_host == null ? aws_db_instance.boundary[0].address : var.db_host + dbport = var.db_port + dbname = local.db_name, + db_max_open_connections = var.db_max_open_connections + kms_key_id = data.aws_kms_key.kms_key.id, + local_ipv4 = aws_instance.controller[tonumber(each.value)].private_ip + api_port = var.listener_api_port + ops_port = var.listener_ops_port + cluster_port = var.listener_cluster_port + region = var.aws_region + }) + for_each = toset([for idx in range(var.controller_count) : tostring(idx)]) + + transport = { + ssh = { + host = aws_instance.controller[tonumber(each.value)].public_ip + } + } +} + +resource "enos_boundary_init" "controller" { + count = local.is_restored_db ? 0 : 1 // init not required when we restore from a snapshot + + bin_name = var.boundary_binary_name + bin_path = var.boundary_install_dir + config_path = "/etc/boundary" + license = var.boundary_license + + transport = { + ssh = { + host = aws_instance.controller[0].public_ip + } + } + + depends_on = [enos_file.controller_config] +} + +resource "enos_boundary_start" "controller_start" { + for_each = toset([for idx in range(var.controller_count) : tostring(idx)]) + + bin_name = var.boundary_binary_name + bin_path = var.boundary_install_dir + config_path = "/etc/boundary" + license = var.boundary_license + + transport = { + ssh = { + host = aws_instance.controller[tonumber(each.value)].public_ip + } + } + + depends_on = [ + enos_boundary_init.controller, + enos_file.controller_config // required in the case where we restore from a db snapshot, since the init resource will not be created + ] +} + +resource "enos_bundle_install" "worker" { + depends_on = [aws_instance.worker] + for_each = toset([for idx in range(var.worker_count) : tostring(idx)]) + + destination = var.boundary_install_dir + + artifactory = var.boundary_artifactory_release + path = var.local_artifact_path + release = var.boundary_release == null ? var.boundary_release : merge(var.boundary_release, { product = "boundary", edition = "oss" }) + + transport = { + ssh = { + host = aws_instance.worker[tonumber(each.value)].public_ip + } + } +} + +resource "enos_file" "worker_config" { + depends_on = [enos_bundle_install.worker] + destination = "/etc/boundary/boundary.hcl" + content = templatefile("${path.module}/${var.worker_config_file_path}", { + id = each.value + kms_key_id = data.aws_kms_key.kms_key.id, + controller_ips = jsonencode(aws_instance.controller.*.private_ip), + public_addr = aws_instance.worker.0.public_ip + region = var.aws_region + }) + for_each = toset([for idx in range(var.worker_count) : tostring(idx)]) + + transport = { + ssh = { + host = aws_instance.worker[tonumber(each.value)].public_ip + } + } +} + +resource "enos_boundary_start" "worker_start" { + depends_on = [enos_boundary_start.controller_start, enos_file.worker_config] + for_each = toset([for idx in range(var.worker_count) : tostring(idx)]) + + bin_name = var.boundary_binary_name + bin_path = var.boundary_install_dir + config_path = "/etc/boundary" + license = var.boundary_license + transport = { + ssh = { + host = aws_instance.worker[tonumber(each.value)].public_ip + } + } +} diff --git a/enos/modules/aws_boundary/iam.tf b/enos/modules/aws_boundary/iam.tf new file mode 100644 index 0000000000..3c6aea894d --- /dev/null +++ b/enos/modules/aws_boundary/iam.tf @@ -0,0 +1,48 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +data "aws_iam_policy_document" "boundary_instance_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "boundary_profile" { + statement { + resources = ["*"] + + actions = ["ec2:DescribeInstances"] + } + + statement { + resources = [var.kms_key_arn] + + actions = [ + "kms:DescribeKey", + "kms:ListKeys", + "kms:Encrypt", + "kms:Decrypt", + ] + } +} + +resource "aws_iam_role" "boundary_instance_role" { + name = "boundary_instance_role-${random_string.cluster_id.result}" + assume_role_policy = data.aws_iam_policy_document.boundary_instance_role.json +} + +resource "aws_iam_instance_profile" "boundary_profile" { + name = "boundary_instance_profile-${random_string.cluster_id.result}" + role = aws_iam_role.boundary_instance_role.name +} + +resource "aws_iam_role_policy" "boundary_policy" { + name = "boundary_policy-${random_string.cluster_id.result}" + role = aws_iam_role.boundary_instance_role.id + policy = data.aws_iam_policy_document.boundary_profile.json +} diff --git a/enos/modules/aws_boundary/infra.tf b/enos/modules/aws_boundary/infra.tf new file mode 100644 index 0000000000..62256e1314 --- /dev/null +++ b/enos/modules/aws_boundary/infra.tf @@ -0,0 +1,22 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +data "aws_vpc" "infra" { + id = var.vpc_id +} + +data "aws_subnets" "infra" { + filter { + name = "vpc-id" + values = [var.vpc_id] + } + + filter { + name = "tag:Module" + values = [var.vpc_tag_module] + } +} + +data "aws_kms_key" "kms_key" { + key_id = var.kms_key_arn +} diff --git a/enos/modules/aws_boundary/main.tf b/enos/modules/aws_boundary/main.tf new file mode 100644 index 0000000000..e25624c179 --- /dev/null +++ b/enos/modules/aws_boundary/main.tf @@ -0,0 +1,40 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +terraform { + required_version = ">= 1.1.2" + + required_providers { + enos = { + source = "app.terraform.io/hashicorp-qti/enos" + version = ">= 0.3.25" + } + } +} + +locals { + name_prefix = "${var.project_name}-${var.environment}" + boundary_cluster_tag = "boundary-server-${random_string.cluster_id.result}" + + is_restored_db = var.db_snapshot_identifier != null + default_boundary_db_name = "boundary" + db_name = coalesce(var.db_name, local.default_boundary_db_name) + common_tags = merge(var.common_tags, + { + Module = "aws_boundary" + Pet = random_pet.default.id + }, + ) +} + +resource "random_string" "cluster_id" { + length = 8 + lower = true + upper = false + numeric = false + special = false +} + +resource "random_pet" "default" { + separator = "_" +} diff --git a/enos/modules/aws_boundary/outputs.tf b/enos/modules/aws_boundary/outputs.tf new file mode 100644 index 0000000000..937ec666c7 --- /dev/null +++ b/enos/modules/aws_boundary/outputs.tf @@ -0,0 +1,223 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +output "controller_ips" { + description = "Public IPs of boundary controllers" + value = aws_instance.controller.*.public_ip +} + +output "worker_ips" { + description = "Public IPs of boundary workers" + value = aws_instance.worker.*.public_ip +} + +output "alb_hostname" { + description = "Public hostname of Controller ALB" + value = aws_alb.boundary_alb.dns_name +} + +output "rds_hostname" { + description = "Public hostname of the RDS database" + value = var.db_create ? aws_db_instance.boundary[0].endpoint : var.db_host +} + +output "rds_identifier" { + description = "Unique identifier of the RDS database" + value = var.db_create ? aws_db_instance.boundary[0].identifier : null +} + +output "rds_db_name" { + description = "The name of the rds database created for the boundary cluster" + value = var.db_create ? aws_db_instance.boundary[0].db_name : null +} + +output "alb_boundary_api_addr" { + description = "The address of the boundary API" + value = "http://${aws_alb.boundary_alb.dns_name}:${var.alb_listener_api_port}" +} + +// Boundary init outputs +output "auth_method_id" { + description = "Generated auth method id from boundary init" + value = try(enos_boundary_init.controller[0].auth_method_id, null) +} + +output "auth_method_name" { + description = "Generated auth method name from boundary init" + value = try(enos_boundary_init.controller[0].auth_method_name, null) +} + +output "auth_login_name" { + description = "Generated login name from boundary init" + value = try(enos_boundary_init.controller[0].auth_login_name, null) +} + +output "auth_password" { + description = "Generated auth password from boundary init" + value = try(enos_boundary_init.controller[0].auth_password, null) +} + +output "auth_scope_id" { + description = "Generated auth scope id from boundary init" + value = try(enos_boundary_init.controller[0].auth_scope_id, null) +} + +output "auth_user_id" { + description = "Generated user id from boundary init" + value = try(enos_boundary_init.controller[0].auth_user_id, null) +} + +output "auth_user_name" { + description = "Generated user naem from boundary init" + value = try(enos_boundary_init.controller[0].auth_user_name, null) +} + +output "host_catalog_id" { + description = "Generated host catalog id from boundary init" + value = try(enos_boundary_init.controller[0].host_catalog_id, null) +} + +output "host_set_id" { + description = "Generated host set id from boundary init" + value = try(enos_boundary_init.controller[0].host_set_id, null) +} + +output "host_id" { + description = "Generated host id from boundary init" + value = try(enos_boundary_init.controller[0].host_id, null) +} + +output "host_type" { + description = "Generated host type from boundary init" + value = try(enos_boundary_init.controller[0].host_type, null) +} + +output "host_scope_id" { + description = "Generated host scope id from boundary init" + value = try(enos_boundary_init.controller[0].host_scope_id, null) +} + +output "host_catalog_name" { + description = "Generated host catalog name from boundary init" + value = try(enos_boundary_init.controller[0].host_catalog_name, null) +} + +output "host_set_name" { + description = "Generated host set name from boundary init" + value = try(enos_boundary_init.controller[0].host_set_name, null) +} + +output "host_name" { + description = "Generated host name from boundary init" + value = try(enos_boundary_init.controller[0].host_name, null) +} + +output "login_role_scope_id" { + description = "Generated login role scope id from boundary init" + value = try(enos_boundary_init.controller[0].login_role_scope_id, null) +} + +output "login_role_name" { + description = "Generated login role name from boundary init" + value = try(enos_boundary_init.controller[0].login_role_name, null) +} + +output "org_scope_id" { + description = "Generated org scope id from boundary init" + value = try(enos_boundary_init.controller[0].org_scope_id, null) +} + +output "org_scope_type" { + description = "Generated org scope type from boundary init" + value = try(enos_boundary_init.controller[0].org_scope_type, null) +} + +output "org_scope_name" { + description = "Generated org scope name from boundary init" + value = try(enos_boundary_init.controller[0].org_scope_name, null) +} + +output "project_scope_id" { + description = "Generated project scope id from boundary init" + value = try(enos_boundary_init.controller[0].project_scope_id, null) +} + +output "project_scope_type" { + description = "Generated project scope type from boundary init" + value = try(enos_boundary_init.controller[0].project_scope_type, null) +} + +output "project_scope_name" { + description = "Generated project scope name from boundary init" + value = try(enos_boundary_init.controller[0].project_scope_name, null) +} + +output "target_id" { + description = "Generated target id from boundary init" + value = try(enos_boundary_init.controller[0].target_id, null) +} + +output "target_default_port" { + description = "Generated target default port from boundary init" + value = try(enos_boundary_init.controller[0].target_default_port, null) +} + +output "target_session_max_seconds" { + description = "Generated target session max from boundary init" + value = try(enos_boundary_init.controller[0].target_session_max_seconds, null) +} + +output "target_session_connection_limit" { + description = "Generated target session connection limit from boundary init" + value = try(enos_boundary_init.controller[0].target_session_connection_limit, null) +} + +output "target_type" { + description = "Generated target type from boundary init" + value = try(enos_boundary_init.controller[0].target_type, null) +} + +output "target_scope_id" { + description = "Generated target scope id from boundary init" + value = try(enos_boundary_init.controller[0].target_scope_id, null) +} + +output "target_name" { + description = "Generated target name from boundary init" + value = try(enos_boundary_init.controller[0].target_name, null) +} + +output "iam_instance_profile_name" { + description = "The name of the IAM instance profile used with this cluster" + value = aws_iam_instance_profile.boundary_profile.name +} + + +output "name_prefix" { + description = "The prefix used when naming this cluster's components" + value = local.name_prefix +} + +output "cluster_tag" { + description = "The tag for this cluster" + value = local.boundary_cluster_tag +} + +output "public_controller_addresses" { + value = aws_instance.controller[*].public_ip +} + +output "controller_aux_sg_id" { + description = "A security group ID that covers the controllers for adding extra rules to" + value = aws_security_group.boundary_aux_sg.id +} + +output "subnet_ids" { + description = "A list of the subnets created by the infra modules to share with other modules" + value = tolist(data.aws_subnets.infra.ids) +} + +output "pet_id" { + description = "The ID of the random_pet used in this module" + value = random_pet.default.id +} diff --git a/enos/modules/aws_boundary/rds.tf b/enos/modules/aws_boundary/rds.tf new file mode 100644 index 0000000000..2010945c93 --- /dev/null +++ b/enos/modules/aws_boundary/rds.tf @@ -0,0 +1,39 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +resource "aws_db_subnet_group" "boundary" { + name = "boundary-db-subnet-${random_string.cluster_id.result}" + subnet_ids = data.aws_subnets.infra.ids +} + +resource "aws_db_instance" "boundary" { + count = var.db_create == true ? 1 : 0 + identifier = "boundary-db-${random_string.cluster_id.result}" + allocated_storage = var.db_storage + storage_type = var.db_storage_type + iops = var.db_storage_iops + engine = var.db_engine + engine_version = var.db_engine == "aurora-postgres" ? null : var.db_version + instance_class = var.db_class + monitoring_interval = var.db_monitoring_interval + monitoring_role_arn = var.db_monitoring_role_arn + publicly_accessible = true + db_name = local.db_name + + // username and password must not be provided when restoring from a snapshot + username = local.is_restored_db ? null : var.db_user + password = local.is_restored_db ? null : var.db_pass + port = var.db_port + skip_final_snapshot = true + db_subnet_group_name = aws_db_subnet_group.boundary.name + vpc_security_group_ids = [aws_security_group.boundary_db_sg.id] + apply_immediately = true + snapshot_identifier = var.db_snapshot_identifier + performance_insights_enabled = true + tags = merge(local.common_tags, + { + Name = "boundary-db-${random_string.cluster_id.result}" + Type = local.boundary_cluster_tag + }, + ) +} diff --git a/enos/modules/aws_boundary/security-groups.tf b/enos/modules/aws_boundary/security-groups.tf new file mode 100644 index 0000000000..6c4d302d35 --- /dev/null +++ b/enos/modules/aws_boundary/security-groups.tf @@ -0,0 +1,150 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +data "enos_environment" "localhost" {} + +locals { + listener_ports = { + "api" : var.listener_api_port, + "cluster" : var.listener_cluster_port, + "proxy_port" : var.listener_proxy_port, + "ops" : var.listener_ops_port, + } + alb_listener_ports = { + "api" : var.alb_listener_api_port, + } +} + +resource "aws_security_group" "boundary_sg" { + name = "boundary-sg-${random_string.cluster_id.result}" + description = "SSH and boundary Traffic" + vpc_id = var.vpc_id + + # SSH + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = flatten([formatlist("%s/32", data.enos_environment.localhost.public_ipv4_addresses)]) + } + + dynamic "ingress" { + for_each = local.listener_ports + + content { + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.localhost.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + description = ingress.key + from_port = ingress.value + to_port = ingress.value + ipv6_cidr_blocks = [] + prefix_list_ids = [] + protocol = "tcp" + self = null + security_groups = [] + } + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge( + local.common_tags, + { + Name = "${local.name_prefix}-boundary-sg" + }, + ) +} + +# Other modules use this SG to add rules for the Boundary controllers +resource "aws_security_group" "boundary_aux_sg" { + name = "boundary-sg-aux-${random_string.cluster_id.result}" + description = "Extra controller rules" + vpc_id = var.vpc_id + + tags = merge( + local.common_tags, + { + Name = "${local.name_prefix}-boundary-aux-sg" + }, + ) +} + +resource "aws_security_group" "boundary_alb_sg" { + name = "boundary-alb-sg-${random_string.cluster_id.result}" + description = "boundary Traffic" + vpc_id = var.vpc_id + + dynamic "ingress" { + for_each = local.alb_listener_ports + + content { + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.localhost.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + format("%s/32", aws_instance.controller.0.public_ip), + formatlist("%s/32", var.alb_sg_additional_ips) + ]) + description = ingress.key + from_port = ingress.value + to_port = ingress.value + ipv6_cidr_blocks = [] + prefix_list_ids = [] + protocol = "tcp" + self = null + security_groups = [] + } + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge( + local.common_tags, + { + Name = "${local.name_prefix}-boundary-alb-sg" + }, + ) +} + + +resource "aws_security_group" "boundary_db_sg" { + name = "boundary-db-sg-${random_string.cluster_id.result}" + description = "Postgres Traffic" + vpc_id = var.vpc_id + + ingress { + cidr_blocks = flatten([formatlist("%s/32", data.enos_environment.localhost.public_ipv4_addresses), join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block)]) + description = "database" + from_port = 5432 + to_port = 5432 + ipv6_cidr_blocks = [] + prefix_list_ids = [] + protocol = "tcp" + self = null + security_groups = [] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(local.common_tags, + { + Name = "${local.name_prefix}-boundary-db-sg" + }, + ) +} diff --git a/enos/modules/aws_boundary/templates/controller.hcl b/enos/modules/aws_boundary/templates/controller.hcl new file mode 100644 index 0000000000..eed7b15004 --- /dev/null +++ b/enos/modules/aws_boundary/templates/controller.hcl @@ -0,0 +1,54 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +disable_mlock = true + +controller { + name = "boundary-controller-${id}" + description = "Enos Boundary controller ${id}" + database { + url = "postgresql://${dbuser}:${dbpass}@${dbhost}:${dbport}/${dbname}" + max_open_connections = ${db_max_open_connections} + } +} + +# API listener configuration block +listener "tcp" { + # Should be the address of the NIC that the controller server will be reached on + address = "${local_ipv4}:${api_port}" + # The purpose of this listener block + purpose = "api" + tls_disable = true + + # Uncomment to enable CORS for the Admin UI. Be sure to set the allowed origin(s) + # to appropriate values. + #cors_enabled = true + #cors_allowed_origins = ["https://yourcorp.yourdomain.com", "serve://boundary"] +} + +# API listener configuration block +listener "tcp" { + address = "${local_ipv4}:${ops_port}" + purpose = "ops" + tls_disable = true +} + +# Data-plane listener configuration block (used for worker coordination) +listener "tcp" { + # Should be the IP of the NIC that the worker will connect on + address = "${local_ipv4}:${cluster_port}" + # The purpose of this listener + purpose = "cluster" +} + +kms "awskms" { + purpose = "root" + region = "${region}" + kms_key_id = "${kms_key_id}" +} + +kms "awskms" { + purpose = "worker-auth" + region = "${region}" + kms_key_id = "${kms_key_id}" +} diff --git a/enos/modules/aws_boundary/templates/controller_bsr.hcl b/enos/modules/aws_boundary/templates/controller_bsr.hcl new file mode 100644 index 0000000000..1e40006a23 --- /dev/null +++ b/enos/modules/aws_boundary/templates/controller_bsr.hcl @@ -0,0 +1,60 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +disable_mlock = true + +controller { + name = "boundary-controller-${id}" + description = "Enos Boundary controller ${id}" + database { + url = "postgresql://${dbuser}:${dbpass}@${dbhost}:${dbport}/${dbname}" + max_open_connections = ${db_max_open_connections} + } +} + +# API listener configuration block +listener "tcp" { + # Should be the address of the NIC that the controller server will be reached on + address = "${local_ipv4}:${api_port}" + # The purpose of this listener block + purpose = "api" + tls_disable = true + + # Uncomment to enable CORS for the Admin UI. Be sure to set the allowed origin(s) + # to appropriate values. + #cors_enabled = true + #cors_allowed_origins = ["https://yourcorp.yourdomain.com", "serve://boundary"] +} + +# API listener configuration block +listener "tcp" { + address = "${local_ipv4}:${ops_port}" + purpose = "ops" + tls_disable = true +} + +# Data-plane listener configuration block (used for worker coordination) +listener "tcp" { + # Should be the IP of the NIC that the worker will connect on + address = "${local_ipv4}:${cluster_port}" + # The purpose of this listener + purpose = "cluster" +} + +kms "awskms" { + purpose = "root" + region = "${region}" + kms_key_id = "${kms_key_id}" +} + +kms "awskms" { + purpose = "worker-auth" + region = "${region}" + kms_key_id = "${kms_key_id}" +} + +kms "awskms" { + purpose = "bsr" + region = "${region}" + kms_key_id = "${kms_key_id}" +} diff --git a/enos/modules/aws_boundary/templates/worker.hcl b/enos/modules/aws_boundary/templates/worker.hcl new file mode 100644 index 0000000000..0125a32381 --- /dev/null +++ b/enos/modules/aws_boundary/templates/worker.hcl @@ -0,0 +1,31 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +listener "tcp" { + purpose = "proxy" + tls_disable = true + address = "0.0.0.0" +} + +worker { + # Name attr must be unique across workers + name = "demo-worker-${id}" + description = "Enos Boundary worker ${id}" + + # Workers must be able to reach controllers on :9201 + controllers = ${controller_ips} + + public_addr = "${public_addr}" + + tags { + type = ["prod", "webservers"] + region = ["${region}"] + } +} + +# must be same key as used on controller config +kms "awskms" { + purpose = "worker-auth" + region = "${region}" + kms_key_id = "${kms_key_id}" +} diff --git a/enos/modules/aws_boundary/variables.tf b/enos/modules/aws_boundary/variables.tf new file mode 100644 index 0000000000..7c7a3edb72 --- /dev/null +++ b/enos/modules/aws_boundary/variables.tf @@ -0,0 +1,354 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +variable "project_name" { + description = "Name of the project." + type = string +} + +variable "environment" { + description = "Name of the environment. (CI/Dev/Test/etc)" + type = string +} + +variable "common_tags" { + description = "Tags to set for all resources" + type = map(string) +} + +variable "worker_count" { + description = "Number of Boundary worker instances in each subnet" + type = number + default = 3 +} + +variable "worker_instance_type" { + description = "EC2 Instance type" + type = string + default = "t2.micro" +} + +variable "worker_ebs_iops" { + description = "EBS IOPS for the root volume" + type = number + default = null +} + +variable "worker_ebs_size" { + description = "EBS volume size" + type = number + default = 8 +} + +variable "worker_ebs_type" { + description = "EBS volume type" + type = string + default = "gp2" +} + +variable "worker_ebs_throughput" { + description = "EBS data throughput (MiB/s) (only for gp2)" + default = null +} + +variable "worker_monitoring" { + description = "Enable detailed monitoring for workers" + type = bool + default = false +} + +variable "controller_count" { + description = "Number of Boundary controller instances in each subnet" + type = number + default = 3 +} + +variable "controller_instance_type" { + description = "EC2 Instance type" + type = string + default = "t2.micro" +} + +variable "controller_ebs_iops" { + description = "EBS IOPS for the root volume" + type = number + default = null +} + +variable "controller_ebs_size" { + description = "EBS volume size" + type = number + default = 8 +} + +variable "controller_ebs_type" { + description = "EBS volume type" + type = string + default = "gp2" +} + +variable "controller_ebs_throughput" { + description = "EBS data throughput (MiB/s) (only for gp2)" + default = null +} + +variable "controller_monitoring" { + description = "Enable detailed monitoring for controllers" + type = bool + default = false +} + +variable "ssh_user" { + description = "SSH user to authenticate as" + type = string + default = "ubuntu" +} + +variable "ssh_aws_keypair" { + description = "SSH keypair used to connect to EC2 instances" + type = string +} + +variable "ubuntu_ami_id" { + description = "Ubuntu LTS AMI from enos-infra" + type = string +} + +variable "vpc_id" { + description = "VPC ID from enos-infra" + type = string +} + +variable "kms_key_arn" { + description = "ARN of KMS Key from enos-infra" + type = string +} + +variable "db_class" { + description = "AWS RDS DB instance class (size/type)" + type = string + default = "db.t4g.small" +} + +variable "db_version" { + description = "AWS RDS DBS engine version (for postgres/mysql)" + type = string + default = "15.3" +} + +variable "db_engine" { + description = "AWS RDS DB engine type" + type = string + default = "postgres" +} + +variable "db_storage" { + description = "AWS RDS DB storage volume (in GB)" + type = number + default = 10 +} + +variable "db_storage_type" { + description = "AWS RDS DB storage type" + type = string + default = "gp2" +} + +variable "db_storage_iops" { + description = "AWS RDS DB storage IOPS (optional)" + type = string + default = null +} + +variable "db_name" { + description = "Name of the RDS Database" + type = string + default = null // default value defined in the locals +} + +variable "db_create" { + description = "Enables module to create RDS resources" + type = bool + default = true +} + +variable "db_host" { + description = "Address of a pre-configured PostgreSQL host" + type = string + default = null +} + +variable "db_port" { + description = "Address of a pre-configured PostgreSQL host" + type = number + default = 5432 +} + +variable "db_user" { + description = "Default username for RDS database" + type = string + default = "boundary" +} + +variable "db_pass" { + description = "Default password for RDS database" + type = string + default = "" +} + +variable "db_monitoring_interval" { + description = "Interval (in seconds) to report enhanced DB metrics. Disabled by default" + type = number + default = 0 +} + +variable "db_monitoring_role_arn" { + description = "The ARN of the IAM role to be used to report enhanced DB metrics. Must be set if db_monitoring_interval is set" + type = string + default = "" +} + +variable "db_max_open_connections" { + description = "The maximum number of open connections to the database. Limiting this limits the load a controller can handle." + type = number + default = 5 + + validation { + condition = var.db_max_open_connections >= 5 + error_message = "Max open connections must be at least 5." + } +} + +variable "db_snapshot_identifier" { + description = "The name of the DB snapshot to restore into the created RDS instance. Will be applied to all clusters created. If not set, no DB restore will be made." + type = string + default = null +} + +variable "boundary_release" { + description = "boundary release version and edition to install from releases.hashicorp.com" + type = object({ + version = string + }) + default = null +} + +variable "boundary_artifactory_release" { + description = "Boundary release version and edition to install from artifactory.hashicorp.engineering" + type = object({ + username = string + token = string + url = string + sha256 = string + }) + default = null +} + +variable "boundary_install_dir" { + description = "The remote directory where the boundary binary will be installed" + type = string + default = "/opt/boundary/bin" +} + +variable "boundary_binary_name" { + description = "Boundary binary name" + type = string + default = "boundary" +} + +variable "boundary_data_dir" { + description = "The directory where the boundary will store data" + type = string + default = "/opt/boundary/data" +} + +variable "boundary_log_dir" { + description = "The directory where the boundary will write log output" + type = string + default = "/var/log/boundary.d" +} + +variable "boundary_config_dir" { + description = "The directory where boundary config files will live" + type = string + default = "/etc/boundary" +} + +variable "local_artifact_path" { + description = "path to a local boundary.zip" + type = string + default = null +} + +variable "alb_listener_api_port" { + description = "The load balancer port that will expose controller APIs" + type = number + default = 9200 +} + +variable "listener_api_port" { + description = "The port controller instances will bind the controller API to" + type = number + default = 9200 +} + +variable "listener_cluster_port" { + description = "The port controller and worker instances will bind to for communication" + type = number + default = 9201 +} + +variable "listener_proxy_port" { + description = "The port worker instances will bind the worker API to" + type = number + default = 9202 +} + +variable "listener_ops_port" { + description = "The port controller instances will bind the operational API to" + type = number + default = 9203 +} + +variable "healthcheck_path" { + type = string + description = "Path to use for ALB healthcheck" + default = "/health" +} + +variable "alb_sg_additional_ips" { + description = "Additional IPs to be allowed (ingress) on an ALB Security Group" + type = list(string) + default = [] +} + +variable "boundary_license" { + description = "Boundary license (not needed for OSS, required for enterprise)" + type = string + sensitive = true + default = null +} + +variable "controller_config_file_path" { + description = "Path to config file to use (relative to module directory)" + type = string + default = "templates/controller.hcl" +} + +variable "worker_config_file_path" { + description = "Path to config file to use (relative to module directory)" + type = string + default = "templates/worker.hcl" +} + +variable "aws_region" { + description = "AWS Region to create resources in" + type = string + default = "us-east-1" +} + +variable "vpc_tag_module" { + description = "Name of the Module Tag tied to the VPC" + type = string + default = "aws_vpc" +} diff --git a/enos/modules/bucket/locals.tf b/enos/modules/aws_bucket/locals.tf similarity index 100% rename from enos/modules/bucket/locals.tf rename to enos/modules/aws_bucket/locals.tf diff --git a/enos/modules/bucket/main.tf b/enos/modules/aws_bucket/main.tf similarity index 100% rename from enos/modules/bucket/main.tf rename to enos/modules/aws_bucket/main.tf diff --git a/enos/modules/bucket/outputs.tf b/enos/modules/aws_bucket/outputs.tf similarity index 100% rename from enos/modules/bucket/outputs.tf rename to enos/modules/aws_bucket/outputs.tf diff --git a/enos/modules/bucket/variables.tf b/enos/modules/aws_bucket/variables.tf similarity index 100% rename from enos/modules/bucket/variables.tf rename to enos/modules/aws_bucket/variables.tf diff --git a/enos/modules/iam_setup/main.tf b/enos/modules/aws_iam_setup/main.tf similarity index 100% rename from enos/modules/iam_setup/main.tf rename to enos/modules/aws_iam_setup/main.tf diff --git a/enos/modules/target/main.tf b/enos/modules/aws_target/main.tf similarity index 100% rename from enos/modules/target/main.tf rename to enos/modules/aws_target/main.tf diff --git a/enos/modules/aws_vpc/main.tf b/enos/modules/aws_vpc/main.tf index fa8ab46839..f5632731c8 100644 --- a/enos/modules/aws_vpc/main.tf +++ b/enos/modules/aws_vpc/main.tf @@ -46,10 +46,11 @@ variable "ami_architectures" { locals { // AWS AMIs standardized on the x86_64 label for 64bit x86 architectures, therefore amd64 should be rather x86_64. architecture_filters = [for arch in var.ami_architectures : (arch == "amd64" ? "x86_64" : arch)] + tag_module = "aws_vpc" common_tags = merge( var.common_tags, { - "Module" = "terraform-enos-aws-infra" // TODO: Rename this once terraform-enos-aws-boundary is moved + "Module" = local.tag_module }, ) } @@ -237,3 +238,7 @@ output "ami_ids" { rhel = { for idx, arch in var.ami_architectures : arch => data.aws_ami.rhel[idx].id } } } + +output "vpc_tag_module" { + value = local.tag_module +} diff --git a/enos/modules/worker/main.tf b/enos/modules/aws_worker/main.tf similarity index 100% rename from enos/modules/worker/main.tf rename to enos/modules/aws_worker/main.tf diff --git a/enos/modules/worker/outputs.tf b/enos/modules/aws_worker/outputs.tf similarity index 100% rename from enos/modules/worker/outputs.tf rename to enos/modules/aws_worker/outputs.tf diff --git a/enos/modules/worker/templates/worker.hcl b/enos/modules/aws_worker/templates/worker.hcl similarity index 100% rename from enos/modules/worker/templates/worker.hcl rename to enos/modules/aws_worker/templates/worker.hcl diff --git a/enos/modules/worker/templates/worker_bsr.hcl b/enos/modules/aws_worker/templates/worker_bsr.hcl similarity index 100% rename from enos/modules/worker/templates/worker_bsr.hcl rename to enos/modules/aws_worker/templates/worker_bsr.hcl diff --git a/enos/modules/worker/variables.tf b/enos/modules/aws_worker/variables.tf similarity index 100% rename from enos/modules/worker/variables.tf rename to enos/modules/aws_worker/variables.tf