|
variable "database_password" { |
|
type = "string" |
|
} |
|
variable "subnet_ids" { |
|
description = "Comma-delimited string of subnet ids" |
|
type = "string" |
|
} |
|
variable "security_group_id" { |
|
type = "string" |
|
} |
|
|
|
provider "archive" { |
|
version = "~> 1.1" |
|
} |
|
|
|
provider "aws" { |
|
version = "~> 1.23" |
|
} |
|
|
|
locals { |
|
# Solution from this comment to open issue on non-relative paths |
|
# https://github.com/hashicorp/terraform/issues/8204#issuecomment-332239294 |
|
|
|
# +1 for removing the "/" |
|
creation_filename = "${substr(data.archive_file.rds_creation_zip.output_path, length(path.cwd) + 1, -1)}" |
|
setup_filename = "${substr(data.archive_file.rds_setup_zip.output_path, length(path.cwd) + 1, -1)}" |
|
} |
|
|
|
data "archive_file" "rds_creation_zip" { |
|
type = "zip" |
|
output_path = "${path.module}/rds_creation.zip" |
|
source_dir = "${path.module}/rds_creation/" |
|
} |
|
|
|
data "archive_file" "rds_setup_zip" { |
|
type = "zip" |
|
output_path = "${path.module}/rds_setup.zip" |
|
source_dir = "${path.module}/rds_setup/" |
|
} |
|
|
|
# The SQL script that gets evaluated on new database instances. |
|
# Clearly, you will want to change this! |
|
data "template_file" "sql_script" { |
|
template = <<SQL |
|
SELECT 1; |
|
SQL |
|
} |
|
|
|
# Subscribe to all new database creation notifications |
|
resource "aws_db_event_subscription" "creation" { |
|
name = "rds-creation" |
|
sns_topic = "${aws_sns_topic.rds.arn}" |
|
source_type = "db-instance" |
|
event_categories = ["creation"] |
|
} |
|
|
|
# 'External' Lambda function that gets the new database SNS notification |
|
# and queries the AWS API to obtain further details about this. |
|
# |
|
# It then sends those details off to another SNS notification, which is |
|
# picked up by the 'internal' Lambda function. |
|
resource "aws_lambda_function" "rds_creation" { |
|
function_name = "rds-creation" |
|
handler = "index.handler" |
|
filename = "${local.creation_filename}" |
|
source_code_hash = "${base64sha256(file("${local.creation_filename}"))}" |
|
|
|
role = "${aws_iam_role.rds_external_lambda.arn}" |
|
runtime = "nodejs8.10" |
|
timeout = 10 |
|
|
|
environment { |
|
variables { |
|
SNS_TOPIC_ARN = "${aws_sns_topic.internal.arn}" |
|
} |
|
} |
|
} |
|
|
|
# 'Internal' Lambda function which receives database information from |
|
# the external function (via SNS) and then connects to the database |
|
# and evaluates the script against it. |
|
# |
|
# This operates within the VPC, and hence does not have access to the |
|
# internet or AWS APIs. |
|
resource "aws_lambda_function" "rds_setup" { |
|
function_name = "rds-setup" |
|
handler = "index.handler" |
|
filename = "${local.setup_filename}" |
|
source_code_hash = "${base64sha256(file("${local.setup_filename}"))}" |
|
|
|
role = "${aws_iam_role.rds_internal_lambda.arn}" |
|
runtime = "nodejs8.10" |
|
timeout = 10 |
|
|
|
vpc_config { |
|
subnet_ids = ["${split(",", var.subnet_ids)}"] |
|
security_group_ids = ["${var.security_group_id}"] |
|
} |
|
|
|
environment { |
|
variables = { |
|
PGPASSWORD = "${var.database_password}" |
|
SQL_SCRIPT = "${replace(trimspace(data.template_file.sql_script.rendered), "/\n/", " ")}" |
|
} |
|
} |
|
} |
|
|
|
resource "aws_lambda_permission" "rds_creation" { |
|
statement_id = "AllowExecutionFromSNS" |
|
action = "lambda:InvokeFunction" |
|
function_name = "${aws_lambda_function.rds_creation.function_name}" |
|
principal = "sns.amazonaws.com" |
|
source_arn = "${aws_sns_topic.rds.arn}" |
|
} |
|
|
|
resource "aws_lambda_permission" "rds_setup" { |
|
statement_id = "AllowExecutionFromSNS" |
|
action = "lambda:InvokeFunction" |
|
function_name = "${aws_lambda_function.rds_setup.function_name}" |
|
principal = "sns.amazonaws.com" |
|
source_arn = "${aws_sns_topic.internal.arn}" |
|
} |
|
|
|
# SNS Topic for new database creations (via RDS events) |
|
resource "aws_sns_topic" "rds" { |
|
name = "rds-creation" |
|
} |
|
|
|
# SNS Topic for database credentials (via the external lambda) |
|
resource "aws_sns_topic" "internal" { |
|
name = "rds-setup" |
|
} |
|
|
|
# Subscriptions connecting topics to lambdas |
|
resource "aws_sns_topic_subscription" "rds" { |
|
topic_arn = "${aws_sns_topic.rds.arn}" |
|
protocol = "lambda" |
|
endpoint = "${aws_lambda_function.rds_creation.arn}" |
|
} |
|
|
|
resource "aws_sns_topic_subscription" "rds_internal" { |
|
topic_arn = "${aws_sns_topic.internal.arn}" |
|
protocol = "lambda" |
|
endpoint = "${aws_lambda_function.rds_setup.arn}" |
|
} |
|
|
|
# IAM Role for 'External' lambda which has access to |
|
# CloudWatch, SNS, and RDS. |
|
resource "aws_iam_role" "rds_external_lambda" { |
|
name = "RDSExternal" |
|
|
|
assume_role_policy = <<EOF |
|
{ |
|
"Version": "2012-10-17", |
|
"Statement": [ |
|
{ |
|
"Action": "sts:AssumeRole", |
|
"Principal": { |
|
"Service": "lambda.amazonaws.com" |
|
}, |
|
"Effect": "Allow" |
|
} |
|
] |
|
} |
|
EOF |
|
} |
|
|
|
# IAM Role for 'Internal' lambda which has access to |
|
# CloudWatch and VPC behaviour. |
|
resource "aws_iam_role" "rds_internal_lambda" { |
|
name = "RDSInternal" |
|
|
|
assume_role_policy = <<EOF |
|
{ |
|
"Version": "2012-10-17", |
|
"Statement": [ |
|
{ |
|
"Action": "sts:AssumeRole", |
|
"Principal": { |
|
"Service": "lambda.amazonaws.com" |
|
}, |
|
"Effect": "Allow" |
|
} |
|
] |
|
} |
|
EOF |
|
} |
|
|
|
resource "aws_iam_role_policy" "rds_internal" { |
|
name = "RDSInternalNotifications" |
|
role = "${aws_iam_role.rds_internal_lambda.id}" |
|
policy = <<EOF |
|
{ |
|
"Version": "2012-10-17", |
|
"Statement": [{ |
|
"Effect": "Allow", |
|
"Action": [ |
|
"logs:CreateLogGroup", |
|
"logs:CreateLogStream", |
|
"logs:PutLogEvents" |
|
], |
|
"Resource": "*" |
|
}] |
|
} |
|
EOF |
|
} |
|
|
|
resource "aws_iam_role_policy" "rds_external" { |
|
name = "RDSExternalNotifications" |
|
role = "${aws_iam_role.rds_external_lambda.id}" |
|
policy = <<EOF |
|
{ |
|
"Version": "2012-10-17", |
|
"Statement": [{ |
|
"Effect": "Allow", |
|
"Action": [ |
|
"logs:CreateLogGroup", |
|
"logs:CreateLogStream", |
|
"logs:PutLogEvents", |
|
"rds:DescribeDBInstances", |
|
"sns:Publish" |
|
], |
|
"Resource": "*" |
|
}] |
|
} |
|
EOF |
|
} |
|
|
|
resource "aws_iam_role_policy_attachment" "rds_lambda_vpc" { |
|
role = "${aws_iam_role.rds_internal_lambda.id}" |
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" |
|
} |
@marcandjulien I've just added an MIT License to this, so feel free to use it as you see fit.
It's worth noting that apparently it's now possible to talk to the RDS API from within a VPC, so you may only need one lambda instead of two. The project I wrote this for is no longer active, so I can't test it myself (but certainly, what I've written worked well for me).