Skip to content

Repository files navigation

AWS Serverless Database Module

Main Checks GitHub Release

This module launches an Aurora Serverless v2 database cluster. Aurora serverless clusters measure capacity in ACUs (Aurora Capacity Units); each unit is approximately 2 GB of memory with corresponding CPU and networking.

Usage

Add this module to your main.tf (or appropriate) file and configure the inputs to match your desired configuration. For example:

module "database" {
  source = "github.com/codeforamerica/tofu-modules-aws-serverless-database?ref=1.7.0"

  project     = "my-project"
  environment = "dev"
  service     = "web"

  logging_key_arn = module.logging.kms_key_arn
  secrets_key_arn = module.secrets.kms_key_arn
  vpc_id          = module.vpc.vpc_id
  subnets         = module.vpc.private_subnets
  ingress_cidrs   = module.vpc.private_subnets_cidr_blocks

  min_capacity = 2
  max_capacity = 32
}

Make sure you re-run tofu init after adding the module to your configuration.

tofu init
tofu plan

To update the source for this module, pass -upgrade to tofu init:

tofu init -upgrade

Role name limitations

When creating the database cluster, a role will be created for database monitoring. A random string will be appended to the role name to ensure it is unique and allow replacement without a conflict. However, this means the rest of the role name must be 38 characters or fewer.

The role name is constructed as follows (before the suffix is added):

role_name = "${project}-${environment}-[${service}-]-db-mon"

If this combined string is longer than 38 characters, the module will fail to create the database cluster. You can help to reduce the length of the role by specifying short names for your project and (optionally) service using the project_short and service_short input variables, respectively.

Inputs

Name Description Type Default Required
logging_key_arn ARN of the KMS key for logging. string n/a yes
ingress_cidrs List of CIDR blocks to allow ingress. This is typically your private subnets. list(string) n/a yes
project Name of the project. string n/a yes
secrets_key_arn ARN of the KMS key for secrets. This will be used to encrypt database credentials. string n/a yes
subnets List of subnet ids the database instances may be placed in. list(string) n/a yes
vpc_id Id of the VPC to launch the database cluster into. string n/a yes
apply_immediately Whether to apply changes immediately rather than during the next maintenance window. WARNING: This may result in a restart of the cluster! bool false no
apps Map of applications to provision on the cluster, keyed by the app's full, unsanitized name. Derives database and IAM role names for you. Requires enable_data_api = true and iam_authentication = true. map(object) {} no
automatic_backup_retention_period Number of days to retain automatic backups, between 1 and 35. number 31 no
backup_namespace Namespace for backups, prefixed to vault names to produce globally unique names. string cfa no
backup_replica_region Region to use for cross-region backup replication. If not specified, no replica will be created. If specified, the module will create a backup vault in the specified region and configure the backup schedules to replicate to the replica vault. string null no
backup_retention_period Deprecated: Use automatic_backup_retention_period instead. number null no
backup_schedules Backup schedules to create for the database cluster. list(object) list(object) no
cluster_parameters Parameters to be set on the database cluster. list(object) [] no
configure_aws_backup Whether to configure AWS Backup with the defined backup schedules. bool false no
db_users Map of database users to create on the cluster. The map key becomes the database username. Requires enable_data_api = true and the AWS CLI must be installed on the OpenTofu runner. map(object) {} no
enable_data_api Whether to enable the Data API for the database cluster. bool false no
engine Database engine to use for the cluster. Valid values are "mysql" and "postgresql". string "postgresql" no
engine_version Version of the database engine to use. If left empty, the latest version will be used. Changing this value will result in downtime. string null no
environment Environment for the project. string "dev" no
force_delete Force deletion of resources. If changing to true, be sure to apply before destroying. bool false no
iam_authentication Whether to enable IAM authentication for the database cluster. bool true no
[iam_db_users] Map of IAM database users to create on the cluster. The map key becomes the database username. Requires iam_authentication = true and the AWS CLI must be installed on the OpenTofu runner. map(object) {} no
instances Number of instances to create in the database cluster. number 2 no
key_recovery_period Recovery period for deleted KMS keys in days. Must be between 7 and 30. number 30 no
min_capacity Minimum capacity for the serverless cluster in ACUs. number 2 no
max_capacity Maximum capacity for the serverless cluster in ACUs. number 10 no
password_rotation_frequency Number of days between automatic password rotations for the root user Set to 0 to disable automatic rotation. number 30 no
project_short Short name for the project. Used in resource names with character limits. Defaults to project. string "" no
service Optional service that these resources are supporting. Example: "api", "web", "worker". Used in resource names to differentiate from other services. string "" no
service_short Short name for the service. Used in resource names with character limits. Defaults to the same value as service. string "" no
security_group_rules Security group rules to control cluster ingress and egress. map(object) {} no
skip_final_snapshot Whether to skip the final snapshot when destroying the database cluster. bool false no
snapshot_identifier Optional name or ARN of the snapshot to restore the cluster from. Only applicable on create. bool false no
tags Optional tags to be applied to all resources. map(string) {} no

