Skip to content

Feat: use PostgreSQL for the database backend #131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
"name": "caltrans/pems",
"dockerComposeFile": ["../compose.yml"],
"service": "dev",
"runServices": ["dev", "pgweb"],
"forwardPorts": ["docs:8000"],
"workspaceFolder": "/caltrans/app",
"postStartCommand": ["/bin/bash", "bin/reset_db.sh"],
"postStartCommand": ["/bin/bash", "bin/setup.sh"],
"postAttachCommand": ["/bin/bash", ".devcontainer/postAttach.sh"],
"customizations": {
"vscode": {
Expand Down
17 changes: 14 additions & 3 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@ DJANGO_SUPERUSER_USERNAME=pems-admin
DJANGO_SUPERUSER_EMAIL=pems-admin@compiler.la
DJANGO_SUPERUSER_PASSWORD=superuser12345!

# Django storage
# Django Database settings
DJANGO_DB_RESET=true
DJANGO_STORAGE_DIR=.
DJANGO_DB_FILE=django.db
DJANGO_DB_NAME=django
DJANGO_DB_USER=django
DJANGO_DB_PASSWORD=django_password
DJANGO_DB_FIXTURES="pems/local_fixtures.json"

# PostgreSQL settings
POSTGRES_HOSTNAME=postgres
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres_password
POSTGRES_PORT=5432
POSTGRES_SSLMODE=disable

PGWEB_PORT=8081

# Streamlit
STREAMLIT_LOCAL_PORT=8501
# options: hidden, sidebar
Expand Down
8 changes: 0 additions & 8 deletions .github/workflows/tests-pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ jobs:
- name: Check out code
uses: actions/checkout@v4

- name: Install system packages
run: |
sudo apt-get update -y
sudo apt-get install -y gettext

- uses: actions/setup-python@v5
with:
python-version-file: .github/workflows/.python-version
Expand All @@ -33,9 +28,6 @@ jobs:
pip install -e .[test]
pip install -r streamlit_app/requirements.txt

- name: Run setup
run: ./bin/init.sh

- name: Run tests
run: ./tests/pytest/run.sh

Expand Down
3 changes: 3 additions & 0 deletions appcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ ENV GUNICORN_CONF "/$USER/run/gunicorn.conf.py"
# overwrite default nginx.conf
COPY appcontainer/nginx.conf /etc/nginx/nginx.conf

# copy certs for PostgreSQL verify-full
COPY appcontainer/certs/aws_global_postgres_ca_bundle.pem app/certs/aws_global_postgres_ca_bundle.pem
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting a small change I made to how the cert bundle is added to the CDRC image:

This ultimately was because of a change related to the Buildkit caching, where I wasn't copying the entire source directory in anymore during Docker build. That caused some cascading issues with the runtime directory being different from e.g. /cdt/app, and one of the problems was this cert bundle couldn't be found by Django.

This solves it by not making Django have to construct the path, instead it happens at Docker build time and is baked into the image as an env var.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! If it's ok, I'll add this small change to #132 which is about implementing Buildkit caching.


WORKDIR /$USER/app

# copy runtime files
Expand Down
2,660 changes: 2,660 additions & 0 deletions appcontainer/certs/aws_global_postgres_ca_bundle.pem

Large diffs are not rendered by default.

10 changes: 0 additions & 10 deletions bin/init.sh

This file was deleted.

33 changes: 0 additions & 33 deletions bin/reset_db.sh

This file was deleted.

20 changes: 20 additions & 0 deletions bin/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -ex

# Ensure databases, users, migrations, and superuser are set up
should_reset=${REMOTE_CONTAINERS:-false}
if [[ $should_reset == "true" ]]; then
# running in a devcontainer, reset the DB
python manage.py ensure_db --reset
else
python manage.py ensure_db
fi

# Load data fixtures (if any)
valid_fixtures=$(echo "$DJANGO_DB_FIXTURES" | grep -e fixtures\.json$ || test $? = 1)

if [[ -n "$valid_fixtures" ]]; then
python manage.py loaddata $DJANGO_DB_FIXTURES
else
echo "No JSON fixtures to load"
fi
4 changes: 2 additions & 2 deletions bin/start.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
set -eu

# initialize Django
# prepare static files

bin/init.sh
python manage.py collectstatic --no-input

# start the web server

Expand Down
22 changes: 3 additions & 19 deletions bin/start_aws.sh
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
#!/usr/bin/env bash
set -eu

#
# S3 bucket name is injected by Copilot as an environment variable
# since it was created via copilot storage init --name pems-db, the variable is 'PEMSDB_NAME'
S3_BUCKET_NAME="$PEMSDB_NAME"
S3_FIXTURE_PATH="fixtures.json"
LOCAL_FIXTURE_PATH="fixtures.json"

echo "Downloading $S3_FIXTURE_PATH from bucket $S3_BUCKET_NAME"
aws s3 cp "s3://${S3_BUCKET_NAME}/${S3_FIXTURE_PATH}" "${LOCAL_FIXTURE_PATH}"
aws s3 cp "s3://${S3_BUCKET_NAME}/${S3_FIXTURE_PATH}" "${DJANGO_DB_FIXTURES}"
echo "Download complete"

# initialize Django

bin/init.sh

# effectively reset database by loading downloaded fixtures into the database
echo "Loading data from ${LOCAL_FIXTURE_PATH}"
python manage.py loaddata "${LOCAL_FIXTURE_PATH}"
echo "Data loading complete"

# start the web server

nginx

# start the application server

python -m gunicorn -c $GUNICORN_CONF pems.wsgi
bin/setup.sh
bin/start.sh
39 changes: 39 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ services:
context: .
dockerfile: appcontainer/Dockerfile
image: caltrans/pems:web
command: ["-c", "bin/setup.sh && exec bin/start.sh"]
depends_on:
postgres:
condition: service_healthy
env_file: .env
ports:
- "${DJANGO_LOCAL_PORT:-8000}:8000"
Expand All @@ -15,13 +19,44 @@ services:
context: .
dockerfile: .devcontainer/Dockerfile
image: caltrans/pems:dev
depends_on:
- postgres
- pgweb
env_file: .env
# https://code.visualstudio.com/docs/remote/create-dev-container#_use-docker-compose
entrypoint: sleep infinity
volumes:
- ./:/caltrans/app
- ${HOME}/.aws:/home/caltrans/.aws

postgres:
image: postgres:16
environment:
- POSTGRES_DB
- POSTGRES_USER
- POSTGRES_PASSWORD
healthcheck:
test:
["CMD", "pg_isready", "-d", "${POSTGRES_DB}", "-U", "${POSTGRES_USER}"]
interval: 10s
timeout: 60s
retries: 6
start_period: 10s
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- pgdata:/var/lib/postgresql/data

pgweb:
image: sosedoff/pgweb
ports:
- "${PGWEB_PORT:-8081}:8081"
depends_on:
postgres:
condition: service_healthy
environment:
- PGWEB_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable

docs:
image: caltrans/pems:dev
entrypoint: mkdocs
Expand All @@ -41,3 +76,7 @@ services:
- "${STREAMLIT_LOCAL_PORT:-8501}:8501"
volumes:
- ./:/caltrans/app

volumes:
pgdata:
driver: local
148 changes: 148 additions & 0 deletions infra/copilot/web/addons/postgres-web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
Parameters:
App:
Type: String
Description: Your application's name.
Env:
Type: String
Description: The environment name your service, job, or workflow is being deployed to.
Name:
Type: String
Description: Your workload's name.
# Customize your Aurora Serverless cluster by setting the default value of the following parameters.
postgreswebDBName:
Type: String
Description: The name of the initial database to be created in the Aurora Serverless v2 cluster.
Default: postgres
# Cannot have special characters
# Naming constraints: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints
Mappings:
postgreswebEnvScalingConfigurationMap:
dev:
"DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128
"DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128

All:
"DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128
"DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128

Resources:
postgreswebDBSubnetGroup:
Type: "AWS::RDS::DBSubnetGroup"
Properties:
DBSubnetGroupDescription: Group of Copilot private subnets for Aurora Serverless v2 cluster.
SubnetIds:
!Split [",", { "Fn::ImportValue": !Sub "${App}-${Env}-PrivateSubnets" }]
postgreswebSecurityGroup:
Metadata:
"aws:copilot:description": "A security group for your workload to access the Aurora Serverless v2 cluster postgresweb"
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: !Sub "The Security Group for ${Name} to access Aurora Serverless v2 cluster postgresweb."
VpcId:
Fn::ImportValue: !Sub "${App}-${Env}-VpcId"
Tags:
- Key: Name
Value: !Sub "copilot-${App}-${Env}-${Name}-Aurora"
postgreswebDBClusterSecurityGroup:
Metadata:
"aws:copilot:description": "A security group for your Aurora Serverless v2 cluster postgresweb"
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: The Security Group for the Aurora Serverless v2 cluster.
SecurityGroupIngress:
- ToPort: 5432
FromPort: 5432
IpProtocol: tcp
Description: !Sub "From the Aurora Security Group of the workload ${Name}."
SourceSecurityGroupId: !Ref postgreswebSecurityGroup
VpcId:
Fn::ImportValue: !Sub "${App}-${Env}-VpcId"
Tags:
- Key: Name
Value: !Sub "copilot-${App}-${Env}-${Name}-Aurora"
postgreswebAuroraSecret:
Metadata:
"aws:copilot:description": "A Secrets Manager secret to store your DB credentials"
Type: AWS::SecretsManager::Secret
Properties:
Description: !Sub Aurora main user secret for ${AWS::StackName}
GenerateSecretString:
SecretStringTemplate: '{"username": "postgres"}'
GenerateStringKey: "password"
ExcludePunctuation: true
IncludeSpace: false
PasswordLength: 16
postgreswebDBClusterParameterGroup:
Metadata:
"aws:copilot:description": "A DB parameter group for engine configuration values"
Type: "AWS::RDS::DBClusterParameterGroup"
Properties:
Description: !Ref "AWS::StackName"
Family: "aurora-postgresql16"
Parameters:
client_encoding: "UTF8"
postgreswebDBCluster:
Metadata:
"aws:copilot:description": "The postgresweb Aurora Serverless v2 database cluster"
Type: "AWS::RDS::DBCluster"
Properties:
MasterUsername:
!Join [
"",
[
"{{resolve:secretsmanager:",
!Ref postgreswebAuroraSecret,
":SecretString:username}}",
],
]
MasterUserPassword:
!Join [
"",
[
"{{resolve:secretsmanager:",
!Ref postgreswebAuroraSecret,
":SecretString:password}}",
],
]
DatabaseName: !Ref postgreswebDBName
Engine: "aurora-postgresql"
EngineVersion: "16.2"
EnableHttpEndpoint: true # enable the Data API feature
DBClusterParameterGroupName: !Ref postgreswebDBClusterParameterGroup
DBSubnetGroupName: !Ref postgreswebDBSubnetGroup
Port: 5432
VpcSecurityGroupIds:
- !Ref postgreswebDBClusterSecurityGroup
ServerlessV2ScalingConfiguration:
# Replace "All" below with "!Ref Env" to set different autoscaling limits per environment.
MinCapacity:
!FindInMap [postgreswebEnvScalingConfigurationMap, All, DBMinCapacity]
MaxCapacity:
!FindInMap [postgreswebEnvScalingConfigurationMap, All, DBMaxCapacity]
postgreswebDBWriterInstance:
Metadata:
"aws:copilot:description": "The postgresweb Aurora Serverless v2 writer instance"
Type: "AWS::RDS::DBInstance"
Properties:
DBClusterIdentifier: !Ref postgreswebDBCluster
DBInstanceClass: db.serverless
Engine: "aurora-postgresql"
PromotionTier: 1
AvailabilityZone: !Select
- 0
- !GetAZs
Ref: AWS::Region

postgreswebSecretAuroraClusterAttachment:
Type: AWS::SecretsManager::SecretTargetAttachment
Properties:
SecretId: !Ref postgreswebAuroraSecret
TargetId: !Ref postgreswebDBCluster
TargetType: AWS::RDS::DBCluster
Outputs:
postgreswebSecret: # injected as POSTGRESWEB_SECRET environment variable by Copilot.
Description: "The JSON secret that holds the database username and password. Fields are 'host', 'port', 'dbname', 'username', 'password', 'dbClusterIdentifier' and 'engine'"
Value: !Ref postgreswebAuroraSecret
postgreswebSecurityGroup:
Description: "The security group to attach to the workload."
Value: !Ref postgreswebSecurityGroup
Loading
Loading