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.
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 planTo update the source for this module, pass -upgrade to tofu init:
tofu init -upgradeWhen 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.
| 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 |
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.
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 itCREATEimplicitly. - 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 publicrather 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.
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: 1095You 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 |
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 |
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 |
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 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 |
| 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 |