From b8f9eb4638efbb45a7e27ddb66b3a10eef2617a7 Mon Sep 17 00:00:00 2001 From: Jay Anslow Date: Fri, 16 Aug 2024 11:05:44 +0100 Subject: [PATCH] feat: add time zone support for pool schedules (#4063) Fixes #4056 The timezone can be set via the `schedule_expression_timezone` property via the main module or `multi-runners` module. Replaces the `aws_cloudwatch_event_rule` in the `pool` module (which only support UTC) with `aws_scheduler_schedule`, which supports arbitrary time zones via the `schedule_expression_timezone` attribute. In this situation AWS EventBridge Scheduler is a drop in replacement for schedule-based AWS EventBridge Rules ([see AWS blog post](https://aws.amazon.com/blogs/compute/introducing-amazon-eventbridge-scheduler/)), including the expression syntax and pricing. The main (internal) difference is that AWS Scheduler requires the use of an IAM Role, instead of Lambda permissions. --------- Co-authored-by: Niek Palm --- docs/configuration.md | 5 +- examples/ephemeral/main.tf | 6 +- modules/multi-runner/variables.tf | 7 ++- modules/runners/pool/main.tf | 99 +++++++++++++++++++++---------- modules/runners/pool/variables.tf | 5 +- modules/runners/variables.tf | 7 ++- variables.tf | 7 ++- 7 files changed, 91 insertions(+), 45 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1a5af903..f7e936ee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -65,8 +65,9 @@ The pool is introduced in combination with the ephemeral runners and is primaril ```hcl pool_runner_owner = "my-org" # Org to which the runners are added pool_config = [{ - size = 20 # size of the pool - schedule_expression = "cron(* * * * ? *)" # cron expression to trigger the adjustment of the pool + size = 20 # size of the pool + schedule_expression = "cron(* * * * ? *)" # cron expression to trigger the adjustment of the pool + schedule_expression_timezone = "Australia/Sydney" # optional time zone (defaults to UTC) }] ``` diff --git a/examples/ephemeral/main.tf b/examples/ephemeral/main.tf index d7d0dacc..239cd4e7 100644 --- a/examples/ephemeral/main.tf +++ b/examples/ephemeral/main.tf @@ -62,8 +62,10 @@ module "runners" { # # Example of simple pool usages # pool_runner_owner = "philips-test-runners" # pool_config = [{ - # size = 3 - # schedule_expression = "cron(* * * * ? *)" + # size = 3 + # schedule_expression = "cron(0/3 14 * * ? *)" # every 3 minutes between 14:00 and 15:00 + # schedule_expression_timezone = "Europe/Amsterdam" + # }] # # diff --git a/modules/multi-runner/variables.tf b/modules/multi-runner/variables.tf index 00ed56c6..5bd1f52b 100644 --- a/modules/multi-runner/variables.tf +++ b/modules/multi-runner/variables.tf @@ -107,8 +107,9 @@ variable "multi_runner_config" { volume_size = 30 }]) pool_config = optional(list(object({ - schedule_expression = string - size = number + schedule_expression = string + schedule_expression_timezone = optional(string) + size = number })), []) }) @@ -177,7 +178,7 @@ variable "multi_runner_config" { idle_config: "List of time period that can be defined as cron expression to keep a minimum amount of runners active instead of scaling down to 0. By defining this list you can ensure that in time periods that match the cron expression within 5 seconds a runner is kept idle." runner_log_files: "(optional) Replaces the module default cloudwatch log config. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for details." block_device_mappings: "The EC2 instance block device configuration. Takes the following keys: `device_name`, `delete_on_termination`, `volume_type`, `volume_size`, `encrypted`, `iops`, `throughput`, `kms_key_id`, `snapshot_id`." - pool_config: "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1." + pool_config: "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1. Use `schedule_expression_timezone` to override the schedule time zone (defaults to UTC)." } matcherConfig: { labelMatchers: "The list of list of labels supported by the runner configuration. `[[self-hosted, linux, x64, example]]`" diff --git a/modules/runners/pool/main.tf b/modules/runners/pool/main.tf index e7848867..6c6dc82a 100644 --- a/modules/runners/pool/main.tf +++ b/modules/runners/pool/main.tf @@ -118,36 +118,6 @@ data "aws_iam_policy_document" "lambda_assume_role_policy" { } } -# per config object one trigger is created to trigger the lambda. -resource "aws_cloudwatch_event_rule" "pool" { - count = length(var.config.pool) - - name = "${var.config.prefix}-pool-${count.index}-rule" - schedule_expression = var.config.pool[count.index].schedule_expression - tags = var.config.tags -} - -resource "aws_cloudwatch_event_target" "pool" { - count = length(var.config.pool) - - input = jsonencode({ - poolSize = var.config.pool[count.index].size - }) - - rule = aws_cloudwatch_event_rule.pool[count.index].name - arn = aws_lambda_function.pool.arn -} - -resource "aws_lambda_permission" "pool" { - count = length(var.config.pool) - - statement_id = "AllowExecutionFromCloudWatch-${count.index}" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.pool.function_name - principal = "events.amazonaws.com" - source_arn = aws_cloudwatch_event_rule.pool[count.index].arn -} - resource "aws_iam_role_policy_attachment" "ami_id_ssm_parameter_read" { count = var.config.ami_id_ssm_parameter_name != null ? 1 : 0 role = aws_iam_role.pool.name @@ -178,3 +148,72 @@ resource "aws_iam_role_policy" "pool_xray" { policy = data.aws_iam_policy_document.lambda_xray[0].json role = aws_iam_role.pool.name } + +resource "aws_scheduler_schedule_group" "pool" { + name_prefix = "${var.config.prefix}-pool" + + tags = var.config.tags +} + +data "aws_iam_policy_document" "scheduler_assume" { + statement { + sid = "ScheduleGroupAssumeRole" + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["scheduler.amazonaws.com"] + } + + condition { + test = "StringEquals" + variable = "aws:SourceArn" + values = [aws_scheduler_schedule_group.pool.arn] + } + } +} + +data "aws_iam_policy_document" "scheduler" { + statement { + sid = "InvokePoolLambda" + actions = ["lambda:InvokeFunction"] + resources = [aws_lambda_function.pool.arn] + } +} + +resource "aws_iam_role" "scheduler" { + name_prefix = "${var.config.prefix}-pool" + + path = var.config.role_path + permissions_boundary = var.config.role_permissions_boundary + + assume_role_policy = data.aws_iam_policy_document.scheduler_assume.json + + inline_policy { + name = "terraform" + policy = data.aws_iam_policy_document.scheduler.json + } + + tags = var.config.tags +} + +resource "aws_scheduler_schedule" "pool" { + for_each = { for i, v in var.config.pool : i => v } + + name_prefix = "${var.config.prefix}-pool-${each.key}-rule" + group_name = aws_scheduler_schedule_group.pool.name + + flexible_time_window { + mode = "OFF" + } + + schedule_expression = each.value.schedule_expression + schedule_expression_timezone = each.value.schedule_expression_timezone + + target { + arn = aws_lambda_function.pool.arn + role_arn = aws_iam_role.scheduler.arn + input = jsonencode({ + poolSize = each.value.size + }) + } +} diff --git a/modules/runners/pool/variables.tf b/modules/runners/pool/variables.tf index a60c002f..4ebcebca 100644 --- a/modules/runners/pool/variables.tf +++ b/modules/runners/pool/variables.tf @@ -50,8 +50,9 @@ variable "config" { instance_max_spot_price = string prefix = string pool = list(object({ - schedule_expression = string - size = number + schedule_expression = string + schedule_expression_timezone = string + size = number })) role_permissions_boundary = string kms_key_arn = string diff --git a/modules/runners/variables.tf b/modules/runners/variables.tf index d8683088..01da8d4d 100644 --- a/modules/runners/variables.tf +++ b/modules/runners/variables.tf @@ -538,10 +538,11 @@ variable "pool_lambda_reserved_concurrent_executions" { } variable "pool_config" { - description = "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1." + description = "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1. Use `schedule_expression_timezone ` to override the schedule time zone (defaults to UTC)." type = list(object({ - schedule_expression = string - size = number + schedule_expression = string + schedule_expression_timezone = optional(string) + size = number })) default = [] } diff --git a/variables.tf b/variables.tf index 758d149a..ccfeac26 100644 --- a/variables.tf +++ b/variables.tf @@ -686,10 +686,11 @@ variable "pool_lambda_reserved_concurrent_executions" { } variable "pool_config" { - description = "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for weekdays to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1." + description = "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for weekdays to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1. Use `schedule_expression_timezone` to override the schedule time zone (defaults to UTC)." type = list(object({ - schedule_expression = string - size = number + schedule_expression = string + schedule_expression_timezone = optional(string) + size = number })) default = [] }