Skip to content

Commit 8c9a7ab

Browse files
feat: add extra submodule for ec2_deployment (#216)
* feat: add submodule ec2_update_runner_ssm_ami * feat: add submodule ec2_redrive_deadletter * feat: add submodule ec2_update_runner_tags * feat: add new field extraction for new lambdas * fix: add provider * fix: fix ec2_redrive_deadletter * fix: fix ec2_update_runner_ssm_ami * deprecated: remove old lambdas * fix: add missing code for ec2_redrive_deadletter.py
1 parent ea8ef28 commit 8c9a7ab

File tree

23 files changed

+829
-208
lines changed

23 files changed

+829
-208
lines changed

modules/integrations/splunk_cloud_conf_shared/transforms_lambda.tf

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@ resource "splunk_configs_conf" "forgecicd_extra_lambda_tenant_fields" {
3030
}
3131
}
3232

33+
resource "splunk_configs_conf" "forgecicd_extra_lambda_ec2_tenant_fields" {
34+
name = "transforms/forgecicd_extra_lambda_ec2_tenant_fields"
35+
36+
variables = {
37+
"REGEX" = "(?<aws_region>[^:]+):\\/aws\\/lambda\\/(?<forgecicd_tenant>[a-z0-9]+)-(?<forgecicd_region_alias>[a-z0-9]+)-(?<forgecicd_vpc_alias>[a-z0-9]+)-(?<forgecicd_log_type>ec2-redrive-deadletter|ec2-update-runner-ssm-ami|ec2-update-runner-tags)"
38+
"FORMAT" = "aws_region::$1 forgecicd_tenant::$2 forgecicd_region_alias::$3 forgecicd_vpc_alias::$4 forgecicd_log_type::$5 forgecicd_type::ec2"
39+
"SOURCE_KEY" = "source"
40+
"CLEAN_KEYS" = "0"
41+
}
42+
acl {
43+
app = var.splunk_conf.acl.app
44+
owner = var.splunk_conf.acl.owner
45+
sharing = var.splunk_conf.acl.sharing
46+
read = var.splunk_conf.acl.read
47+
write = var.splunk_conf.acl.write
48+
}
49+
lifecycle {
50+
ignore_changes = [
51+
variables["CAN_OPTIMIZE"],
52+
variables["DEFAULT_VALUE"],
53+
variables["DEPTH_LIMIT"],
54+
variables["DEST_KEY"],
55+
variables["KEEP_EMPTY_VALS"],
56+
variables["LOOKAHEAD"],
57+
variables["MATCH_LIMIT"],
58+
variables["MV_ADD"],
59+
variables["WRITE_META"],
60+
variables["disabled"]
61+
]
62+
}
63+
}
3364

