Skip to content

Commit edce5bf

Browse files
feat: transform and pass in lambda vars (#3)
* feat: set deployment strategy at module level * feat: provisioned concurrency variable * fix: variable validations * feat: better local consumption * fix: only use provisioned concurrency when fixed count > 0 * fix: allow oidc application-autoscaling * chore: allow cloudwatch oidc * feat: readme on provisioned con usage + fixes * chore: readme updates * chore: rm comments * fix: fmt
1 parent ed82ce6 commit edce5bf

File tree

6 files changed

+278
-11
lines changed

6 files changed

+278
-11
lines changed

README.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,93 @@ Given a terragrunt file is found at `infra/live/dev/aws/api/terragrunt.hcl`
1414

1515
```sh
1616
just tg dev aws/api plan
17-
```
17+
```
18+
19+
## types of lambda provisioned concurrency
20+
21+
```hcl
22+
module "lambda_example" {
23+
source = "../lambda"
24+
...
25+
provisioned_config = var.your_provisioned_config
26+
}
27+
```
28+
29+
#### [default] No provisioned lambdas
30+
- use case: background processes
31+
- we can handle an initial lag while lambda warms up/boots
32+
```hcl
33+
provisioned_config = {
34+
fixed = 0
35+
}
36+
```
37+
38+
#### X number of provisioned lambdas
39+
- use case: high predictable usage
40+
- we never want lag due to warm up and can predict traffic
41+
```hcl
42+
provisioned_config = {
43+
fixed = 1
44+
}
45+
```
46+
47+
#### Scale provisioning when usage exceeds % tolerance
48+
- use case: react to traffic i.e. api backend
49+
- limit the cost with autoscale.max
50+
- ensure minimal concurrency (no cold starts) with autoscale.min
51+
- set tolerance to amount of used concurrent executions. Below will trigger when 70% are used and add more to meet demands.
52+
- set cool down seconds to reasonable time before you would like the system to react.
53+
```hcl
54+
provisioned_config = {
55+
auto_scale = {
56+
max = 3,
57+
min = 1,
58+
trigger_percent = 70
59+
cool_down_seconds = 60
60+
}
61+
}
62+
```
63+
64+
## types of lambda deploy
65+
66+
```hcl
67+
module "lambda_example" {
68+
source = "../lambda"
69+
...
70+
deployment_config = var.your_deployment_config
71+
}
72+
```
73+
74+
#### [default] All at once (fastest):
75+
76+
- use case: background processes
77+
```hcl
78+
deployment_config = {
79+
strategy = "all_at_once"
80+
}
81+
```
82+
83+
#### canary deployment:
84+
85+
- use case: api or service serving traffic
86+
- incrementally rolls out new version to 10% of lambdas and rolls back if errors detected. If not goes to 100%.
87+
- waits to make a decision on health after 1 minute
88+
```hcl
89+
deployment_config = {
90+
strategy = "linear"
91+
percentage = 10
92+
interval_minutes = 1
93+
}
94+
```
95+
96+
#### linear deployment:
97+
98+
- use case: api or service serving traffic
99+
- checks for lambda health on 10% of lambdas and rolls back if errors detected
100+
- rolls out changes on increments of 1 minute
101+
```hcl
102+
deployment_config = {
103+
strategy = "linear"
104+
percentage = 10
105+
interval_minutes = 1
106+
}

infra/live/global_vars.hcl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ locals {
66
"lambda:*",
77
"logs:*",
88
"apigateway:*",
9-
"codedeploy:*"
9+
"codedeploy:*",
10+
"application-autoscaling:*",
11+
"cloudwatch:*"
1012
]
1113
}
1214

infra/modules/aws/api/main.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ module "lambda_api" {
77

88
lambda_name = "api"
99
lambda_version = var.lambda_version
10+
11+
deployment_config = {
12+
strategy = "all_at_once"
13+
}
14+
15+
provisioned_config = {
16+
fixed = 0
17+
}
1018
}
1119

