- Watch Ashish's Week 4 - Securing Your Amazon RDS Postgres Database
- Create a RDS Postgres Instance - Live Stream
- Bash scripting for common database actions
- Install Postgres Driver in Backend Application
- Connect Gitpod to RDS Instance
- Create Cognito Trigger to insert user into database
- Create new activities with a database insert
None assigned - leaving here in the event I think of any that I want to add to come back to after the bootcamp is finished or as time permits.
- Add Shell Script to Install & Upgrade pip Upon Launching Gitpod
- Configure Gitpod to Auto Configure Private Commit Email for GitHub
- Use Security through Obscurity as an Added Layer of Protection for PostgreSQL Port
Relational Database (SQL)
- MySQL
- SQL Server
- Postgres
These have tables and rows within the tables.
Non-relational Database (NoSQL)
- dynamoDB
These databases are decoupled, there are no strong relationships between the columns and rows.
- Ensure you are in the appropriate region.
- Master password: create a custom, more secure password
- Make sure encryption is enabled
Once the RDS instance is created, we'll look at some security best practices:
- Ensure the correct region is set
- Make sure the RDS instance is not publicly accessible
- This may have to be edited to Publicly accessible, however make sure inbound security group rules are configured to ONLY allow the appropriate users on specific, allowed, IP ranges
- Check the security group rules:
- Outbound rules can go anywhere
- Inbound rules, by default, only allow it to talk to itself
- Security group may need to be edited/another rule may need to be added
- Do NOT allow Source to be set to Anywhere, this will allow anyone anywhere to connect to the database
- For working with a live organization, use custom, set the custom IP address range for the org, OR the VPN IP address that's been provided for all devs
- IAM privileges must be assigned to extend access the RDS database has to other resources in the cloud.
- For production environments: make sure to enable deletion protection & Multi-AZ
If the database is no longer being used, it is best practice to delete the database.
- This will require you to confirm the deletion
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_BestPractices.Security.html
- Use VPCs: Use Amazon Virtual Private Cloud (VPC) to create a private network for your RDS instance.
- This helps prevent unauthorized access to your instance from the public internet.
- Compliance requirements that the organization must meet.
- RDS Instances should only be in the AWS region that you are legally allowed to store user data in.
- GDPR, ISO 27001, PCI DSS, etc.
- Amazon Organizations SCP - manages RDS deletion, creation, region lock, enforce encryption, etc.
- AWS CloudTrail is enabled & monitored to trigger alerts based on malicious RDS behavior by an identity in AWS.
- Amazon Guardduty is enabled on the same account & region of RDS.
- RDS Instance should use appropriate authentication
- Use IAM authentication, Kerberos, etc.
- Do NOT use the default
- Database User Lifecycle Management - Create, modify, & delete users
- AWS User Access Lifecycle Management - Change of roles, revoke roles, etc.
- Security groups should be restricted to only allow known IP addresses
- Do not allow RDS to be internet/publicly accessible
- Encryption for data in transit for comms between Apps & RDS
- Secret Management: Master user passwords can be used with AWS Secrets Manager to automatically rotate the secrets for Amazon RDS
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.html
Add this code into the CLI to create the instance
aws rds create-db-instance \
--db-instance-identifier cruddur-db-instance \
--db-instance-class db.t3.micro \
--engine postgres \
--engine-version 14.6 \
--master-username root \
--master-user-password huEE33z2Qvl383 \
--allocated-storage 20 \
--availability-zone ca-central-1a \
--backup-retention-period 0 \
--port 5432 \
--no-multi-az \
--db-name cruddur \
--storage-type gp2 \
--publicly-accessible \
--storage-encrypted \
--enable-performance-insights \
--performance-insights-retention-period 7 \
--no-deletion-protection
This will take about 10-15 mins
We can temporarily stop an RDS instance for 4 days when we aren't using it.
Notes: Master Password requirements: must be 8 characters long. RDS requirements are between 8-30 characters
- DO NOT SAVE THE PASSWORD IN THE FILE
- We can set this as an env var
- For enhanced security: Change the port to something other than 5432
Error occurred InvalidClientTokenID – the security token included in the request is invalid
More errors occurred while attempting to create my own RDS instance --- this appears to have been an issue due to the indention not being how it needed to be.
Comment out DynamoDB in docker-compose.yml
then run docker compose up
Go back into AWS RDS and check on the RDS instance.
- Click into the instance
- Click Actions
- Click Stop temporarily
Stopping the instance temporarily will result in the instance starting automatically after 7 days.
To connect to psql via the psql client cli tool remember to use the host flag to specific localhost.
psql -Upostgres --host localhost
Common PSQL commands:
\x on -- expanded display when looking at data
\q -- Quit PSQL
\l -- List all databases
\c database_name -- Connect to a specific database
\dt -- List all tables in the current database
\d table_name -- Describe a specific table
\du -- List all users and their roles
\dn -- List all schemas in the current database
CREATE DATABASE database_name; -- Create a new database
DROP DATABASE database_name; -- Delete a database
CREATE TABLE table_name (column1 datatype1, column2 datatype2, ...); -- Create a new table
DROP TABLE table_name; -- Delete a table
SELECT column1, column2, ... FROM table_name WHERE condition; -- Select data from a table
INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...); -- Insert data into a table
UPDATE table_name SET column1 = value1, column2 = value2, ... WHERE condition; -- Update data in a table
DELETE FROM table_name WHERE condition; -- Delete data from a table
MySQL doesn’t use \q – just q
Enter the following into the CLI to list the databases postgres template0 and template1 should display
\l
We can use the createdb
command to create our database:
https://www.postgresql.org/docs/current/app-createdb.html
createdb cruddur -h localhost -U postgres
psql -U postgres -h localhost
\l
DROP database cruddur;
We can create the database within the PSQL client
CREATE database cruddur;
We'll create a new SQL file called schema.sql
and we'll place it in backend-flask/db
We are going to have Postgres generate out UUIDs. We'll need to use an extension called:
CREATE EXTENSION "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
Postgres comes with some extensions to turn on.
- Make sure these extensions are available on AWS.
The command to import:
cd backend-flask
psql cruddur < db/schema.sql -h localhost -U postgres
Test the connection_url by typing in:
psql postgresql://postgres:password@localhost:5432/cruddur
Successfully connected to postgres DB without having to enter the password in.
\q
export CONNECTION_URL="postgresql://postgres:password@localhost:5432/cruddur"
psql $CONNECTION_URL
\q
gp env CONNECTION_URL="postgresql://postgres:password@localhost:5432/cruddur"
Set aside
export PROD_CONNECTION_URL="postgresql://masterusername:masterpassword@cruddur-db-instance.endpoint.amazonaws.com:5432/cruddur"
gp env PROD_CONNECTION_URL="postgresql://masterusername:masterpassword@cruddur-db-instance.endpoint.amazonaws.com:5432/cruddur"
In the backend-flask directory, create these folders and files without extensions.
mkdir /workspace/aws-bootcamp-cruddur-2023/backend-flask/bin
Within these files, we need to run a bash script.
- Before adding the shebang to the files, we need to find where bash is.
Run the following command:
whereis bash
bin/db-drop
#! /usr/bin/bash
psql $CONNECTION_URL -c "DROP database cruddur;"
Before we can run this, we need to give it permission otherwise running the following will result in "Permission denied"
./bin/db-create
ls -l ./bin
r= read
w= write
x= executable
We want to allow these files to become executable
chmod u+x bin/db-create
chmod u+x bin/db-drop
chmod u+x bin/db-schema-load
OR
chmod 644 bin/db-create
chmod 644 bin/db-drop
chmod 644 bin/db-schema-load
./bin/db-drop
This resulted in an error due to us being currently connected to the open database.
Our CONNECTION_URL needs to exclude the database name.
- To do this in bash, we can use a tool called sed to manipulate text in place.
- This tool doesn't always work the same way in every linux system.
echo "db-drop"
# sed allows us to manipulate strings
# s = substitute
# what do we want to select - in the first / /
# what do we want to replace it with in the 2nd / /
# \ will escape the / that we want to replace
# g = global, which means that all matching occurrences in the line would be replaced
NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$CONNECTION_URL")
psql $NO_DB_CONNECTION_URL -c "DROP database cruddur;"
Run the following command
./bin/db-drop
After adding sed to db-drop
the DB successfully dropped.
Add the following in db-create
echo "db-create"
NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$CONNECTION_URL")
CREATE database cruddur;
Run the following command
./bin/db-create
echo "db-schema-load"
psql $CONNECTION_URL cruddur < db/schema.sql
cd ..
./backend-flask/bin/db-schema-load
The path is executing relative to where we are.
- In order to get this to work with the current way
db-schema-load
is written, we would need to edit it slightly to bebackend-flask/db/schema.sql
. But we don't want this. - We will need to use real path to find the
schema.sql
file
$echo realpath
echo "db-schema-load"
psql $CONNECTION_URL cruddur < db/schema.sql
Try to execute the file again
./backend-flask/bin/db-schema-load
$echo realpath .
echo "db-schema-load"
psql $CONNECTION_URL cruddur < db/schema.sql
Try to execute the file again
./backend-flask/bin/db-schema-load
Same error occurred...
$echo $(realpath .)
echo "db-schema-load"
psql $CONNECTION_URL cruddur < db/schema.sql
Try to execute the file again
./backend-flask/bin/db-schema-load
cd backend-flask
./bin/db-schema-load
- This time it worked
schema_path = $(realpath ..)/db/schema.sql
echo $schema_path
echo "db-schema-load"
psql $CONNECTION_URL cruddur < db/schema.sql
echo "db-schema-load"
schema_path="$(realpath ..)/db/schema.sql"
echo $schema_path
psql $CONNECTION_URL cruddur < $schema_path
The absolute path for schema.sql
is listed:
/workspace/aws-bootcamp-cruddur-2023/db/schema.sql
Only works from within backend-flask
In db-schema-load
add the following & make the appropriate edits:
echo "ARG FIRST"
# echo $0 --- not needed
echo $1
echo "db-schema-load"
schema_path="$(realpath .)/db/schema.sql"
echo $schema_path
psql $CONNECTION_URL cruddur < db/schema.sql
Then run the following command
./bin/db-schema-load prod
Using $0
& $1
didn't narrow down to what we expected which was just prod
Removed $0
and $1
was the key to showing prod environment
echo "db-schema-load"
schema_path="$(realpath .)/db/schema.sql"
echo $schema_path
if [ "$1" = "prod" ]; then
echo "using production"
CON_URL=$CONNECTION_URL
else
CON_URL=$CONNECTION_URL
fi
psql $CON_URL cruddur < $schema_path
Then run the following
./bin/db-schema-load prod
We we can make prints for our shell scripts coloured so we can see what we're doing:
https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux
CYAN='\033[1;36m'
NO_COLOR='\033[0m'
LABEL="db-schema-load"
printf "${CYAN}== ${LABEL}${NO_COLOR}\n"
https://www.postgresql.org/docs/current/sql-createtable.html
Think of these schemas as namespaces, by default they are set to public.
- Defining them as public out of good habit
DROP TABLE IF EXISTS public.users;
DROP TABLE IF EXISTS public.activities;
CREATE TABLE public.users (
uuid UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
display_name text,
handle text,
cognito_user_id text,
created_at TIMESTAMP default current_timestamp NOT NULL
);
CREATE TABLE public.activities (
uuid UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_uuid UUID NOT NULL,
message text NOT NULL,
replies_count integer DEFAULT 0,
reposts_count integer DEFAULT 0,
likes_count integer DEFAULT 0,
reply_to_activity_uuid integer,
expires_at TIMESTAMP,
created_at TIMESTAMP default current_timestamp NOT NULL
);
./bin/db-schema-load
#! /usr/bin/bash
psql $CONNECTION_URL
Run the following:
chmod u+x ./bin/db-connect
./bin/db-connect
\dt
- Both tables should display in the CLI.
#! /usr/bin/bash
CYAN='\033[1;36m'
NO_COLOR='\033[0m'
LABEL="db-seed"
printf "${CYAN}== ${LABEL}${NO_COLOR}\n"
echo "db-schema-load"
seed_path="$(realpath .)/db/seed.sql"
echo $seed_path
if [ "$1" = "prod" ]; then
echo "Running in production mode"
CON_URL=$PROD_CONNECTION_URL
else
CON_URL=$CONNECTION_URL
fi
psql $CON_URL cruddur < $seed_path
INSERT INTO public.users (display_name, handle, cognito_user_id)
VALUES
('Andrew Brown', 'andrewbrown' ,'MOCK'),
('Andrew Bayko', 'bayko' ,'MOCK');
INSERT INTO public.activities (user_uuid, message, expires_at)
VALUES
(
(SELECT uuid from public.users WHERE users.handle = 'andrewbrown' LIMIT 1),
'This was imported as seed data!',
current_timestamp + interval '10 day'
)
chmod u+x ./bin/db-seed
./bin/db-schema-load
./bin/db-seed
Looking in the schema.sql
file, we didn't define the user_uuid.
Add the following under public.activities:
user_uuid UUID NOT NULL,
./bin/db-schema-load
./bin/db-seed
Run the following commands
./bin/db-connect
\dt
\x on
SELECT * FROM activities;
- Timestamps will present us with a challenge.
- Do we store them in UTC or in the local time zone?
Within Gitpod, open the Database Explorer on the left.
- Click onto the PostgreSQL tab
- Type in Cruddur for the Connection Name
- Host: 127.0.0.1
- Port: 5432
- Username: postgres
- Password
- Databases: postgres
- Then click Connect
Try to drop the connection from the DB
We need to create a way to see active connections.
NO_DB_URL=$(sed 's/\/cruddur//g' <<<"$URL")
psql $NO_DB_URL -c "select pid as process_id, \
usename as user, \
datname as db, \
client_addr, \
application_name as app,\
state \
from pg_stat_activity;"
We could have idle connections left open by our Database Explorer extension, try disconnecting and checking again the sessions
#! /usr/bin/bash
CYAN='\033[1;36m'
NO_COLOR='\033[0m'
LABEL="db-sessions"
printf "${CYAN}== ${LABEL}${NO_COLOR}\n"
if [ "$1" = "prod" ]; then
echo "Running in production mode"
URL=$PROD_CONNECTION_URL
else
RL=$CONNECTION_URL
fi
NO_DB_URL=$(sed 's/\/cruddur//g' <<<"$URL")
psql $NO_DB_URL -c "select pid as process_id, \
usename as user, \
datname as db, \
client_addr, \
application_name as app,\
state \
from pg_stat_activity;"
chmod u+x bin/db-sessions
^^ hit the error above after not changing every variable to URL. After correcting this in db-sessions
I was able to successfully display the active connections to my database.
Going back to database explorer
- Right click on the database
- Close connection
I was unable to terminate the active connection.
- Attempting to do docker compose up again before trying to display the active connection again.
One session remains idle.
- Running docker compose down
- Running
./bin/db-sessions
to confirm the connections are no longer active - Running docker compose up again
- Running
./bin/db-sessions
to check active sessions and only one shows as active, as expected
Note: try to stay away from the Database Explorer section since it opens connections and doesn't properly close them out.
#! /usr/bin/bash
-e # stop if it fails at any point
CYAN='\033[1;36m'
NO_COLOR='\033[0m'
LABEL="db-setup"
printf "${CYAN}==== ${LABEL}${NO_COLOR}\n"
bin_path="$(realpath .)/bin"
source "$bin_path/db-drop"
source "$bin_path/db-create"
source "$bin_path/db-schema-load"
source "$bin_path/db-seed"
chmod u+x bin/db-setup
Add the following to our requirments.txt
psycopg[binary]
psycopg[pool]
Then run:
pip install -r requirements.txt
https://www.psycopg.org/psycopg3/
We are using connection pooling.
Create a new file lib/db.py
from psycopg_pool import ConnectionPool
import os
def query_wrap_object(template):
sql = f"""
(SELECT COALESCE(row_to_json(object_row),'{{}}'::json) FROM (
{template}
) object_row);
"""
return sql
def query_wrap_array(template):
sql = f"""
(SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM (
{template}
) array_row);
"""
return sql
connection_url = os.getenv("CONNECTION_URL")
pool = ConnectionPool(connection_url)
Edited the following:
sql = '''
To be
sql = f"""
Set the backend-flask connection env var in docker-compose.yml
CONNECTION_URL: "${CONNECTION_URL}"
We need to replace our mock endpoint with a real API call
Originally, to test connecting home_activities, we added the following:
from lib.db import pool
sql = """
SELECT * FROM activities
"""
with pool.connection() as conn:
with conn.cursor() as cur:
cur.execute(sql)
# this will return a tuple
# the first field being the data
json = cur.fetchall()
from lib.db import pool, query_wrap_array
Add the following to the bottom of home_activities.py
:
sql = query_wrap_array("""
SELECT
activities.uuid,
users.display_name,
users.handle,
activities.message,
activities.replies_count,
activities.reposts_count,
activities.likes_count,
activities.reply_to_activity_uuid,
activities.expires_at,
activities.created_at
FROM public.activities
LEFT JOIN public.users ON users.uuid = activities.user_uuid
ORDER BY activities.created_at DESC
""")
print(sql)
with pool.connection() as conn:
with conn.cursor() as cur:
cur.execute(sql)
# this will return a tuple
# the first field being the data
json = cur.fetchone()
print("-------")
print(json[0])
return json[0]
return sql
Remove the following:
results = [{
'uuid': '68f126b0-1ceb-4a33-88be-d90fa7109eee',
'handle': 'andrew brown',
'message': 'cloud is fun!',
'created_at': (now - timedelta(days=2)).isoformat(),
'expires_at': (now + timedelta(days=5)).isoformat(),
'likes_count': 5,
'replies_count': 1,
'reposts_count': 0,
'replies': [{
'uuid': '26e12864-1c26-5c3a-9658-97a10f8fea67',
'reply_to_activity_uuid': '68f126b0-1ceb-4a33-88be-d90fa7109eee',
'handle': 'worf',
'message': 'This post has no honor!',
'likes_count': 0,
'replies_count': 0,
'reposts_count': 0,
'created_at': (now - timedelta(days=2)).isoformat()
}],
},
{
'uuid': '66e12864-8c26-4c3a-9658-95a10f8fea67',
'handle': 'worf',
'message': 'I am out of prune juice',
'created_at': (now - timedelta(days=7)).isoformat(),
'expires_at': (now + timedelta(days=9)).isoformat(),
'likes': 0,
'replies': []
},
{
'uuid': '248959df-3079-4947-b847-9e0892d1bab4',
'handle': 'garek',
'message': 'my dear doctor, i am just simple tailor',
'created_at': (now - timedelta(hours=1)).isoformat(),
'expires_at': (now + timedelta(hours=12)).isoformat(),
'likes': 0,
'replies': []
}
]
if cognito_user_id != None:
extra_crud = {
'uuid': '248959df-3079-4947-b847-9e0892d1bab4',
'handle': 'Phoenix',
'message': 'My dear human, it is time for bed',
'created_at': (now - timedelta(hours=1)).isoformat(),
'expires_at': (now + timedelta(hours=12)).isoformat(),
'likes': 77,
'replies': []
}
results.insert(0,extra_crud)
Run docker compose up
After running docker compose up, I ran into this error several times. Once I looked at my Docker containers, I noticed my frontend application had not started successfully.
Went into the backend-flask shell to ensure the CONNECTION_URL env var was set correctly.
Double checked that my docker-compose.yml
had the CONNECTION_URL correctly set with 'db' instead of 'localhost'.
I ran docker compose up after changing 'localhost' to 'db' however, I hit a new error.
I ran docker compose down, then docker compose up, but my frontend application was still having issues starting.
Checked the frontend logs.
After viewing the logs, I realized this issue might be because the scripts for my database hadn't been executed. I had closed my Gitpod session before starting to work on installing the postgreSQL driver.
I ran docker compose down, then navigated to the backend-flask directory to attempt running the create script.
cd backend-flask
./bin/db-create
Returned an error: Permission denied
chmod u+x bin/db-create
chmod u+x bin/db-connect
chmod u+x bin/db-drop
chmod u+x bin/db-schema-load
chmod u+x bin/db-seed
chmod u+x bin/db-setup
./bin/db-create
Ran docker compose up again and all services ran as expected.
In order to connect to the RDS instance we need to provide our Gitpod IP and create an allow list for inbound traffic on port 5432 (this port number is the default but may be changed to another port number for additional security).
Note: Security through obscurity should not be used on its own. However, security through obscurity can provide an additional layer of protection while taking a defense in depth approach.
Run the following commands to have your Gitpod IP address returned in the CLI.
GITPOD_IP=$(curl ifconfig.me)
echo $GITPOD_IP
export GITPOD_IP="curl ifconfig.me"
gp env GITPOD_IP="curl ifconfig.me"
We need to get the Security Group Rule ID so we can easily modify it when using Gitpod.
Replace the following with your SGID and SG Rule ID from AWS RDS.
export DB_SG_ID="sg-0b725ebab7e25635e"
gp env DB_SG_ID="sg-0b725ebab7e25635e"
export DB_SG_RULE_ID="sgr-070061bba156cfa88"
gp env DB_SG_RULE_ID="sgr-070061bba156cfa88"
Whenever we need to update our security groups we can do this for access.
aws ec2 modify-security-group-rules \
--group-id $DB_SG_ID \
--security-group-rules "SecurityGroupRuleId=$DB_SG_RULE_ID,SecurityGroupRule={IpProtocol=tcp,FromPort=$PORT,ToPort=$PORT,CidrIpv4=$GITPOD_IP/32}"
Note: I configured my port number to be obscured for added security and ran the following in Gitpod to set the port number as an env var:
export PORT="xxxx"
gp env PORT="xxxx"
https://docs.aws.amazon.com/cli/latest/reference/ec2/modify-security-group-rules.html#examples
Add the following to .gitpod.yml
under postgres
command: |
export GITPOD_IP=$(curl ifconfig.me)
source "$THEIA_WORKSPACE_ROOT/backend-flask/bin/rds-update-sg-rule"
Create a new folder and file aws/lambdas/cruddur-post-confirmation.py
cd aws
mkdir lambdas
cd lambdas
touch cruddur-post-confirmation.py
We need to create a Lambda Function in the same VPC as our RDS instance
- Login and search for Lambda
- Click create function
- Select Author from scratch
- Name the function
- Select the Runtime: we're selecting Python 3.8
- Select the architecture: we're using x86_64
- Skipping advanced settings
- Click Create Function
Note: I took this screenshot after creating the lambda function. My function uses '-' instead of underscores.
We need to add a layer for psycopg2 with one of the below methods for development or production
Added the following layer from https://github.com/jetbridge/psycopg2-lambda-layer
arn:aws:lambda:us-east-1:898466741470:layer:psycopg2-py38:2
Env vars needed for the lambda environment:
PG_HOSTNAME='cruddur-db-instance.czz1cuvepklc.ca-central-1.rds.amazonaws.com'
PG_DATABASE='cruddur'
PG_USERNAME='root'
PG_PASSWORD='huEE33z2Qvl383'
^^ filler text, replaced with my own information by going to the lambda instance and clicking on Configuration > Environment Variables
Add the following to cruddur-post-confirmation.py
import json
import psycopg2
def lambda_handler(event, context):
user = event['request']['userAttributes']
try:
conn = psycopg2.connect(
host=(os.getenv('PG_HOSTNAME')),
database=(os.getenv('PG_DATABASE')),
user=(os.getenv('PG_USERNAME')),
password=(os.getenv('PG_SECRET'))
)
cur = conn.cursor()
cur.execute("INSERT INTO users (display_name, handle, cognito_user_id) VALUES(%s, %s, %s)", (user['name'], user['email'], user['sub']))
conn.commit()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
cur.close()
conn.close()
print('Database connection closed.')
return event
Then copy & paste this into our Code source within AWS Lambda
- Select User Pool Properties
- Add Lambda Trigger
- Trigger type: Sign-up
- Sign-up: Post confirmation trigger
- Assign the Lambda function we created
- Click Add Lambda Trigger
Let's go back over to Lambda to check out the logs.
- Click on Monitor
- Click on Logs
Let's test out the app with a user in production.
- I needed to delete my user from Cognito before testing.
- Then I signed back up for an account
Let's check out Cloudwatch Logs
- Click on
/aws/lambda/cruddur-post-confirmation
- We should see the first log entry appear on the next page
Let's open this up and inspect it to see what's going on!
I decided to ask ChatGPT what was going on with my code after reviewing it manually didn't help me discover the syntax error on line 15. Here's what ChatGPT told me:
I went back to my cruddur-post-confirmation.py
file and edited line 14 with an additional ')' then tried confirming my user account.
I was able to successfully sign in after leaving this page and going back to the sign in page, however, I deleted my user from Cognito again to continue testing.
After deleting my user and signing back up, I received a new error message.
Looking back at the logs in Cloudwatch, I found the recent entry and reviewed it.
After getting this error, I asked ChatGPT to review my code and the error again as I was unsure why line 37 was an issue. The following is its response:
Updated cruddur-post-confirmation.py
with the code from ChatGPT and tested again after deleting the user account from Cognito.
Attempted to resent the activation code but this resulted in the same error as before:
"User cannot be confirmed. Current status is CONFIRMED"
Checked the logs in Cloudwatch.
From here, I updated my Lambda function with the following:
import json
import psycopg2
import os
def lambda_handler(event, context):
user = event['request']['userAttributes']
print('userAttributes')
print(user)
user_display_name = user['name']
user_email = user['email']
user_handle = user['preferred_username']
user_cognito_id = user['sub']
try:
print('entered-try')
sql = f"""
"INSERT INTO users (
display_name,
email,
handle,
cognito_user_id
)
VALUES(
'{user_display_name}',
'{user_email}',
'{user_handle}',
'{user_cognito_id}'
)"
"""
print('SQL Statement ----')
print(sql)
conn = psycopg2.connect(os.getenv('CONNECTION_URL'))
cur = conn.cursor()
cur.execute(sql)
conn.commit()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
cur.close()
conn.close()
print('Database connection closed.')
return event
Going back to our function, I looked under Configuration and Permissions, to check for the Execution role.
We only have a basic role configured for the Execution role so we need to add another in IAM.
- Click on the Role name
- Click on Add permissions
- Attach Policies
- Search for VPC
- All of the options displayed are AWS managed, we want to have a customer managed policy. This will require us to create a policy.
- Click Create policy
- From the Visual editor page
- Select the service: EC2
- From the JSON page:
- Add the following
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DeleteNetworkInterface",
"ec2:DescribeNetworkInterfaces"
],
"Resource": "*"
}
]
}
- Click Next: We will skip the Tags section
- Click Next: Review
- Give the policy a name: AWSLambdaVPCAccessExecutionRole
- Description: Allow Lambda VPC Access
- Click Create policy
- Select the AWSLambdaVPCAccessExecutionRole
- Click Actions
- Click Attach policy
- Select cruddur-post-confirmation
- Click Attach policy
Head back to Lambda and check to make sure the permissions are showing under the Resource Summary for EC2.
The function isn't connected to a VPC, so we will need to connect it.
- Go to Lambda in AWS
- Click on Configuration
- Click on VPC
- Click on Edit
- Select the VPC: we only have one currently
- Select the subnet: we're choosing to select 1a as this is the AZ we are using
- It's recommended to choose at least 2 subnets for Lambda to run functions in HA mode
- We will select 1b as well for redundancy
- Select the security groups: we only have one and it is the default
- Click Save
Now we can go back to our Cognito User pool and delete our user to attempt the sign-up experience again.
We signed in successfully, however, we our table didn't have the user get inserted.
Heading over to Cloudwatch to review the logs, I discovered there was an error.
Deleted user and attempted again after editing cruddur-post-confirmation.py
& the lambda code function in AWS.
Removing an additional " and trying again.
We needed to edit the schema with the following to successfully insert the data into the user table:
display_name text NOT NULL,
handle text NOT NULL,
email text NOT NULL,
cognito_user_id text NOT NULL,
Starting up Gitpod environment after previously closing the session.
We need to make sure our database is created so I am running the following commands
docker compose up
cd backend-flask
./bin/db-setup
After we get our database setup, let's sign into the app.
class Db:
def __init__(self):
self.init_pool()
def init_pool(self):
connection_url = os.getenv("CONNECTION_URL")
self.pool = ConnectionPool(connection_url)
def query_commit():
try:
conn = self.pool.connection()
curr = conn.cursor()
cur.execute(sql)
conn.commit()
except Exception as err:
self.psycopg_exception(err)
# conn.rollback()
def query_wrap_object(template):
sql = f"""
(SELECT COALESCE(row_to_json(object_row),'{{}}'::json) FROM (
{template}
) object_row);
"""
return sql
def query_wrap_array(template):
sql = f"""
(SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM (
{template}
) array_row);
"""
return sql
# define a function that handles and parses psycopg2 exceptions
def psycopg_exception(err):
# get details about the exception
err_type, err_obj, traceback = sys.exc_info()
# get the line number when exception occured
line_num = traceback.tb_lineno
# print the connect() error
print ("\npsycopg ERROR:", err, "on line number:", line_num)
print ("psycopg traceback:", traceback, "-- type:", err_type)
# psycopg2 extensions.Diagnostics object attribute
print ("\nextensions.Diagnostics:", err.diag)
# print the pgcode and pgerror exceptions
print ("pgerror:", err.pgerror)
print ("pgcode:", err.pgcode, "\n")
db = Db()
Add the following:
from lib.db import db
Add the following under the else statement:
self.create_activity()
Under return model, add a new function:
def create_activity(user_uuid, message, expires_at):
sql = f"""
INSERT INTO (
user_uuid,
message,
expires_at
)
VALUES (
"{user_uuid}",
"{message}",
"{expires_at}"
)
"""
# Note: Since we are inserting this data directly into sql, this might be a security concern since we're not sanitizing the data first. We will want to look for a way to do this with python.
query_commit(sql)
# when we want to return an array of json objects
def query_array_json(self,sql):
print("SQL STATEMENT-[array]------")
print(sql + "\n")
wrapped_sql = self.query_wrap_array(sql)
with self.pool.connection() as conn:
with conn.cursor() as cur:
cur.execute(wrapped_sql)
# this will return a tuple
# the first field being the data
json = cur.fetchone()
return json[0]
# when we want to return a json object
def query_object_json(self,sql):
print("SQL STATEMENT-[object]------")
print(sql + "\n")
wrapped_sql = self.query_wrap_object(sql)
with self.pool.connection() as conn:
with conn.cursor() as cur:
cur.execute(wrapped_sql)
# this will return a tuple
# the first field being the data
json = cur.fetchone()
return json[0]
Change
from lib.db import pool, query_wrap_array
to
from lib.db import db
```py
class HomeActivities:
def run(cognito_user_id=None):
#logger.info("HomeActivities")
#with tracer.start_as_current_span("home-activities-handler"):
# now = datetime.now(timezone.utc).astimezone()
results = db.query_array_json("""
SELECT
activities.uuid,
users.display_name,
users.handle,
activities.message,
activities.replies_count,
activities.reposts_count,
activities.likes_count,
activities.reply_to_activity_uuid,
activities.expires_at,
activities.created_at
FROM public.activities
LEFT JOIN public.users ON users.uuid = activities.user_uuid
ORDER BY activities.created_at DESC
""")
return results
Refresh app and view backend logs
I needed to make a few changes to resolve the error.
Added self to the following in db.py
def query_commit(self):
def query_array_json(self,sql):
def query_object_json(self,sql):
def query_wrap_object(self,template):
- We need to use a splat
Splatting is a way to pass a variable number of arguments to a function. There are two types of splatting in Python
*args
used to pass a variable number of arguments**kwargs
used to pass a variable number of keyword arguments to a function
Add the following to db.py
def query_commit_returning_id(self,sql,*kwargs):
print("SQL STATEMENT-[commit-with returning]------")
try:
conn = self.pool.connection()
cur = conn.cursor()
cur.execute(sql,kwargs)
returning_id = cur.fetchone()[0]
conn.commit()
return returning_id
except Exception as err:
self.psycopg_exception(err)
def query_commit(self,sql):
else:
# return an object
expires_at = (now + ttl_offset)
self.create_activity(user_handle,message,expires_at)
def create_activity(handle, message, expires_at):
sql = f"""
INSERT INTO (
user_uuid,
message,
expires_at
)
VALUES (
(SELECT uuid
FROM public.users
WHERE users.handle = %(handle)s
LIMIT 1
),
%(message)s,
%(expires_at)s,
) RETURNING uuid;
"""
uuid = db.query_commit(sql,
handle,
message,
expires_at
)
We have the user's handle but the handle doesn't tell us who they are.
Go back to app, refresh, then click Crud and post a new Crud.
Check the backend logs.
Edited create_activity.py
else:
# return an object
expires_at = (now + ttl_offset)
CreateActivity.create_activity(user_handle,message,expires_at)
Changing query_commit_returning_id
to the following in both db.py
and create_activity.py
def query_commit(self,sql,*kwargs):
Adding the following to db.py
import re
def query_commit(self,sql,*kwargs):
print("SQL STATEMENT-[commit-with returning]------")
print(sql + "\n")
pattern = r"\bRETURNING\b"
is_returning_id = re.search(pattern,sql)
try:
conn = self.pool.connection()
cur = conn.cursor()
cur.execute(sql,kwargs)
if is_returning_id:
returning_id = cur.fetchone()[0]
conn.commit()
if is_returning_id:
return returning_id
except Exception as err:
self.psycopg_exception(err)
Remove the following from db.py
:
def query_commit(self,sql):
print("SQL STATEMENT-[commit]------")
try:
conn = self.pool.connection()
cur = conn.cursor()
cur.execute(sql)
conn.commit()
except Exception as err:
self.psycopg_exception(err)
# conn.rollback()
The following was commented out, removing and trying again.
from lib.db import db
Removing *kwargs
from db.py
& editing the following in create_activity.py
uuid = db.query_commit(sql,{
'handle': handle,
'message': message,
'expires_at': expires_at
})
Trying to post a new crud again but get the following error:
Editing db/py
# missing self
def psycopg_exception(self, err):
Commenting out the following in db.py
# psycopg extensions.Diagnostics object attribute
# print ("\nextensions.Diagnostics:", err.diag)
Commenting out the following in db.py
# print the pgcode and pgerror exceptions
# print ("pgerror:", err.pgerror)
# print ("pgcode:", err.pgcode, "\n")
Editing db.py
after having ChatGPT review my code and the error received.
with self.pool.connection() as conn:
cur = conn.cursor()
VALUES(%s,%s,%s,%s)
"""
print('SQL Statement ----')
print(sql)
conn = psycopg2.connect(os.getenv('CONNECTION_URL'))
cur = conn.cursor()
params = [
user_display_name,
user_email,
user_handle,
user_cognito_id
]
cur.execute(sql,*params)
Making this change will help prevent SQL injection
import json
import psycopg2
import os
def lambda_handler(event, context):
user = event['request']['userAttributes']
print('userAttributes')
print(user)
user_display_name = user['name']
user_email = user['email']
user_handle = user['preferred_username']
user_cognito_id = user['sub']
try:
print('entered-try')
sql = f"""
INSERT INTO public.users (
display_name,
email,
handle,
cognito_user_id
)
VALUES(%s,%s,%s,%s)
"""
print('SQL Statement ----')
print(sql)
conn = psycopg2.connect(os.getenv('CONNECTION_URL'))
cur = conn.cursor()
params = [
user_display_name,
user_email,
user_handle,
user_cognito_id
]
cur.execute(sql,*params)
conn.commit()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
cur.close()
conn.close()
print('Database connection closed.')
return event
cd backend-flask/db
mkdir sql
cd sql
touch create_activity.sql
Cut the following from create_activity.py
to add to create_activity.sql
INSERT INTO public.activities (
user_uuid,
message,
expires_at
)
VALUES (
(SELECT uuid
FROM public.users
WHERE users.handle = %(handle)s
LIMIT 1
),
%(message)s,
%(expires_at)s
) RETURNING uuid;
"""
Edit create_activity.py
sql = db.template('create_activity')
uuid = db.query_commit(sql,{
'handle': handle,
'message': message,
'expires_at': expires_at
})
We need to load a text file into a variable
def template(self, name):
os.path.join(app.root_path, 'db', 'sql', "name" + '.sql')
with open(template_path, 'r') as f:
template_content = f.read()
return template_content
Add the following to the top of db.py
:
from flask import current_app as app
def print_sql(self, title, sql):
cyan = '\033[96m'
no_color = '\033[0m'
print("\n")
print(f'{cyan}SQL STATEMENT-[{title}]------{no_color}')
print(sql + "\n")
Created a new script located under backend-flask/bin
titled install.sh
#!/bin/bash
CYAN='\033[1;36m'
NO_COLOR='\033[0m'
LABEL="install.sh"
printf "${CYAN}== ${LABEL}${NO_COLOR}\n"
pip install --upgrade pip && pip install -r requirements.txt
Updated .gitpod.yml
with the following:
- name: backend-flask
command: |
cd backend-flask
chmod u+x bin/install.sh
./bin/install.sh
I got annoyed having to configure my email and username every time I launched Gitpod so I configured my .gitpod.yml
file to automatically configure these for me.
Set my private email and Github username as Gitpod Env Vars by running the following commands:
export user_email="xxxxxxxx+xxxxxxxxxxxx@users.noreply.github.com"
gp env user_email="xxxxxxxx+xxxxxxxxxxxx@users.noreply.github.com"
export user_name="xxxxxxxxxxxx"
gp env user_name="xxxxxxxxxxxx"
Added the following to .gitpod.yml
- name: github
command: |
git config --global user.email "$user_email"
git config --global user.name "$user_name"
I selected a different port number to use for PROD and wanted to keep it private to ensure additional security was implemented for the PostgreSQL service. When we started configuring a script to automatically update our VPC SG Rules, I attempted to create my port number as an env var to obfuscate the exact port number used so I could push the code to my public repo.
I ran the following in Gitpod to set the port number as an env var:
export PORT="xxxx"
gp env PORT="xxxx"
echo $PORT
After determining this method was successful, I was about to modify my script to obfuscate the port number I selected for the service.
aws ec2 modify-security-group-rules \
--group-id $DB_SG_ID \
--security-group-rules "SecurityGroupRuleId=$DB_SG_RULE_ID,SecurityGroupRule={IpProtocol=tcp,FromPort=$PORT,ToPort=$PORT,CidrIpv4=$GITPOD_IP/32}"
What did I accomplish?
- I successfully created an RDS postgreSQL instance in AWS then created bash scripts for db-connect, db-create, db-drop, db-schema-load, db-seed, db-sessions, and db-setup.
- I also successfully installed the postgreSQL driver for the backend-flask application.
- I connected Gitpod to my RDS instance using a script stored in rds-update-sg-rule.
Were there any obstacles (did I overcome them)?
What were the homework challenges I attempted?
- This week, no homework challenges were assigned, however, I decided to add in a few things to make things easier for myself while using Gitpod.
Added a Shell Script to Install & Upgrade pip Upon Launching Gitpod
- I created a new file in backend-flask/bin to install pip upgrades and the requirements from requirements.txt; this was mainly to save time and prevent user error in the event that I forgot to run pip install -r requirements.txt while in the backend-flask directory.
Configure Gitpod to Auto Configure Private Commit Email for GitHub
- My local environment was originally setup years ago while I was in a software engineering bootcamp. When we setup our environments, we created our GItHub with email privacy enabled. This has caused problems for me when using Gitpod when trying to commit and push changes to my repo.
- I solved this problem by creating Gitpod env vars for my private email provided by GitHub and my GitHub username.
- After setting the env vars, I updated my .gitpod.yml file with a new task for GitHub & the git config --global user.email & user.name to automatically configure the correct information for me upon launching Gitpod.
Use Security through Obscurity as an Added Layer of Protection for PostgreSQL Port
- During the setup process to create an RDS instance in AWS, I wanted to take an additional step to secure the database so I changed the port from 5432 to another port number.
- When we built our script to update the VPC Security Group Inbound Rules in AWS, I added the port number as a GItpod env var to obscure the real port my service is running on for production.
- This env var was added into the script as $PORT for both the FromPort and ToPort.