3465
resource "splunk_configs_conf" "forgecicd_trust_validation" {
3566
name = "transforms/forgecicd_trust_validation"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module "ec2_redrive_deadletter" {
2+
source = "./ec2_redrive_deadletter"
3+
4+
providers = {
5+
aws = aws
6+
}
7+
8+
prefix = var.runner_configs.prefix
9+
logging_retention_in_days = var.runner_configs.logging_retention_in_days
10+
log_level = var.runner_configs.log_level
11+
tags = var.tenant_configs.tags
12+
13+
sqs_map = {
14+
for key in keys(var.runner_configs.runner_specs) :
15+
key => {
16+
dlq = "${var.runner_configs.prefix}-${key}-queued-builds_dead_letter"
17+
main = "${var.runner_configs.prefix}-${key}-queued-builds"
18+
}
19+
}
20+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!-- BEGIN_TF_DOCS -->
2+
## Requirements
3+
4+
| Name | Version |
5+
|------|---------|
6+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.9.1 |
7+
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 5.27 |
8+
| <a name="requirement_random"></a> [random](#requirement\_random) | >= 3.6 |
9+
10+
## Providers
11+
12+
| Name | Version |
13+
|------|---------|
14+
| <a name="provider_aws"></a> [aws](#provider\_aws) | 6.19.0 |
15+
| <a name="provider_random"></a> [random](#provider\_random) | 3.7.2 |
16+
17+
## Modules
18+
19+
| Name | Source | Version |
20+
|------|--------|---------|
21+
| <a name="module_github_webhook_relay_source"></a> [github\_webhook\_relay\_source](#module\_github\_webhook\_relay\_source) | ../../../integrations/github_webhook_relay_source | n/a |
22+
23+
## Resources
24+
25+
| Name | Type |
26+
|------|------|
27+
| [aws_iam_role.secret_reader](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
28+
| [aws_iam_role_policy.secret_reader_inline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
29+
| [aws_kms_alias.github_webhook_relay](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource |
30+
| [aws_kms_key.github_webhook_relay](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource |
31+
| [aws_secretsmanager_secret.github_webhook_relay](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource |
32+
| [aws_secretsmanager_secret_version.github_webhook_relay](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource |
33+
| [random_id.github_webhook_relay_source_secret](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |
34+
| [aws_iam_policy_document.secret_reader_permissions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
35+
| [aws_iam_policy_document.secret_reader_trust](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
36+
| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |
37+
38+
## Inputs
39+
40+
| Name | Description | Type | Default | Required |
41+
|------|-------------|------|---------|:--------:|
42+
| <a name="input_github_webhook_relay"></a> [github\_webhook\_relay](#input\_github\_webhook\_relay) | Configuration for the (optional) webhook relay source module.<br/>If enabled=true we provision the API Gateway + source EventBridge forwarding rule.<br/>destination\_event\_bus\_name must already exist or be created in the destination account (or via the destination submodule run there). | <pre>object({<br/> enabled = bool<br/> destination_account_id = optional(string)<br/> destination_event_bus_name = optional(string)<br/> destination_region = optional(string)<br/> destination_reader_role_arn = optional(string)<br/> })</pre> | <pre>{<br/> "destination_account_id": "",<br/> "destination_event_bus_name": "",<br/> "destination_reader_role_arn": "",<br/> "destination_region": "",<br/> "enabled": false<br/>}</pre> | no |
43+
| <a name="input_log_level"></a> [log\_level](#input\_log\_level) | Log level for application logging (e.g., INFO, DEBUG, WARN, ERROR) | `string` | `"INFO"` | no |
44+
| <a name="input_logging_retention_in_days"></a> [logging\_retention\_in\_days](#input\_logging\_retention\_in\_days) | Retention in days for CloudWatch Log Group for the Lambdas. | `number` | `30` | no |
45+
| <a name="input_prefix"></a> [prefix](#input\_prefix) | Prefix for all resources | `string` | n/a | yes |
46+
| <a name="input_secret_prefix"></a> [secret\_prefix](#input\_secret\_prefix) | Prefix for secret | `string` | n/a | yes |
47+
| <a name="input_tags"></a> [tags](#input\_tags) | Tags to apply to created resources. | `map(string)` | `{}` | no |
48+
49+
## Outputs
50+
51+
| Name | Description |
52+
|------|-------------|
53+
| <a name="output_source_secret_arn"></a> [source\_secret\_arn](#output\_source\_secret\_arn) | ARN of the GitHub webhook relay secret |
54+
| <a name="output_source_secret_region"></a> [source\_secret\_region](#output\_source\_secret\_region) | AWS region the secret resides in |
55+
| <a name="output_source_secret_role_arn"></a> [source\_secret\_role\_arn](#output\_source\_secret\_role\_arn) | ARN of IAM role permitted to read/decrypt the webhook relay secret |
56+
<!-- END_TF_DOCS -->
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
data "aws_caller_identity" "current" {}
2+
3+
data "aws_region" "current" {}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import json
2+
import logging
3+
import os
4+
import time
5+
from typing import Dict, List
6+
7+
import boto3
8+
9+
LOG = logging.getLogger()
10+
level_str = os.environ.get('LOG_LEVEL', 'INFO').upper()
11+
LOG.setLevel(getattr(logging, level_str, logging.INFO))
12+
13+
sqs = boto3.client('sqs')
14+
15+
16+
def parse_sqs_map(raw: str) -> List[Dict[str, str]]:
17+
"""
18+
Expected SQS_MAP env var (from Terraform map):
19+
{
20+
"runner-a": {"main": "queue-a-main", "dlq": "queue-a-dlq"},
21+
"runner-b": {"main": "queue-b-main", "dlq": "queue-b-dlq"}
22+
}
23+
24+
Returns a list of:
25+
[{"key": "runner-a", "main": "...", "dlq": "..."}, ...]
26+
"""
27+
if not raw.strip():
28+
return []
29+
30+
try:
31+
parsed = json.loads(raw)
32+
except json.JSONDecodeError as e:
33+
raise Exception(f"Invalid SQS_MAP JSON: {e}. Value: {raw}") from e
34+
35+
if not isinstance(parsed, dict):
36+
raise Exception(
37+
f"SQS_MAP must be a JSON object/map, got: {type(parsed)}")
38+
39+
mappings: List[Dict[str, str]] = []
40+
for key, value in parsed.items():
41+
if not isinstance(value, dict):
42+
raise Exception(
43+
f"SQS_MAP['{key}'] must be an object with 'main' and 'dlq'")
44+
if 'main' not in value or 'dlq' not in value:
45+
raise Exception(
46+
f"SQS_MAP['{key}'] missing 'main' or 'dlq' keys: {value}")
47+
mappings.append(
48+
{
49+
'key': key,
50+
'main': str(value['main']),
51+
'dlq': str(value['dlq']),
52+
}
53+
)
54+
55+
return mappings
56+
57+
58+
def resolve_queue_url(sqs_client, name_or_url: str) -> str:
59+
"""Return URL if already full URL, else resolve by queue name."""
60+
if name_or_url.startswith('https://'):
61+
return name_or_url
62+
63+
resp = sqs_client.get_queue_url(QueueName=name_or_url)
64+
return resp['QueueUrl']
65+
66+
67+
def drain_dlq(sqs_client, dlq_url: str, main_url: str) -> int:
68+
"""Drain DLQ → main queue, returns number of moved messages."""
69+
moved = 0
70+
71+
while True:
72+
resp = sqs_client.receive_message(
73+
QueueUrl=dlq_url,
74+
MaxNumberOfMessages=10,
75+
WaitTimeSeconds=0,
76+
VisibilityTimeout=30,
77+
)
78+
79+
messages = resp.get('Messages', [])
80+
if not messages:
81+
break
82+
83+
for msg in messages:
84+
sqs_client.send_message(
85+
QueueUrl=main_url,
86+
MessageBody=msg['Body'],
87+
)
88+
sqs_client.delete_message(
89+
QueueUrl=dlq_url,
90+
ReceiptHandle=msg['ReceiptHandle'],
91+
)
92+
moved += 1
93+
94+
# small throttle to avoid hammering SQS too hard
95+
time.sleep(0.1)
96+
97+
return moved
98+
99+
100+
def lambda_handler(event, context):
101+
raw_sqs_map = os.getenv('SQS_MAP', '')
102+
mappings = parse_sqs_map(raw_sqs_map)
103+
104+
if not mappings:
105+
LOG.warning('SQS_MAP is empty; nothing to do.')
106+
return {'status': 'noop', 'message': 'SQS_MAP is empty', 'results': []}
107+
108+
LOG.info('Starting DLQ drain for %d mapping(s)', len(mappings))
109+
110+
results = []
111+
for entry in mappings:
112+
key = entry['key']
113+
dlq_name_or_url = entry['dlq']
114+
main_name_or_url = entry['main']
115+
116+
LOG.info('Processing SQS mapping key=%s dlq=%s main=%s',
117+
key, dlq_name_or_url, main_name_or_url)
118+
119+
dlq_url = resolve_queue_url(sqs, dlq_name_or_url)
120+
main_url = resolve_queue_url(sqs, main_name_or_url)
121+
122+
moved = drain_dlq(sqs, dlq_url, main_url)
123+
124+
LOG.info(
125+
'Finished SQS mapping key=%s dlq=%s main=%s moved=%d',
126+
key,
127+
dlq_name_or_url,
128+
main_name_or_url,
129+
moved,
130+
)
131+
132+
results.append(
133+
{
134+
'key': key,
135+
'dlq': dlq_name_or_url,
136+
'main': main_name_or_url,
137+
'moved': moved,
138+
}
139+
)
140+
141+
return {'status': 'ok', 'results': results}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
module "ec2_redrive_deadletter_lambda" {
2+
source = "terraform-aws-modules/lambda/aws"
3+
version = "8.1.2"
4+
5+
function_name = "${var.prefix}-ec2-redrive-deadletter"
6+
handler = "ec2_redrive_deadletter.lambda_handler"
7+
runtime = "python3.12"
8+
timeout = 900
9+
architectures = ["x86_64"]
10+
11+
source_path = [{
12+
path = "${path.module}/lambda"
13+
}]
14+
15+
logging_log_group = aws_cloudwatch_log_group.ec2_redrive_deadletter_lambda.name
16+
use_existing_cloudwatch_log_group = true
17+
18+
trigger_on_package_timestamp = false
19+
20+
environment_variables = {
21+
SQS_MAP = jsonencode(var.sqs_map)
22+
LOG_LEVEL = var.log_level
23+
}
24+
25+
attach_policy_json = true
26+
27+
policy_json = data.aws_iam_policy_document.ec2_redrive_deadletter_lambda.json
28+
29+
function_tags = var.tags
30+
role_tags = var.tags
31+
tags = var.tags
32+
33+
depends_on = [aws_cloudwatch_log_group.ec2_redrive_deadletter_lambda]
34+
}
35+
36+
data "aws_iam_policy_document" "ec2_redrive_deadletter_lambda" {
37+
statement {
38+
sid = "SQSReceiveFromDLQ"
39+
effect = "Allow"
40+
41+
actions = [
42+
"sqs:ReceiveMessage",
43+
"sqs:GetQueueUrl",
44+
"sqs:GetQueueAttributes",
45+
"sqs:DeleteMessage",
46+
]
47+
48+
resources = [
49+
for key, cfg in var.sqs_map :
50+
"arn:aws:sqs:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:${cfg.dlq}"
51+
]
52+
}
53+
54+
statement {
55+
sid = "SQSSendToMainQueue"
56+
effect = "Allow"
57+
58+
actions = [
59+
"sqs:SendMessage",
60+
"sqs:GetQueueUrl",
61+
"sqs:GetQueueAttributes",
62+
]
63+
64+
resources = [
65+
for key, cfg in var.sqs_map :
66+
"arn:aws:sqs:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:${cfg.main}"
67+
]
68+
}
69+
}
70+
71+
72+
73+
resource "aws_cloudwatch_log_group" "ec2_redrive_deadletter_lambda" {
74+
name = "/aws/lambda/${var.prefix}-ec2-redrive-deadletter"
75+
retention_in_days = var.logging_retention_in_days
76+
tags = var.tags
77+
tags_all = var.tags
78+
}
79+
80+
resource "aws_cloudwatch_event_rule" "ec2_redrive_deadletter_lambda" {
81+
name = "${var.prefix}-ec2-redrive-deadletter"
82+
description = "Trigger Lambda every 10 minutes"
83+
schedule_expression = "cron(*/10 * * * ? *)"
84+
85+
tags = var.tags
86+
tags_all = var.tags
87+
88+
depends_on = [module.ec2_redrive_deadletter_lambda]
89+
}
90+
91+
resource "aws_cloudwatch_event_target" "ec2_redrive_deadletter_lambda" {
92+
rule = aws_cloudwatch_event_rule.ec2_redrive_deadletter_lambda.name
93+
arn = module.ec2_redrive_deadletter_lambda.lambda_function_arn
94+
95+
depends_on = [module.ec2_redrive_deadletter_lambda]
96+
}
97+
98+
resource "aws_lambda_permission" "ec2_redrive_deadletter_lambda" {
99+
action = "lambda:InvokeFunction"
100+
function_name = "${var.prefix}-ec2-redrive-deadletter"
101+
principal = "events.amazonaws.com"
102+
statement_id = "AllowExecutionFromCloudWatch"
103+
source_arn = aws_cloudwatch_event_rule.ec2_redrive_deadletter_lambda.arn
104+
105+
depends_on = [module.ec2_redrive_deadletter_lambda]
106+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
variable "prefix" {
2+
description = "Prefix for all resources"
3+
type = string
4+
}
5+
6+
variable "tags" {
7+
description = "Tags to apply to created resources."
8+
type = map(string)
9+
default = {}
10+
}
11+
12+
variable "logging_retention_in_days" {
13+
description = "Retention in days for CloudWatch Log Group for the Lambdas."
14+
type = number
15+
default = 30
16+
}
17+
18+
variable "log_level" {
19+
type = string
20+
description = "Log level for application logging (e.g., INFO, DEBUG, WARN, ERROR)"
21+
default = "INFO"
22+
}
23+
24+
variable "sqs_map" {
25+
description = "Map of runner SQS queue names."
26+
type = map(object({
27+
main = string
28+
dlq = string
29+
}))
30+
}

0 commit comments

Comments
 (0)