1220
resource "aws_apigatewayv2_api" "http_api" {

infra/modules/aws/lambda/locals.tf

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,26 @@ locals {
44

55
lambda_name = "${var.environment}-${var.project_name}-${var.lambda_name}"
66
lambda_code_zip_key = "${var.lambda_version}/${var.lambda_name}.zip"
7-
}
7+
8+
deploy_all_at_once_type = "AllAtOnce"
9+
deploy_canary_type = "TimeBasedCanary"
10+
deploy_linear_type = "TimeBasedLinear"
11+
12+
deploy_config_type_map = {
13+
all_at_once = local.deploy_all_at_once_type
14+
canary = local.deploy_canary_type
15+
linear = local.deploy_linear_type
16+
}
17+
deploy_config = {
18+
type = local.deploy_config_type_map[var.deployment_config.strategy]
19+
percent = var.deployment_config.percentage
20+
minutes = var.deployment_config.interval_minutes
21+
}
22+
23+
fixed_mode = try(var.provisioned_config.fixed != null, true)
24+
pc_fixed_count = try(var.provisioned_config.fixed, 0)
25+
pc_min_capacity = try(var.provisioned_config.auto_scale.min, 0)
26+
pc_max_capacity = try(var.provisioned_config.auto_scale.max, 0)
27+
pc_trigger_percent = try(var.provisioned_config.auto_scale.trigger_percent, var.provisioned_config_defaults.trigger_percent) / 100
28+
pc_cool_down_seconds = try(var.provisioned_config.auto_scale.cool_down_seconds, var.provisioned_config_defaults.cool_down_seconds)
29+
}

infra/modules/aws/lambda/main.tf

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ resource "aws_lambda_alias" "live" {
4141
}
4242
}
4343

