Skip to content

Commit 06d8ca9

Browse files
committed
fix(database): Aurora Serverless setup and configuration
this commit fixes the original Aurora Serverless setup by setting the initial database that copilot creates to postgres, and using the ensure_db.py script to configure the Django database. - copilot creates an Aurora Serverless storage cluster with an initial database called 'postgres' - the web service manifest defines secrets related to the Django database - to create the Django database, ensure_db.py and settings.py are updated to use the database credentials from the environment variable automatically created by copilot - the start_aws.sh script now uses ensure_db.py - the Aurora Data API is enabled to allow us to interact with the Aurora database by running SQL queries on the AWS console
1 parent 332fb4a commit 06d8ca9

File tree

6 files changed

+87
-63
lines changed

6 files changed

+87
-63
lines changed

bin/start_aws.sh

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,10 @@ set -eu
55
# since it was created via copilot storage init --name pems-db, the variable is 'PEMSDB_NAME'
66
S3_BUCKET_NAME="$PEMSDB_NAME"
77
S3_FIXTURE_PATH="fixtures.json"
8-
DJANGO_DB_FIXTURES="fixtures.json"
98

109
echo "Downloading $S3_FIXTURE_PATH from bucket $S3_BUCKET_NAME"
1110
aws s3 cp "s3://${S3_BUCKET_NAME}/${S3_FIXTURE_PATH}" "${DJANGO_DB_FIXTURES}"
1211
echo "Download complete"
1312

14-
# PostgreSQL database settings (username, host, dbname, password, port) are injected by Copilot as an environment variable
15-
# called 'POSTGRESWEB_SECRET' since the database was created via
16-
# copilot storage init -l workload -t Aurora -w web -n postgres-web --engine PostgreSQL --initial-db django
17-
18-
python manage.py migrate
19-
20-
# Load data fixtures (if any)
21-
valid_fixtures=$(echo "$DJANGO_DB_FIXTURES" | grep -e fixtures\.json$ || test $? = 1)
22-
23-
if [[ -n "$valid_fixtures" ]]; then
24-
python manage.py loaddata $DJANGO_DB_FIXTURES
25-
else
26-
echo "No JSON fixtures to load"
27-
fi
28-
13+
bin/setup.sh
2914
bin/start.sh

infra/copilot/web/addons/postgres-web.yml

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,59 +12,57 @@ Parameters:
1212
postgreswebDBName:
1313
Type: String
1414
Description: The name of the initial database to be created in the Aurora Serverless v2 cluster.
15-
Default: django
15+
Default: postgres
1616
# Cannot have special characters
1717
# Naming constraints: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints
1818
Mappings:
1919
postgreswebEnvScalingConfigurationMap:
2020
dev:
2121
"DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128
22-
"DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128
22+
"DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128
2323

2424
All:
2525
"DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128
26-
"DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128
26+
"DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128
2727

2828
Resources:
2929
postgreswebDBSubnetGroup:
30-
Type: 'AWS::RDS::DBSubnetGroup'
30+
Type: "AWS::RDS::DBSubnetGroup"
3131
Properties:
3232
DBSubnetGroupDescription: Group of Copilot private subnets for Aurora Serverless v2 cluster.
3333
SubnetIds:
34-
!Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' }]
34+
!Split [",", { "Fn::ImportValue": !Sub "${App}-${Env}-PrivateSubnets" }]
3535
postgreswebSecurityGroup:
3636
Metadata:
37-
'aws:copilot:description': 'A security group for your workload to access the Aurora Serverless v2 cluster postgresweb'
38-
Type: 'AWS::EC2::SecurityGroup'
37+
"aws:copilot:description": "A security group for your workload to access the Aurora Serverless v2 cluster postgresweb"
38+
Type: "AWS::EC2::SecurityGroup"
3939
Properties:
40-
GroupDescription: !Sub 'The Security Group for ${Name} to access Aurora Serverless v2 cluster postgresweb.'
40+
GroupDescription: !Sub "The Security Group for ${Name} to access Aurora Serverless v2 cluster postgresweb."
4141
VpcId:
42-
Fn::ImportValue:
43-
!Sub '${App}-${Env}-VpcId'
42+
Fn::ImportValue: !Sub "${App}-${Env}-VpcId"
4443
Tags:
4544
- Key: Name
46-
Value: !Sub 'copilot-${App}-${Env}-${Name}-Aurora'
45+
Value: !Sub "copilot-${App}-${Env}-${Name}-Aurora"
4746
postgreswebDBClusterSecurityGroup:
4847
Metadata:
49-
'aws:copilot:description': 'A security group for your Aurora Serverless v2 cluster postgresweb'
48+
"aws:copilot:description": "A security group for your Aurora Serverless v2 cluster postgresweb"
5049
Type: AWS::EC2::SecurityGroup
5150
Properties:
5251
GroupDescription: The Security Group for the Aurora Serverless v2 cluster.
5352
SecurityGroupIngress:
5453
- ToPort: 5432
5554
FromPort: 5432
5655
IpProtocol: tcp
57-
Description: !Sub 'From the Aurora Security Group of the workload ${Name}.'
56+
Description: !Sub "From the Aurora Security Group of the workload ${Name}."
5857
SourceSecurityGroupId: !Ref postgreswebSecurityGroup
5958
VpcId:
60-
Fn::ImportValue:
61-
!Sub '${App}-${Env}-VpcId'
59+
Fn::ImportValue: !Sub "${App}-${Env}-VpcId"
6260
Tags:
6361
- Key: Name
64-
Value: !Sub 'copilot-${App}-${Env}-${Name}-Aurora'
62+
Value: !Sub "copilot-${App}-${Env}-${Name}-Aurora"
6563
postgreswebAuroraSecret:
6664
Metadata:
67-
'aws:copilot:description': 'A Secrets Manager secret to store your DB credentials'
65+
"aws:copilot:description": "A Secrets Manager secret to store your DB credentials"
6866
Type: AWS::SecretsManager::Secret
6967
Properties:
7068
Description: !Sub Aurora main user secret for ${AWS::StackName}
@@ -76,42 +74,59 @@ Resources:
7674
PasswordLength: 16
7775
postgreswebDBClusterParameterGroup:
7876
Metadata:
79-
'aws:copilot:description': 'A DB parameter group for engine configuration values'
80-
Type: 'AWS::RDS::DBClusterParameterGroup'
77+
"aws:copilot:description": "A DB parameter group for engine configuration values"
78+
Type: "AWS::RDS::DBClusterParameterGroup"
8179
Properties:
82-
Description: !Ref 'AWS::StackName'
83-
Family: 'aurora-postgresql16'
80+
Description: !Ref "AWS::StackName"
81+
Family: "aurora-postgresql16"
8482
Parameters:
85-
client_encoding: 'UTF8'
83+
client_encoding: "UTF8"
8684
postgreswebDBCluster:
8785
Metadata:
88-
'aws:copilot:description': 'The postgresweb Aurora Serverless v2 database cluster'
89-
Type: 'AWS::RDS::DBCluster'
86+
"aws:copilot:description": "The postgresweb Aurora Serverless v2 database cluster"
87+
Type: "AWS::RDS::DBCluster"
9088
Properties:
9189
MasterUsername:
92-
!Join [ "", [ '{{resolve:secretsmanager:', !Ref postgreswebAuroraSecret, ":SecretString:username}}" ]]
90+
!Join [
91+
"",
92+
[
93+
"{{resolve:secretsmanager:",
94+
!Ref postgreswebAuroraSecret,
95+
":SecretString:username}}",
96+
],
97+
]
9398
MasterUserPassword:
94-
!Join [ "", [ '{{resolve:secretsmanager:', !Ref postgreswebAuroraSecret, ":SecretString:password}}" ]]
99+
!Join [
100+
"",
101+
[
102+
"{{resolve:secretsmanager:",
103+
!Ref postgreswebAuroraSecret,
104+
":SecretString:password}}",
105+
],
106+
]
95107
DatabaseName: !Ref postgreswebDBName
96-
Engine: 'aurora-postgresql'
97-
EngineVersion: '16.2'
108+
Engine: "aurora-postgresql"
109+
EngineVersion: "16.2"
110+
EnableHttpEndpoint: true # enable the Data API feature
98111
DBClusterParameterGroupName: !Ref postgreswebDBClusterParameterGroup
99112
DBSubnetGroupName: !Ref postgreswebDBSubnetGroup
100113
Port: 5432
101114
VpcSecurityGroupIds:
102115
- !Ref postgreswebDBClusterSecurityGroup
103116
ServerlessV2ScalingConfiguration:
104117
# Replace "All" below with "!Ref Env" to set different autoscaling limits per environment.
105-
MinCapacity: !FindInMap [postgreswebEnvScalingConfigurationMap, All, DBMinCapacity]
106-
MaxCapacity: !FindInMap [postgreswebEnvScalingConfigurationMap, All, DBMaxCapacity]
118+
MinCapacity:
119+
!FindInMap [postgreswebEnvScalingConfigurationMap, All, DBMinCapacity]
120+
MaxCapacity:
121+
!FindInMap [postgreswebEnvScalingConfigurationMap, All, DBMaxCapacity]
107122
postgreswebDBWriterInstance:
108123
Metadata:
109-
'aws:copilot:description': 'The postgresweb Aurora Serverless v2 writer instance'
110-
Type: 'AWS::RDS::DBInstance'
124+
"aws:copilot:description": "The postgresweb Aurora Serverless v2 writer instance"
125+
Type: "AWS::RDS::DBInstance"
111126
Properties:
112127
DBClusterIdentifier: !Ref postgreswebDBCluster
113128
DBInstanceClass: db.serverless
114-
Engine: 'aurora-postgresql'
129+
Engine: "aurora-postgresql"
115130
PromotionTier: 1
116131
AvailabilityZone: !Select
117132
- 0

