diff --git a/README.md b/README.md index 2c3cb49f..9f16f045 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,59 @@ jobs: additional_tags: '{\"key\":\"value\",\"key2\":\"value2\"}' ``` +### ALB with WAF example +```yaml +name: Deploy with Application Load Balancer and WAF +on: + push: + branches: [ main ] + +jobs: + EC2-Deploy: + runs-on: ubuntu-latest + steps: + - id: deploy + uses: bitovi/github-actions-deploy-commons@main + with: + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws_default_region: us-east-1 + env_ghs: ${{ secrets.DOT_ENV }} + # Load Balancer Configuration + aws_elb_create: true + aws_lb_type: alb # Use Application Load Balancer instead of Classic ELB + aws_alb_enable_waf: true # Enable AWS WAF v2 protection + aws_alb_subnets: subnet-12345,subnet-67890 # Specify subnets for ALB + # VPC Configuration (required for ALB) + aws_vpc_create: true + aws_vpc_public_subnets: 10.0.1.0/24,10.0.2.0/24 + aws_vpc_availability_zones: us-east-1a,us-east-1b +``` + +### ALB with default VPC example +```yaml +name: Deploy with ALB using default VPC (single-AZ) +on: + push: + branches: [ main ] + +jobs: + EC2-Deploy: + runs-on: ubuntu-latest + steps: + - id: deploy + uses: bitovi/github-actions-deploy-commons@main + with: + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws_default_region: us-east-1 + env_ghs: ${{ secrets.DOT_ENV }} + # Simple ALB setup using default VPC + aws_elb_create: true + aws_lb_type: alb # Use ALB instead of Classic ELB + # Note: aws_alb_subnets not specified = single-AZ deployment +``` + ## Customizing ### Inputs @@ -198,6 +251,7 @@ The following inputs can be used as `step.with` keys | Name | Type | Description | |------------------|---------|------------------------------------| | `aws_elb_create` | Boolean | Toggles the creation of a load balancer and map ports to the EC2 instance. Defaults to `false`.| +| `aws_lb_type` | String | Type of load balancer to create. Options: `elb` (Classic Load Balancer) or `alb` (Application Load Balancer). Defaults to `elb`. ALB supports WAF integration and Layer 7 routing. | | `aws_elb_security_group_name` | String | The name of the ELB security group. Defaults to `SG for ${aws_resource_identifier} - ELB`. | | `aws_elb_app_port` | String | Port in the EC2 instance to be redirected to. Default is `3000`. Accepts comma separated values like `3000,3001`. | | `aws_elb_app_protocol` | String | Protocol to enable. Could be HTTP, HTTPS, TCP or SSL. Defaults to `TCP`. If length doesn't match, will use `TCP` for all.| @@ -207,6 +261,11 @@ The following inputs can be used as `step.with` keys | `aws_elb_access_log_bucket_name` | String | S3 bucket name to store the ELB access logs. Defaults to `${aws_resource_identifier}-logs` (or `-lg `depending of length). **Bucket will be deleted if stack is destroyed.** | | `aws_elb_access_log_expire` | String | Delete the access logs after this amount of days. Defaults to `90`. Set to `0` in order to disable this policy. | | `aws_elb_additional_tags` | JSON | Add additional tags to the terraform [default tags](https://www.hashicorp.com/blog/default-tags-in-the-terraform-aws-provider), any tags put here will be added to elb provisioned resources.| +| **ALB-specific inputs (when aws_lb_type = "alb")** | | | +| `aws_alb_enable_waf` | Boolean | Enable AWS WAF v2 integration for the ALB. Only works with ALB (`aws_lb_type = "alb"`). Defaults to `false`. | +| `aws_alb_subnets` | String | Comma-separated list of subnet IDs for ALB placement. If not provided, will use the same subnet as the EC2 instance (single-AZ deployment). For production, specify multiple subnets across different AZs for high availability. | + +**Note**: When using ALB with default VPC (no explicit VPC creation), the ALB will be placed in the same subnet as the EC2 instance. While this works functionally, it results in a single Availability Zone deployment which may not be suitable for production workloads requiring high availability.

diff --git a/action.yaml b/action.yaml index 0d0cf2bd..0dfc2385 100644 --- a/action.yaml +++ b/action.yaml @@ -279,6 +279,17 @@ inputs: aws_elb_additional_tags: description: 'A JSON object of additional tags that will be included on created resources. Example: `{"key1": "value1", "key2": "value2"}`' required: false + + # AWS Load Balancer Type + aws_lb_type: + description: 'Load balancer type: "elb" or "alb". Defaults to "elb"' + required: false + aws_alb_enable_waf: + description: 'Enable AWS WAF v2 for ALB (only applicable when aws_lb_type is "alb")' + required: false + aws_alb_subnets: + description: 'Comma-separated list of subnet IDs for ALB. If empty, will use single subnet from VPC configuration (only applicable when aws_lb_type is "alb")' + required: false # AWS EFS aws_efs_create: @@ -1200,6 +1211,11 @@ runs: AWS_ELB_ACCESS_LOG_EXPIRE: ${{ inputs.aws_elb_access_log_expire }} AWS_ELB_ADDITIONAL_TAGS: ${{ inputs.aws_elb_additional_tags }} + # AWS Load Balancer Type + AWS_LB_TYPE: ${{ inputs.aws_lb_type }} + AWS_ALB_ENABLE_WAF: ${{ inputs.aws_alb_enable_waf }} + AWS_ALB_SUBNETS: ${{ inputs.aws_alb_subnets }} + # AWS EFS AWS_EFS_CREATE: ${{ inputs.aws_efs_create }} AWS_EFS_FS_ID: ${{ inputs.aws_efs_fs_id }} diff --git a/operations/_scripts/generate/generate_vars_terraform.sh b/operations/_scripts/generate/generate_vars_terraform.sh index 14e5b458..941c7b3f 100644 --- a/operations/_scripts/generate/generate_vars_terraform.sh +++ b/operations/_scripts/generate/generate_vars_terraform.sh @@ -130,6 +130,10 @@ if [[ $(alpha_only "$AWS_ELB_CREATE") == true ]]; then aws_elb_access_log_bucket_name=$(generate_var aws_elb_access_log_bucket_name $AWS_ELB_ACCESS_LOG_BUCKET_NAME) aws_elb_access_log_expire=$(generate_var aws_elb_access_log_expire $AWS_ELB_ACCESS_LOG_EXPIRE) aws_elb_additional_tags=$(generate_var aws_elb_additional_tags $AWS_ELB_ADDITIONAL_TAGS) + # ALB/LB wrapper variables + aws_lb_type=$(generate_var aws_lb_type $AWS_LB_TYPE) + aws_alb_enable_waf=$(generate_var aws_alb_enable_waf $AWS_ALB_ENABLE_WAF) + aws_alb_subnets=$(generate_var aws_alb_subnets $AWS_ALB_SUBNETS) fi #-- AWS EFS --# diff --git a/operations/deployment/terraform/aws/aws_variables.tf b/operations/deployment/terraform/aws/aws_variables.tf index cba19a2e..4fd8bbf4 100644 --- a/operations/deployment/terraform/aws/aws_variables.tf +++ b/operations/deployment/terraform/aws/aws_variables.tf @@ -327,6 +327,30 @@ variable "aws_elb_additional_tags" { default = "{}" } +variable "aws_lb_type" { + type = string + description = "Load balancer type: 'elb' or 'alb'" + default = "elb" +} + +variable "aws_alb_enable_waf" { + type = bool + description = "Enable AWS WAF v2 for ALB" + default = false +} + +variable "aws_waf_rules" { + type = list(string) + description = "WAF rule types to enable" + default = ["AWSManagedRulesCommonRuleSet"] +} + +variable "aws_alb_subnets" { + type = string + description = "Comma-separated list of subnet IDs for ALB. If empty, will use single subnet from VPC configuration." + default = "" +} + # AWS EFS ### This variable is hidden for the end user. Is built in deploy.sh based on the next 3 variables. diff --git a/operations/deployment/terraform/aws/bitovi_main.tf b/operations/deployment/terraform/aws/bitovi_main.tf index 7597b904..58021150 100644 --- a/operations/deployment/terraform/aws/bitovi_main.tf +++ b/operations/deployment/terraform/aws/bitovi_main.tf @@ -100,8 +100,10 @@ module "aws_route53" { } module "aws_elb" { - source = "../modules/aws/elb" + source = "../modules/aws/aws_lb" count = var.aws_ec2_instance_create && var.aws_elb_create ? 1 : 0 + # Load Balancer Type + aws_lb_type = var.aws_lb_type # ELB Values aws_elb_security_group_name = var.aws_elb_security_group_name aws_elb_app_port = var.aws_elb_app_port @@ -119,6 +121,9 @@ module "aws_elb" { aws_elb_target_sg_id = module.ec2[0].aws_security_group_ec2_sg_id # Certs aws_certificates_selected_arn = var.aws_r53_enable_cert && var.aws_r53_domain_name != "" ? module.aws_certificates[0].selected_arn : "" + # ALB specific variables + aws_alb_enable_waf = var.aws_alb_enable_waf + aws_alb_subnets = var.aws_alb_subnets != "" ? [for n in split(",", var.aws_alb_subnets) : n] : [] # Others aws_resource_identifier = var.aws_resource_identifier aws_resource_identifier_supershort = var.aws_resource_identifier_supershort diff --git a/operations/deployment/terraform/modules/aws/alb/aws_alb.tf b/operations/deployment/terraform/modules/aws/alb/aws_alb.tf new file mode 100644 index 00000000..853bce57 --- /dev/null +++ b/operations/deployment/terraform/modules/aws/alb/aws_alb.tf @@ -0,0 +1,202 @@ +data "aws_elb_service_account" "main" {} + +# S3 bucket for ALB access logs +resource "aws_s3_bucket" "lb_access_logs" { + bucket = var.aws_elb_access_log_bucket_name + force_destroy = true + tags = { + Name = var.aws_elb_access_log_bucket_name + } +} + +resource "aws_s3_bucket_lifecycle_configuration" "lb_access_logs_lifecycle" { + count = tonumber(var.aws_elb_access_log_expire) > 0 ? 1 : 0 + bucket = aws_s3_bucket.lb_access_logs.id + rule { + id = "ExpirationRule" + status = "Enabled" + filter { + prefix = "" + } + expiration { + days = tonumber(var.aws_elb_access_log_expire) + } + } +} + +resource "aws_s3_bucket_policy" "allow_access_from_elb_account" { + bucket = aws_s3_bucket.lb_access_logs.id + policy = < 0 ? var.aws_alb_subnets : [var.aws_vpc_subnet_selected] +} + +# Outputs +output "aws_elb_dns_name" { + value = aws_lb.alb.dns_name +} + +output "aws_elb_zone_id" { + value = aws_lb.alb.zone_id +} + +output "alb_arn" { + value = aws_lb.alb.arn +} + +output "alb_target_group_arns" { + value = aws_lb_target_group.alb_targets[*].arn +} diff --git a/operations/deployment/terraform/modules/aws/alb/aws_alb_providers.tf b/operations/deployment/terraform/modules/aws/alb/aws_alb_providers.tf new file mode 100644 index 00000000..1ac87da5 --- /dev/null +++ b/operations/deployment/terraform/modules/aws/alb/aws_alb_providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/operations/deployment/terraform/modules/aws/alb/aws_alb_vars.tf b/operations/deployment/terraform/modules/aws/alb/aws_alb_vars.tf new file mode 100644 index 00000000..dc298bbc --- /dev/null +++ b/operations/deployment/terraform/modules/aws/alb/aws_alb_vars.tf @@ -0,0 +1,32 @@ +# ALB compatible variables (mapped from ELB naming for consistency) +variable "aws_elb_security_group_name" {} +variable "aws_elb_app_port" {} +variable "aws_elb_app_protocol" {} +variable "aws_elb_listen_port" {} +variable "aws_elb_listen_protocol" {} +variable "aws_elb_healthcheck" {} +variable "aws_elb_access_log_bucket_name" {} +variable "aws_elb_access_log_expire" {} + +variable "aws_instance_server_az" {} +variable "aws_vpc_selected_id" {} +variable "aws_vpc_subnet_selected" {} +variable "aws_instance_server_id" {} +variable "aws_certificates_selected_arn" {} +variable "aws_elb_target_sg_id" {} + +variable "aws_resource_identifier" {} +variable "aws_resource_identifier_supershort" {} + +# ALB specific variables +variable "aws_alb_enable_waf" { + type = bool + description = "Enable AWS WAF v2 for ALB" + default = false +} + +variable "aws_alb_subnets" { + type = list(string) + description = "List of subnet IDs for ALB (will default to using single subnet if not provided)" + default = [] +} diff --git a/operations/deployment/terraform/modules/aws/aws_lb/README.md b/operations/deployment/terraform/modules/aws/aws_lb/README.md new file mode 100644 index 00000000..236f996f --- /dev/null +++ b/operations/deployment/terraform/modules/aws/aws_lb/README.md @@ -0,0 +1,92 @@ +# AWS Load Balancer Wrapper Module + +This module provides a unified interface for deploying either Classic ELB or Application Load Balancer (ALB) based on the `aws_lb_type` variable. + +## Usage + +```terraform +module "aws_lb" { + source = "./modules/aws/aws_lb" + + # Choose load balancer type + aws_lb_type = "alb" # or "elb" + + # Common configuration for both ELB and ALB + aws_elb_security_group_name = "my-lb-sg" + aws_elb_app_port = "3000" + aws_elb_listen_port = "80,443" + aws_elb_healthcheck = "HTTP:3000/" + aws_elb_access_log_bucket_name = "my-lb-logs-bucket" + + # ALB-specific options (ignored when using ELB) + aws_alb_enable_waf = true + aws_alb_subnets = ["subnet-123", "subnet-456"] + + # Required variables + aws_vpc_selected_id = "vpc-123" + aws_instance_server_id = "i-123" + # ... other required variables +} +``` + +## Load Balancer Types + +### Classic ELB (`aws_lb_type = "elb"`) +- Layer 4 load balancer +- Supports TCP, HTTP, HTTPS, SSL protocols +- Single availability zone deployment (using `aws_vpc_subnet_selected`) +- Legacy option but still supported + +### Application Load Balancer (`aws_lb_type = "alb"`) +- Layer 7 load balancer +- HTTP/HTTPS only +- Multi-AZ deployment (using `aws_alb_subnets` or falls back to single subnet) +- Advanced routing capabilities +- WAF support (when `aws_alb_enable_waf = true`) +- Modern option with more features + +## Variables + +### Common Variables (used by both ELB and ALB) +- `aws_lb_type` - Type of load balancer: "elb" or "alb" (default: "elb") +- `aws_elb_security_group_name` - Security group name +- `aws_elb_app_port` - Application port(s) (comma-separated) +- `aws_elb_listen_port` - Listener port(s) (comma-separated) +- `aws_elb_healthcheck` - Health check configuration +- `aws_elb_access_log_bucket_name` - S3 bucket for access logs +- And other standard ELB variables... + +### ALB-Specific Variables +- `aws_alb_enable_waf` - Enable AWS WAF v2 (boolean, default: false) +- `aws_alb_subnets` - List of subnet IDs for multi-AZ ALB deployment + +## Outputs + +### Common Outputs (available for both types) +- `aws_elb_dns_name` - DNS name of the load balancer +- `aws_elb_zone_id` - Route53 zone ID of the load balancer +- `lb_type` - Type of load balancer deployed + +### ALB-Specific Outputs +- `alb_arn` - ARN of the ALB (empty if using ELB) +- `alb_target_group_arns` - ARNs of ALB target groups (empty if using ELB) + +## Migration from ELB to ALB + +To migrate from ELB to ALB, simply change: +```terraform +aws_lb_type = "alb" +``` + +And optionally add ALB-specific configuration: +```terraform +aws_alb_enable_waf = true +aws_alb_subnets = ["subnet-1", "subnet-2"] # For multi-AZ +``` + +## Notes + +- When using ALB, ensure you have at least one public subnet +- ALB requires multiple subnets for high availability, but will fall back to single subnet if `aws_alb_subnets` is empty +- WAF is only available with ALB, not Classic ELB +- The module maintains backward compatibility with existing ELB configurations diff --git a/operations/deployment/terraform/modules/aws/aws_lb/aws_lb.tf b/operations/deployment/terraform/modules/aws/aws_lb/aws_lb.tf new file mode 100644 index 00000000..6b48d3bc --- /dev/null +++ b/operations/deployment/terraform/modules/aws/aws_lb/aws_lb.tf @@ -0,0 +1,77 @@ +# ELB Module (Classic Load Balancer) +module "elb" { + source = "../elb" + count = var.aws_lb_type == "elb" ? 1 : 0 + + # Pass all ELB variables + aws_elb_security_group_name = var.aws_elb_security_group_name + aws_elb_app_port = var.aws_elb_app_port + aws_elb_app_protocol = var.aws_elb_app_protocol + aws_elb_listen_port = var.aws_elb_listen_port + aws_elb_listen_protocol = var.aws_elb_listen_protocol + aws_elb_healthcheck = var.aws_elb_healthcheck + aws_elb_access_log_bucket_name = var.aws_elb_access_log_bucket_name + aws_elb_access_log_expire = var.aws_elb_access_log_expire + aws_instance_server_az = var.aws_instance_server_az + aws_vpc_selected_id = var.aws_vpc_selected_id + aws_vpc_subnet_selected = var.aws_vpc_subnet_selected + aws_instance_server_id = var.aws_instance_server_id + aws_certificates_selected_arn = var.aws_certificates_selected_arn + aws_elb_target_sg_id = var.aws_elb_target_sg_id + aws_resource_identifier = var.aws_resource_identifier + aws_resource_identifier_supershort = var.aws_resource_identifier_supershort +} + +# ALB Module (Application Load Balancer) +module "alb" { + source = "../alb" + count = var.aws_lb_type == "alb" ? 1 : 0 + + # Pass all ALB variables (using ELB variable names for consistency) + aws_elb_security_group_name = var.aws_elb_security_group_name + aws_elb_app_port = var.aws_elb_app_port + aws_elb_app_protocol = var.aws_elb_app_protocol + aws_elb_listen_port = var.aws_elb_listen_port + aws_elb_listen_protocol = var.aws_elb_listen_protocol + aws_elb_healthcheck = var.aws_elb_healthcheck + aws_elb_access_log_bucket_name = var.aws_elb_access_log_bucket_name + aws_elb_access_log_expire = var.aws_elb_access_log_expire + aws_instance_server_az = var.aws_instance_server_az + aws_vpc_selected_id = var.aws_vpc_selected_id + aws_vpc_subnet_selected = var.aws_vpc_subnet_selected + aws_instance_server_id = var.aws_instance_server_id + aws_certificates_selected_arn = var.aws_certificates_selected_arn + aws_elb_target_sg_id = var.aws_elb_target_sg_id + aws_resource_identifier = var.aws_resource_identifier + aws_resource_identifier_supershort = var.aws_resource_identifier_supershort + # ALB specific variables + aws_alb_enable_waf = var.aws_alb_enable_waf + aws_alb_subnets = var.aws_alb_subnets +} + +# Unified outputs that work regardless of LB type +output "aws_elb_dns_name" { + description = "DNS name of the load balancer" + value = var.aws_lb_type == "elb" ? try(module.elb[0].aws_elb_dns_name, "") : try(module.alb[0].aws_elb_dns_name, "") +} + +output "aws_elb_zone_id" { + description = "Zone ID of the load balancer" + value = var.aws_lb_type == "elb" ? try(module.elb[0].aws_elb_zone_id, "") : try(module.alb[0].aws_elb_zone_id, "") +} + +# Additional outputs for ALB-specific features +output "alb_arn" { + description = "ARN of the ALB (empty if using ELB)" + value = var.aws_lb_type == "alb" ? try(module.alb[0].alb_arn, "") : "" +} + +output "alb_target_group_arns" { + description = "ARNs of ALB target groups (empty if using ELB)" + value = var.aws_lb_type == "alb" ? try(module.alb[0].alb_target_group_arns, []) : [] +} + +output "lb_type" { + description = "Type of load balancer deployed" + value = var.aws_lb_type +} diff --git a/operations/deployment/terraform/modules/aws/aws_lb/aws_lb_providers.tf b/operations/deployment/terraform/modules/aws/aws_lb/aws_lb_providers.tf new file mode 100644 index 00000000..1ac87da5 --- /dev/null +++ b/operations/deployment/terraform/modules/aws/aws_lb/aws_lb_providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/operations/deployment/terraform/modules/aws/aws_lb/aws_lb_vars.tf b/operations/deployment/terraform/modules/aws/aws_lb/aws_lb_vars.tf new file mode 100644 index 00000000..ee6b88a1 --- /dev/null +++ b/operations/deployment/terraform/modules/aws/aws_lb/aws_lb_vars.tf @@ -0,0 +1,39 @@ +# Load balancer type +variable "aws_lb_type" { + type = string + description = "Load balancer type: 'elb' or 'alb'" + default = "elb" +} + +# Common variables for both ELB and ALB +variable "aws_elb_security_group_name" {} +variable "aws_elb_app_port" {} +variable "aws_elb_app_protocol" {} +variable "aws_elb_listen_port" {} +variable "aws_elb_listen_protocol" {} +variable "aws_elb_healthcheck" {} +variable "aws_elb_access_log_bucket_name" {} +variable "aws_elb_access_log_expire" {} + +variable "aws_instance_server_az" {} +variable "aws_vpc_selected_id" {} +variable "aws_vpc_subnet_selected" {} +variable "aws_instance_server_id" {} +variable "aws_certificates_selected_arn" {} +variable "aws_elb_target_sg_id" {} + +variable "aws_resource_identifier" {} +variable "aws_resource_identifier_supershort" {} + +# ALB specific variables +variable "aws_alb_enable_waf" { + type = bool + description = "Enable AWS WAF v2 for ALB" + default = false +} + +variable "aws_alb_subnets" { + type = list(string) + description = "List of subnet IDs for ALB (will default to using single subnet if not provided)" + default = [] +} diff --git a/operations/deployment/terraform/modules/aws/elb/aws_elb.tf b/operations/deployment/terraform/modules/aws/elb/aws_elb.tf index 621fb7b6..19e18eea 100644 --- a/operations/deployment/terraform/modules/aws/elb/aws_elb.tf +++ b/operations/deployment/terraform/modules/aws/elb/aws_elb.tf @@ -14,6 +14,9 @@ resource "aws_s3_bucket_lifecycle_configuration" "lb_access_logs_lifecycle" { rule { id = "ExpirationRule" status = "Enabled" + filter { + prefix = "" + } expiration { days = tonumber(var.aws_elb_access_log_expire) }