apps

You can provision applications directly, without pre-computing database or role names yourself. This is the recommended way to onboard applications onto a shared cluster: provide the app's full, unsanitized name, its extra_databases (if any), and its services, and the module derives and collision-checks everything for you.

Note

Like iam_db_users, this requires enable_data_api = true and iam_authentication = true, and shares the same underlying scripts — apps and iam_db_users entries end up in the same iam_db_user_policy_arns output, keyed by the derived (or literal, for iam_db_users) username.

The database name is derived from the app's key: lowercased, with - replaced by _ (my-application becomes my_application). Each entry in extra_databases gets the same treatment, suffixed onto the primary name (queue becomes my_application_queue). Each service gets its own role, named <database>_<service>.

Every service always gets its app's primary database. An extra_databases entry only reaches a service if that service lists it in its own extra_databases, and it's only ever created if at least one service does — an app must declare at least one service, and every extra_databases entry must be claimed by at least one service's own extra_databases, or the plan fails. Two services in the same app can end up with completely different databases; nothing grants a service access to a sibling service's database by default.

enable_data_api = true
apps = {
  "my-application" = {
    extra_databases = ["queue", "reports"]
    services = {
      "web" = {
        privileges = "all"
      },
      "worker" = {
        privileges      = "all"
        extra_databases = ["queue"]
      },
      "report-runner" = {
        privileges      = "readonly"
        extra_databases = ["reports"]
      }
    }
  }
}

The above creates databases my_application, my_application_queue, and my_application_reports. web only gets my_application; worker gets my_application and my_application_queue, but not my_application_reports; report-runner gets my_application and my_application_reports, but not my_application_queue.

Important

Derived database and role names are checked for collisions across every entry in apps (not just within one app — two entries in the same app can collide too) and the plan fails with a clear error if two would derive the same name — rename the conflicting app or extra_databases entry instead of picking a different name for the role. Collisions with iam_db_users and db_users usernames are also caught, and a service listing an extra_databases entry its app never declared fails the plan too. Postgres silently truncates identifiers over 63 bytes, so a derived username over that limit is rejected outright — shorten the app or service name if this fires.

Database creation and ownership

Each app's databases are created automatically — you don't need to run CREATE DATABASE yourself. Every database is guaranteed at least one service using it: an app must declare at least one service, and every extra_databases entry must be claimed by at least one service's own extra_databases (both checked at plan time). apps only supports engine = "postgresql"; using it with MySQL fails the plan with a clear error instead of creating roles and grants against databases that were never created.

