Skip to content

Adding ALB option #89

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.|
Expand 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.
<hr/>
<br/>

Expand Down
16 changes: 16 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Expand Down
4 changes: 4 additions & 0 deletions operations/_scripts/generate/generate_vars_terraform.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 --#
Expand Down
24 changes: 24 additions & 0 deletions operations/deployment/terraform/aws/aws_variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion operations/deployment/terraform/aws/bitovi_main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
202 changes: 202 additions & 0 deletions operations/deployment/terraform/modules/aws/alb/aws_alb.tf
Original file line number Diff line number Diff line change
@@ -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 = <<POLICY
{
"Version": "2012-10-17",
"Id": "Policy",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": ["${data.aws_elb_service_account.main.arn}"]
},
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::${var.aws_elb_access_log_bucket_name}/*"
}
]
}
POLICY
lifecycle {
ignore_changes = [policy]
}
}

# ALB Security Group
resource "aws_security_group" "alb_security_group" {
name = var.aws_elb_security_group_name != "" ? "${var.aws_elb_security_group_name}-alb" : "SG for ${var.aws_resource_identifier} - ALB"
description = "SG for ${var.aws_resource_identifier} - ALB"
vpc_id = var.aws_vpc_selected_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.aws_resource_identifier}-alb"
}
}

# Security group rule to allow traffic from ALB to target
resource "aws_security_group_rule" "incoming_alb" {
type = "ingress"
from_port = 0
to_port = 0
protocol = -1
source_security_group_id = aws_security_group.alb_security_group.id
security_group_id = var.aws_elb_target_sg_id
}

# ALB Security Group Rules for incoming connections
resource "aws_security_group_rule" "incoming_alb_ports" {
count = local.aws_ports_amount
type = "ingress"
from_port = local.aws_alb_listen_port[count.index]
to_port = local.aws_alb_listen_port[count.index]
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.alb_security_group.id
}

# Application Load Balancer
resource "aws_lb" "alb" {
name = var.aws_resource_identifier_supershort
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_security_group.id]
subnets = local.alb_subnets

enable_deletion_protection = false

access_logs {
bucket = aws_s3_bucket.lb_access_logs.id
prefix = "alb"
enabled = true
}

tags = {
Name = "${var.aws_resource_identifier_supershort}"
}
}

# ALB Target Group
resource "aws_lb_target_group" "alb_targets" {
count = length(local.aws_alb_app_port)
name = "${var.aws_resource_identifier_supershort}${count.index}"
port = local.aws_alb_app_port[count.index]
protocol = local.alb_app_protocol[count.index]
vpc_id = var.aws_vpc_selected_id

health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 30
path = local.health_check_path[count.index]
matcher = "200"
protocol = local.alb_app_protocol[count.index]
port = "traffic-port"
}

tags = {
Name = "${var.aws_resource_identifier_supershort}-tg-${count.index}"
}
}

# ALB Target Group Attachment
resource "aws_lb_target_group_attachment" "alb_target_attachment" {
count = length(local.aws_alb_app_port)
target_group_arn = aws_lb_target_group.alb_targets[count.index].arn
target_id = var.aws_instance_server_id
port = local.aws_alb_app_port[count.index]
}

# ALB Listeners
resource "aws_lb_listener" "alb_listener" {
count = length(local.listener_for_each)
load_balancer_arn = aws_lb.alb.arn
port = local.aws_alb_listen_port[count.index]
protocol = local.alb_listen_protocol[count.index]
ssl_policy = local.alb_ssl_available && local.alb_listen_protocol[count.index] == "HTTPS" ? "ELBSecurityPolicy-TLS13-1-2-2021-06" : null
certificate_arn = local.alb_ssl_available && local.alb_listen_protocol[count.index] == "HTTPS" ? var.aws_certificates_selected_arn : null

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.alb_targets[count.index].arn
}
}

# Locals for processing variables
locals {
# Check if there is a cert available
alb_ssl_available = var.aws_certificates_selected_arn != "" ? true : false

# Transform CSV values into arrays
aws_alb_listen_port = var.aws_elb_listen_port != "" ? [for n in split(",", var.aws_elb_listen_port) : tonumber(n)] : (local.alb_ssl_available ? [443] : [80])
aws_alb_app_port = var.aws_elb_app_port != "" ? [for n in split(",", var.aws_elb_app_port) : tonumber(n)] : var.aws_elb_listen_port != "" ? local.aws_alb_listen_port : [3000]
aws_alb_app_protocol = var.aws_elb_app_protocol != "" ? [for n in split(",", var.aws_elb_app_protocol) : upper(n)] : []

# Store the lowest array length
aws_ports_amount = length(local.aws_alb_listen_port) < length(local.aws_alb_app_port) ? length(local.aws_alb_listen_port) : length(local.aws_alb_app_port)

# Store the shortest array for listener creation
listener_for_each = length(local.aws_alb_listen_port) < length(local.aws_alb_app_port) ? local.aws_alb_listen_port : local.aws_alb_app_port

# Protocol handling
alb_app_protocol = length(local.aws_alb_app_protocol) < local.aws_ports_amount ? [for _ in range(local.aws_ports_amount) : "HTTP"] : local.aws_alb_app_protocol
alb_listen_protocol = local.alb_ssl_available ? [for _ in range(local.aws_ports_amount) : "HTTPS"] : [for _ in range(local.aws_ports_amount) : "HTTP"]

# Health check path extraction from healthcheck string
health_check_path = [for i in range(length(local.aws_alb_app_port)) :
can(regex("^HTTP:", var.aws_elb_healthcheck)) ?
try(split(":", var.aws_elb_healthcheck)[1], "/") :
"/"
]

# ALB subnets - use provided subnets or fall back to single subnet
alb_subnets = length(var.aws_alb_subnets) > 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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
Loading