diff --git a/modules/ec2/README.md b/modules/ec2/linux/README.md similarity index 100% rename from modules/ec2/README.md rename to modules/ec2/linux/README.md diff --git a/modules/ec2/data.tf b/modules/ec2/linux/data.tf similarity index 100% rename from modules/ec2/data.tf rename to modules/ec2/linux/data.tf diff --git a/modules/ec2/locals.tf b/modules/ec2/linux/locals.tf similarity index 100% rename from modules/ec2/locals.tf rename to modules/ec2/linux/locals.tf diff --git a/modules/ec2/main.tf b/modules/ec2/linux/main.tf similarity index 100% rename from modules/ec2/main.tf rename to modules/ec2/linux/main.tf diff --git a/modules/ec2/module_name.tf b/modules/ec2/linux/module_name.tf similarity index 100% rename from modules/ec2/module_name.tf rename to modules/ec2/linux/module_name.tf diff --git a/modules/ec2/outputs.tf b/modules/ec2/linux/outputs.tf similarity index 100% rename from modules/ec2/outputs.tf rename to modules/ec2/linux/outputs.tf diff --git a/modules/ec2/variables.common.tf b/modules/ec2/linux/variables.common.tf similarity index 100% rename from modules/ec2/variables.common.tf rename to modules/ec2/linux/variables.common.tf diff --git a/modules/ec2/variables.product.tf b/modules/ec2/linux/variables.product.tf similarity index 100% rename from modules/ec2/variables.product.tf rename to modules/ec2/linux/variables.product.tf diff --git a/modules/ec2/variables.servicecatalog.tf b/modules/ec2/linux/variables.servicecatalog.tf similarity index 100% rename from modules/ec2/variables.servicecatalog.tf rename to modules/ec2/linux/variables.servicecatalog.tf diff --git a/modules/ec2/variables.tags.tf b/modules/ec2/linux/variables.tags.tf similarity index 100% rename from modules/ec2/variables.tags.tf rename to modules/ec2/linux/variables.tags.tf diff --git a/modules/ec2/versions.tf b/modules/ec2/linux/versions.tf similarity index 100% rename from modules/ec2/versions.tf rename to modules/ec2/linux/versions.tf diff --git a/modules/ec2/windows/README.md b/modules/ec2/windows/README.md new file mode 100644 index 0000000..d80ac57 --- /dev/null +++ b/modules/ec2/windows/README.md @@ -0,0 +1,244 @@ +# Service Catalog EC2 Module + +## Overview + +This module provisions EC2 instances via AWS Service Catalog. It provides a simplified Terraform interface to the Service Catalog EC2 product, handling all the complexity of parameter mapping, networking lookups, and provisioning. + +This is a product-specific wrapper around the base [`product`](../product/) module, adding EC2-specific parameters and defaults. + +## Features + +- **Simplified Interface**: Clean Terraform variables instead of raw Service Catalog parameters +- **Automatic VPC/Subnet Resolution**: Looks up VPC and subnet information by name tags +- **Latest Version by Default**: Automatically uses the latest active provisioning artifact +- **Flexible Parameter Override**: Pass additional parameters via the `parameters` variable +- **Standard Tagging**: Consistent tagging across all resources +- **EC2-Specific Validations**: Built-in validation for EC2 parameters + +## Usage + +### Basic Example + +```hcl +module "ec2_instance" { + source = "git::https://your-repo/terraform-modules/aws-servicecatalog//modules/ec2?ref=v1.0.0" + + # Service Catalog Configuration + portfolio_id = "port-pgj3zvoqca7ya" + product_id = "prod-43foqxjcq5isw" + + # Instance Identity + provisioned_product_name = "app-web-01" + project_name = "csvd_myproject_dev-229685449397" + + # Networking + vpc_name = "vpc-dev-us-east-1" + subnets_name = "*-apps-*" + + # EC2 Configuration + instance_type = "t3a.medium" + os_name = "RHEL9" + power_schedule = "Weekday_Core_Hours_7-7" + requires_backup = "yes" + + # Contact Information + creator = "jsmith" + contact_email = "jsmith@example.com" + inc_poc_email = "team@example.com" + fisma_id = "OCIO_CSVD (CEN16.09)" + + tags = { + Environment = "dev" + Application = "web-server" + } +} +``` + +### Advanced Example with Custom Parameters + +```hcl +module "ec2_instance" { + source = "git::https://your-repo/terraform-modules/aws-servicecatalog//modules/ec2?ref=v1.0.0" + + portfolio_id = "port-pgj3zvoqca7ya" + product_id = "prod-43foqxjcq5isw" + provisioned_product_name = "app-db-01" + project_name = "csvd_database_prod-229685449397" + + vpc_name = "vpc-prod-us-east-1" + subnets_name = "subnet-data-*" + + instance_type = "r5.xlarge" + os_name = "RHEL9" + power_schedule = "24x7" + requires_backup = "yes" + + creator = "dbadmin" + contact_email = "dbadmin@example.com" + inc_poc_email = "dba-team@example.com" + fisma_id = "OCIO_DATA (DAT01.01)" + + # Custom parameters not covered by named variables + parameters = { + VolumeAppsSize = "200" + SecurityGroupNames = "sg-database,sg-monitoring" + pDescription = "Production database server" + Volume1Mount = "/data01" + Volume1Size = "500" + } + + # Service Catalog Options + timeout = "30m" + notification_arns = ["arn:aws:sns:us-east-1:123456789012:sc-notifications"] + retain_physical_resources = true + + tags = { + Environment = "prod" + Application = "database" + CostCenter = "engineering" + } +} +``` + +## Requirements + +| Name | Version | +|------|---------| +| terraform | >= 1.0 | +| aws | >= 6.0 | + +## Providers + +No providers are used directly - all AWS resources are provisioned by the underlying `product` module. + +## Variables + +### Required Variables + +| Name | Description | Type | +|------|-------------|------| +| `provisioned_product_name` | Name of the provisioned product (1-128 characters) | `string` | + +### Service Catalog Configuration + +| Name | Description | Type | Default | +|------|-------------|------|---------| +| `portfolio_id` | Portfolio ID containing the EC2 product | `string` | `"port-pgj3zvoqca7ya"` | +| `product_id` | Product ID for the EC2 product | `string` | `"prod-43foqxjcq5isw"` | +| `path_id` | Path identifier (version). Uses latest if not provided | `string` | `null` | +| `timeout` | Timeout for provisioning operations | `string` | `"15m"` | +| `accept_language` | Language code for API calls | `string` | `"en"` | +| `ignore_errors` | Ignore errors during provisioning | `bool` | `false` | +| `notification_arns` | SNS topic ARNs for notifications | `list(string)` | `[]` | +| `retain_physical_resources` | Retain resources when deleted | `bool` | `false` | +| `stack_set_provisioning_preferences` | StackSet preferences | `object` | `null` | + +### EC2 Configuration + +| Name | Description | Type | Default | +|------|-------------|------|---------| +| `instance_type` | EC2 instance type | `string` | `"t3.small"` | +| `os_name` | Operating system version | `string` | `"RHEL9"` | +| `requires_backup` | Backup requirement (yes/no) | `string` | `"no"` | +| `power_schedule` | Power schedule name | `string` | `""` | + +### Common Parameters + +| Name | Description | Type | Default | +|------|-------------|------|---------| +| `project_name` | Project name (must end with account ID) | `string` | `""` | +| `creator` | Creator's JBID | `string` | `""` | +| `contact_email` | Provisioning user's email | `string` | `""` | +| `inc_poc_email` | Incident POC email | `string` | `""` | +| `fisma_id` | FISMA ID | `string` | `""` | + +### Networking + +| Name | Description | Type | Default | +|------|-------------|------|---------| +| `vpc_name` | Name tag of the VPC | `string` | `""` | +| `subnets_name` | Name tag pattern for subnets | `string` | `"*-apps-*"` | + +### Other + +| Name | Description | Type | Default | +|------|-------------|------|---------| +| `parameters` | Additional Service Catalog parameters | `map(string)` | `{}` | +| `tags` | Tags to apply to the provisioned product | `map(string)` | `{}` | + +## Outputs + +| Name | Description | +|------|-------------| +| `provisioned_product_id` | The ID of the provisioned product | +| `provisioned_product_name` | The name of the provisioned product | +| `provisioned_product_arn` | The ARN of the provisioned product | +| `provisioned_product_type` | The type of the provisioned product | +| `provisioned_product_status` | The status of the provisioned product | +| `provisioned_product_status_message` | The status message | +| `launch_role_arn` | The ARN of the launch role | +| `portfolio_id` | The ID of the portfolio used | +| `product_id` | The ID of the product used | +| `provisioning_artifact_id` | The ID of the provisioning artifact used | +| `vpc_id` | The VPC ID where the instance is provisioned | +| `availability_zone` | The availability zone used | +| `availability_zone_names` | List of all availability zone names | +| `availability_zone_ids` | List of all availability zone IDs | +| `availability_zone_suffixes` | List of availability zone suffixes | + +## Module Architecture + +This module is built on top of the generic [`product`](../product/) module: + +``` +ec2 module (this) + ↓ +product module (base Service Catalog logic) + ↓ +AWS Service Catalog + ↓ +CloudFormation Stack (EC2 instance) +``` + +The `ec2` module: +1. Accepts EC2-specific variables +2. Maps them to Service Catalog parameters +3. Passes them to the `product` module via `product_parameters` +4. Exposes outputs from the `product` module + +## Parameter Mapping + +The module automatically maps Terraform variables to Service Catalog parameters: + +| Terraform Variable | Service Catalog Parameter | +|-------------------|---------------------------| +| `provisioned_product_name` | `NameTag` | +| `project_name` | `ProjectName` | +| `creator` | `Creator` | +| `contact_email` | `ContactEmail` | +| `inc_poc_email` | `IncPocEmail` | +| `fisma_id` | `FISMAID` | +| `instance_type` | `InstanceType` | +| `os_name` | `OSName` | +| `requires_backup` | `RequiresBackup` | +| `power_schedule` | `PowerSchedule` | +| `vpc_name` | Resolved to `VpcId` | +| `subnets_name` | Resolved to `AZName` | + +Additional parameters can be passed via the `parameters` variable, which will override any defaults. + +## Notes + +- The `project_name` must end with the AWS account ID (last 12 digits) +- VPC and subnet lookups are performed automatically based on name tags +- The module uses the latest active provisioning artifact by default unless `path_id` is specified +- All resources created by Service Catalog are managed by CloudFormation +- Deleting the Terraform resource will delete the CloudFormation stack unless `retain_physical_resources = true` + +## Examples + +See the [examples/ec2/](../../examples/ec2/) directory for complete working examples. + +## Support + +For issues or questions, please refer to the main [aws-servicecatalog module documentation](../../README.md). diff --git a/modules/ec2/windows/data.tf b/modules/ec2/windows/data.tf new file mode 100644 index 0000000..c3717ad --- /dev/null +++ b/modules/ec2/windows/data.tf @@ -0,0 +1,36 @@ +data "aws_vpc" "vpc" { + count = var.vpc_name != "" ? 1 : 0 + filter { + name = "tag:Name" + values = [var.vpc_name] + } +} + +# Get all subnets matching name and VPC ID +data "aws_subnets" "subnets" { + count = var.vpc_name != "" && var.subnets_name != "" ? 1 : 0 + filter { + name = "tag:Name" + values = [var.subnets_name] + } + filter { + name = "vpc-id" + values = [data.aws_vpc.vpc[0].id] + } +} + +# get ids for each subnet for use in provisioning +data "aws_subnet" "subnets" { + for_each = var.vpc_name != "" && var.subnets_name != "" ? toset(data.aws_subnets.subnets[0].ids) : [] + id = each.key +} + +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 +} \ No newline at end of file diff --git a/modules/ec2/windows/locals.tf b/modules/ec2/windows/locals.tf new file mode 100644 index 0000000..16368f7 --- /dev/null +++ b/modules/ec2/windows/locals.tf @@ -0,0 +1,38 @@ +locals { + # EC2-specific parameters to pass to the product module + ec2_parameters = merge( + { + InstanceType = var.instance_type + OSName = var.os_name + RequiresBackup = var.requires_backup + PowerSchedule = var.power_schedule + }, + var.parameters # Allow user to override any parameter + ) + + # VPC and networking - only resolve if vpc_name is provided + vpc_id = var.vpc_name != "" ? data.aws_vpc.vpc[0].id : null + az_name = var.vpc_name != "" && var.subnets_name != "" && length(data.aws_subnets.subnets[0].ids) > 0 ? data.aws_subnet.subnets[sort(data.aws_subnets.subnets[0].ids)[0]].availability_zone : null + + # Add networking parameters if available + network_parameters = local.vpc_id != null ? { + VpcId = local.vpc_id + AZName = local.az_name + } : {} + + parameters = merge( + local.network_parameters, + local.ec2_parameters + ) + + base_tags = { + "boc:tf_module_name" = local.module_name + "boc:tf_module_version" = local.module_version + "boc:created_by" = "terraform" + } + + tags = merge( + local.base_tags, + var.tags + ) +} diff --git a/modules/ec2/windows/main.tf b/modules/ec2/windows/main.tf new file mode 100644 index 0000000..7313160 --- /dev/null +++ b/modules/ec2/windows/main.tf @@ -0,0 +1,33 @@ +# EC2 Product Module +# +# Provisions an EC2 instance via Service Catalog +# This is a thin wrapper around the product module with EC2-specific parameters + +module "ec2" { + source = "../product" + + # Service Catalog configuration + portfolio_id = var.portfolio_id + product_id = var.product_id + path_id = var.path_id + accept_language = var.accept_language + timeout = var.timeout + ignore_errors = var.ignore_errors + notification_arns = var.notification_arns + retain_physical_resources = var.retain_physical_resources + stack_set_provisioning_preferences = var.stack_set_provisioning_preferences + + # Common parameters + provisioned_product_name = var.provisioned_product_name + project_name = var.project_name + creator = var.creator + contact_email = var.contact_email + inc_poc_email = var.inc_poc_email + fisma_id = var.fisma_id + + # EC2-specific + networking parameters (merged in locals.tf) + product_parameters = local.parameters + + # Tags + tags = local.tags +} diff --git a/modules/ec2/windows/module_name.tf b/modules/ec2/windows/module_name.tf new file mode 100644 index 0000000..d20f85f --- /dev/null +++ b/modules/ec2/windows/module_name.tf @@ -0,0 +1,4 @@ +locals { + module_name = "aws-servicecatalog/ec2" + module_version = "0.0.0" +} diff --git a/modules/ec2/windows/outputs.tf b/modules/ec2/windows/outputs.tf new file mode 100644 index 0000000..a455516 --- /dev/null +++ b/modules/ec2/windows/outputs.tf @@ -0,0 +1,75 @@ +output "provisioned_product_id" { + description = "The ID of the provisioned product" + value = module.ec2.provisioned_product_id +} + +output "provisioned_product_name" { + description = "The name of the provisioned product" + value = module.ec2.provisioned_product_name +} + +output "provisioned_product_arn" { + description = "The ARN of the provisioned product" + value = module.ec2.provisioned_product_arn +} + +output "provisioned_product_type" { + description = "The type of the provisioned product" + value = module.ec2.provisioned_product_type +} + +output "provisioned_product_status" { + description = "The status of the provisioned product" + value = module.ec2.provisioned_product_status +} + +output "provisioned_product_status_message" { + description = "The status message for the provisioned product" + value = module.ec2.provisioned_product_status_message +} + +output "launch_role_arn" { + description = "The ARN of the launch role" + value = module.ec2.launch_role_arn +} + +output "portfolio_id" { + description = "The ID of the portfolio used" + value = module.ec2.portfolio_id +} + +output "product_id" { + description = "The ID of the product used" + value = module.ec2.product_id +} + +output "provisioning_artifact_id" { + description = "The ID of the provisioning artifact used" + value = module.ec2.provisioning_artifact_id +} + +output "stack_outputs" { + description = "CloudFormation stack outputs from the provisioned product" + value = module.ec2.stack_outputs + sensitive = true +} + +output "vpc_id" { + description = "The VPC ID where the instance will be provisioned" + value = local.vpc_id +} + +output "availability_zone" { + description = "The availability zone of the first selected subnet" + value = local.az_name +} + +output "availability_zone_names" { + description = "Available availability zone names" + value = data.aws_availability_zones.zones.names +} + +output "availability_zone_ids" { + description = "Available availability zone IDs" + value = data.aws_availability_zones.zones.zone_ids +} \ No newline at end of file diff --git a/modules/ec2/windows/variables.common.tf b/modules/ec2/windows/variables.common.tf new file mode 100644 index 0000000..fc183d9 --- /dev/null +++ b/modules/ec2/windows/variables.common.tf @@ -0,0 +1,60 @@ +#--- +# 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 "parameters" { + description = "Additional parameters to pass to the Service Catalog product. Map of parameter names to values." + type = map(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 "project_name" { + description = "Project name (ProjectName parameter). Make sure to select the project designated for your account. The build will fail if an incorrect project is selected. The list of values can be found in the ProjectName parameter of the product's provisioning artifact." + 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 "fisma_id" { + description = "FISMA ID" + type = string + default = "" +} diff --git a/modules/ec2/windows/variables.product.tf b/modules/ec2/windows/variables.product.tf new file mode 100644 index 0000000..ba7d465 --- /dev/null +++ b/modules/ec2/windows/variables.product.tf @@ -0,0 +1,43 @@ +# EC2-specific product parameters + +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 "availability_zones" { + description = "AWS Availability Zones to use (by default will use all available)" + type = list(string) + default = [] +} + +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-*" +} diff --git a/modules/ec2/windows/variables.servicecatalog.tf b/modules/ec2/windows/variables.servicecatalog.tf new file mode 100644 index 0000000..f50eea4 --- /dev/null +++ b/modules/ec2/windows/variables.servicecatalog.tf @@ -0,0 +1,65 @@ +variable "portfolio_id" { + description = "Portfolio ID. If not provided, will lookup by portfolio_name_pattern" + type = string + default = "port-l2v5cyrvr4exa" +} + +variable "product_id" { + description = "Product ID. If not provided, will lookup by product_name_pattern" + type = string + default = "prod-2dfxmxx3vj7ko" +} + +variable "path_id" { + description = "Path identifier of the product. If not provided, will use the latest active artifact" + type = string + default = null +} + +variable "timeout" { + description = "Timeout for provisioned product operations (create/update/delete)" + type = string + default = "5m" +} + +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" + } +} + +variable "ignore_errors" { + description = "Whether to ignore errors during provisioning" + type = bool + default = false +} + +variable "notification_arns" { + description = "List of SNS topic ARNs to send provisioning notifications to" + type = list(string) + default = [] +} + +variable "retain_physical_resources" { + description = "Whether to retain physical resources when deleting the provisioned product" + type = bool + default = false +} + +variable "stack_set_provisioning_preferences" { + description = "StackSet provisioning preferences to use when provisioning the product" + 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 +} \ No newline at end of file diff --git a/modules/ec2/windows/variables.tags.tf b/modules/ec2/windows/variables.tags.tf new file mode 100644 index 0000000..c1a1c3f --- /dev/null +++ b/modules/ec2/windows/variables.tags.tf @@ -0,0 +1,5 @@ +variable "tags" { + description = "AWS Tags to apply to appropriate resources." + type = map(string) + default = {} +} \ No newline at end of file diff --git a/modules/ec2/windows/versions.tf b/modules/ec2/windows/versions.tf new file mode 100644 index 0000000..773514a --- /dev/null +++ b/modules/ec2/windows/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + } +}