diff --git a/.gitignore b/.gitignore index 7f041e9b..7b4139cf 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ override.tf.json # Ignore CLI configuration files .terraformrc terraform.rc + +# Lambda directories +builds/ diff --git a/README.md b/README.md index 81ac06dc..b243943b 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [db\_instance](#module\_db\_instance) | ./modules/db_instance | n/a | +| [db\_instance\_role\_association](#module\_db\_instance\_role\_association) | ./modules/db_instance_role_association | n/a | | [db\_option\_group](#module\_db\_option\_group) | ./modules/db_option_group | n/a | | [db\_parameter\_group](#module\_db\_parameter\_group) | ./modules/db_parameter_group | n/a | | [db\_subnet\_group](#module\_db\_subnet\_group) | ./modules/db_subnet_group | n/a | @@ -252,6 +253,7 @@ No resources. | [create\_db\_subnet\_group](#input\_create\_db\_subnet\_group) | Whether to create a database subnet group | `bool` | `false` | no | | [create\_monitoring\_role](#input\_create\_monitoring\_role) | Create IAM role with a defined name that permits RDS to send enhanced monitoring metrics to CloudWatch Logs | `bool` | `false` | no | | [custom\_iam\_instance\_profile](#input\_custom\_iam\_instance\_profile) | RDS custom iam instance profile | `string` | `null` | no | +| [db\_instance\_role\_associations](#input\_db\_instance\_role\_associations) | A map of DB instance supported feature name to role association ARNs. | `map(any)` | `{}` | no | | [db\_instance\_tags](#input\_db\_instance\_tags) | Additional tags for the DB instance | `map(string)` | `{}` | no | | [db\_name](#input\_db\_name) | The DB name to create. If omitted, no database is created initially | `string` | `null` | no | | [db\_option\_group\_tags](#input\_db\_option\_group\_tags) | Additional tags for the DB option group | `map(string)` | `{}` | no | @@ -342,6 +344,7 @@ No resources. | [db\_instance\_name](#output\_db\_instance\_name) | The database name | | [db\_instance\_port](#output\_db\_instance\_port) | The database port | | [db\_instance\_resource\_id](#output\_db\_instance\_resource\_id) | The RDS Resource ID of this instance | +| [db\_instance\_role\_associations](#output\_db\_instance\_role\_associations) | A map of DB Instance Identifiers and IAM Role ARNs separated by a comma | | [db\_instance\_status](#output\_db\_instance\_status) | The RDS instance status | | [db\_instance\_username](#output\_db\_instance\_username) | The master username for the database | | [db\_listener\_endpoint](#output\_db\_listener\_endpoint) | Specifies the listener connection endpoint for SQL Server Always On | diff --git a/examples/role-association-postgres/README.md b/examples/role-association-postgres/README.md new file mode 100644 index 00000000..1af47b7b --- /dev/null +++ b/examples/role-association-postgres/README.md @@ -0,0 +1,70 @@ +# RDS DB instance role association example for PostgreSQL + +Configuration in this directory creates a DB instance role association to invoke a lambda function. + +Further database configurations for creating extension and invoking from postgres: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL-Lambda.html + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 5.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [db](#module\_db) | ../../ | n/a | +| [lambda](#module\_lambda) | terraform-aws-modules/lambda/aws | ~> 6.0 | +| [rds\_invoke\_lambda\_policy](#module\_rds\_invoke\_lambda\_policy) | terraform-aws-modules/iam/aws//modules/iam-policy | ~> 5.28.0 | +| [rds\_invoke\_lambda\_role](#module\_rds\_invoke\_lambda\_role) | terraform-aws-modules/iam/aws//modules/iam-assumable-role | ~> 5.28.0 | +| [security\_group](#module\_security\_group) | terraform-aws-modules/security-group/aws | ~> 4.0 | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.rds_invoke_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.rds_invoke_lambda_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [db\_enhanced\_monitoring\_iam\_role\_arn](#output\_db\_enhanced\_monitoring\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the monitoring role | +| [db\_instance\_address](#output\_db\_instance\_address) | The address of the RDS instance | +| [db\_instance\_arn](#output\_db\_instance\_arn) | The ARN of the RDS instance | +| [db\_instance\_availability\_zone](#output\_db\_instance\_availability\_zone) | The availability zone of the RDS instance | +| [db\_instance\_cloudwatch\_log\_groups](#output\_db\_instance\_cloudwatch\_log\_groups) | Map of CloudWatch log groups created and their attributes | +| [db\_instance\_endpoint](#output\_db\_instance\_endpoint) | The connection endpoint | +| [db\_instance\_engine](#output\_db\_instance\_engine) | The database engine | +| [db\_instance\_engine\_version\_actual](#output\_db\_instance\_engine\_version\_actual) | The running version of the database | +| [db\_instance\_hosted\_zone\_id](#output\_db\_instance\_hosted\_zone\_id) | The canonical hosted zone ID of the DB instance (to be used in a Route 53 Alias record) | +| [db\_instance\_identifier](#output\_db\_instance\_identifier) | The RDS instance identifier | +| [db\_instance\_master\_user\_secret\_arn](#output\_db\_instance\_master\_user\_secret\_arn) | The ARN of the master user secret (Only available when manage\_master\_user\_password is set to true) | +| [db\_instance\_name](#output\_db\_instance\_name) | The database name | +| [db\_instance\_port](#output\_db\_instance\_port) | The database port | +| [db\_instance\_resource\_id](#output\_db\_instance\_resource\_id) | The RDS Resource ID of this instance | +| [db\_instance\_role\_associations](#output\_db\_instance\_role\_associations) | The outputs for the role associations | +| [db\_instance\_status](#output\_db\_instance\_status) | The RDS instance status | +| [db\_instance\_username](#output\_db\_instance\_username) | The master username for the database | +| [db\_parameter\_group\_arn](#output\_db\_parameter\_group\_arn) | The ARN of the db parameter group | +| [db\_parameter\_group\_id](#output\_db\_parameter\_group\_id) | The db parameter group id | +| [db\_subnet\_group\_arn](#output\_db\_subnet\_group\_arn) | The ARN of the db subnet group | +| [db\_subnet\_group\_id](#output\_db\_subnet\_group\_id) | The db subnet group name | + diff --git a/examples/role-association-postgres/fixtures/lambda_function.py b/examples/role-association-postgres/fixtures/lambda_function.py new file mode 100644 index 00000000..337b133b --- /dev/null +++ b/examples/role-association-postgres/fixtures/lambda_function.py @@ -0,0 +1,3 @@ +def lambda_handler(event, context): + + return "Triggered by RDS Lambda!" diff --git a/examples/role-association-postgres/main.tf b/examples/role-association-postgres/main.tf new file mode 100644 index 00000000..07d7202e --- /dev/null +++ b/examples/role-association-postgres/main.tf @@ -0,0 +1,195 @@ +provider "aws" { + region = local.region +} + +data "aws_caller_identity" "current" {} +data "aws_availability_zones" "available" {} + +locals { + name = "role-association-invoke-lambda" + region = "eu-west-1" + + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + + tags = { + Name = local.name + Example = local.name + Repository = "https://github.com/terraform-aws-modules/terraform-aws-rds" + } +} + +################################################################################ +# RDS Module +################################################################################ + +module "db" { + source = "../../" + + identifier = local.name + + # All available versions: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html#PostgreSQL.Concepts + engine = "postgres" + engine_version = "14" + family = "postgres14" # DB parameter group + major_engine_version = "14" # DB option group + instance_class = "db.t4g.large" + + allocated_storage = 20 + + # NOTE: Do NOT use 'user' as the value for 'username' as it throws: + # "Error creating DB Instance: InvalidParameterValue: MasterUsername + # user cannot be used as it is a reserved word used by the engine" + db_name = "RoleAssociationInvokeLambda" + username = "role_association_invoke_lambda" + port = 5432 + + multi_az = true + db_subnet_group_name = module.vpc.database_subnet_group + vpc_security_group_ids = [module.security_group.security_group_id] + + maintenance_window = "Mon:00:00-Mon:03:00" + backup_window = "03:00-06:00" + backup_retention_period = 0 + + deletion_protection = false + + # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL-Lambda.html + db_instance_role_associations = { + Lambda = module.rds_invoke_lambda_role.iam_role_arn + } + + parameters = [ + { + name = "rds.custom_dns_resolution" + value = 1 + apply_method = "pending-reboot" + }, + ] + + tags = local.tags +} + +################################################################################ +# Supporting Resources +################################################################################ + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = local.name + cidr = local.vpc_cidr + + azs = local.azs + public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)] + private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 3)] + database_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 6)] + + create_database_subnet_group = true + enable_nat_gateway = true + + tags = local.tags +} + +module "security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "~> 4.0" + + name = local.name + description = "Complete PostgreSQL example security group" + vpc_id = module.vpc.vpc_id + + # ingress + ingress_with_cidr_blocks = [ + { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + description = "PostgreSQL access from within VPC" + cidr_blocks = module.vpc.vpc_cidr_block + }, + ] + + # egress + egress_with_cidr_blocks = [ + { + from_port = 443 + to_port = 443 + protocol = "tcp" + description = "Egress to AWS Lambda VPC" + cidr_blocks = "0.0.0.0/0" + } + ] + + tags = local.tags +} + +module "rds_invoke_lambda_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role" + version = "~> 5.28.0" + + create_role = true + role_requires_mfa = false + + role_name_prefix = local.name + + custom_role_policy_arns = [ + module.rds_invoke_lambda_policy.arn + ] + custom_role_trust_policy = data.aws_iam_policy_document.rds_invoke_lambda_assume_role.json +} + +module "rds_invoke_lambda_policy" { + source = "terraform-aws-modules/iam/aws//modules/iam-policy" + version = "~> 5.28.0" + + name = "${local.name}-policy" + path = "/" + description = "Invoke Lambda from RDS Postgresql policy" + + policy = data.aws_iam_policy_document.rds_invoke_lambda.json +} + +data "aws_iam_policy_document" "rds_invoke_lambda" { + statement { + sid = "InvokeLambda" + actions = [ + "lambda:InvokeFunction" + ] + resources = [ + module.lambda.lambda_function_arn + ] + } +} + +data "aws_iam_policy_document" "rds_invoke_lambda_assume_role" { + statement { + sid = "AssumeRole" + + principals { + type = "Service" + identifiers = ["rds.amazonaws.com"] + } + + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.id] + variable = "aws:SourceAccount" + } + + effect = "Allow" + + actions = ["sts:AssumeRole"] + } +} + +module "lambda" { + source = "terraform-aws-modules/lambda/aws" + version = "~> 6.0" + + function_name = local.name + handler = "lambda_function.lambda_handler" + runtime = "python3.10" + source_path = "${path.module}/fixtures/lambda_function.py" +} diff --git a/examples/role-association-postgres/outputs.tf b/examples/role-association-postgres/outputs.tf new file mode 100644 index 00000000..3021e301 --- /dev/null +++ b/examples/role-association-postgres/outputs.tf @@ -0,0 +1,105 @@ +output "db_instance_address" { + description = "The address of the RDS instance" + value = module.db.db_instance_address +} + +output "db_instance_arn" { + description = "The ARN of the RDS instance" + value = module.db.db_instance_arn +} + +output "db_instance_availability_zone" { + description = "The availability zone of the RDS instance" + value = module.db.db_instance_availability_zone +} + +output "db_instance_endpoint" { + description = "The connection endpoint" + value = module.db.db_instance_endpoint +} + +output "db_instance_engine" { + description = "The database engine" + value = module.db.db_instance_engine +} + +output "db_instance_engine_version_actual" { + description = "The running version of the database" + value = module.db.db_instance_engine_version_actual +} + +output "db_instance_hosted_zone_id" { + description = "The canonical hosted zone ID of the DB instance (to be used in a Route 53 Alias record)" + value = module.db.db_instance_hosted_zone_id +} + +output "db_instance_identifier" { + description = "The RDS instance identifier" + value = module.db.db_instance_identifier +} + +output "db_instance_resource_id" { + description = "The RDS Resource ID of this instance" + value = module.db.db_instance_resource_id +} + +output "db_instance_status" { + description = "The RDS instance status" + value = module.db.db_instance_status +} + +output "db_instance_name" { + description = "The database name" + value = module.db.db_instance_name +} + +output "db_instance_username" { + description = "The master username for the database" + value = module.db.db_instance_username + sensitive = true +} + +output "db_instance_port" { + description = "The database port" + value = module.db.db_instance_port +} + +output "db_subnet_group_id" { + description = "The db subnet group name" + value = module.db.db_subnet_group_id +} + +output "db_subnet_group_arn" { + description = "The ARN of the db subnet group" + value = module.db.db_subnet_group_arn +} + +output "db_parameter_group_id" { + description = "The db parameter group id" + value = module.db.db_parameter_group_id +} + +output "db_parameter_group_arn" { + description = "The ARN of the db parameter group" + value = module.db.db_parameter_group_arn +} + +output "db_enhanced_monitoring_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the monitoring role" + value = module.db.enhanced_monitoring_iam_role_arn +} + +output "db_instance_cloudwatch_log_groups" { + description = "Map of CloudWatch log groups created and their attributes" + value = module.db.db_instance_cloudwatch_log_groups +} + +output "db_instance_master_user_secret_arn" { + description = "The ARN of the master user secret (Only available when manage_master_user_password is set to true)" + value = module.db.db_instance_master_user_secret_arn +} + +output "db_instance_role_associations" { + description = "The outputs for the role associations" + value = module.db.db_instance_role_associations +} diff --git a/examples/role-association-postgres/variables.tf b/examples/role-association-postgres/variables.tf new file mode 100644 index 00000000..e69de29b diff --git a/examples/role-association-postgres/versions.tf b/examples/role-association-postgres/versions.tf new file mode 100644 index 00000000..ddfcb0e0 --- /dev/null +++ b/examples/role-association-postgres/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} diff --git a/main.tf b/main.tf index d3662668..522d6315 100644 --- a/main.tf +++ b/main.tf @@ -143,3 +143,13 @@ module "db_instance" { tags = merge(var.tags, var.db_instance_tags) } + +module "db_instance_role_association" { + source = "./modules/db_instance_role_association" + + for_each = { for k, v in var.db_instance_role_associations : k => v if var.create_db_instance } + + feature_name = each.key + role_arn = each.value + db_instance_identifier = module.db_instance.db_instance_identifier +} diff --git a/modules/db_instance_role_association/README.md b/modules/db_instance_role_association/README.md new file mode 100644 index 00000000..9f428d8e --- /dev/null +++ b/modules/db_instance_role_association/README.md @@ -0,0 +1,41 @@ +# aws_db_instance_role_association + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 5.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_db_instance_role_association.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance_role_association) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [create](#input\_create) | Determines whether to create a DB instance role association | `bool` | `true` | no | +| [db\_instance\_identifier](#input\_db\_instance\_identifier) | The database instance identifier to associate the role | `string` | `null` | no | +| [feature\_name](#input\_feature\_name) | Name of the feature for association | `string` | `null` | no | +| [role\_arn](#input\_role\_arn) | Amazon Resource Name (ARN) of the IAM Role to associate with the DB Instance | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [db\_instance\_role\_association\_id](#output\_db\_instance\_role\_association\_id) | DB Instance Identifier and IAM Role ARN separated by a comma | + diff --git a/modules/db_instance_role_association/main.tf b/modules/db_instance_role_association/main.tf new file mode 100644 index 00000000..0a11a4c2 --- /dev/null +++ b/modules/db_instance_role_association/main.tf @@ -0,0 +1,7 @@ +resource "aws_db_instance_role_association" "this" { + count = var.create ? 1 : 0 + + db_instance_identifier = var.db_instance_identifier + feature_name = var.feature_name + role_arn = var.role_arn +} diff --git a/modules/db_instance_role_association/outputs.tf b/modules/db_instance_role_association/outputs.tf new file mode 100644 index 00000000..9152a0c0 --- /dev/null +++ b/modules/db_instance_role_association/outputs.tf @@ -0,0 +1,4 @@ +output "db_instance_role_association_id" { + description = "DB Instance Identifier and IAM Role ARN separated by a comma" + value = try(aws_db_instance_role_association.this[0].id, "") +} diff --git a/modules/db_instance_role_association/variables.tf b/modules/db_instance_role_association/variables.tf new file mode 100644 index 00000000..d548d7fd --- /dev/null +++ b/modules/db_instance_role_association/variables.tf @@ -0,0 +1,23 @@ +variable "create" { + description = "Determines whether to create a DB instance role association" + type = bool + default = true +} + +variable "feature_name" { + description = "Name of the feature for association" + type = string + default = null +} + +variable "role_arn" { + description = "Amazon Resource Name (ARN) of the IAM Role to associate with the DB Instance" + type = string + default = null +} + +variable "db_instance_identifier" { + description = "The database instance identifier to associate the role" + type = string + default = null +} diff --git a/modules/db_instance_role_association/versions.tf b/modules/db_instance_role_association/versions.tf new file mode 100644 index 00000000..ddfcb0e0 --- /dev/null +++ b/modules/db_instance_role_association/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} diff --git a/outputs.tf b/outputs.tf index f9c771ec..cb0ee1cb 100644 --- a/outputs.tf +++ b/outputs.tf @@ -138,3 +138,12 @@ output "db_instance_cloudwatch_log_groups" { description = "Map of CloudWatch log groups created and their attributes" value = module.db_instance.db_instance_cloudwatch_log_groups } + +################################################################################ +# DB Instance Role Association +################################################################################ + +output "db_instance_role_associations" { + description = "A map of DB Instance Identifiers and IAM Role ARNs separated by a comma" + value = module.db_instance_role_association +} diff --git a/variables.tf b/variables.tf index c882721c..db9375da 100644 --- a/variables.tf +++ b/variables.tf @@ -544,3 +544,13 @@ variable "putin_khuylo" { type = bool default = true } + +################################################################################ +# DB Instance Role Association +################################################################################ + +variable "db_instance_role_associations" { + description = "A map of DB instance supported feature name to role association ARNs." + type = map(any) + default = {} +}