infra/copilot/web/manifest.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ variables: # Pass environment variables as key value pairs.
4242

4343
secrets: # Pass secrets from AWS Systems Manager (SSM) Parameter Store.
4444
DJANGO_ALLOWED_HOSTS: /pems/web/DJANGO_ALLOWED_HOSTS # The key is the name of the environment variable, the value is the name of the SSM parameter.
45-
45+
DJANGO_DB_NAME: /pems/web/DJANGO_DB_NAME
46+
DJANGO_DB_USER: /pems/web/DJANGO_DB_USER
47+
DJANGO_DB_PASSWORD: /pems/web/DJANGO_DB_PASSWORD
48+
DJANGO_DB_FIXTURES: /pems/web/DJANGO_DB_FIXTURES
4649
# You can override any of the values defined above by environment.
4750
#environments:
4851
# test:

pems/core/management/commands/ensure_db.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from django.db import DEFAULT_DB_ALIAS
99
from psycopg import Connection, sql
1010

11+
from pems.settings import RUNTIME_ENVIRONMENT, RUNTIME_ENVS, aws_db_credentials
12+
1113

1214
class Command(BaseCommand):
1315
help = (
@@ -16,16 +18,22 @@ class Command(BaseCommand):
1618
)
1719

1820
def _admin_connection(self, database=None):
19-
# Try to get HOST/PORT from the default database settings first
20-
# Fallback to environment variables if not found in settings
21+
# Get HOST/PORT from the default database settings
2122
default_db_settings = settings.DATABASES.get(DEFAULT_DB_ALIAS, {})
2223
db_host = default_db_settings.get("HOST")
2324
db_port = default_db_settings.get("PORT")
2425

25-
postgres_maintenance_db = os.environ.get("POSTGRES_DB", "postgres")
26+
if RUNTIME_ENVIRONMENT() == RUNTIME_ENVS.DEV:
27+
db_credentials = aws_db_credentials()
28+
postgres_maintenance_db = db_credentials.get("dbname")
29+
admin_user = db_credentials.get("username")
30+
admin_password = db_credentials.get("password")
31+
else:
32+
postgres_maintenance_db = os.environ.get("POSTGRES_DB", "postgres")
33+
admin_user = os.environ.get("POSTGRES_USER", "postgres")
34+
admin_password = os.environ.get("POSTGRES_PASSWORD")
35+
2636
target_db = database or postgres_maintenance_db
27-
admin_user = os.environ.get("POSTGRES_USER", "postgres")
28-
admin_password = os.environ.get("POSTGRES_PASSWORD")
2937

3038
if not admin_password:
3139
raise CommandError("POSTGRES_PASSWORD environment variable not set. Cannot establish admin connection.")

pems/settings.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ def RUNTIME_ENVIRONMENT():
4444
return env
4545

4646

47+
def aws_db_credentials():
48+
"""Helper to get AWS database credentials from environment variable."""
49+
postgres_secret = os.environ.get("POSTGRESWEB_SECRET")
50+
return json.loads(postgres_secret)
51+
52+
4753
# Application definition
4854

4955
INSTALLED_APPS = [
@@ -101,26 +107,22 @@ def RUNTIME_ENVIRONMENT():
101107
"default": {
102108
"ENGINE": "django.db.backends.postgresql",
103109
"OPTIONS": {"sslmode": sslmode, "sslrootcert": sslrootcert},
110+
"NAME": os.environ.get("DJANGO_DB_NAME", "django"),
111+
"USER": os.environ.get("DJANGO_DB_USER", "django"),
112+
"PASSWORD": os.environ.get("DJANGO_DB_PASSWORD"),
104113
}
105114
}
106115
if RUNTIME_ENVIRONMENT() == RUNTIME_ENVS.DEV:
107-
postgres_web_secret = os.environ.get("POSTGRESWEB_SECRET")
108-
db_credentials = json.loads(postgres_web_secret)
116+
db_credentials = aws_db_credentials()
109117
DATABASES["default"].update(
110118
{
111-
"NAME": db_credentials.get("dbname"),
112-
"USER": db_credentials.get("username"),
113-
"PASSWORD": db_credentials.get("password"),
114119
"HOST": db_credentials.get("host"),
115120
"PORT": db_credentials.get("port"),
116121
}
117122
)
118123
else:
119124
DATABASES["default"].update(
120125
{
121-
"NAME": os.environ.get("DJANGO_DB_NAME", "django"),
122-
"USER": os.environ.get("DJANGO_DB_USER", "django"),
123-
"PASSWORD": os.environ.get("DJANGO_DB_PASSWORD"),
124126
"HOST": os.environ.get("POSTGRES_HOSTNAME", "postgres"),
125127
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
126128
}

tests/pytest/test_settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import json
2+
3+
from pems.settings import aws_db_credentials
4+
5+
16
def test_runtime_environment_default(settings):
27
assert settings.RUNTIME_ENVIRONMENT() == "local"
38

@@ -10,3 +15,9 @@ def test_runtime_environment__local(settings):
1015
def test_runtime_environment_dev(settings):
1116
settings.ALLOWED_HOSTS = ["*"]
1217
assert settings.RUNTIME_ENVIRONMENT() == "dev"
18+
19+
20+
def test_aws_db_credentials(monkeypatch):
21+
creds = {"host": "db.example.com", "port": 5432}
22+
monkeypatch.setenv("POSTGRESWEB_SECRET", json.dumps(creds))
23+
assert aws_db_credentials() == creds

0 commit comments

Comments
 (0)