Skip to content

Commit edc84b6

Browse files
committed
Initial commit
1 parent 2699467 commit edc84b6

File tree

12 files changed

+564
-1
lines changed

12 files changed

+564
-1
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,59 @@ jobs:
4545
additional_tags: '{\"key\":\"value\",\"key2\":\"value2\"}'
4646
```
4747
48+
### ALB with WAF example
49+
```yaml
50+
name: Deploy with Application Load Balancer and WAF
51+
on:
52+
push:
53+
branches: [ main ]
54+
55+
jobs:
56+
EC2-Deploy:
57+
runs-on: ubuntu-latest
58+
steps:
59+
- id: deploy
60+
uses: bitovi/github-actions-deploy-commons@main
61+
with:
62+
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
63+
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
64+
aws_default_region: us-east-1
65+
env_ghs: ${{ secrets.DOT_ENV }}
66+
# Load Balancer Configuration
67+
aws_elb_create: true
68+
aws_lb_type: alb # Use Application Load Balancer instead of Classic ELB
69+
aws_alb_enable_waf: true # Enable AWS WAF v2 protection
70+
aws_alb_subnets: subnet-12345,subnet-67890 # Specify subnets for ALB
71+
# VPC Configuration (required for ALB)
72+
aws_vpc_create: true
73+
aws_vpc_public_subnets: 10.0.1.0/24,10.0.2.0/24
74+
aws_vpc_availability_zones: us-east-1a,us-east-1b
75+
```
76+
77+
### ALB with default VPC example
78+
```yaml
79+
name: Deploy with ALB using default VPC (single-AZ)
80+
on:
81+
push:
82+
branches: [ main ]
83+
84+
jobs:
85+
EC2-Deploy:
86+
runs-on: ubuntu-latest
87+
steps:
88+
- id: deploy
89+
uses: bitovi/github-actions-deploy-commons@main
90+
with:
91+
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
92+
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
93+
aws_default_region: us-east-1
94+
env_ghs: ${{ secrets.DOT_ENV }}
95+
# Simple ALB setup using default VPC
96+
aws_elb_create: true
97+
aws_lb_type: alb # Use ALB instead of Classic ELB
98+
# Note: aws_alb_subnets not specified = single-AZ deployment
99+
```
100+
48101
## Customizing
49102

50103
### Inputs
@@ -198,6 +251,7 @@ The following inputs can be used as `step.with` keys
198251
| Name | Type | Description |
199252
|------------------|---------|------------------------------------|
200253
| `aws_elb_create` | Boolean | Toggles the creation of a load balancer and map ports to the EC2 instance. Defaults to `false`.|
254+
| `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. |
201255
| `aws_elb_security_group_name` | String | The name of the ELB security group. Defaults to `SG for ${aws_resource_identifier} - ELB`. |
202256
| `aws_elb_app_port` | String | Port in the EC2 instance to be redirected to. Default is `3000`. Accepts comma separated values like `3000,3001`. |
203257
| `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
207261
| `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.** |
208262
| `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. |
209263
| `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.|
264+
| **ALB-specific inputs (when aws_lb_type = "alb")** | | |
265+
| `aws_alb_enable_waf` | Boolean | Enable AWS WAF v2 integration for the ALB. Only works with ALB (`aws_lb_type = "alb"`). Defaults to `false`. |
266+
| `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. |
267+
268+
**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.
210269
<hr/>
211270
<br/>
212271

action.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,17 @@ inputs:
279279
aws_elb_additional_tags:
280280
description: 'A JSON object of additional tags that will be included on created resources. Example: `{"key1": "value1", "key2": "value2"}`'
281281
required: false
282+
283+
# AWS Load Balancer Type
284+
aws_lb_type:
285+
description: 'Load balancer type: "elb" or "alb". Defaults to "elb"'
286+
required: false
287+
aws_alb_enable_waf:
288+
description: 'Enable AWS WAF v2 for ALB (only applicable when aws_lb_type is "alb")'
289+
required: false
290+
aws_alb_subnets:
291+
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")'
292+
required: false
282293

283294
# AWS EFS
284295
aws_efs_create:
@@ -1200,6 +1211,11 @@ runs:
12001211
AWS_ELB_ACCESS_LOG_EXPIRE: ${{ inputs.aws_elb_access_log_expire }}
12011212
AWS_ELB_ADDITIONAL_TAGS: ${{ inputs.aws_elb_additional_tags }}
12021213

1214+
# AWS Load Balancer Type
1215+
AWS_LB_TYPE: ${{ inputs.aws_lb_type }}
1216+
AWS_ALB_ENABLE_WAF: ${{ inputs.aws_alb_enable_waf }}
1217+
AWS_ALB_SUBNETS: ${{ inputs.aws_alb_subnets }}
1218+
12031219
# AWS EFS
12041220
AWS_EFS_CREATE: ${{ inputs.aws_efs_create }}
12051221
AWS_EFS_FS_ID: ${{ inputs.aws_efs_fs_id }}

operations/_scripts/generate/generate_vars_terraform.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ if [[ $(alpha_only "$AWS_ELB_CREATE") == true ]]; then
130130
aws_elb_access_log_bucket_name=$(generate_var aws_elb_access_log_bucket_name $AWS_ELB_ACCESS_LOG_BUCKET_NAME)
131131
aws_elb_access_log_expire=$(generate_var aws_elb_access_log_expire $AWS_ELB_ACCESS_LOG_EXPIRE)
132132
aws_elb_additional_tags=$(generate_var aws_elb_additional_tags $AWS_ELB_ADDITIONAL_TAGS)
133+
# ALB/LB wrapper variables
134+
aws_lb_type=$(generate_var aws_lb_type $AWS_LB_TYPE)
135+
aws_alb_enable_waf=$(generate_var aws_alb_enable_waf $AWS_ALB_ENABLE_WAF)
136+
aws_alb_subnets=$(generate_var aws_alb_subnets $AWS_ALB_SUBNETS)
133137
fi
134138

135139
#-- AWS EFS --#

operations/deployment/terraform/aws/aws_variables.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,30 @@ variable "aws_elb_additional_tags" {
327327
default = "{}"
328328
}
329329

330+
variable "aws_lb_type" {
331+
type = string
332+
description = "Load balancer type: 'elb' or 'alb'"
333+
default = "elb"
334+
}
335+
336+
variable "aws_alb_enable_waf" {
337+
type = bool
338+
description = "Enable AWS WAF v2 for ALB"
339+
default = false
340+
}
341+
342+
variable "aws_waf_rules" {
343+
type = list(string)
344+
description = "WAF rule types to enable"
345+
default = ["AWSManagedRulesCommonRuleSet"]
346+
}
347+
348+
variable "aws_alb_subnets" {
349+
type = string
350+
description = "Comma-separated list of subnet IDs for ALB. If empty, will use single subnet from VPC configuration."
351+
default = ""
352+
}
353+
330354
# AWS EFS
331355

332356
### This variable is hidden for the end user. Is built in deploy.sh based on the next 3 variables.

operations/deployment/terraform/aws/bitovi_main.tf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,10 @@ module "aws_route53" {
100100
}
101101

102102
module "aws_elb" {
103-
source = "../modules/aws/elb"
103+
source = "../modules/aws/aws_lb"
104104
count = var.aws_ec2_instance_create && var.aws_elb_create ? 1 : 0
105+
# Load Balancer Type
106+
aws_lb_type = var.aws_lb_type
105107
# ELB Values
106108
aws_elb_security_group_name = var.aws_elb_security_group_name
107109
aws_elb_app_port = var.aws_elb_app_port
@@ -119,6 +121,9 @@ module "aws_elb" {
119121
aws_elb_target_sg_id = module.ec2[0].aws_security_group_ec2_sg_id
120122
# Certs
121123
aws_certificates_selected_arn = var.aws_r53_enable_cert && var.aws_r53_domain_name != "" ? module.aws_certificates[0].selected_arn : ""
124+
# ALB specific variables
125+
aws_alb_enable_waf = var.aws_alb_enable_waf
126+
aws_alb_subnets = var.aws_alb_subnets != "" ? [for n in split(",", var.aws_alb_subnets) : n] : []
122127
# Others
123128
aws_resource_identifier = var.aws_resource_identifier
124129
aws_resource_identifier_supershort = var.aws_resource_identifier_supershort
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
data "aws_elb_service_account" "main" {}
2+
3+
# S3 bucket for ALB access logs
4+
resource "aws_s3_bucket" "lb_access_logs" {
5+
bucket = var.aws_elb_access_log_bucket_name
6+
force_destroy = true
7+
tags = {
8+
Name = var.aws_elb_access_log_bucket_name
9+
}
10+
}
11+
12+
resource "aws_s3_bucket_lifecycle_configuration" "lb_access_logs_lifecycle" {
13+
count = tonumber(var.aws_elb_access_log_expire) > 0 ? 1 : 0
14+
bucket = aws_s3_bucket.lb_access_logs.id
15+
rule {
16+
id = "ExpirationRule"
17+
status = "Enabled"
18+
expiration {
19+
days = tonumber(var.aws_elb_access_log_expire)
20+
}
21+
}
22+
}
23+
24+
resource "aws_s3_bucket_policy" "allow_access_from_elb_account" {
25+
bucket = aws_s3_bucket.lb_access_logs.id
26+
policy = <<POLICY
27+
{
28+
"Version": "2012-10-17",
29+
"Id": "Policy",
30+
"Statement": [
31+
{
32+
"Effect": "Allow",
33+
"Principal": {
34+
"AWS": ["${data.aws_elb_service_account.main.arn}"]
35+
},
36+
"Action": ["s3:PutObject"],
37+
"Resource": "arn:aws:s3:::${var.aws_elb_access_log_bucket_name}/*"
38+
}
39+
]
40+
}
41+
POLICY
42+
lifecycle {
43+
ignore_changes = [policy]
44+
}
45+
}
46+
47+
# ALB Security Group
48+
resource "aws_security_group" "alb_security_group" {
49+
name = var.aws_elb_security_group_name != "" ? "${var.aws_elb_security_group_name}-alb" : "SG for ${var.aws_resource_identifier} - ALB"
50+
description = "SG for ${var.aws_resource_identifier} - ALB"
51+
vpc_id = var.aws_vpc_selected_id
52+
egress {
53+
from_port = 0
54+
to_port = 0
55+
protocol = "-1"
56+
cidr_blocks = ["0.0.0.0/0"]
57+
}
58+
tags = {
59+
Name = "${var.aws_resource_identifier}-alb"
60+
}
61+
}
62+
63+
# Security group rule to allow traffic from ALB to target
64+
resource "aws_security_group_rule" "incoming_alb" {
65+
type = "ingress"
66+
from_port = 0
67+
to_port = 0
68+
protocol = -1
69+
source_security_group_id = aws_security_group.alb_security_group.id
70+
security_group_id = var.aws_elb_target_sg_id
71+
}
72+
73+
# ALB Security Group Rules for incoming connections
74+
resource "aws_security_group_rule" "incoming_alb_ports" {
75+
count = local.aws_ports_amount
76+
type = "ingress"
77+
from_port = local.aws_alb_listen_port[count.index]
78+
to_port = local.aws_alb_listen_port[count.index]
79+
protocol = "tcp"
80+
cidr_blocks = ["0.0.0.0/0"]
81+
security_group_id = aws_security_group.alb_security_group.id
82+
}
83+
84+
# Application Load Balancer
85+
resource "aws_lb" "alb" {
86+
name = var.aws_resource_identifier_supershort
87+
internal = false
88+
load_balancer_type = "application"
89+
security_groups = [aws_security_group.alb_security_group.id]
90+
subnets = local.alb_subnets
91+
92+
enable_deletion_protection = false
93+
94+
access_logs {
95+
bucket = aws_s3_bucket.lb_access_logs.id
96+
prefix = "alb"
97+
enabled = true
98+
}
99+
100+
tags = {
101+
Name = "${var.aws_resource_identifier_supershort}"
102+
}
103+
}
104+
105+
# ALB Target Group
106+
resource "aws_lb_target_group" "alb_targets" {
107+
count = length(local.aws_alb_app_port)
108+
name = "${var.aws_resource_identifier_supershort}${count.index}"
109+
port = local.aws_alb_app_port[count.index]
110+
protocol = local.alb_app_protocol[count.index]
111+
vpc_id = var.aws_vpc_selected_id
112+
113+
health_check {
114+
enabled = true
115+
healthy_threshold = 2
116+
unhealthy_threshold = 2
117+
timeout = 5
118+
interval = 30
119+
path = local.health_check_path[count.index]
120+
matcher = "200"
121+
protocol = local.alb_app_protocol[count.index]
122+
port = "traffic-port"
123+
}
124+
125+
tags = {
126+
Name = "${var.aws_resource_identifier_supershort}-tg-${count.index}"
127+
}
128+
}
129+
130+
# ALB Target Group Attachment
131+
resource "aws_lb_target_group_attachment" "alb_target_attachment" {
132+
count = length(local.aws_alb_app_port)
133+
target_group_arn = aws_lb_target_group.alb_targets[count.index].arn
134+
target_id = var.aws_instance_server_id
135+
port = local.aws_alb_app_port[count.index]
136+
}
137+
138+
# ALB Listeners
139+
resource "aws_lb_listener" "alb_listener" {
140+
count = length(local.listener_for_each)
141+
load_balancer_arn = aws_lb.alb.arn
142+
port = local.aws_alb_listen_port[count.index]
143+
protocol = local.alb_listen_protocol[count.index]
144+
ssl_policy = local.alb_ssl_available && local.alb_listen_protocol[count.index] == "HTTPS" ? "ELBSecurityPolicy-TLS13-1-2-2021-06" : null
145+
certificate_arn = local.alb_ssl_available && local.alb_listen_protocol[count.index] == "HTTPS" ? var.aws_certificates_selected_arn : null
146+
147+
default_action {
148+
type = "forward"
149+
target_group_arn = aws_lb_target_group.alb_targets[count.index].arn
150+
}
151+
}
152+
153+
# Locals for processing variables
154+
locals {
155+
# Check if there is a cert available
156+
alb_ssl_available = var.aws_certificates_selected_arn != "" ? true : false
157+
158+
# Transform CSV values into arrays
159+
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])
160+
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]
161+
aws_alb_app_protocol = var.aws_elb_app_protocol != "" ? [for n in split(",", var.aws_elb_app_protocol) : upper(n)] : []
162+
163+
# Store the lowest array length
164+
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)
165+
166+
# Store the shortest array for listener creation
167+
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
168+
169+
# Protocol handling
170+
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
171+
alb_listen_protocol = local.alb_ssl_available ? [for _ in range(local.aws_ports_amount) : "HTTPS"] : [for _ in range(local.aws_ports_amount) : "HTTP"]
172+
173+
# Health check path extraction from healthcheck string
174+
health_check_path = [for i in range(length(local.aws_alb_app_port)) :
175+
can(regex("^HTTP:", var.aws_elb_healthcheck)) ?
176+
try(split(":", var.aws_elb_healthcheck)[1], "/") :
177+
"/"
178+
]
179+
180+
# ALB subnets - use provided subnets or fall back to single subnet
181+
alb_subnets = length(var.aws_alb_subnets) > 0 ? var.aws_alb_subnets : [var.aws_vpc_subnet_selected]
182+
}
183+
184+
# Outputs
185+
output "aws_elb_dns_name" {
186+
value = aws_lb.alb.dns_name
187+
}
188+
189+
output "aws_elb_zone_id" {
190+
value = aws_lb.alb.zone_id
191+
}
192+
193+
output "alb_arn" {
194+
value = aws_lb.alb.arn
195+
}
196+
197+
output "alb_target_group_arns" {
198+
value = aws_lb_target_group.alb_targets[*].arn
199+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
terraform {
2+
required_providers {
3+
aws = {
4+
source = "hashicorp/aws"
5+
version = "~> 5.0"
6+
}
7+
}
8+
}

0 commit comments

Comments
 (0)