There are several components involved here that lack proper support for what you're asking, namely :
- RDS does not provide a way to create an instance direcly with the
rds_iam
role assigned to the master user
- cyrilgdn/postgresql does not support IAM database authentication
We can however work around both.
Requirements
I'll be assuming that the machine running terraform has :
- Network connectivity to the RDS instance
- A psql cli client installed
- jq installed
- The AWS cli (v2) installed
- An AWS identity configured, with enough permissions to :
- create a RDS instance
- authenticate with RDS IAM
How exactly you achieve the network connectivity (exposing the instance to the internet, running terraform on an ec2, a proxy...) and the AWS identity (access keys, AWS SSO, default role on an ec2...) is up to you and your context.
I'm personally using an AWS cli named profile with AWS SSO, so you'll be seiing --profile
everywhere, feel free to remove them.
The configuration
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.58"
}
postgresql = {
source = "cyrilgdn/postgresql"
version = "~> 1.20"
}
}
required_version = ">= 0.14.9"
}
# Feel free to change the AWS auth config to match your own
provider "aws" {
region = "<some_region>"
profile = "my-aws-profile"
assume_role {
role_arn = "<some_role>"
external_id = "<some_external_id>"
}
allowed_account_ids = ["<some_account_id>"]
}
# Instead of using passwords, we use IAM database authentication whenever possible :
# https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
# Users set to use it will not have a long-lived password, and will instead derive a temporary
# password from their AWS IAM identity.
# When this is enabled on an instance, any given pg role can only connect through one
# method ; either password or IAM ; depending on whether it has the rds_iam role.
#
# AWS does not provide a way to create an instance where the master user, created with
# the instance, has this role. So we initially create it with a password, and manually connect
# with a psql client immediately after creating the RDS instance, to switch it to RDS IAM auth.
# Once this is done, we can connect normally (ie through IAM) with the pg provider
resource "random_password" "rds_bootstrap_master_password" {
length = 30
upper = true
lower = true
numeric = true
special = false
}
# Now let's create the RDS instance itself with this password
resource "aws_db_instance" "app" {
username = "terraform"
password = random_password.rds_bootstrap_master_password.result
iam_database_authentication_enabled = true
# Put the rest of your configuration here, there are many more required parameters
# Once the RDS instance is created, manually switch the terraform user to
# IAM database authentication. We also delete its password, although technically
# it isn't really required, as enabling IAM db auth disables it.
# You can get the CA file with :
# `curl -o rds-ca-2019.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem`
provisioner "local-exec" {
interpreter = ["bash", "-c"]
command = <<EOF
psql "
host=${self.address}
port=${self.port}
sslmode=verify-full
sslrootcert=rds-ca-2019.pem
dbname=postgres
user=${self.username}
password=${self.password}
" --command="
GRANT rds_iam TO ${self.username};
ALTER ROLE ${self.username} WITH PASSWORD NULL;
"
EOF
}
}
# Since the postgresql provider does not natively support deriving short-lived postgresql creds from
# an AWS identity, we use the AWS cli to do it
data "external" "rds_auth_token" {
program = [
"bash", "-c", replace(
<<-EOF
aws rds generate-db-auth-token
--hostname ${aws_db_instance.app.address}
--port ${aws_db_instance.app.port}
--region ${replace(aws_db_instance.app.availability_zone, "/[[:lower:]]$/", "")}
--username ${aws_db_instance.app.username}
--profile my-aws-profile
| jq --raw-input '{ password: . }'
EOF
, "\n", " ")
]
}
# Please note that for this provider to work, you need direct network access to the RDS instance
provider "postgresql" {
scheme = "awspostgres"
host = aws_db_instance.app.address
port = aws_db_instance.app.port
sslmode = "verify-full" # NB : the "awspostgres" scheme takes care of finding the CA by itself
username = aws_db_instance.app.username
password = data.external.rds_auth_token.result.password
superuser = false
}
# This is a dummy user to test your configuration. Create whatever you need with the postgres provider here
resource "postgresql_role" "dummy" {
name = "tf_dummy_role"
login = false
roles = ["rds_iam"]
}
Explanation
In both cases, we work around the lack of first-party support by using the AWS cli.
- For the master user, we only need a single initial connection, to run a single SQL query. A local-exec is exactly what we're looking for here, as it does not add anything into the terraform dependency graph, while doing exactly what we need (one-time local command immediately after resource creation)
- For every subsequent run, we need to generate a short-lived access token, to use in the postgresql provider. The external provider is literally made for that. It does require json output though ; fortunately, we can get away with some jq-fu, which saves us from having to write a separate wrapper script.
This configuration should allow you to entirely bootstrap a RDS instance, with no long-lived secrets of any kind !
I hope it helps.