Skip to content
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

Task Notifications #10

Merged
merged 2 commits into from
Nov 11, 2024
Merged
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
60 changes: 60 additions & 0 deletions .terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
AVD-DS-0001 # https://avd.aquasec.com/misconfig/dockerfile/general/avd-ds-0001/
AVD-DS-0002 # https://avd.aquasec.com/misconfig/dockerfile/general/avd-ds-0002/
AVD-DS-0013 # https://avd.aquasec.com/misconfig/dockerfile/general/avd-ds-0013/
AVD-DS-0015 # https://avd.aquasec.com/misconfig/dockerfile/general/avd-ds-0015/
AVD-DS-0026 # https://avd.aquasec.com/misconfig/dockerfile/general/avd-ds-0026/
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,15 @@ The `terraform-docs` utility is used to generate this README. Follow the below s
| Name | Source | Version |
|------|--------|---------|
| <a name="module_kms"></a> [kms](#module\_kms) | terraform-aws-modules/kms/aws | 3.1.1 |
| <a name="module_lambda_function"></a> [lambda\_function](#module\_lambda\_function) | terraform-aws-modules/lambda/aws | 7.14.0 |

## Resources

| Name | Type |
|------|------|
| [aws_cloudwatch_event_rule.ecs_task_stopped_rule](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/cloudwatch_event_rule) | resource |
| [aws_cloudwatch_event_rule.tasks](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/cloudwatch_event_rule) | resource |
| [aws_cloudwatch_event_target.invoke_lambda](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/cloudwatch_event_target) | resource |
| [aws_cloudwatch_event_target.tasks](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/cloudwatch_event_target) | resource |
| [aws_cloudwatch_log_group.tasks](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/cloudwatch_log_group) | resource |
| [aws_ecs_cluster.current](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/ecs_cluster) | resource |
Expand All @@ -224,6 +227,7 @@ The `terraform-docs` utility is used to generate this README. Follow the below s
| [aws_iam_role_policy_attachment.cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.execution](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.task_permissions_arns](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/iam_role_policy_attachment) | resource |
| [aws_lambda_permission.allow_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/lambda_permission) | resource |
| [aws_secretsmanager_secret.configuration](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/secretsmanager_secret) | resource |
| [aws_secretsmanager_secret_version.configuration](https://registry.terraform.io/providers/hashicorp/aws/5.70.0/docs/resources/secretsmanager_secret_version) | resource |

Expand All @@ -235,7 +239,7 @@ The `terraform-docs` utility is used to generate this README. Follow the below s
| <a name="input_region"></a> [region](#input\_region) | The region to use for the resources | `string` | n/a | yes |
| <a name="input_subnet_ids"></a> [subnet\_ids](#input\_subnet\_ids) | The subnet id's to use for the nuke service | `list(string)` | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | Map of tags to apply to resources created by this module | `map(string)` | n/a | yes |
| <a name="input_tasks"></a> [tasks](#input\_tasks) | A collection of nuke tasks to run and when to run them | <pre>map(object({<br/> additional_permissions = optional(map(object({<br/> policy = string<br/> })), {})<br/> configuration = string<br/> description = string<br/> dry_run = optional(bool, true)<br/> permission_boundary_arn = optional(string, null)<br/> permission_arns = optional(list(string), ["arn:aws:iam::aws:policy/AdministratorAccess"])<br/> retention_in_days = optional(number, 7)<br/> schedule = string<br/> }))</pre> | n/a | yes |
| <a name="input_tasks"></a> [tasks](#input\_tasks) | A collection of nuke tasks to run and when to run them | <pre>map(object({<br/> additional_permissions = optional(map(object({<br/> policy = string<br/> })), {})<br/> configuration = string<br/> description = string<br/> dry_run = optional(bool, true)<br/> notifications = optional(object({<br/> sns_topic_arn = optional(string, null)<br/> }), {<br/> sns_topic_arn = null<br/> })<br/> permission_boundary_arn = optional(string, null)<br/> permission_arns = optional(list(string), ["arn:aws:iam::aws:policy/AdministratorAccess"])<br/> retention_in_days = optional(number, 7)<br/> schedule = string<br/> }))</pre> | n/a | yes |
| <a name="input_assign_public_ip"></a> [assign\_public\_ip](#input\_assign\_public\_ip) | Indicates if the task should be assigned a public IP | `bool` | `false` | no |
| <a name="input_cloudwatch_event_role_name"></a> [cloudwatch\_event\_role\_name](#input\_cloudwatch\_event\_role\_name) | The name of the role to use for the cloudwatch event rule | `string` | `"nuke-cloudwatch"` | no |
| <a name="input_configuration_secret_name_prefix"></a> [configuration\_secret\_name\_prefix](#input\_configuration\_secret\_name\_prefix) | The prefix to use for AWS Secrets Manager secrets to store the nuke configuration | `string` | `"/lza/configuration/nuke"` | no |
Expand Down
86 changes: 86 additions & 0 deletions assets/lambda/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
This Lambda function is triggered by the completion of the ECS task
that runs the AWS Nuke container. It retrieves the logs from the
CloudWatch log group and sends a notification via SNS if any resources
are going to be deleted.
"""

import os
import logging

import boto3

# Initialize AWS clients
logs_client = boto3.client('logs')
sns_client = boto3.client('sns')

# Environment variables for configuration
LOG_GROUP_NAME = os.getenv('LOG_GROUP_NAME')
SNS_TOPIC_ARN = os.getenv('SNS_TOPIC_ARN')
DEBUG = os.getenv('DEBUG', 'false').lower() == 'true'

# Set up logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)


def retrieve_cloudwatch_logs():
"""
We retrieve the logs from cloudwatch, parsing line by line for
'would remove' and produce a summary of the logs to send.
"""

logging.debug("Retrieving %s cloudwatch log group", LOG_GROUP_NAME)

matching_lines = []
# Get the list of log streams (assuming each ECS task has
# its own log stream)
streams = logs_client.describe_log_streams(
logGroupName=LOG_GROUP_NAME,
orderBy='LastEventTime',
descending=True,
limit=1
)['logStreams']
for stream in streams:
log_stream_name = stream['logStreamName']
# Get log events for each log stream
events = logs_client.get_log_events(
logGroupName=LOG_GROUP_NAME,
logStreamName=log_stream_name,
startFromHead=False
)['events']
# Check each event for the "would remove" text
for event in events:
message = event['message']
if 'would remove' in message:
matching_lines.append(message)

return matching_lines


def lambda_handler(event, context):
"""
The main entry point of the Lambda function. This method is
called on the completion of the ECS task. We query the logs
within cloudwatch, parse and send a notification via SNS
"""

matching_lines = retrieve_cloudwatch_logs()
message = {
"Number of Resources ", len(matching_lines),
}
logging.debug("Finished processing event, %s", message)

if matching_lines and len(SNS_TOPIC_ARN) > 0:
sns_client.publish(
TopicArn=SNS_TOPIC_ARN,
Message=message,
Subject="AWS Nuke - Resource Deletion Notification"
)


# If not being called from the lambda_handler, but via the CLI,
# we need to call the lambda_handler
if __name__ == '__main__':
lambda_handler(None, None)
60 changes: 60 additions & 0 deletions examples/basic/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/basic/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ module "nuke" {
description = "Runs a dry run to validate what would be deleted"
## Indicates if the task should be a dry run (default is true)
dry_run = true
## The configuration for a notification to be sent
notifications = {
## The SNS topic to send the notification to
sns_topic_arn = "arn:aws:sns:eu-west-1:123456789012:nuke-dry-run"
}
## The log retention in days for the task
retention_in_days = 7
## The schedule expression for the task - every monday at 09:00
Expand Down
3 changes: 3 additions & 0 deletions locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ locals {
region = local.region
}

## A map of tasks with notifications enabled
tasks_with_notifications = { for task in var.tasks : task => task if try(task.notifications.sns_topic_arn, null) != null }

## We need to create a map of task->permission_arn for every task
task_permissions_all = flatten([
for k, v in var.tasks : [
Expand Down
92 changes: 92 additions & 0 deletions notifications.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@

## Configure a event bridge rule to trigger a lambda function when an ECS task stops
resource "aws_cloudwatch_event_rule" "ecs_task_stopped_rule" {
for_each = local.tasks_with_notifications

name_prefix = "lza-ecs-task-stopped-${lower(each.key)}-"
description = "Trigger Lambda when an ECS task in the specified cluster stops."
force_destroy = true
tags = var.tags

event_pattern = jsonencode({
"source" : ["aws.ecs"],
"detail-type" : ["ECS Task State Change"],
"detail" : {
"clusterArn" : [aws_ecs_cluster.current.arn],
"lastStatus" : ["STOPPED"],
"taskDefinitionArn" : [{
"prefix" : each.key
}]
}
})
}

## Configure the lambda function to be triggered by the event bridge rule
module "lambda_function" {
for_each = local.tasks_with_notifications
source = "terraform-aws-modules/lambda/aws"
version = "7.14.0"

create_package = true
description = "Send notifications on the intention to delete resources"
function_name = format("lza-nuke-notifications-%s", lower(each.key))
handler = "lambda_function.lambda_handler"
memory_size = "128"
runtime = "python3.9"
source_path = format("%s/assets/functions/notification.py", path.module)
tags = var.tags
timeout = 10

policy_jsons = [
jsonencode({
"Sid" : "AllowPublishToSNS",
"Effect" : "Allow",
"Action" : "sns:Publish",
"Resource" : each.value.sns_topic_arn
}),
jsonencode({
"Sid" : "AllowDescribeLogGroups",
"Effect" : "Allow",
"Action" : "logs:DescribeLogGroups",
"Resource" : "*"
}),
jsonencode({
"Sid" : "AllowQueryLogGroups",
"Effect" : "Allow",
"Action" : "logs:FilterLogEvents",
"Resource" : aws_cloudwatch_log_group.tasks[each.key].arn
}),
]

## We are using the log group created above to ensure we control the
## configuration and the retention period of the logs
logging_log_group = format("/aws/lambda/%s", format("lza-nuke-notification-%s", lower(each.key)))
cloudwatch_logs_log_group_class = "STANDARD"
cloudwatch_logs_retention_in_days = 5
cloudwatch_logs_skip_destroy = false

## Envionment variables for the Lambda function
environment_variables = {
"LOG_GROUP_NAME" = aws_cloudwatch_log_group.tasks[each.key].name
"SNS_TOPIC_ARN" = each.value.sns_topic_arn
}
}

## Allow the event bridge rule to trigger the lambda function
resource "aws_lambda_permission" "allow_eventbridge" {
for_each = local.tasks_with_notifications

statement_id = "AllowExecutionFromEventBridge"
action = "lambda:InvokeFunction"
function_name = module.lambda_function[each.key].lambda_function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.ecs_task_stopped_rule[each.key].arn
}

## Configure the event target to invoke the lambda function
resource "aws_cloudwatch_event_target" "invoke_lambda" {
for_each = local.tasks_with_notifications

rule = aws_cloudwatch_event_rule.ecs_task_stopped_rule[each.key].name
arn = module.lambda_function[each.key].lambda_function_arn
}
11 changes: 8 additions & 3 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ variable "tasks" {
additional_permissions = optional(map(object({
policy = string
})), {})
configuration = string
description = string
dry_run = optional(bool, true)
configuration = string
description = string
dry_run = optional(bool, true)
notifications = optional(object({
sns_topic_arn = optional(string, null)
}), {
sns_topic_arn = null
})
permission_boundary_arn = optional(string, null)
permission_arns = optional(list(string), ["arn:aws:iam::aws:policy/AdministratorAccess"])
retention_in_days = optional(number, 7)
Expand Down