Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This Terraform module provisions an Amazon RDS PostgreSQL database on AWS. Amazo
6. Supports encryption at rest using AWS Key Management Service (KMS) for enhanced security.
7. Enables fine-grained control over network access through security groups and VPC settings.
8. Offers customizable tags for resource categorization and management.
9. CloudWatch Alerts: Set up CloudWatch alarms to monitor the health and performance of your Redis cluster. Integrate these alarms with AWS Simple Notification Service (SNS) to receive real-time alerts. Use AWS Lambda functions to customize your alerting logic, and send notifications to Slack channels for immediate visibility into your RDS POstgresql status.

## Usage Examples
```hcl
Expand All @@ -41,9 +42,15 @@ module "rds-pg" {
deletion_protection = false
allowed_security_groups = ["sg-013cbf880"]
final_snapshot_identifier_prefix = "final"
cloudwatch_metric_alarms_enabled = true
alarm_cpu_threshold_percent = 70
disk_free_storage_space = "10000000" # in bytes
slack_username = "John"
slack_channel = "skaf-dev"
slack_webhook_url = "https://hooks/xxxxxxxx"
}
```
Refer [examples](https://github.com/squareops/terraform-aws-rds-postgresql/tree/main/example/complete) for more details.
Refer [examples](https://github.com/squareops/terraform-aws-rds-postgresql/tree/main/examples/complete) for more details.

## IAM Permissions
The required IAM permissions to create resources from this module can be found [here](https://github.com/squareops/terraform-aws-rds-postgresql/blob/main/IAM.md)
Expand All @@ -60,21 +67,31 @@ The required IAM permissions to create resources from this module can be found [

| Name | Version |
|------|---------|
| <a name="provider_archive"></a> [archive](#provider\_archive) | 2.4.0 |
| <a name="provider_aws"></a> [aws](#provider\_aws) | 3.43.0 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_cw_sns_slack"></a> [cw\_sns\_slack](#module\_cw\_sns\_slack) | ./lambda | n/a |
| <a name="module_db"></a> [db](#module\_db) | terraform-aws-modules/rds/aws | ~> 3.0 |
| <a name="module_security_group_rds"></a> [security\_group\_rds](#module\_security\_group\_rds) | terraform-aws-modules/security-group/aws | ~> 4 |

## Resources

| Name | Type |
|------|------|
| [aws_cloudwatch_metric_alarm.cache_cpu](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/cloudwatch_metric_alarm) | resource |
| [aws_cloudwatch_metric_alarm.disk_free_storage_space_too_low](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/cloudwatch_metric_alarm) | resource |
| [aws_kms_ciphertext.slack_url](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/kms_ciphertext) | resource |
| [aws_kms_key.this](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/kms_key) | resource |
| [aws_lambda_permission.sns_lambda_slack_invoke](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/lambda_permission) | resource |
| [aws_security_group_rule.cidr_ingress](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/security_group_rule) | resource |
| [aws_security_group_rule.default_ingress](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/security_group_rule) | resource |
| [aws_sns_topic.slack_topic](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/sns_topic) | resource |
| [aws_sns_topic_subscription.slack-endpoint](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/resources/sns_topic_subscription) | resource |
| [archive_file.lambdazip](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |
| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/data-sources/availability_zones) | data source |
| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/3.43.0/docs/data-sources/region) | data source |

Expand All @@ -83,16 +100,21 @@ The required IAM permissions to create resources from this module can be found [
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_additional_tags"></a> [additional\_tags](#input\_additional\_tags) | A map of additional tags to apply to the AWS resources | `map(string)` | <pre>{<br> "automation": "true"<br>}</pre> | no |
| <a name="input_alarm_actions"></a> [alarm\_actions](#input\_alarm\_actions) | Alarm action list | `list(string)` | `[]` | no |
| <a name="input_alarm_cpu_threshold_percent"></a> [alarm\_cpu\_threshold\_percent](#input\_alarm\_cpu\_threshold\_percent) | CPU threshold alarm level | `number` | `75` | no |
| <a name="input_allocated_storage"></a> [allocated\_storage](#input\_allocated\_storage) | The allocated storage capacity for the database in gibibytes (GiB) | `number` | `20` | no |
| <a name="input_allowed_cidr_blocks"></a> [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | A list of CIDR blocks that are allowed to access the database | `list(any)` | `[]` | no |
| <a name="input_allowed_security_groups"></a> [allowed\_security\_groups](#input\_allowed\_security\_groups) | A list of Security Group IDs to allow access to the database | `list(any)` | `[]` | no |
| <a name="input_apply_immediately"></a> [apply\_immediately](#input\_apply\_immediately) | Specifies whether any cluster modifications are applied immediately or during the next maintenance window | `bool` | `false` | no |
| <a name="input_backup_retention_period"></a> [backup\_retention\_period](#input\_backup\_retention\_period) | The number of days to retain backups for | `number` | `5` | no |
| <a name="input_backup_window"></a> [backup\_window](#input\_backup\_window) | The preferred window for taking automated backups of the database | `string` | `""` | no |
| <a name="input_cloudwatch_metric_alarms_enabled"></a> [cloudwatch\_metric\_alarms\_enabled](#input\_cloudwatch\_metric\_alarms\_enabled) | Boolean flag to enable/disable CloudWatch metrics alarms | `bool` | `false` | no |
| <a name="input_create_random_password"></a> [create\_random\_password](#input\_create\_random\_password) | Whether to create a random password for the RDS primary cluster | `bool` | `true` | no |
| <a name="input_create_security_group"></a> [create\_security\_group](#input\_create\_security\_group) | Whether to create a security group for the database | `bool` | `true` | no |
| <a name="input_cw_sns_topic_arn"></a> [cw\_sns\_topic\_arn](#input\_cw\_sns\_topic\_arn) | The username to use when sending notifications to Slack. | `string` | `""` | no |
| <a name="input_db_name"></a> [db\_name](#input\_db\_name) | The name of the automatically created database on cluster creation | `string` | `""` | no |
| <a name="input_deletion_protection"></a> [deletion\_protection](#input\_deletion\_protection) | Specifies whether accidental deletion protection is enabled | `bool` | `true` | no |
| <a name="input_disk_free_storage_space"></a> [disk\_free\_storage\_space](#input\_disk\_free\_storage\_space) | Alarm threshold for the 'lowFreeStorageSpace' alarm | `string` | `"10000000000"` | no |
| <a name="input_enable_ssl_connection"></a> [enable\_ssl\_connection](#input\_enable\_ssl\_connection) | Whether to enable SSL connection to the database | `bool` | `false` | no |
| <a name="input_engine"></a> [engine](#input\_engine) | The name of the database engine to be used for this DB cluster | `string` | `"postgres"` | no |
| <a name="input_engine_version"></a> [engine\_version](#input\_engine\_version) | The database engine version. Updating this argument results in an outage | `string` | `""` | no |
Expand All @@ -106,11 +128,15 @@ The required IAM permissions to create resources from this module can be found [
| <a name="input_master_username"></a> [master\_username](#input\_master\_username) | The username for the RDS primary cluster | `string` | `""` | no |
| <a name="input_multi_az"></a> [multi\_az](#input\_multi\_az) | Enable multi-AZ for disaster recovery | `bool` | `false` | no |
| <a name="input_name"></a> [name](#input\_name) | The name of the RDS instance | `string` | `""` | no |
| <a name="input_ok_actions"></a> [ok\_actions](#input\_ok\_actions) | The list of actions to execute when this alarm transitions into an OK state from any other state. Each action is specified as an Amazon Resource Number (ARN) | `list(string)` | `[]` | no |
| <a name="input_port"></a> [port](#input\_port) | The port number for the database | `number` | `5432` | no |
| <a name="input_publicly_accessible"></a> [publicly\_accessible](#input\_publicly\_accessible) | Specifies whether the RDS instance is publicly accessible over the internet | `bool` | `false` | no |
| <a name="input_random_password_length"></a> [random\_password\_length](#input\_random\_password\_length) | The length of the randomly generated password for the RDS primary cluster (default: 10) | `number` | `10` | no |
| <a name="input_replicate_source_db"></a> [replicate\_source\_db](#input\_replicate\_source\_db) | Specifies that this resource is a replicate database, and uses the specified value as the source database identifier | `string` | `null` | no |
| <a name="input_skip_final_snapshot"></a> [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Determines whether a final DB snapshot is created before the DB instance is deleted. If set to true, no DB snapshot is created. If set to false, a DB snapshot is created before the DB instance is deleted, using the value from final\_snapshot\_identifier | `bool` | `true` | no |
| <a name="input_slack_channel"></a> [slack\_channel](#input\_slack\_channel) | The Slack channel where notifications will be posted. | `string` | `""` | no |
| <a name="input_slack_username"></a> [slack\_username](#input\_slack\_username) | The username to use when sending notifications to Slack. | `string` | `""` | no |
| <a name="input_slack_webhook_url"></a> [slack\_webhook\_url](#input\_slack\_webhook\_url) | The Slack Webhook URL where notifications will be sent. | `string` | `""` | no |
| <a name="input_snapshot_identifier"></a> [snapshot\_identifier](#input\_snapshot\_identifier) | Specifies whether to create the database from a snapshot. Use the snapshot ID found in the RDS console, e.g., rds:production-2015-06-26-06-05 | `string` | `null` | no |
| <a name="input_storage_encrypted"></a> [storage\_encrypted](#input\_storage\_encrypted) | Specifies whether to enable database encryption | `bool` | `true` | no |
| <a name="input_subnet_ids"></a> [subnet\_ids](#input\_subnet\_ids) | A list of subnet IDs used by the database subnet group | `list(any)` | `[]` | no |
Expand Down
2 changes: 1 addition & 1 deletion example/complete/README.md → examples/complete/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ No providers.

| Name | Source | Version |
|------|--------|---------|
| <a name="module_rds-pg"></a> [rds-pg](#module\_rds-pg) | squareops/postgresql-rds/aws | n/a |
| <a name="module_rds-pg"></a> [rds-pg](#module\_rds-pg) | squareops/rds-postgresql/aws | n/a |

## Resources

Expand Down
14 changes: 10 additions & 4 deletions example/complete/main.tf → examples/complete/main.tf
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
locals {
region = "us-east-2"
name = "postgresql"
vpc_id = "vpc-00ae5571c1"
vpc_id = "vpc-06861ba817a8cda10"
family = "postgres15"
subnet_ids = ["subnet-0d9a8193d2a6e","subnet-0fd263dc9e73d"]
subnet_ids = ["subnet-09e8f6ea27b7e36d0","subnet-0b070110454617a90"]
environment = "prod"
kms_key_arn = "arn:aws:kms:us-east-2:22222222:key/73ff9e84-83e1-fe29623338a9"
kms_key_arn = ""
engine_version = "15.2"
instance_class = "db.m5d.large"
allowed_security_groups = ["sg-0a680afd35"]
allowed_security_groups = ["sg-0ef14212995d67a2d"]
additional_tags = {
Owner = "Organization_Name"
Expires = "Never"
Expand Down Expand Up @@ -38,4 +38,10 @@ module "rds-pg" {
allowed_security_groups = local.allowed_security_groups
major_engine_version = local.engine_version
deletion_protection = false
cloudwatch_metric_alarms_enabled = true
alarm_cpu_threshold_percent = 70
disk_free_storage_space = "10000000" # in bytes
slack_username = ""
slack_channel = ""
slack_webhook_url = ""
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
59 changes: 59 additions & 0 deletions lambda/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## Lambda for SNS
![squareops_avatar]

[squareops_avatar]: https://squareops.com/wp-content/uploads/2022/12/squareops-logo.png

### [SquareOps Technologies](https://squareops.com/) Your DevOps Partner for Accelerating cloud journey.
<br>

Here is Lambda that calls the Slack webhook and passes the alarm message as the payload.
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## Requirements

No requirements.

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | 5.17.0 |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [aws_cloudwatch_log_group.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource |
| [aws_iam_role.lambda_exec_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy.lambda_cwl_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_lambda_function.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource |
| [aws_iam_policy_document.lambda_cwl_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.lambda_exec_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_artifact_file"></a> [artifact\_file](#input\_artifact\_file) | The path to the function's deployment package within the local filesystem | `string` | `null` | no |
| <a name="input_cwl_retention_days"></a> [cwl\_retention\_days](#input\_cwl\_retention\_days) | The retention time in days for the CloudWatch Logs Stream. | `number` | `30` | no |
| <a name="input_description"></a> [description](#input\_description) | Description of what the Lambda Function does. | `string` | `null` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | The Lambda environment's configuration settings. | `map(string)` | `{}` | no |
| <a name="input_handler"></a> [handler](#input\_handler) | The function entrypoint in the code. | `string` | `"index.handler"` | no |
| <a name="input_memory_size"></a> [memory\_size](#input\_memory\_size) | Amount of memory in MB your Lambda Function can use at runtime. | `number` | `128` | no |
| <a name="input_name"></a> [name](#input\_name) | A unique name for the Lambda Function. | `string` | n/a | yes |
| <a name="input_runtime"></a> [runtime](#input\_runtime) | The Runtime used in the Lambda Function. | `string` | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | A mapping of tags to assign to the module resources. | `map(string)` | `{}` | no |
| <a name="input_timeout"></a> [timeout](#input\_timeout) | The amount of time your Lambda Function has to run in seconds. | `number` | `6` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_arn"></a> [arn](#output\_arn) | The ARN identifying the Lambda Function. |
| <a name="output_exec_role_id"></a> [exec\_role\_id](#output\_exec\_role\_id) | The ID of the Function's IAM Role. |
| <a name="output_invoke_arn"></a> [invoke\_arn](#output\_invoke\_arn) | The ARN to be used for invoking Lambda Function from API Gateway. |
| <a name="output_name"></a> [name](#output\_name) | The name of the Lambda Function. |
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
32 changes: 32 additions & 0 deletions lambda/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Lambda Assume Role policy
data "aws_iam_policy_document" "lambda_exec_role_policy" {
statement {
sid = "LambdaExecRolePolicy"
effect = "Allow"
principals {
identifiers = [
"lambda.amazonaws.com",
]
type = "Service"
}
actions = [
"sts:AssumeRole",
]
}
}

# Lambda CloudWatch Logs access
data "aws_iam_policy_document" "lambda_cwl_access" {
statement {
sid = "LambdaCreateCloudWatchLogGroup"
effect = "Allow"
actions = [
"logs:PutLogEvents",
"logs:CreateLogStream",
"logs:CreateLogGroup"
]
resources = [
"arn:aws:logs:*:*:log-group:/aws/lambda/*:*:*"
]
}
}
10 changes: 10 additions & 0 deletions lambda/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resource "aws_iam_role" "lambda_exec_role" {
name = "${replace(title(var.name), "-", "")}LambdaExecRole"
assume_role_policy = data.aws_iam_policy_document.lambda_exec_role_policy.json
}

resource "aws_iam_role_policy" "lambda_cwl_policy" {
name = "${replace(title(var.name), "-", "")}LambdaCWLogsPolicy"
role = aws_iam_role.lambda_exec_role.id
policy = data.aws_iam_policy_document.lambda_cwl_access.json
}
26 changes: 26 additions & 0 deletions lambda/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
resource "aws_cloudwatch_log_group" "lambda" {
name = "/aws/lambda/${var.name}"
retention_in_days = var.cwl_retention_days
tags = var.tags
}

resource "aws_lambda_function" "this" {
function_name = var.name
description = var.description
filename = var.artifact_file
source_code_hash = var.artifact_file != null ? filebase64sha256(var.artifact_file) : null
role = aws_iam_role.lambda_exec_role.arn
handler = var.handler
runtime = var.runtime
memory_size = var.memory_size
timeout = var.timeout

dynamic "environment" {
for_each = (length(var.environment) > 0 ? [1] : [])
content {
variables = var.environment
}
}

tags = var.tags
}
19 changes: 19 additions & 0 deletions lambda/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "name" {
description = "The name of the Lambda Function."
value = aws_lambda_function.this.function_name
}

output "arn" {
description = "The ARN identifying the Lambda Function."
value = aws_lambda_function.this.arn
}

output "invoke_arn" {
description = "The ARN to be used for invoking Lambda Function from API Gateway."
value = aws_lambda_function.this.invoke_arn
}

output "exec_role_id" {
description = "The ID of the Function's IAM Role."
value = aws_iam_role.lambda_exec_role.id
}
51 changes: 51 additions & 0 deletions lambda/sns_slack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import json
import re
import os
import boto3
import urllib3

# Lambda global variables
region = os.environ["AWS_REGION"] # from Lambda default envs
slack_url = os.environ["SLACK_URL"]
slack_channel = os.environ["SLACK_CHANNEL"]
slack_user = os.environ["SLACK_USER"]


http = urllib3.PoolManager()
def format_cloudwatch_alarm_message(event):
alarm_data = json.loads(event['Records'][0]['Sns']['Message'])

alarm_name = alarm_data["AlarmName"]
alarm_description = alarm_data["AlarmDescription"]
new_state = alarm_data["NewStateValue"]
reason = alarm_data["NewStateReason"]
metric_name = alarm_data["Trigger"]["MetricName"]
threshold = alarm_data["Trigger"]["Threshold"]

message = f"*:exclamation: CloudWatch Alarm Alert :exclamation:*\n\n"
message += f" *Alarm Name:* {alarm_name}\n"
message += f" *Description:* _{alarm_description}_\n"
message += f" *New State:* {new_state}\n"
message += f" *Reason:* _{reason}_\n"
message += f" *Metric Name:* {metric_name}\n"
message += f" *Threshold:* {threshold}\n"

return message

def lambda_handler(event, context):
url = slack_url
msg = {
"channel": slack_channel,
"username": slack_user,
"text": format_cloudwatch_alarm_message(event),
"icon_emoji": ":cloudwatch:"
}

encoded_msg = json.dumps(msg).encode('utf-8')
resp = http.request('POST', url, body=encoded_msg)

print({
"message": msg,
"status_code": resp.status,
"response": resp.data
})
Loading