44+
resource "aws_lambda_provisioned_concurrency_config" "alias_pc_fixed" {
45+
count = local.fixed_mode && coalesce(local.pc_fixed_count, 0) > 0 ? 1 : 0
46+
47+
function_name = aws_lambda_function.lambda.function_name
48+
qualifier = aws_lambda_alias.live.name
49+
provisioned_concurrent_executions = local.pc_fixed_count
50+
51+
depends_on = [aws_lambda_alias.live]
52+
}
53+
4454
resource "aws_codedeploy_app" "app" {
4555
name = "${local.lambda_name}-app"
4656
compute_platform = "Lambda"
@@ -57,16 +67,27 @@ resource "aws_iam_role_policy" "cd_lambda" {
5767
policy = data.aws_iam_policy_document.codedeploy_lambda.json
5868
}
5969

60-
resource "aws_codedeploy_deployment_config" "lambda_deployment_config" {
61-
# A custom Lambda deployment config that sends 50% traffic for 1 minute, then shifts to 100% (with your DG using it and auto-rollback on failure/alarms).
62-
deployment_config_name = "${local.lambda_name}-deployment-config"
70+
resource "aws_codedeploy_deployment_config" "lambda_config" {
71+
deployment_config_name = "${local.lambda_name}-deploy-config"
6372
compute_platform = "Lambda"
6473

6574
traffic_routing_config {
66-
type = "TimeBasedCanary"
67-
time_based_canary {
68-
percentage = 50
69-
interval = 1
75+
type = local.deploy_config.type
76+
77+
dynamic "time_based_canary" {
78+
for_each = local.deploy_config.type == local.deploy_canary_type ? [1] : []
79+
content {
80+
percentage = local.deploy_config.percent
81+
interval = local.deploy_config.minutes
82+
}
83+
}
84+
85+
dynamic "time_based_linear" {
86+
for_each = local.deploy_config.type == local.deploy_linear_type ? [1] : []
87+
content {
88+
percentage = local.deploy_config.percent
89+
interval = local.deploy_config.minutes
90+
}
7091
}
7192
}
7293
}
@@ -81,10 +102,36 @@ resource "aws_codedeploy_deployment_group" "dg" {
81102
deployment_option = "WITH_TRAFFIC_CONTROL"
82103
}
83104

84-
deployment_config_name = aws_codedeploy_deployment_config.lambda_deployment_config.id
105+
deployment_config_name = aws_codedeploy_deployment_config.lambda_config.deployment_config_name
85106

86107
auto_rollback_configuration {
87108
enabled = true
88109
events = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
89110
}
90111
}
112+
113+
resource "aws_appautoscaling_target" "pc_target" {
114+
min_capacity = local.pc_min_capacity
115+
max_capacity = local.pc_max_capacity
116+
resource_id = "function:${local.lambda_name}:${var.environment}"
117+
scalable_dimension = "lambda:function:ProvisionedConcurrency"
118+
service_namespace = "lambda"
119+
}
120+
121+
resource "aws_appautoscaling_policy" "pc_policy" {
122+
count = local.fixed_mode ? 0 : 1
123+
name = "${local.lambda_name}-pc-tt"
124+
policy_type = "TargetTrackingScaling"
125+
resource_id = aws_appautoscaling_target.pc_target.resource_id
126+
scalable_dimension = aws_appautoscaling_target.pc_target.scalable_dimension
127+
service_namespace = aws_appautoscaling_target.pc_target.service_namespace
128+
129+
target_tracking_scaling_policy_configuration {
130+
target_value = local.pc_trigger_percent
131+
scale_in_cooldown = local.pc_min_capacity
132+
scale_out_cooldown = local.pc_max_capacity
133+
predefined_metric_specification {
134+
predefined_metric_type = "LambdaProvisionedConcurrencyUtilization"
135+
}
136+
}
137+
}

infra/modules/aws/lambda/variables.tf

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,103 @@ variable "log_retention_days" {
3333
type = number
3434
description = "Number of days to hold logs"
3535
default = 1
36+
}
37+
38+
variable "deployment_config" {
39+
description = "Traffic shifting: all_at_once | canary | linear"
40+
type = object({
41+
strategy = string # all_at_once | canary | linear
42+
percentage = optional(number) # 1..99 (req for canary/linear)
43+
interval_minutes = optional(number) # >=1 (req for canary/linear)
44+
})
45+
default = { strategy = "all_at_once" }
46+
47+
validation {
48+
condition = (
49+
contains(["all_at_once", "canary", "linear"], var.deployment_config.strategy)
50+
&&
51+
(
52+
var.deployment_config.strategy == "all_at_once"
53+
||
54+
(
55+
coalesce(var.deployment_config.percentage, 0) >= 1
56+
&& coalesce(var.deployment_config.percentage, 0) <= 99
57+
&& coalesce(var.deployment_config.interval_minutes, 0) >= 1
58+
)
59+
)
60+
)
61+
error_message = "Use strategy all_at_once | canary | linear. For canary/linear, set percentage (1..99) and interval_minutes (>=1)."
62+
}
63+
}
64+
65+
variable "provisioned_config_defaults" {
66+
description = "Fall back values for provisioned_config.auto_scale.trigger_percent and provisioned_config.auto_scale.cool_down_seconds"
67+
type = object({
68+
trigger_percent = number
69+
cool_down_seconds = number
70+
})
71+
default = {
72+
trigger_percent = 70
73+
cool_down_seconds = 60
74+
}
75+
}
76+
77+
variable "provisioned_config" {
78+
description = "Either fixed provisioned concurrency (fixed) or autoscaled (auto_scale); omit/zero = none"
79+
type = object({
80+
fixed = optional(number) # 0/omit = off, >0 = fixed PC
81+
auto_scale = optional(object({
82+
min = number
83+
max = number
84+
trigger_percent = optional(number)
85+
cool_down_seconds = optional(number)
86+
}))
87+
})
88+
default = {
89+
fixed = 0
90+
# auto_scale = {
91+
# max = 1,
92+
# min = 0,
93+
# trigger_percent = 70
94+
# cool_down_seconds = 60
95+
# }
96+
}
97+
98+
validation {
99+
condition = !(
100+
(var.provisioned_config.fixed != null) &&
101+
(var.provisioned_config.auto_scale != null)
102+
)
103+
error_message = "Specify either 'fixed' or 'auto_scale' (or neither), not both."
104+
}
105+
106+
# When autoscale is set, ensure max > min
107+
validation {
108+
condition = (
109+
var.provisioned_config.auto_scale != null
110+
? (var.provisioned_config.auto_scale.max > var.provisioned_config.auto_scale.min)
111+
: true
112+
)
113+
error_message = "When auto_scale is set, 'max' must be greater than 'min'."
114+
}
115+
116+
# When autoscale.trigger_percent is set, ensure is 1-99
117+
validation {
118+
condition = (
119+
var.provisioned_config.auto_scale != null
120+
? (var.provisioned_config.auto_scale.trigger_percent > 0 && var.provisioned_config.auto_scale.trigger_percent < 100)
121+
: true
122+
)
123+
error_message = "When autoscale.trigger_percent, must be > 0 && < 100"
124+
}
125+
126+
# When autoscale.cool_down_seconds is set, ensure is at least a minute, max and hour
127+
validation {
128+
condition = (
129+
var.provisioned_config.auto_scale != null
130+
? (var.provisioned_config.auto_scale.cool_down_seconds > 59 && var.provisioned_config.auto_scale.cool_down_seconds < 3600)
131+
: true
132+
)
133+
error_message = "When autoscale.cool_down_seconds, must be > 59 && < 3600"
134+
}
36135
}

0 commit comments

Comments
 (0)