diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b868a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data +*.tfvars +*.tfvars.json + +# Ignore override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Ignore Mac .DS_Store files +.DS_Store + +# Ignore lock files +.terraform.lock.hcl \ No newline at end of file diff --git a/.pre-commit.yaml b/.pre-commit.yaml new file mode 100644 index 0000000..d258b1e --- /dev/null +++ b/.pre-commit.yaml @@ -0,0 +1,8 @@ +repos: + - repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.83.5 + hooks: + - id: terraform_fmt + - id: terraform_docs + - id: terraform_validate + - id: terraform_tflint \ No newline at end of file diff --git a/README.md b/README.md index e69de29..916f2c0 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,55 @@ +# AWS Service Catalog Terraform Module + +This module provides a standardized way to provision and manage AWS Service Catalog products using Terraform. + +## Features + +- **Product Provisioning**: Provision EC2, RDS, ECS, and S3 products from existing Service Catalog portfolios +- **Lifecycle Management**: Update and terminate provisioned products +- **Service Actions**: Execute service actions (start, stop, reboot, etc.) on provisioned products +- **Automatic Discovery**: Lookup portfolios and products by naming patterns +- **Stack Outputs**: Access CloudFormation stack outputs from provisioned products + +## Module Structure + +- `ec2/` - Provision EC2 instances from Service Catalog +- `ec2-actions/` - Execute service actions on EC2 provisioned products +- `rds/` - Provision RDS databases from Service Catalog +- `rds-actions/` - Execute service actions on RDS provisioned products +- `ecs/` - Provision ECS services from Service Catalog +- `ecs-actions/` - Execute service actions on ECS provisioned products +- `s3/` - Provision S3 buckets from Service Catalog +- `s3-actions/` - Execute service actions on S3 provisioned products +- `common/` - Shared components and utilities + +## Usage + +See individual submodule README files for detailed usage examples. + +### Basic EC2 Example + +```hcl +module "ec2_instance" { + source = "path/to/aws-servicecatalog/ec2" + + provisioned_product_name = "my-web-server" + portfolio_name_pattern = "edl-portfolio" + product_name_pattern = "linux-product" + + parameters = { + InstanceType = "t3.medium" + KeyName = "my-key" + SubnetId = "subnet-12345" + } + + tags = { + Environment = "production" + Team = "platform" + } +} +``` + +## Requirements + +- Terraform >= 1.0 +- AWS Provider >= 5.0 \ No newline at end of file diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..37d5136 --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,35 @@ +# Requirements + +## Terraform Version +- Terraform >= 1.0 + +## Provider Requirements +- AWS Provider >= 5.0 + +## Permissions + +The IAM role/user executing this module must have the following permissions: + +### Service Catalog Permissions +- `servicecatalog:DescribePortfolio` +- `servicecatalog:ListPortfolios` +- `servicecatalog:SearchProducts` +- `servicecatalog:DescribeProduct` +- `servicecatalog:ProvisionProduct` +- `servicecatalog:UpdateProvisionedProduct` +- `servicecatalog:TerminateProvisionedProduct` +- `servicecatalog:DescribeProvisionedProduct` +- `servicecatalog:DescribeRecord` +- `servicecatalog:ExecuteProvisionedProductServiceAction` +- `servicecatalog:ListServiceActionsForProvisioningArtifact` + +### Additional Permissions +- `cloudformation:DescribeStacks` - To retrieve stack outputs +- `ssm:DescribeDocument` - For service actions using SSM documents +- `ssm:StartAutomationExecution` - For executing service actions + +## Prerequisites + +- Existing Service Catalog portfolios must be deployed and shared with the AWS account +- User must have access to the portfolio (via principal association) +- Products must be associated with the portfolio \ No newline at end of file diff --git a/ec2/README.md b/ec2/README.md new file mode 100644 index 0000000..b305736 --- /dev/null +++ b/ec2/README.md @@ -0,0 +1,52 @@ +# AWS Service Catalog - EC2 Module + +Provisions EC2 instances from AWS Service Catalog. + +## Usage + +```hcl +module "ec2_instance" { + source = "path/to/aws-servicecatalog/ec2" + + provisioned_product_name = "my-web-server" + portfolio_name_pattern = "edl-portfolio" + product_name_pattern = "linux-product" + + parameters = { + InstanceType = "t3.medium" + KeyName = "my-key" + SubnetId = "subnet-12345" + VpcId = "vpc-12345" + } + + tags = { + Environment = "production" + Application = "web" + } +} +``` + +## Requirements + +| Name | Version | +|------|---------| +| terraform | >= 1.0 | +| aws | >= 5.0 | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| provisioned_product_name | Name of the provisioned product | `string` | n/a | yes | +| parameters | Product parameters | `map(string)` | `{}` | no | +| portfolio_name_pattern | Pattern to search for portfolio | `string` | `"edl-portfolio"` | no | +| product_name_pattern | Pattern to search for product | `string` | `"linux-product"` | no | +| tags | Additional tags | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| provisioned_product_id | The ID of the provisioned product | +| provisioned_product_arn | The ARN of the provisioned product | +| stack_outputs | CloudFormation stack outputs | \ No newline at end of file diff --git a/ec2/base_settings.tf b/ec2/base_settings.tf new file mode 100644 index 0000000..646ba15 --- /dev/null +++ b/ec2/base_settings.tf @@ -0,0 +1,9 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} +data "aws_partition" "current" {} + +locals { + account_id = data.aws_caller_identity.current.account_id + region = data.aws_region.current.name + partition = data.aws_partition.current.partition +} \ No newline at end of file diff --git a/ec2/base_tags.tf b/ec2/base_tags.tf new file mode 100644 index 0000000..40588ba --- /dev/null +++ b/ec2/base_tags.tf @@ -0,0 +1,16 @@ +locals { + 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/ec2/data.tf b/ec2/data.tf new file mode 100644 index 0000000..3e42b73 --- /dev/null +++ b/ec2/data.tf @@ -0,0 +1,43 @@ +# Lookup portfolio by name pattern +data "aws_servicecatalog_portfolio" "this" { + count = var.portfolio_id == null ? 1 : 0 + + id = var.portfolio_id != null ? var.portfolio_id : null + accept_language = var.accept_language + + filter { + key = "Name" + value = var.portfolio_name_pattern + } +} + +# Search for product in portfolio +data "aws_servicecatalog_product" "this" { + id = var.product_id != null ? var.product_id : try(data.aws_servicecatalog_portfolio.this[0].id, null) + + accept_language = var.accept_language + + filter { + key = "FullTextSearch" + value = var.product_name_pattern + } +} + +# Get the latest provisioning artifact (product version) +data "aws_servicecatalog_provisioning_artifacts" "this" { + accept_language = var.accept_language + product_id = data.aws_servicecatalog_product.this.id +} + +# Get CloudFormation stack outputs if provisioned product exists +data "aws_cloudformation_stack" "this" { + count = var.retrieve_stack_outputs ? 1 : 0 + + name = format("SC-%s-%s", aws_servicecatalog_provisioned_product.this.id, aws_servicecatalog_provisioned_product.this.name) + + depends_on = [aws_servicecatalog_provisioned_product.this] +} + +data "aws_iam_policy_document" "empty" { + # Empty policy document for conditional logic +} \ No newline at end of file diff --git a/ec2/defaults.tf b/ec2/defaults.tf new file mode 100644 index 0000000..e69de29 diff --git a/ec2/locals.tf b/ec2/locals.tf new file mode 100644 index 0000000..e69de29 diff --git a/ec2/outputs.tf b/ec2/outputs.tf new file mode 100644 index 0000000..74365e6 --- /dev/null +++ b/ec2/outputs.tf @@ -0,0 +1,59 @@ +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 "cloudformation_stack_arn" { + description = "The ARN of the CloudFormation stack" + value = aws_servicecatalog_provisioned_product.this.cloudformation_stack_arn +} + +output "launch_role_arn" { + description = "The ARN of the launch role" + value = aws_servicecatalog_provisioned_product.this.launch_role_arn +} + +output "stack_outputs" { + description = "CloudFormation stack outputs" + value = var.retrieve_stack_outputs ? try(data.aws_cloudformation_stack.this[0].outputs, {}) : {} +} + +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 = data.aws_servicecatalog_product.this.id +} + +output "provisioning_artifact_id" { + description = "The ID of the provisioning artifact used" + value = local.provisioning_artifact_id +} \ No newline at end of file diff --git a/ec2/resources.tf b/ec2/resources.tf new file mode 100644 index 0000000..e69de29 diff --git a/ec2/variables.parameters.tf b/ec2/variables.parameters.tf new file mode 100644 index 0000000..5bcefc5 --- /dev/null +++ b/ec2/variables.parameters.tf @@ -0,0 +1,5 @@ +variable "parameters" { + description = "Parameters to pass to the Service Catalog product. Map of parameter names to values" + type = map(string) + default = {} +} \ No newline at end of file diff --git a/ec2/variables.product.tf b/ec2/variables.product.tf new file mode 100644 index 0000000..92308ed --- /dev/null +++ b/ec2/variables.product.tf @@ -0,0 +1,88 @@ +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 = null +} + +variable "portfolio_name_pattern" { + description = "Pattern to search for portfolio by name. Used when portfolio_id is not provided" + type = string + default = "edl-portfolio" +} + +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_create" { + description = "Timeout for provisioned product creation" + type = string + default = "60m" +} + +variable "timeout_update" { + description = "Timeout for provisioned product updates" + type = string + default = "60m" +} + +variable "timeout_delete" { + description = "Timeout for provisioned product deletion" + type = string + default = "60m" +} \ No newline at end of file diff --git a/ec2/variables.safeguards.tf b/ec2/variables.safeguards.tf new file mode 100644 index 0000000..80f8e94 --- /dev/null +++ b/ec2/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 + } +} \ No newline at end of file diff --git a/ec2/variables.tags.tf b/ec2/variables.tags.tf new file mode 100644 index 0000000..e69de29 diff --git a/ec2/version.tf b/ec2/version.tf new file mode 100644 index 0000000..ae4e884 --- /dev/null +++ b/ec2/version.tf @@ -0,0 +1,3 @@ +locals { + module_name = "aws-servicecatalog/ec2" +} \ No newline at end of file diff --git a/ec2/versions.tf b/ec2/versions.tf new file mode 100644 index 0000000..4c65a71 --- /dev/null +++ b/ec2/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} \ No newline at end of file