PG15+ revoked the public schema's default CREATE privilege, so a service needs either to own its database or an explicit grant to run migrations against it:

  • A database used by exactly one service is owned by that service (CREATE DATABASE ... OWNER), which is what grants it CREATE implicitly.
  • A database used by multiple services (e.g. an app's primary database, shared by every service by default) is owned by the cluster's master user instead, and each service gets an explicit GRANT CREATE ON SCHEMA public rather than ownership.

Which service, if any, should own a shared database is an open product question — deliberately left undecided until a real multi-service use case exists to inform it, rather than picking an arbitrary tiebreaker now. The master-user fallback above is what keeps migrations working for every service in the meantime, not the final design. If a service is later added to or removed from a database, ownership is reconciled automatically on the next apply.

Note

An owning service can also DROP its own database — an accepted trade-off for using ownership as the mechanism.

Warning

Moving an existing database (e.g. one currently under iam_db_users) into apps reassigns its ownership on the next apply, with no plan-time signal that this will happen — whether it ends up single-owned or shared. Either way, this requires the master user to already have (or be granted) membership in whatever role currently owns it — otherwise the apply fails partway through.

backup_schedules

You can enable backups using AWS Backup by setting configure_aws_backup to true. When enabled, the module will create a backup vault in the current region.

module "database" {
  source = "github.com/codeforamerica/tofu-modules-aws-serverless-database?ref=1.7.0"

  project     = "my-project"
  environment = "dev"
  service     = "web"

  logging_key_arn = module.logging.kms_key_arn
  secrets_key_arn = module.secrets.kms_key_arn
  vpc_id          = module.vpc.vpc_id
  subnets         = module.vpc.private_subnets
  ingress_cidrs   = module.vpc.private_subnets_cidr_blocks

  configure_aws_backup = true
}

This will configure AWS Backup to use the following default schedule:

Note

Cron schedules are evaluated as UTC. With the defaults below, backups are scheduled for 9am UTC/4am EST/1am PST.

- name: daily
  schedule: cron(0 9 ? * * *)
  start_window: 320
  completion_window: 1440
  retention: 31
- name: monthly
  schedule: cron(0 9 1 * ? *)
  start_window: 320
  completion_window: 1440
  retention: 395
- name: yearly
  schedule: cron(0 9 1 1 ? *)
  start_window: 320
  completion_window: 1440
  retention: 1095

You can override the default schedule by setting backup_schedules:

backup_schedules = [{
    name = "daily"
    schedule = "cron(0 4 ? * * *)"
    start_window = 320
    completion_window = 1440
    retention = 8
  }, {
    name = "weekly"
    schedule = "cron(0 4 * * * 0)"
    start_window = 320
    completion_window = 1440
    retention = 53
  }]

Additionally, you can configure replication of backups to another region by setting backup_replica_region. If left empty, no replicas will be created.

backup_replica_region = "us-west-2"
Name Description Type Default Required
name Name of schedule. string n/a yes
schedule The cron schedule on which to create the backup, evaluated as UTC. string n/a yes
retention Number of days to retain the backup before it is deleted. number n/a yes
completion_window Number of minutes after a backup has successfully started in which it must complete before being cancelled. number 1440 no
start_window Number of minutes after the scheduled time in which the backup must be successfully started before being cancelled. number 320 no

cluster_parameters

You can override the default cluster parameters by passing a list of parameters and their values. Some parameters can be applied immediately, while others will require a restart of the cluster. See the documentation for the appropriate database engine to determine which parameters can be applied immediately.

Note

If a parameter requires a restart, you must set the apply_method to "pending-reboot".

Note

The apply_method above only controls how a parameter value is applied once the cluster is already on a custom parameter group. It doesn't cover the first time cluster_parameters goes from unset to set on an already-existing cluster — that flips create_db_cluster_parameter_group from false to true and switches the cluster off AWS's default parameter group, which AWS always treats as pending-reboot regardless of apply_method or the parameter's own dynamism. A manual aws rds failover-db-cluster (or instance reboot, for a single-instance cluster) is required to clear it. Clusters created with cluster_parameters already set from the start don't hit this, since the custom group is attached at creation.

cluster_parameters = [
  {
    name  = "log_statement"
    value = "all"
  },
  {
    name = "shared_preload_libraries"
    value = "pg_stat_statements,pglogical"
    apply_method = "pending-reboot"
  }
]
Name Description Type Default Required
name Name of the parameter to set. string n/a yes
value Value to set the parameter to. string n/a yes
apply_method How to apply the parameter. Can be immediate or pending-reboot. string "immediate" no

db_users

Warning

Creation of standard database users is limited to read-only users. We highly recommend using [iam_db_users][iam_db_users] instead for enhanced security. If you must rely on a user with a password, ensure that password is kept secure and rotated regularly.

The same dependency on the RDS Data API exists for db_users as iam_db_users

Although discouraged, it is occasionally necessary to create a read-only database user with a password. This option gives you the ability to create read-only database users with access to specific databases.

Provide a map of users, the keys of which will become the username. For example:

Caution

Database usernames should be treated as sensitive values and should not be committed directly to your repository.

Usernames are presented as strings here for demonstration purposes only.

enable_data_api = true
db_users    = {
  "myreadonlyuser" = {
    databases = ["my_stats"]
  }
}

This will create a new database user with SELECT access to the specified database(s). The user will be assigned a randomly generated password, and their credentials will be stored in AWS Secrets Manager. The ARN of the secret for each user is available in the db_user_secret_arns output.

Name Description Type Default Required
databases List of databases to grant the user access to. list(string) n/a yes
privileges Privileges to grant on the databases for the user. The only valid value is "readonly". string "readonly" no

iam_db_users

You can optionally create database users to be used for IAM based authentication. This allows your application(s) to connect to the database without managing a static password that needs to be rotated regularly.

Warning

Removing a privileges = "all" user reassigns any tables (or other objects) it owns to the master user before dropping it. Previously, a user that owned tables would silently fail to actually drop, leaving it in place — this fixes that, but it means table ownership can now move on a destroy with no plan-time signal, similar to the apps adoption warning above.

Note

To avoid needing to connect directly to the database, these users are managed using RDS Data API. As a result, enable_data_api must be set to true for this to succeed, and the OpenTofu runner must have the AWS CLI installed.

Provide a map of users, the keys of which will become the username. For example:

Caution

Database usernames should be treated as sensitive values and should not be committed directly to your repository.

Usernames are presented as strings here for demonstration purposes only.

enable_data_api = true
iam_db_users    = {
  "myapp" = {
    privileges = "all"
  },
  "myapp_ro" = {
    databases  = ["myapp", "myapp_queue"]
    privileges = "readonly"
  }
}

To grant your resources the privileges to connect to the database as the user, an IAM policy is created for each user, and mapped to the username in the iam_db_user_policy_arns output. You can attach this policy to one or more IAM roles which are attached to your resources.

Tip

You can also pass the policy ARN to another module that's responsible for configuring your roles, such as our aws_fargate_service module.

Name Description Type Default Required
databases List of databases to grant the user access to. Leave empty to grant access to all databases. list(string) [] no
privileges Privileges to grant on the databases for the user. Valid values are "all" and "readonly". string "all" no

security_group_rules

Security group rules control network access to the cluster. By default, the cluster will not be available on the network can only be accessed via the Data API. You can use security_group_rules to define rules to ingress and/or egress traffic.

Tip

If you just want to allow access to the database from one or more CIDR blocks, you can use the ingress_cidrs input variable for convenience.

security_group_rules = {
  vpc_peer = {
    description = "Allow access from VPC peer"
    type        = "ingress"
    protocol    = "tcp"
    from_port   = 5432
    to_port     = 5432
    cidr_blocks = ["10.123.0.0/16"]
  }
  replication = {
    description = "Allow egress for replication"
    type        = "egress"
    cidr_blocks = ["10.123.0.0/16"]
  }
}

Caution

Be careful when using egress rules. In most cases, this will not be necessary and can present a security risk. If you do need to use egress rules, be sure to restrict the narrowest set of destinations that are necessary.

Leaving your egress rules too broad can allow your data to be exfiltrated by a bad actor.

Name Description Type Default Required
description Description of the rule. string "Managed by OpenTofu" no
type Type of rule. Can be ingress or egress. string "ingress" no
protocol Protocol to use. Valid values: icmp, icmpv6, tcp, udp, or all. string "tcp" no
from_port Starting port for the rule. Defaults to the port for the database engine. number 5432 or 3306 no
to_port Ending port for the rule. Defaults to the port for the database engine. number 5432 or 3306 no
cidr_blocks List of CIDR blocks to allow access. list(string) [] no
ipv6_cidr_blocks List of IPv6 CIDR blocks to allow access. list(string) [] no
prefix_list_ids List of prefix list IDs to allow access. list(string) [] no
source_security_group_id ID of another security group to allow access. string null no

Outputs

Name Description Type
backup_key_arn ARN of the primary KMS key for backups, if created. string
backup_vault_arn ARN of the backup vault, if created. string
backup_vault_replica_arn ARN of the backup vault replica, if created. string
cluster_endpoint DNS endpoint to connect to the database cluster. string
cluster_id ID of the RDS database cluster. string
cluster_resource_id Resource ID of the RDS database cluster. string
db_user_secret_arns Map of database username to the ARN of the Secrets Manager secret containing their credentials. map(string)
iam_db_user_policy_arns Map of IAM database username to the ARN of the IAM policy granting rds-db:connect access for that user. map(string)
secret_arn ARN of the secret holding database credentials. string

About

OpenTofu module to create a manage an AWS Aurora serverless database.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages