I have been playing around with terraform for the last few days and it is an amazing tool to manage infrastructure. For my AWS infrastructure I needed an aurora postgresql cluster which would allow hosting of multiple databases, each for one of my side projects, while also keeping them isolated and preventing an app user from accessing other app’s databases.
Terraform has an awesome postgresql provider which can be used for managing databases, However there are a few parts which are tricky and needed trial and error to get right.
Connecting to an RDS database via an SSH tunnel
The first roadblock was that my RDS cluster wasn’t accessible publicly (which is how it should be for security reasons). I do have a way to connect to my postgres servers via a bastion host. I thought we could use an SSH tunnel over the bastion host to get to our RDS cluster from my local computer. However, terraform doesn’t support connecting to the postgres server over an SSH tunnel via its configuration.
So, it required a little bit of jerry-rigging. The postgresql provider was happy as long as it could reach the postgres cluster using a host, port and password. So, I set up a local tunnel outside terraform via my SSH config like so:
1 | Host bastion |
The relevant line here is the LocalForward
declaration which wires up a local
port forward so that when you network traffic hits port 3333
on your
localhost
it is tunneled over the bastion and then the ecs server and is
routed to your cluster’s port 5432
. One thing to note here is that your ecs
cluster should be able to connect to your RDS cluster via proper security group
rules.
Setting up the postgres provider
Once you have the ssh tunnel set up, you can start wiring up your postgres provider for terraform like so:
1 | provider "postgresql" { |
The provider config is pretty straightforward, we point it to localhost:3333
with a root
user (which is the master user created by the rds cluster). So,
when you connect to localhost:3333
, you are actually connecting to the RDS
cluster through an SSH tunnel (make sure that your ssh connection is open at
this point via ssh ecs1-pg
in a separate terminal). We also need to set the
superuser
to false
because RDS doesn’t give us a postgres superuser, getting
this wrong initially caused me a lot of frustration.
Setting up the database and it’s user
Now that our cluster connectivity is set up, we can start creating the databases and users, each for one of our apps.
Below is a sensible configuration for a database called liveform_prod
and it’s
user called liveform
.
1 | locals { |
A few things to note here:
- The database
liveform_prod
is owned by a new user calledliveform
. - It has a connection limit of
5
, You should always set a sensible connection limit to prevent this app from crashing the cluster. - The db user too has a connection limit of
5
and a statement timeout of 1 minute which is big enough for web apps, you should set it to the least duration which works for your app. - A random password (via the
random_password
resource) is used as the password of our newliveform
role. This can be viewed by runningterraform show
Isolating this database from other users
By default postgres allows all users to connect to all databases and create/view
from all the tables. We want our databases to be isolated properly so that a
user for one app cannot access another app’s database. This requires running of
some SQL on the newly created database. We can easily do this using a
null_resource
and a local-exec
provisioner like so:
1 | resource "null_resource" "liveform_db_after_create" { |
./pg_database_roles_setup.sh
script:
1 |
|
The pg_database_roles_setup.sh
script connects to our rds cluster over the SSH
tunnel to the newly created database as the newly created user and revokes
connect privileges for all users on this database, and then adds connect
privileges to the app user and the root user. You can add more queries to this
script that you might want to run after the database is set up. Finally, the
local-exec
provisioner passes the right data via environment variables and
calls the database setup script.
Gotchas
If you create a posgresql_role
before setting the connection’s superuser
to
false
, you’ll get stuck trying to update or delete the new role. To work around
this, manually log in to the rds cluster via psql and DROP
the role, and remove
this state from terraform using: terraform state rm
postgresql_role.liveform_db_role