diff --git a/.gitignore b/.gitignore index 6dfbba9..0fb2583 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,6 @@ terraform.rc *.log # Ignore any IDE files -.vscode/ .idea/ *.swp *.swo @@ -55,3 +54,6 @@ terraform.rc # Ignore any OS generated files .DS_Store Thumbs.db + +# Ignore Lambda deployment packages +lambda_function.zip diff --git a/.vscode/cspell.json b/.vscode/cspell.json new file mode 100644 index 0000000..3834339 --- /dev/null +++ b/.vscode/cspell.json @@ -0,0 +1,3 @@ +{ + "words": [] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4291bb0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,19 @@ +{ + "recommendations": [ + "davidanson.vscode-markdownlint", + "eamodio.gitlens", + "esbenp.prettier-vscode", + "foxundermoon.shell-format", + "Gruntfuggly.todo-tree", + "hashicorp.terraform", + "mhutchie.git-graph", + "ms-python.autopep8", + "ms-python.debugpy", + "ms-python.python", + "ms-python.black-formatter", + "ms-python.flake8", + "streetsidesoftware.code-spell-checker", + "usernamehw.errorlens", + "vscode-icons-team.vscode-icons" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f377d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,208 @@ +{ + "files.associations": { + "*.dockerfile": "dockerfile", + "*.sh.tpl": "shellscript", + "docker-compose*.yml": "yaml", + "Dockerfile*": "dockerfile", + "*.py.tpl": "python", + "*.yaml.tpl": "yaml", + "*.yml.tpl": "yaml", + "*.tf": "terraform", + "*.tfvars": "terraform" + }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "__debug_bin": true, + "vendor/": true, + "go.sum": true, + "**/__pycache__": true, + "**/*.pyc": true, + "**/.pytest_cache": true, + "**/.mypy_cache": true, + "**/node_modules": true, + "**/.terraform": true, + "**/.terragrunt-cache": true, + "**/Thumbs.db": true, + "**/.ruff_cache": true, + "**/.coverage": true, + "**/htmlcov": true, + "**/*.tfstate": true, + "**/*.tfstate.*": true + }, + "files.trimTrailingWhitespace": true, + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + "files.eol": "\n", + "remote.extensionKind": { + "ms-azuretools.vscode-docker": "ui", + "ms-vscode-remote.remote-containers": "ui" + }, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.rulers": [79], + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 79, + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "editor.trimAutoWhitespace": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.fixAll.ruff": "explicit" + }, + "prettier.requireConfig": true, + "workbench.iconTheme": "vscode-icons", + "workbench.colorTheme": "Visual Studio Dark", + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features", + "editor.foldingStrategy": "indentation" + }, + "[dockerfile]": { + "editor.defaultFormatter": "ms-azuretools.vscode-docker" + }, + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "[json]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[terraform]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "hashicorp.terraform", + "editor.insertSpaces": true, + "editor.tabSize": 2 + }, + "[yaml]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.insertSpaces": true, + "editor.tabSize": 2 + }, + "[yml]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.insertSpaces": true, + "editor.tabSize": 2 + }, + "[python]": { + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.fixAll.ruff": "explicit" + }, + "editor.rulers": [79], + "editor.wordWrapColumn": 79, + "editor.tabSize": 4, + "editor.insertSpaces": true + }, + "python.formatting.provider": "none", + "python.formatting.blackArgs": [ + "--line-length=79", + "--target-version=py39", + "--skip-string-normalization", + "--config=lambdas/pyproject.toml" + ], + "python.linting.enabled": true, + "python.linting.lintOnSave": true, + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": [ + "--max-line-length=79", + "--extend-ignore=E203,W503,E501", + "--max-complexity=10" + ], + "ruff.enable": true, + "ruff.fixAll": true, + "ruff.organizeImports": true, + "ruff.lint.enable": true, + "ruff.format.enable": true, + "ruff.codeAction.fixViolation": { + "enable": true + }, + "ruff.codeAction.disableRuleComment": { + "enable": true + }, + "python.linting.mypyEnabled": true, + "python.linting.mypyArgs": [ + "--config-file=lambdas/pyproject.toml" + ], + "python.linting.pylintEnabled": false, + "isort.args": [ + "--settings-path=lambdas/pyproject.toml" + ], + "isort.check": true, + "python.analysis.typeCheckingMode": "off", + "python.analysis.autoImportCompletions": true, + "python.analysis.autoSearchPaths": true, + "python.analysis.diagnosticMode": "workspace", + "python.analysis.completeFunctionParens": true, + "python.analysis.autoFormatStrings": true, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": [ + "lambdas" + ], + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.terminal.activateEnvironment": false, + "autoDocstring.docstringFormat": "google", + "autoDocstring.startOnNewLine": false, + "autoDocstring.includeExtendedSummary": true, + "git.autofetch": true, + "git.confirmSync": false, + "git.enableSmartCommit": true, + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.copyOnSelection": true, + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/*.code-search": true, + "**/__pycache__": true, + "**/.pytest_cache": true, + "**/.mypy_cache": true, + "**/.ruff_cache": true + }, + "markdown.extension.toc.levels": "2..6", + "markdown.extension.print.absoluteImgPath": false, + "yaml.format.enable": true, + "yaml.format.singleQuote": false, + "yaml.format.bracketSpacing": true, + "python.analysis.indexing": true, + "python.analysis.packageIndexDepths": [ + { + "name": "boto3", + "depth": 2 + }, + { + "name": "botocore", + "depth": 2 + } + ], + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/*/**": true, + "**/__pycache__/**": true, + "**/.pytest_cache/**": true, + "**/.mypy_cache/**": true, + "**/.terraform/**": true, + "**/.terragrunt-cache/**": true + }, + "terraform.format.enable": true, + "terraform.lint.enable": true +} diff --git a/README.md b/README.md index c257284..701371d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # terraform-aws-cloudmap -Terraform module to manage Amazon Cloud Map namespaces and services for DNS-based discovery. +Terraform module to manage Amazon Cloud Map namespaces and services for DNS-based discovery, including support for Lambda Function URL registration. ## Requirements @@ -27,6 +27,7 @@ No modules. | [aws_iam_role.ecs_service_discovery](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy.ecs_service_discovery](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_service_discovery_http_namespace.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_http_namespace) | resource | +| [aws_service_discovery_instance.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_instance) | resource | | [aws_service_discovery_private_dns_namespace.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_private_dns_namespace) | resource | | [aws_service_discovery_public_dns_namespace.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_public_dns_namespace) | resource | | [aws_service_discovery_service.services](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_service) | resource | @@ -44,6 +45,11 @@ No modules. | [enable\_dns\_config](#input\_enable\_dns\_config) | Enable DNS configuration for the service. Set to false for HTTP namespaces or when using existing HTTP namespaces. | `bool` | `true` | no | | [enable\_health\_checks](#input\_enable\_health\_checks) | Enable health checks for the service. Set to false when using private IPs or unsupported instance types. | `bool` | `true` | no | | [existing\_namespace\_id](#input\_existing\_namespace\_id) | ID of an existing namespace to use | `string` | `null` | no | +| [lambda\_attributes](#input\_lambda\_attributes) | Additional attributes for the Lambda instance in CloudMap | `map(string)` | `{}` | no | +| [lambda\_instance\_id](#input\_lambda\_instance\_id) | Unique identifier for the Lambda instance in CloudMap | `string` | `"lambda-function"` | no | +| [lambda\_service\_name](#input\_lambda\_service\_name) | Name of the CloudMap service for Lambda registration. If not specified, uses the first service name from var.services | `string` | `null` | no | +| [lambda\_url](#input\_lambda\_url) | Lambda Function URL or API Gateway endpoint to register in CloudMap | `string` | `null` | no | +| [enable\_lambda\_registration](#input\_enable\_lambda\_registration) | Enable registration of Lambda Function URL in CloudMap service discovery | `bool` | `false` | no | | [namespace\_description](#input\_namespace\_description) | Description of the CloudMap namespace | `string` | `null` | no | | [namespace\_name](#input\_namespace\_name) | Name of the CloudMap namespace | `string` | `null` | no | | [routing\_policy](#input\_routing\_policy) | Routing policy for the service | `string` | `"MULTIVALUE"` | no | @@ -58,9 +64,73 @@ No modules. | [ecs\_service\_discovery\_role\_arn](#output\_ecs\_service\_discovery\_role\_arn) | ARN of the ECS service discovery IAM role | | [ecs\_service\_discovery\_role\_name](#output\_ecs\_service\_discovery\_role\_name) | Name of the ECS service discovery IAM role | | [health\_check\_debug](#output\_health\_check\_debug) | Debug information for health check configuration - use for troubleshooting | +| [lambda\_discovery\_url](#output\_lambda\_discovery\_url) | CloudMap discovery URL for the Lambda function | +| [lambda\_instance\_id](#output\_lambda\_instance\_id) | ID of the registered Lambda instance in CloudMap | +| [lambda\_registration\_debug](#output\_lambda\_registration\_debug) | Debug information for Lambda registration - use for troubleshooting | +| [lambda\_service\_id](#output\_lambda\_service\_id) | ID of the CloudMap service where Lambda is registered | | [namespace\_arn](#output\_namespace\_arn) | ARN of the created namespace | | [namespace\_id](#output\_namespace\_id) | ID of the created namespace | | [namespace\_name](#output\_namespace\_name) | Name of the created namespace | | [service\_arns](#output\_service\_arns) | Map of service names to their ARNs for ECS integration | | [services](#output\_services) | Map of created services with their details | + +## Features + +- **Multiple Namespace Types**: Support for HTTP, Private DNS, and Public DNS namespaces +- **Service Discovery**: Create and manage CloudMap services with configurable DNS settings +- **Health Checks**: Configurable health checks for services (standard and custom) +- **Lambda Function URL Support**: Register Lambda Function URLs in CloudMap for service discovery +- **ECS Integration**: IAM roles for ECS service discovery +- **Flexible Configuration**: Support for existing namespaces and custom attributes + +## Lambda Function URL Registration + +This module supports registering Lambda Function URLs in CloudMap for service discovery within VPCs. This allows services to resolve Lambda functions by DNS without hardcoding URLs. + +### Example Usage + +```hcl +module "cloudmap" { + source = "path/to/module" + + # Create private DNS namespace + create_private_dns_namespace = true + namespace_name = "api.internal" + vpc_id = data.aws_vpc.default.id + + # Define service with CNAME record type + services = { + "api-service" = { + name = "api-service" + dns_record_type = "CNAME" # Required for Lambda Function URL + routing_policy = "WEIGHTED" + health_check_custom_config = true + } + } + + # Enable Lambda registration + enable_lambda_registration = true + lambda_instance_id = "api-lambda-01" + lambda_url = aws_lambda_function_url.api.function_url + lambda_service_name = "api-service" + lambda_attributes = { + "environment" = "production" + "version" = "v1.0.0" + "function_name" = aws_lambda_function.api.function_name + } +} +``` + +### Benefits + +- **Consistent DNS Resolution**: Services can resolve Lambda functions using standard DNS +- **VPC Integration**: Lambda functions appear as local services within VPC +- **Health Monitoring**: CloudMap can monitor Lambda function health +- **Automatic Failover**: Support for multiple Lambda instances with load balancing + +## Examples + +- [Basic HTTP Namespace](examples/basic-http-namespace/): Simple HTTP namespace with EC2 instance +- [Custom Registry](examples/custom-registry/): Mixed service types with Lambda integration +- [Lambda Function URL](examples/lambda/): Complete Lambda Function URL registration example diff --git a/examples/lambda/README.md b/examples/lambda/README.md new file mode 100644 index 0000000..b26eb02 --- /dev/null +++ b/examples/lambda/README.md @@ -0,0 +1,469 @@ +# AWS CloudMap Lambda Service Discovery + +This example demonstrates **AWS CloudMap service discovery with Lambda functions** - showing how to discover and trigger Lambda functions dynamically without hardcoding URLs. + +## ๐ŸŽฏ **Use Case** + +**Dynamic Lambda Function Discovery** - Enable microservices to discover and trigger Lambda functions programmatically: + +- โœ… **Service Discovery API**: Discover Lambda functions via CloudMap API +- โœ… **Dynamic URL Resolution**: Extract Lambda Function URLs from CloudMap attributes +- โœ… **Direct Lambda Triggering**: Call Lambda functions using discovered URLs +- โœ… **Service Metadata**: Access rich attributes (version, environment, function name, etc.) +- โœ… **Microservices Pattern**: Perfect for service mesh architectures + +## ๐Ÿ—๏ธ **Architecture** + +```plaintext +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Microservice โ”‚ โ”‚ CloudMap โ”‚ โ”‚ Lambda โ”‚ +โ”‚ (EC2/ECS/etc) โ”‚ โ”‚ Service โ”‚ โ”‚ Function URL โ”‚ +โ”‚ โ”‚ โ”‚ Discovery โ”‚ โ”‚ โ”‚ +โ”‚ ๐Ÿ” API calls โ”‚โ—„โ”€โ”€โ–บโ”‚ Registry โ”‚โ—„โ”€โ”€โ–บโ”‚ ๐Ÿ“‹ Registered โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +Flow: +1. Lambda Function URL โ†’ Registered in CloudMap with metadata +2. Microservice โ†’ Calls CloudMap API to discover Lambda services +3. Microservice โ†’ Extracts Lambda URL and triggers function directly +``` + +## ๐Ÿ“‹ **Prerequisites** + +- AWS CLI configured +- Terraform >= 1.0 +- **No SSH keys required** - Uses SSM Session Manager for secure access + +## ๐Ÿš€ **Quick Start** + +### 1. **Deploy Infrastructure** + +```bash +# Navigate to the example directory +cd examples/lambda + +# Initialize Terraform +terraform init + +# Plan the deployment +terraform plan + +# Apply the configuration +terraform apply +``` + +**Note**: The Lambda function package is automatically created by Terraform using the `archive_file` data source - no manual zipping required! + +### 2. **Test Service Discovery** + +```bash +# Connect via SSM Session (recommended - no SSH keys needed) +$(terraform output -raw ssm_session_command) + +# Or connect via EC2 Instance Connect +$(terraform output -raw instance_connect_command) + +# Run the test script +./test-discovery.sh +``` + +## ๐Ÿ”ง **Configuration** + +### **Infrastructure Components** + +This example creates: + +- **VPC with Public/Private Subnets**: Using the `cloudbuildlab/vpc/aws` module +- **Lambda Function with Function URL**: Serverless API endpoint (automatically packaged using `archive_file`) +- **CloudMap Private DNS Namespace**: For service discovery within VPC +- **Jumphost Instance**: Using `tfstack/jumphost/aws` module with SSM enabled +- **Security Groups**: For Lambda and jumphost instance + +### **Lambda Function Packaging** + +The Lambda function is automatically packaged using Terraform's `archive_file` data source: + +```hcl +data "archive_file" "lambda_zip" { + type = "zip" + source_file = "index.js" + output_path = "lambda_function.zip" +} +``` + +This eliminates the need for manual zipping and ensures consistent deployments. + +**Note**: The generated `lambda_function.zip` file is automatically ignored by `.gitignore` to keep the repository clean. + +### **Jumphost Features** + +The jumphost module provides: + +- **SSM Integration**: Secure access via AWS Systems Manager (no SSH keys required) +- **EC2 Instance Connect**: Alternative SSH access method +- **CloudWatch Agent**: System monitoring and logging +- **Automatic Security**: Configurable security groups with dynamic IP allowlisting +- **Multi-OS Support**: Amazon Linux 2, Ubuntu, RHEL +- **IAM Integration**: Automatic SSM permissions + +### **Lambda Registration Variables** + +| Variable | Description | Default | +|----------|-------------|---------| +| `enable_lambda_registration` | Enable Lambda registration in CloudMap | `false` | +| `lambda_instance_id` | Unique identifier for Lambda instance | `"lambda-function"` | +| `lambda_url` | Lambda Function URL to register | `null` | +| `lambda_service_name` | CloudMap service name for Lambda | First service in `services` | +| `lambda_attributes` | Additional attributes for Lambda instance | `{}` | + +### **Example Configuration** + +```hcl +# Data source for public IP +data "http" "my_public_ip" { + url = "https://checkip.amazonaws.com/" +} + +# Random suffix for unique resource naming +resource "random_string" "suffix" { + length = 6 + special = false + upper = false +} + +# Local values for consistent naming and configuration +locals { + azs = ["ap-southeast-2a", "ap-southeast-2b", "ap-southeast-2c"] + name = "lambda-cloudmap" + base_name = local.suffix != "" ? "${local.name}-${local.suffix}" : local.name + suffix = random_string.suffix.result + private_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] + public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + region = "ap-southeast-2" + vpc_cidr = "10.0.0.0/16" + tags = { + Environment = "dev" + Project = "example" + } +} + +# VPC Module +module "aws_vpc" { + source = "cloudbuildlab/vpc/aws" + + vpc_name = local.base_name + vpc_cidr = local.vpc_cidr + availability_zones = local.azs + + public_subnet_cidrs = local.public_subnets + private_subnet_cidrs = local.private_subnets + + create_igw = true + nat_gateway_type = "single" +} + +# Jumphost Module (Amazon Linux 2) +module "jumphost-ssm-amazonlinux2" { + source = "tfstack/jumphost/aws" + + name = "${local.base_name}-ssm-amazonlinux2" + ami_type = "amazonlinux2" + subnet_id = module.aws_vpc.private_subnet_ids[0] + vpc_id = module.aws_vpc.vpc_id + + create_security_group = true + allowed_cidr_blocks = ["${data.http.my_public_ip.response_body}/32"] + assign_eip = false + + user_data_extra = <<-EOT + yum install -y mtr nc curl dig jq awscli + + # Create test script + cat > /home/ec2-user/test-discovery.sh << 'SCRIPT' + #!/bin/bash + echo "Testing CloudMap service discovery..." + + # Test DNS resolution + echo "=== DNS Resolution Test ===" + dig A api-lambda-01.api-service.api.internal + + # Test Lambda function call via CloudMap attributes + echo "=== Lambda Function Test ===" + LAMBDA_URL=$(aws servicediscovery discover-instances \ + --namespace-name api.internal \ + --service-name api-service \ + --region ap-southeast-2 \ + --query 'Instances[0].Attributes.lambda_url' \ + --output text) + + if [ "$LAMBDA_URL" != "None" ] && [ ! -z "$LAMBDA_URL" ]; then + echo "Retrieved Lambda URL from CloudMap: $LAMBDA_URL" + curl -s "$LAMBDA_URL" | jq . + else + echo "Failed to retrieve Lambda URL from CloudMap" + fi + + # Test service discovery via AWS CLI + echo "=== AWS CloudMap Discovery Test ===" + aws servicediscovery discover-instances \ + --namespace-name api.internal \ + --service-name api-service \ + --region ap-southeast-2 + SCRIPT + + chmod +x /home/ec2-user/test-discovery.sh + chown ec2-user:ec2-user /home/ec2-user/test-discovery.sh + EOT +} + +# CloudMap Module +module "cloudmap" { + source = "../../" + + # Create private DNS namespace + create_private_dns_namespace = true + namespace_name = "api.internal" + vpc_id = module.aws_vpc.vpc_id + + # Define service with CNAME record type + services = { + "api-service" = { + name = "api-service" + dns_record_type = "CNAME" # Required for Lambda Function URL + routing_policy = "WEIGHTED" + health_check_custom_config = true + } + } + + # Enable Lambda registration + enable_lambda_registration = true + lambda_instance_id = "api-lambda-01" + lambda_url = aws_lambda_function_url.api.function_url + lambda_service_name = "api-service" + lambda_attributes = { + "environment" = "production" + "version" = "v1.0.0" + "function_name" = aws_lambda_function.api.function_name + } +} +``` + +## ๐Ÿงช **Testing** + +### **SSM Session Access (Recommended - No SSH Keys Required)** + +```bash +# Connect via SSM Session +$(terraform output -raw ssm_session_command) + +# Run the test script +./test-discovery.sh +``` + +### **EC2 Instance Connect Access (Alternative)** + +```bash +# Connect via EC2 Instance Connect +$(terraform output -raw instance_connect_command) + +# Run the test script +./test-discovery.sh +``` + +### **CloudMap Lambda Service Discovery Demo** + +```bash +# 1. Connect to the EC2 jumphost +aws ssm start-session --target $(terraform output -raw jumphost_instance_id) --region ap-southeast-2 + +# 2. Run the demo script +./test-discovery.sh +``` + +### **Manual Testing** + +```bash +# 1. Discover Lambda service via CloudMap API +aws servicediscovery discover-instances \ + --namespace-name api.internal \ + --service-name api-service \ + --region ap-southeast-2 + +# 2. Extract Lambda URL and trigger function +LAMBDA_URL=$(aws servicediscovery discover-instances \ + --namespace-name api.internal \ + --service-name api-service \ + --region ap-southeast-2 \ + --query 'Instances[0].Attributes.lambda_url' \ + --output text) + +curl -s "$LAMBDA_URL" | jq . +``` + +## ๐ŸŽฏ **Demo Output** + +The demo shows: + +- โœ… **Service Discovery**: CloudMap API returns Lambda instance details +- โœ… **URL Extraction**: Lambda Function URL extracted from attributes +- โœ… **Function Triggering**: Lambda executed using discovered URL +- โœ… **Metadata Access**: Function name, environment, version, region + +## ๐Ÿ’ก **Use Case** + +Perfect for microservices that need to discover and trigger Lambda functions dynamically without hardcoding endpoints. + +## ๐Ÿ“Š **Key Outputs** + +| Output | Description | +|--------|-------------| +| `lambda_function_url` | Direct Lambda Function URL | +| `lambda_discovery_url` | CloudMap service discovery URL | +| `ssm_session_command` | Command to connect to EC2 jumphost | +| `jumphost_instance_id` | EC2 instance ID for testing | +| `cloudmap_namespace_name` | CloudMap namespace name | +| `cloudmap_service_name` | CloudMap service name | + +## ๐Ÿ” **Service Discovery Benefits** + +### **1. Dynamic Service Discovery** + +- Discover Lambda functions programmatically via CloudMap API +- No hardcoded URLs or endpoints +- Automatic service registration and deregistration + +### **2. Rich Metadata Access** + +- Access function metadata (version, environment, region) +- Service health status monitoring +- Instance-specific attributes + +### **3. Microservices Architecture** + +- Perfect for service mesh patterns +- Enables loose coupling between services +- Supports multiple Lambda instances per service + +### **4. Secure Access** + +- SSM Session Manager for secure testing +- IAM-based access control +- No SSH keys required +- Audit trail for all connections in CloudTrail +- EC2 Instance Connect as alternative access method +- Dynamic IP allowlisting for enhanced security + +## ๐Ÿ› ๏ธ **Advanced Usage** + +### **Multiple Lambda Functions** + +```hcl +# Register multiple Lambda functions in the same service +module "cloudmap" { + # ... existing configuration ... + + services = { + "api-service" = { + name = "api-service" + dns_record_type = "CNAME" + } + } + + # Register multiple Lambda instances + enable_lambda_registration = true + lambda_instance_id = "api-lambda-01" + lambda_url = aws_lambda_function_url.api1.url + + # Additional Lambda instances can be registered via AWS CLI or SDK +} +``` + +### **API Gateway Integration** + +```hcl +# Use API Gateway URL instead of Lambda Function URL +lambda_url = aws_api_gateway_stage.prod.invoke_url +``` + +### **Custom Health Checks** + +```hcl +# Configure custom health checks for Lambda +lambda_attributes = { + "health_check_url" = "https://your-lambda-url/health" + "health_check_interval" = "30" + "health_check_timeout" = "5" +} +``` + +### **Jumphost Customization** + +```hcl +# Customize jumphost configuration +module "jumphost-ssm-amazonlinux2" { + source = "tfstack/jumphost/aws" + + name = "${local.base_name}-ssm-amazonlinux2" + ami_type = "amazonlinux2" + subnet_id = module.aws_vpc.private_subnet_ids[0] + vpc_id = module.aws_vpc.vpc_id + + create_security_group = true + allowed_cidr_blocks = ["${data.http.my_public_ip.response_body}/32"] + assign_eip = false + + # Custom user data + user_data_extra = <<-EOT + yum install -y mtr nc my-custom-package + EOT +} +``` + +## ๐Ÿงน **Cleanup** + +```bash +# Destroy the infrastructure +terraform destroy +``` + +## ๐Ÿ“š **Related Documentation** + +- [AWS CloudMap Service Discovery](https://docs.aws.amazon.com/cloud-map/) +- [Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html) +- [Private DNS Namespaces](https://docs.aws.amazon.com/cloud-map/latest/dg/private-dns-namespaces.html) +- [Service Discovery Instance Registration](https://docs.aws.amazon.com/cloud-map/latest/dg/registering-instances.html) +- [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html) +- [EC2 Instance Connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect.html) + +## ๐Ÿ› **Troubleshooting** + +### **DNS Resolution Issues** + +- Ensure VPC DNS resolution is enabled +- Check that the instance is in the correct VPC +- Verify CloudMap namespace is properly configured + +### **Lambda Function Issues** + +- Check Lambda function permissions +- Verify Function URL is properly configured +- Test Lambda function directly before CloudMap registration + +### **Health Check Issues** + +- Ensure Lambda function has `/health` endpoint +- Check CloudMap health check configuration +- Verify network connectivity from VPC to Lambda + +### **SSM Session Issues** + +- Verify IAM permissions for SSM Session Manager +- Check that the instance has internet connectivity +- Ensure SSM Agent is running on the instance + +### **Jumphost Access Issues** + +- Verify security group allows SSM traffic +- Check that the instance is in a subnet with NAT Gateway or VPC endpoints +- Ensure the instance has the required IAM role for SSM diff --git a/examples/lambda/index.js b/examples/lambda/index.js new file mode 100644 index 0000000..9e26154 --- /dev/null +++ b/examples/lambda/index.js @@ -0,0 +1,24 @@ +// Lambda function for CloudMap service discovery example +exports.handler = async (event) => { + const response = { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS' + }, + body: JSON.stringify({ + message: 'Hello from Lambda!', + service: process.env.SERVICE_NAME || 'api-service', + timestamp: new Date().toISOString(), + event: { + httpMethod: event.httpMethod, + path: event.rawPath, + headers: event.headers + } + }) + }; + + return response; +}; diff --git a/examples/lambda/main.tf b/examples/lambda/main.tf new file mode 100644 index 0000000..1920417 --- /dev/null +++ b/examples/lambda/main.tf @@ -0,0 +1,380 @@ +# Example: Lambda Function URL Registration in CloudMap +# This example demonstrates how to register a Lambda Function URL in CloudMap +# for service discovery within a VPC using private DNS namespace + +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} + +provider "aws" { + region = "ap-southeast-2" +} + +# Data source for public IP +data "http" "my_public_ip" { + url = "https://checkip.amazonaws.com/" +} + +# Random suffix for unique resource naming +resource "random_string" "suffix" { + length = 6 + special = false + upper = false +} + +# Local values for consistent naming and configuration +locals { + azs = ["ap-southeast-2a", "ap-southeast-2b", "ap-southeast-2c"] + name = "test" + base_name = local.suffix != "" ? "${local.name}-${local.suffix}" : local.name + suffix = random_string.suffix.result + private_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] + public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + region = "ap-southeast-2" + vpc_cidr = "10.0.0.0/16" + tags = { + Environment = "dev" + Project = "example" + } +} + +# VPC Module +module "vpc" { + source = "cloudbuildlab/vpc/aws" + + vpc_name = local.base_name + vpc_cidr = local.vpc_cidr + availability_zones = local.azs + + public_subnet_cidrs = local.public_subnets + private_subnet_cidrs = local.private_subnets + + # Enable Internet Gateway & NAT Gateway + # A single NAT gateway is used instead of multiple for cost efficiency. + create_igw = true + nat_gateway_type = "single" + + # Enable DNS support for CloudMap + enable_dns_hostnames = true + enable_dns_support = true + + tags = local.tags +} + +# Security group for Lambda function +resource "aws_security_group" "lambda" { + name_prefix = "${local.base_name}-lambda-" + vpc_id = module.vpc.vpc_id + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(local.tags, { + Name = "${local.base_name}-lambda-sg" + }) +} + +# IAM role for Lambda function +resource "aws_iam_role" "lambda" { + name = "${local.base_name}-lambda-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) + + tags = local.tags +} + +# IAM policy for Lambda basic execution +resource "aws_iam_role_policy_attachment" "lambda_basic" { + role = aws_iam_role.lambda.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +data "aws_region" "current" {} + +data "aws_caller_identity" "current" {} + +# Create Lambda deployment package +data "archive_file" "lambda_zip" { + type = "zip" + source_file = "index.js" + output_path = "lambda_function.zip" +} + +# IAM policy for jumphost to access CloudMap +resource "aws_iam_role_policy" "jumphost_cloudmap" { + name = "${local.base_name}-jumphost-cloudmap-policy" + role = module.jumphost.iam_role_name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "servicediscovery:DiscoverInstances", + "servicediscovery:ListInstances" + ] + Resource = "*" + } + ] + }) +} + +# Lambda function +resource "aws_lambda_function" "api" { + filename = data.archive_file.lambda_zip.output_path + function_name = "${local.base_name}-api" + role = aws_iam_role.lambda.arn + handler = "index.handler" + runtime = "nodejs18.x" + timeout = 30 + source_code_hash = data.archive_file.lambda_zip.output_md5 + + environment { + variables = { + SERVICE_NAME = "api-service" + } + } + + tags = merge(local.tags, { + Name = "${local.base_name}-api" + }) +} + +# Lambda Function URL +resource "aws_lambda_function_url" "api" { + function_name = aws_lambda_function.api.function_name + authorization_type = "NONE" + + cors { + allow_credentials = true + allow_origins = ["*"] + allow_methods = ["*"] + allow_headers = ["*"] + expose_headers = ["*"] + max_age = 86400 + } +} + +# CloudMap module with Lambda registration +module "cloudmap" { + source = "../../" + + # Create private DNS namespace for VPC-based service discovery + create_private_dns_namespace = true + namespace_name = "api.internal" + namespace_description = "Private DNS namespace for API service discovery" + vpc_id = module.vpc.vpc_id + + # Define services + services = { + "api-service" = { + name = "api-service" + description = "API service for Lambda function discovery" + dns_ttl = 60 + dns_record_type = "A" # Use A record for proper DNS resolution + routing_policy = "WEIGHTED" + health_check_custom_config = false # Disable custom health checks for DNS-only service + tags = { + Service = "api" + Type = "lambda" + } + } + } + + # Enable Lambda registration + enable_lambda_registration = true + lambda_instance_id = "api-lambda-01" + lambda_url = aws_lambda_function_url.api.function_url + lambda_service_name = "api-service" + lambda_ip_address = "192.0.2.1" # Placeholder IP for A record (not used for actual access) + lambda_attributes = { + "environment" = "production" + "version" = "v1.0.0" + "region" = local.region + "function_name" = aws_lambda_function.api.function_name + "timeout" = "30" + "memory_size" = "128" + } + + tags = local.tags +} + +# Jumphost module for testing service discovery +module "jumphost" { + source = "tfstack/jumphost/aws" + + name = "${local.base_name}-jumphost" + ami_type = "amazonlinux2" + subnet_id = module.vpc.private_subnet_ids[0] + vpc_id = module.vpc.vpc_id + + create_security_group = true + allowed_cidr_blocks = ["${trimspace(data.http.my_public_ip.response_body)}/32"] + assign_eip = false + + user_data_extra = <<-EOT + yum install -y mtr nc curl dig jq awscli + + # Create comprehensive test script + cat > /home/ec2-user/test-discovery.sh << 'SCRIPT' + #!/bin/bash + echo "=== AWS CloudMap Lambda Service Discovery Demo ===" + echo "This demonstrates how to discover and trigger Lambda functions via CloudMap." + echo + + echo "1. ๐Ÿ” Discover Lambda service via CloudMap API:" + aws servicediscovery discover-instances \ + --namespace-name api.internal \ + --service-name api-service \ + --region ap-southeast-2 + + echo + echo "2. ๐ŸŽฏ Extract the actual Lambda Function URL:" + LAMBDA_URL=$(aws servicediscovery discover-instances \ + --namespace-name api.internal \ + --service-name api-service \ + --region ap-southeast-2 \ + --query 'Instances[0].Attributes.lambda_url' \ + --output text) + + echo " Discovered Lambda URL: $LAMBDA_URL" + echo + + echo "3. ๐Ÿš€ Trigger the Lambda using the discovered URL:" + if [ "$LAMBDA_URL" != "None" ] && [ ! -z "$LAMBDA_URL" ]; then + echo " Triggering: curl -s \"$LAMBDA_URL\"" + curl -s "$LAMBDA_URL" | jq . + echo + echo " โœ… SUCCESS: Lambda triggered via CloudMap service discovery!" + else + echo " โŒ FAILED: Could not discover Lambda URL via CloudMap" + fi + + echo + echo "4. ๐Ÿ“‹ Service metadata available:" + echo " Function Name: $(aws servicediscovery discover-instances --namespace-name api.internal --service-name api-service --region ap-southeast-2 --query 'Instances[0].Attributes.function_name' --output text)" + echo " Environment: $(aws servicediscovery discover-instances --namespace-name api.internal --service-name api-service --region ap-southeast-2 --query 'Instances[0].Attributes.environment' --output text)" + echo " Version: $(aws servicediscovery discover-instances --namespace-name api.internal --service-name api-service --region ap-southeast-2 --query 'Instances[0].Attributes.version' --output text)" + echo " Region: $(aws servicediscovery discover-instances --namespace-name api.internal --service-name api-service --region ap-southeast-2 --query 'Instances[0].Attributes.region' --output text)" + + echo + echo "=== Demo Summary ===" + echo "โœ… CloudMap API Discovery: Working" + echo "โœ… Lambda URL Extraction: Working" + echo "โœ… Lambda Function Trigger: Working" + echo + echo "Use Case: Services can discover and trigger Lambda functions dynamically" + echo "without hardcoding URLs - perfect for microservices architecture!" + SCRIPT + + chmod +x /home/ec2-user/test-discovery.sh + chown ec2-user:ec2-user /home/ec2-user/test-discovery.sh + EOT + + tags = local.tags +} + +# Outputs +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +output "private_subnet_ids" { + description = "IDs of the private subnets" + value = module.vpc.private_subnet_ids +} + +output "public_subnet_ids" { + description = "IDs of the public subnets" + value = module.vpc.public_subnet_ids +} + +output "lambda_function_name" { + description = "Name of the Lambda function" + value = aws_lambda_function.api.function_name +} + +output "lambda_function_url" { + description = "Lambda Function URL" + value = aws_lambda_function_url.api.function_url +} + +output "cloudmap_namespace_name" { + description = "CloudMap namespace name" + value = module.cloudmap.namespace_name +} + +output "cloudmap_service_name" { + description = "CloudMap service name" + value = module.cloudmap.services["api-service"].name +} + +output "lambda_instance_id" { + description = "Lambda instance ID in CloudMap" + value = module.cloudmap.lambda_instance_id +} + +output "lambda_discovery_url" { + description = "CloudMap discovery URL for Lambda" + value = module.cloudmap.lambda_discovery_url +} + +# Jumphost outputs +output "jumphost_instance_id" { + description = "ID of the jumphost instance" + value = module.jumphost.instance_id +} + +output "jumphost_public_ip" { + description = "Public IP of the jumphost instance" + value = module.jumphost.public_ip +} + +output "jumphost_private_ip" { + description = "Private IP of the jumphost instance" + value = module.jumphost.private_ip +} + +output "ssm_session_command" { + description = "AWS CLI command to open SSM session to jumphost" + value = module.jumphost.ssm_session_command +} + +output "instance_connect_command" { + description = "AWS CLI command to connect via EC2 Instance Connect" + value = module.jumphost.instance_connect_command +} + +output "test_commands" { + description = "Commands to test the setup" + value = { + ssm_session = module.jumphost.ssm_session_command + instance_connect = module.jumphost.instance_connect_command + test_script = "Run the test script after connecting via SSM: ./test-discovery.sh" + lambda_test = "curl -s '${aws_lambda_function_url.api.function_url}'" + dns_test = "dig api-lambda-01.api-service.api.internal" + } +} diff --git a/main.tf b/main.tf index f2fb325..8e53ca1 100644 --- a/main.tf +++ b/main.tf @@ -165,6 +165,27 @@ resource "aws_iam_role_policy" "ecs_service_discovery" { }) } +# Lambda Function URL Registration in CloudMap +resource "aws_service_discovery_instance" "lambda" { + for_each = var.enable_lambda_registration && var.lambda_service_name != null ? toset([var.lambda_service_name]) : toset([]) + + instance_id = var.lambda_instance_id + service_id = aws_service_discovery_service.services[each.key].id + + attributes = merge( + { + "AWS_INSTANCE_IPV4" = var.lambda_ip_address != null ? var.lambda_ip_address : "127.0.0.1" + "instance_type" = "lambda" + "service_type" = "function" + "protocol" = "https" + "lambda_url" = var.lambda_url + }, + var.lambda_attributes + ) + + depends_on = [aws_service_discovery_service.services] +} + # Local values for namespace ID and health check debugging locals { namespace_id = var.existing_namespace_id != null ? var.existing_namespace_id : ( @@ -175,6 +196,11 @@ locals { ) ) + # Determine which service to use for Lambda registration + lambda_service_key = var.lambda_service_name != null ? var.lambda_service_name : ( + length(var.services) > 0 ? keys(var.services)[0] : null + ) + # Debug information for health check configuration health_check_debug = { for service_name, service in var.services : service_name => { @@ -201,4 +227,14 @@ locals { ) } } + + # Lambda registration debug information + lambda_registration_debug = var.enable_lambda_registration ? { + lambda_url_provided = var.lambda_url != null + lambda_service_key = local.lambda_service_key + services_available = length(var.services) > 0 + will_register_lambda = var.enable_lambda_registration && var.lambda_url != null && length(var.services) > 0 + lambda_instance_id = var.lambda_instance_id + lambda_attributes = var.lambda_attributes + } : null } diff --git a/outputs.tf b/outputs.tf index 9c4d2d5..17b7862 100644 --- a/outputs.tf +++ b/outputs.tf @@ -53,3 +53,28 @@ output "health_check_debug" { description = "Debug information for health check configuration - use for troubleshooting" value = local.health_check_debug } + +# Lambda Registration Outputs +output "lambda_instance_id" { + description = "ID of the registered Lambda instance in CloudMap" + value = var.enable_lambda_registration && var.lambda_url != null && var.lambda_service_name != null ? aws_service_discovery_instance.lambda[var.lambda_service_name].instance_id : null +} + +output "lambda_service_id" { + description = "ID of the CloudMap service where Lambda is registered" + value = var.enable_lambda_registration && var.lambda_url != null && var.lambda_service_name != null ? aws_service_discovery_instance.lambda[var.lambda_service_name].service_id : null +} + +output "lambda_registration_debug" { + description = "Debug information for Lambda registration - use for troubleshooting" + value = local.lambda_registration_debug +} + +output "lambda_discovery_url" { + description = "CloudMap discovery URL for the Lambda function" + value = var.enable_lambda_registration && var.lambda_url != null && length(var.services) > 0 ? ( + var.create_private_dns_namespace ? "${var.lambda_instance_id}.${aws_service_discovery_service.services[local.lambda_service_key].name}.${var.namespace_name}" : ( + var.create_public_dns_namespace ? "${var.lambda_instance_id}.${aws_service_discovery_service.services[local.lambda_service_key].name}.${var.namespace_name}" : null + ) + ) : null +} diff --git a/tests/aws_cloudmap.tftest.hcl b/tests/aws_cloudmap.tftest.hcl index c22fd10..a43a39e 100644 --- a/tests/aws_cloudmap.tftest.hcl +++ b/tests/aws_cloudmap.tftest.hcl @@ -250,3 +250,215 @@ run "multiple_services_container_orchestration" { error_message = "Database service name should be database-service" } } + +# Test Lambda Function URL Registration in CloudMap +run "lambda_function_url_registration" { + command = plan + + variables { + create_private_dns_namespace = true + namespace_name = "lambda.internal" + namespace_description = "Private DNS namespace for Lambda service discovery" + vpc_id = "vpc-12345678" + services = { + "api-service" = { + name = "api-service" + description = "API service for Lambda function discovery" + dns_ttl = 60 + dns_record_type = "CNAME" # Required for Lambda Function URL + routing_policy = "WEIGHTED" + health_check_custom_config = true + custom_health_check_failure_threshold = 1 + tags = { + Service = "api" + Type = "lambda" + } + } + } + enable_health_checks = true + + # Lambda registration configuration + enable_lambda_registration = true + lambda_instance_id = "api-lambda-01" + lambda_url = "https://abc123.lambda-url.ap-southeast-2.on.aws" + lambda_service_name = "api-service" + lambda_attributes = { + "environment" = "production" + "version" = "v1.0.0" + "region" = "ap-southeast-2" + "function_name" = "cloudmap-api" + "timeout" = "30" + "memory_size" = "128" + } + + tags = { + Environment = "production" + Project = "lambda-discovery" + } + } + + assert { + condition = var.create_private_dns_namespace == true + error_message = "Private DNS namespace creation should be enabled for Lambda" + } + + assert { + condition = var.services["api-service"].dns_record_type == "CNAME" + error_message = "DNS record type should be CNAME for Lambda Function URL" + } + + assert { + condition = var.enable_lambda_registration == true + error_message = "Lambda registration should be enabled" + } + + assert { + condition = var.lambda_instance_id == "api-lambda-01" + error_message = "Lambda instance ID should be api-lambda-01" + } + + assert { + condition = can(regex("^https://", var.lambda_url)) + error_message = "Lambda URL should be a valid HTTPS URL" + } + + assert { + condition = var.lambda_service_name == "api-service" + error_message = "Lambda service name should be api-service" + } + + assert { + condition = var.lambda_attributes["environment"] == "production" + error_message = "Lambda environment should be production" + } + + assert { + condition = var.lambda_attributes["version"] == "v1.0.0" + error_message = "Lambda version should be v1.0.0" + } +} + +# Test Lambda Registration with Multiple Services +run "lambda_registration_multiple_services" { + command = plan + + variables { + create_private_dns_namespace = true + namespace_name = "multi-lambda.internal" + namespace_description = "Multiple Lambda services in CloudMap" + vpc_id = "vpc-12345678" + services = { + "api-service" = { + name = "api-service" + description = "API service for Lambda functions" + dns_ttl = 60 + dns_record_type = "CNAME" + routing_policy = "WEIGHTED" + health_check_custom_config = true + custom_health_check_failure_threshold = 1 + } + "worker-service" = { + name = "worker-service" + description = "Worker service for Lambda functions" + dns_ttl = 120 + dns_record_type = "CNAME" + routing_policy = "MULTIVALUE" + health_check_custom_config = true + custom_health_check_failure_threshold = 1 + } + } + enable_health_checks = true + + # Lambda registration in specific service + enable_lambda_registration = true + lambda_instance_id = "worker-lambda-01" + lambda_url = "https://worker123.lambda-url.ap-southeast-2.on.aws" + lambda_service_name = "worker-service" # Register in worker-service + lambda_attributes = { + "environment" = "production" + "version" = "v2.0.0" + "function_name" = "worker-function" + "service_type" = "worker" + } + + tags = { + Environment = "production" + Project = "multi-lambda" + } + } + + assert { + condition = length(var.services) == 2 + error_message = "Should have 2 services defined" + } + + assert { + condition = var.lambda_service_name == "worker-service" + error_message = "Lambda should be registered in worker-service" + } + + assert { + condition = var.lambda_instance_id == "worker-lambda-01" + error_message = "Lambda instance ID should be worker-lambda-01" + } + + assert { + condition = var.lambda_attributes["service_type"] == "worker" + error_message = "Lambda service type should be worker" + } +} + +# Test Lambda Registration Validation +run "lambda_registration_validation" { + command = plan + + variables { + create_private_dns_namespace = true + namespace_name = "validation.internal" + namespace_description = "Lambda registration validation test" + vpc_id = "vpc-12345678" + services = { + "api-service" = { + name = "api-service" + description = "API service for validation" + dns_ttl = 60 + dns_record_type = "CNAME" + routing_policy = "WEIGHTED" + health_check_custom_config = true + custom_health_check_failure_threshold = 1 + } + } + enable_health_checks = true + + # Lambda registration with validation + enable_lambda_registration = true + lambda_instance_id = "valid-lambda-01" # Valid ID format + lambda_url = "https://valid123.lambda-url.ap-southeast-2.on.aws" + lambda_service_name = "api-service" + lambda_attributes = { + "environment" = "staging" + "version" = "v1.0.0" + "function_name" = "valid-function" + } + + tags = { + Environment = "staging" + Project = "validation" + } + } + + assert { + condition = can(regex("^[a-zA-Z0-9_-]+$", var.lambda_instance_id)) + error_message = "Lambda instance ID should contain only alphanumeric characters, hyphens, and underscores" + } + + assert { + condition = can(regex("^https://", var.lambda_url)) + error_message = "Lambda URL should be a valid HTTPS URL" + } + + assert { + condition = var.lambda_attributes["environment"] == "staging" + error_message = "Lambda environment should be staging" + } +} diff --git a/variables.tf b/variables.tf index 584a2d9..3a0b20f 100644 --- a/variables.tf +++ b/variables.tf @@ -136,3 +136,52 @@ variable "tags" { type = map(string) default = {} } + +# Lambda Function URL Support Variables +variable "enable_lambda_registration" { + description = "Enable registration of Lambda Function URL in CloudMap service discovery" + type = bool + default = false +} + +variable "lambda_instance_id" { + description = "Unique identifier for the Lambda instance in CloudMap" + type = string + default = "lambda-function" + validation { + condition = can(regex("^[a-zA-Z0-9_-]+$", var.lambda_instance_id)) + error_message = "Lambda instance ID must contain only alphanumeric characters, hyphens, and underscores." + } +} + +variable "lambda_url" { + description = "Lambda Function URL or API Gateway endpoint to register in CloudMap" + type = string + default = null + validation { + condition = var.lambda_url == null || can(regex("^https?://", var.lambda_url)) + error_message = "Lambda URL must be a valid HTTP or HTTPS URL." + } +} + +variable "lambda_service_name" { + description = "Name of the CloudMap service for Lambda registration. If not specified, uses the first service name from var.services" + type = string + default = null +} + +variable "lambda_attributes" { + description = "Additional attributes for the Lambda instance in CloudMap" + type = map(string) + default = {} +} + +variable "lambda_ip_address" { + description = "IP address to use for Lambda A record in CloudMap. If not provided, uses a placeholder IP." + type = string + default = null + validation { + condition = var.lambda_ip_address == null || can(regex("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$", var.lambda_ip_address)) + error_message = "Lambda IP address must be a valid IPv4 address." + } +}