From 77b06a1317ee04f8e8bbf5a029ebd3f7e59ad69b Mon Sep 17 00:00:00 2001 From: "Matthew C. Morgan" Date: Thu, 19 Feb 2026 14:33:51 -0500 Subject: [PATCH] stub for s3 --- modules/ec2/data.tf | 4 +- modules/s3/data.tf | 69 +++++++++ modules/s3/locals.tf | 70 +++++++++ modules/s3/main.tf | 48 ++++++ modules/s3/module_name.tf | 3 + modules/s3/outputs.tf | 79 ++++++++++ modules/s3/prefixes.tf | 28 ++++ modules/s3/variables.common.tf | 175 ++++++++++++++++++++++ modules/s3/variables.safeguards.tf | 24 +++ modules/s3/variables.tags.tf | 22 +++ modules/s3/versions.tf | 9 ++ s3/availabilty_zones.tf | 1 - s3/base_settings.tf | 1 - s3/base_tags.tf | 1 - s3/data.tf | 1 - s3/defaults.tf | 1 - s3/locals.tf | 1 - s3/main.tf | 1 - s3/outputs.tf | 1 - s3/prefixes.tf | 1 - s3/resources.tf | 1 - s3/variables.common.availability_zones.tf | 1 - s3/variables.common.tf | 1 - s3/variables.parameters.tf | 1 - s3/variables.product.tf | 1 - s3/variables.safeguards.tf | 1 - s3/variables.tags.tf | 1 - s3/version.tf | 1 - s3/versions.tf | 1 - 29 files changed, 529 insertions(+), 20 deletions(-) create mode 100644 modules/s3/data.tf create mode 100644 modules/s3/locals.tf create mode 100644 modules/s3/main.tf create mode 100644 modules/s3/module_name.tf create mode 100644 modules/s3/outputs.tf create mode 100644 modules/s3/prefixes.tf create mode 100644 modules/s3/variables.common.tf create mode 100644 modules/s3/variables.safeguards.tf create mode 100644 modules/s3/variables.tags.tf create mode 100644 modules/s3/versions.tf delete mode 120000 s3/availabilty_zones.tf delete mode 120000 s3/base_settings.tf delete mode 120000 s3/base_tags.tf delete mode 120000 s3/data.tf delete mode 120000 s3/defaults.tf delete mode 120000 s3/locals.tf delete mode 120000 s3/main.tf delete mode 120000 s3/outputs.tf delete mode 120000 s3/prefixes.tf delete mode 120000 s3/resources.tf delete mode 120000 s3/variables.common.availability_zones.tf delete mode 120000 s3/variables.common.tf delete mode 120000 s3/variables.parameters.tf delete mode 120000 s3/variables.product.tf delete mode 120000 s3/variables.safeguards.tf delete mode 120000 s3/variables.tags.tf delete mode 120000 s3/version.tf delete mode 120000 s3/versions.tf diff --git a/modules/ec2/data.tf b/modules/ec2/data.tf index 4a0532d..bf678aa 100644 --- a/modules/ec2/data.tf +++ b/modules/ec2/data.tf @@ -61,9 +61,9 @@ data "aws_availability_zone" "zone" { } data "external" "portfolio" { - program = ["bash", "-c", "aws servicecatalog list-portfolios --region ${local.region} --query \"PortfolioDetails[?contains(DisplayName, 'Service Portfolio for')]|[0]|{id: Id}\" --output json"] + program = ["bash", "-c", "tf-aws servicecatalog list-portfolios --region ${local.region} --query \"PortfolioDetails[?contains(DisplayName, 'Service Portfolio for')]|[0]|{id: Id}\" --output json"] } data "external" "product" { - program = ["bash", "-c", "aws servicecatalog search-products --region ${local.region} --query \"ProductViewSummaries[?contains(Name, 'RHEL')]|[0]|{id: ProductId}\" --output json"] + program = ["bash", "-c", "tf-aws servicecatalog search-products --region ${local.region} --query \"ProductViewSummaries[?contains(Name, 'RHEL')]|[0]|{id: ProductId}\" --output json"] } diff --git a/modules/s3/data.tf b/modules/s3/data.tf new file mode 100644 index 0000000..bf678aa --- /dev/null +++ b/modules/s3/data.tf @@ -0,0 +1,69 @@ +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} +data "aws_arn" "current" { + arn = data.aws_caller_identity.current.arn +} + +data "aws_region" "current" {} + +data "aws_vpc" "vpc" { + filter { + name = "tag:Name" + values = [var.vpc_name] + } +} + +data "aws_subnets" "subnets" { + filter { + name = "tag:Name" + values = [var.subnets_name] + } + filter { + name = "vpc-id" + values = [data.aws_vpc.vpc.id] + } +} + +data "aws_subnet" "subnets" { + for_each = toset(data.aws_subnets.subnets.ids) + id = each.key +} + +// Get portfolio details if we resolved an ID +data "aws_servicecatalog_portfolio" "by_id" { + count = local.portfolio_id != null ? 1 : 0 + id = local.portfolio_id + accept_language = var.accept_language +} + +// Get product details by ID (requires product_id) +data "aws_servicecatalog_product" "by_id" { + count = local.product_id != null ? 1 : 0 + id = local.product_id + accept_language = var.accept_language +} + +// Get the latest provisioning artifact (product version) +data "aws_servicecatalog_provisioning_artifacts" "this" { + count = local.product_id != null ? 1 : 0 + accept_language = var.accept_language + product_id = local.product_id +} + +data "aws_availability_zones" "zones" { + state = "available" +} + +data "aws_availability_zone" "zone" { + for_each = toset(data.aws_availability_zones.zones.names) + state = "available" + name = each.key +} + +data "external" "portfolio" { + program = ["bash", "-c", "tf-aws servicecatalog list-portfolios --region ${local.region} --query \"PortfolioDetails[?contains(DisplayName, 'Service Portfolio for')]|[0]|{id: Id}\" --output json"] +} + +data "external" "product" { + program = ["bash", "-c", "tf-aws servicecatalog search-products --region ${local.region} --query \"ProductViewSummaries[?contains(Name, 'RHEL')]|[0]|{id: ProductId}\" --output json"] +} diff --git a/modules/s3/locals.tf b/modules/s3/locals.tf new file mode 100644 index 0000000..26bea18 --- /dev/null +++ b/modules/s3/locals.tf @@ -0,0 +1,70 @@ +locals { + account_id = data.aws_caller_identity.current.account_id + az_name = data.aws_subnet.subnets[sort(data.aws_subnets.subnets.ids)[0]].availability_zone + partition = data.aws_partition.current.partition + region = data.aws_region.current.id + vpc_id = data.aws_vpc.vpc.id + + # Use provided portfolio_id or fall back to external data source lookup + portfolio_id = var.portfolio_id != null ? var.portfolio_id : try(data.external.portfolio.result.id, null) + + # Use provided product_id or fall back to external data source lookup + product_id = var.product_id != null ? var.product_id : try(data.external.product.result.id, null) + + # Get the latest provisioning artifact ID + latest_artifact_id = local.product_id == null ? null : try( + [for artifact in data.aws_servicecatalog_provisioning_artifacts.this[0].provisioning_artifact_details : + artifact.id if artifact.active + ][0], + null + ) + + # Use provided path_id or default to latest + provisioning_artifact_id = var.path_id != null ? var.path_id : local.latest_artifact_id + + # Build default parameters from module variables + default_parameters = { + ProjectName = var.project_name + VpcId = local.vpc_id + AZName = local.az_name + InstanceType = var.instance_type + NameTag = var.provisioned_product_name + OSName = var.os_name + Creator = var.creator + ContactEmail = var.contact_email + IncPocEmail = var.inc_poc_email + RequiresBackup = var.requires_backup + PowerSchedule = var.power_schedule + FISMAID = var.fisma_id + } + + # Merge defaults with user-provided parameters (user params override defaults) + parameters = merge( + local.default_parameters, + var.parameters + ) + + # Convert parameters map to the format expected by aws_servicecatalog_provisioned_product + provisioning_parameters = [ + for key, value in local.parameters : { + key = key + value = tostring(value) + } + if value != "" # Only include non-empty values + ] + + standard_tags = { + ManagedBy = "Terraform" + Module = local.module_name + } + + enforced_tags = merge( + local.standard_tags, + var.enforced_tags + ) + + tags = merge( + local.enforced_tags, + var.tags + ) +} \ No newline at end of file diff --git a/modules/s3/main.tf b/modules/s3/main.tf new file mode 100644 index 0000000..b4f97fe --- /dev/null +++ b/modules/s3/main.tf @@ -0,0 +1,48 @@ +# Product Submodule +# +# Provisions a Service Catalog product +# using a pre-configured portfolio and product + +resource "aws_servicecatalog_provisioned_product" "this" { + name = var.provisioned_product_name + product_id = local.product_id + provisioning_artifact_id = local.provisioning_artifact_id + region = local.region + path_id = var.path_id + accept_language = var.accept_language + ignore_errors = var.ignore_errors + notification_arns = var.notification_arns + retain_physical_resources = var.retain_physical_resources + + dynamic "provisioning_parameters" { + for_each = local.provisioning_parameters + content { + key = provisioning_parameters.value.key + value = provisioning_parameters.value.value + } + } + + dynamic "stack_set_provisioning_preferences" { + for_each = var.stack_set_provisioning_preferences != null ? [var.stack_set_provisioning_preferences] : [] + content { + accounts = try(stack_set_provisioning_preferences.value.accounts, null) + failure_tolerance_count = try(stack_set_provisioning_preferences.value.failure_tolerance_count, null) + failure_tolerance_percentage = try(stack_set_provisioning_preferences.value.failure_tolerance_percentage, null) + max_concurrency_count = try(stack_set_provisioning_preferences.value.max_concurrency_count, null) + max_concurrency_percentage = try(stack_set_provisioning_preferences.value.max_concurrency_percentage, null) + regions = try(stack_set_provisioning_preferences.value.regions, null) + } + } + + tags = local.tags + + timeouts { + create = var.timeout + update = var.timeout + delete = var.timeout + } + + depends_on = [ + data.aws_servicecatalog_provisioning_artifacts.this + ] +} diff --git a/modules/s3/module_name.tf b/modules/s3/module_name.tf new file mode 100644 index 0000000..87a6a5c --- /dev/null +++ b/modules/s3/module_name.tf @@ -0,0 +1,3 @@ +locals { + module_name = "aws-servicecatalog/ec2" +} diff --git a/modules/s3/outputs.tf b/modules/s3/outputs.tf new file mode 100644 index 0000000..5ab1436 --- /dev/null +++ b/modules/s3/outputs.tf @@ -0,0 +1,79 @@ +output "provisioned_product_id" { + description = "The ID of the provisioned product" + value = aws_servicecatalog_provisioned_product.this.id +} + +output "provisioned_product_name" { + description = "The name of the provisioned product" + value = aws_servicecatalog_provisioned_product.this.name +} + +output "provisioned_product_arn" { + description = "The ARN of the provisioned product" + value = aws_servicecatalog_provisioned_product.this.arn +} + +output "provisioned_product_type" { + description = "The type of the provisioned product" + value = aws_servicecatalog_provisioned_product.this.type +} + +output "provisioned_product_status" { + description = "The status of the provisioned product" + value = aws_servicecatalog_provisioned_product.this.status +} + +output "provisioned_product_status_message" { + description = "The status message for the provisioned product" + value = aws_servicecatalog_provisioned_product.this.status_message +} + +output "launch_role_arn" { + description = "The ARN of the launch role" + value = aws_servicecatalog_provisioned_product.this.launch_role_arn +} + +output "portfolio_id" { + description = "The ID of the portfolio used" + value = local.portfolio_id +} + +output "product_id" { + description = "The ID of the product used" + value = local.product_id +} + +output "provisioning_artifact_id" { + description = "The ID of the provisioning artifact used" + value = local.provisioning_artifact_id +} + +output "vpc_id" { + description = "The VPC ID where the instance will be provisioned" + value = data.aws_vpc.vpc.id +} + +output "subnet_ids" { + description = "The subnet IDs where the instance can be provisioned" + value = data.aws_subnets.subnets.ids +} + +output "availability_zone" { + description = "The availability zone of the first selected subnet" + value = data.aws_subnet.subnets[sort(data.aws_subnets.subnets.ids)[0]].availability_zone +} + +output "availability_zone_names" { + description = "VPC Availability zone name list" + value = data.aws_availability_zones.zones.names +} + +output "availability_zone_ids" { + description = "VPC Availability zone id list" + value = data.aws_availability_zones.zones.zone_ids +} + +output "availability_zone_suffixes" { + description = "VPC Availability zone suffix list" + value = [for k, v in data.aws_availability_zone.zone : v.name_suffix] +} \ No newline at end of file diff --git a/modules/s3/prefixes.tf b/modules/s3/prefixes.tf new file mode 100644 index 0000000..d2ee1fe --- /dev/null +++ b/modules/s3/prefixes.tf @@ -0,0 +1,28 @@ +locals { + _prefixes = { + "efs" = "v-efs-" + "s3" = "v-s3-" + "ebs" = "v-ebs-" + "kms" = "k-kms-" + "role" = "r-" + "policy" = "p-" + "group" = "g-" + "security-group" = "" # "sg-" + # VPC + "vpc" = "" + "dhcp-options" = "" + "vpc-peer" = "vpcp-" + "route-table" = "route-" + "subnet" = "" + "vpc-endpoint" = "vpce-" + "elastic-ip" = "eip-" + "nat-gateway" = "nat-" + "internet-gateway" = "igw-" + "network-acl" = "nacl-" + "customer-gateway" = "cgw-" + "vpn-gateway" = "vpcg-" + "vpn-connection" = "vpn_" + "log-group" = "lg-" + "log-stream" = "lgs-" + } +} diff --git a/modules/s3/variables.common.tf b/modules/s3/variables.common.tf new file mode 100644 index 0000000..195849d --- /dev/null +++ b/modules/s3/variables.common.tf @@ -0,0 +1,175 @@ +#--- +# account info +#--- +variable "account_id" { + description = "AWS Account ID (default will pull from current user)" + type = string + default = "" +} + +variable "account_alias" { + description = "AWS Account Alias" + type = string + default = "" +} + +variable "override_prefixes" { + description = "Override built-in prefixes by component. This should be used primarily for common infrastructure things" + type = map(string) + default = {} +} + +variable "availability_zones" { + description = "AWS Availability Zones to use (by default will use all available)" + type = list(string) + default = [] +} + +variable "parameters" { + description = "Parameters to pass to the Service Catalog product. Map of parameter names to values" + type = map(string) + default = {} +} + +variable "project_name" { + description = "Project name (ProjectName parameter)" + type = string + default = "" +} + +variable "creator" { + description = "Creator's JBID (Creator parameter)" + type = string + default = "" +} + +variable "contact_email" { + description = "Provisioning user's email (ContactEmail parameter)" + type = string + default = "" +} + +variable "inc_poc_email" { + description = "Incident POC email (IncPocEmail parameter)" + type = string + default = "" +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.small" +} + +variable "os_name" { + description = "Operating system version" + type = string + default = "RHEL9" +} + +variable "requires_backup" { + description = "Backup requirement" + type = string + default = "no" +} + +variable "power_schedule" { + description = "Power schedule" + type = string + default = "" +} + +variable "fisma_id" { + description = "FISMA ID" + type = string + default = "" +} + +variable "provisioned_product_name" { + description = "Name of the provisioned product" + type = string + + validation { + condition = length(var.provisioned_product_name) > 0 && length(var.provisioned_product_name) <= 128 + error_message = "provisioned_product_name must be between 1 and 128 characters" + } +} + +variable "portfolio_id" { + description = "Portfolio ID. If not provided, will lookup by portfolio_name_pattern" + type = string + default = "port-pgj3zvoqca7ya" +} + +variable "product_id" { + description = "Product ID. If not provided, will lookup by product_name_pattern" + type = string + default = "prod-43foqxjcq5isw" +} + +variable "product_name_pattern" { + description = "Pattern to search for product by name" + type = string + default = "linux-product" +} + +variable "path_id" { + description = "Path identifier of the product. If not provided, will use the latest active artifact" + type = string + default = null +} + +variable "ignore_errors" { + description = "Only applies to deleting. If true, errors from the underlying service are ignored" + type = bool + default = false +} + +variable "notification_arns" { + description = "SNS topic ARNs to notify when the provisioned product changes" + type = list(string) + default = [] +} + +variable "retain_physical_resources" { + description = "Whether to retain the physical resources when the provisioned product is terminated" + type = bool + default = false +} + +variable "stack_set_provisioning_preferences" { + description = "Configuration for StackSet provisioning" + type = object({ + accounts = optional(list(string)) + failure_tolerance_count = optional(number) + failure_tolerance_percentage = optional(number) + max_concurrency_count = optional(number) + max_concurrency_percentage = optional(number) + regions = optional(list(string)) + }) + default = null +} + +variable "retrieve_stack_outputs" { + description = "Whether to retrieve CloudFormation stack outputs" + type = bool + default = true +} + +variable "timeout" { + description = "Timeout for provisioned product operations (create/update/delete)" + type = string + default = "15m" +} + +variable "vpc_name" { + description = "Name tag of the VPC to deploy into" + type = string + default = "" +} + +variable "subnets_name" { + description = "Name tag of the subnets to deploy into" + type = string + default = "*-apps-*" +} \ No newline at end of file diff --git a/modules/s3/variables.safeguards.tf b/modules/s3/variables.safeguards.tf new file mode 100644 index 0000000..91c21b1 --- /dev/null +++ b/modules/s3/variables.safeguards.tf @@ -0,0 +1,24 @@ +# This file contains safeguard variables to prevent accidental destruction +# Pattern follows aws-s3 module conventions + +variable "enable_deletion_protection" { + description = "Enable deletion protection to prevent accidental termination" + type = bool + default = false +} + +locals { + deletion_protection_error = "Deletion protection is enabled. Set enable_deletion_protection = false to allow termination." +} + +resource "null_resource" "deletion_protection" { + count = var.enable_deletion_protection ? 1 : 0 + + lifecycle { + prevent_destroy = true + } + + triggers = { + provisioned_product_id = aws_servicecatalog_provisioned_product.this.id + } +} diff --git a/modules/s3/variables.tags.tf b/modules/s3/variables.tags.tf new file mode 100644 index 0000000..18c6001 --- /dev/null +++ b/modules/s3/variables.tags.tf @@ -0,0 +1,22 @@ +variable "tags" { + description = "Additional tags to apply to resources" + type = map(string) + default = {} +} + +variable "enforced_tags" { + description = "Tags enforced on all resources" + type = map(string) + default = {} +} + +variable "accept_language" { + description = "Language code for Service Catalog API calls" + type = string + default = "en" + + validation { + condition = contains(["en", "jp", "zh"], var.accept_language) + error_message = "accept_language must be one of: en, jp, zh" + } +} diff --git a/modules/s3/versions.tf b/modules/s3/versions.tf new file mode 100644 index 0000000..dd0ebb9 --- /dev/null +++ b/modules/s3/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} diff --git a/s3/availabilty_zones.tf b/s3/availabilty_zones.tf deleted file mode 120000 index 00a240c..0000000 --- a/s3/availabilty_zones.tf +++ /dev/null @@ -1 +0,0 @@ -../common/availabilty_zones.tf \ No newline at end of file diff --git a/s3/base_settings.tf b/s3/base_settings.tf deleted file mode 120000 index 396784e..0000000 --- a/s3/base_settings.tf +++ /dev/null @@ -1 +0,0 @@ -../common/base_settings.tf \ No newline at end of file diff --git a/s3/base_tags.tf b/s3/base_tags.tf deleted file mode 120000 index 91c15aa..0000000 --- a/s3/base_tags.tf +++ /dev/null @@ -1 +0,0 @@ -../common/base_tags.tf \ No newline at end of file diff --git a/s3/data.tf b/s3/data.tf deleted file mode 120000 index 995624d..0000000 --- a/s3/data.tf +++ /dev/null @@ -1 +0,0 @@ -../common/data.tf \ No newline at end of file diff --git a/s3/defaults.tf b/s3/defaults.tf deleted file mode 120000 index a5556ac..0000000 --- a/s3/defaults.tf +++ /dev/null @@ -1 +0,0 @@ -../common/defaults.tf \ No newline at end of file diff --git a/s3/locals.tf b/s3/locals.tf deleted file mode 120000 index cad3d5e..0000000 --- a/s3/locals.tf +++ /dev/null @@ -1 +0,0 @@ -../common/locals.tf \ No newline at end of file diff --git a/s3/main.tf b/s3/main.tf deleted file mode 120000 index 4a4ab61..0000000 --- a/s3/main.tf +++ /dev/null @@ -1 +0,0 @@ -../common/main.tf \ No newline at end of file diff --git a/s3/outputs.tf b/s3/outputs.tf deleted file mode 120000 index 93b0065..0000000 --- a/s3/outputs.tf +++ /dev/null @@ -1 +0,0 @@ -../common/outputs.tf \ No newline at end of file diff --git a/s3/prefixes.tf b/s3/prefixes.tf deleted file mode 120000 index 7e265d5..0000000 --- a/s3/prefixes.tf +++ /dev/null @@ -1 +0,0 @@ -../common/prefixes.tf \ No newline at end of file diff --git a/s3/resources.tf b/s3/resources.tf deleted file mode 120000 index 6dd8c84..0000000 --- a/s3/resources.tf +++ /dev/null @@ -1 +0,0 @@ -../common/resources.tf \ No newline at end of file diff --git a/s3/variables.common.availability_zones.tf b/s3/variables.common.availability_zones.tf deleted file mode 120000 index dca20a3..0000000 --- a/s3/variables.common.availability_zones.tf +++ /dev/null @@ -1 +0,0 @@ -../common/variables.common.availability_zones.tf \ No newline at end of file diff --git a/s3/variables.common.tf b/s3/variables.common.tf deleted file mode 120000 index 7439ed8..0000000 --- a/s3/variables.common.tf +++ /dev/null @@ -1 +0,0 @@ -../common/variables.common.tf \ No newline at end of file diff --git a/s3/variables.parameters.tf b/s3/variables.parameters.tf deleted file mode 120000 index 95d9713..0000000 --- a/s3/variables.parameters.tf +++ /dev/null @@ -1 +0,0 @@ -../common/variables.parameters.tf \ No newline at end of file diff --git a/s3/variables.product.tf b/s3/variables.product.tf deleted file mode 120000 index 33b985c..0000000 --- a/s3/variables.product.tf +++ /dev/null @@ -1 +0,0 @@ -../common/variables.product.tf \ No newline at end of file diff --git a/s3/variables.safeguards.tf b/s3/variables.safeguards.tf deleted file mode 120000 index 4c1b2bb..0000000 --- a/s3/variables.safeguards.tf +++ /dev/null @@ -1 +0,0 @@ -../common/variables.safeguards.tf \ No newline at end of file diff --git a/s3/variables.tags.tf b/s3/variables.tags.tf deleted file mode 120000 index 5b14f30..0000000 --- a/s3/variables.tags.tf +++ /dev/null @@ -1 +0,0 @@ -../common/variables.tags.tf \ No newline at end of file diff --git a/s3/version.tf b/s3/version.tf deleted file mode 120000 index b83c5b7..0000000 --- a/s3/version.tf +++ /dev/null @@ -1 +0,0 @@ -../common/version.tf \ No newline at end of file diff --git a/s3/versions.tf b/s3/versions.tf deleted file mode 120000 index 41bb22f..0000000 --- a/s3/versions.tf +++ /dev/null @@ -1 +0,0 @@ -../common/versions.tf \ No newline at end of file