Skip to content

Commit b9795a3

Browse files
authored
Merge pull request #15 from davidsilva/fixes/noticed-during-video
Things noticed during recording of the video: (1) Try to improve Lamb…
2 parents baa2c50 + 5e7f79e commit b9795a3

File tree

18 files changed

+170
-72
lines changed

18 files changed

+170
-72
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ jobs:
160160
outputfile.txt
161161
env:
162162
NODE_ENV: development
163-
DATABASE_URL: postgres://${{ secrets.DB_USERNAME }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/${{ secrets.DB_NAME }}
164163

164+
# sets up Docker Buildx to enable advanced build features in the workflow.
165165
- name: Set up Docker Buildx
166166
uses: docker/setup-buildx-action@v1
167167

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,13 @@ A lot of `ci.yml` is self-explanatory but here are some notes to help clarify so
117117
* Does an install and build for the Lambda function.
118118
* And finally zips up everything.
119119
* **Update Lambda migration function**: Updates the AWS Lambda function code for database migrations using the AWS CLI. The workflow waits for the update to be completed before the function is invoked.
120+
* **Build and push frontend Docker image**: Note that this step uses a Dockerfile in the root of the project, rather than the Dockerfile in the frontend directory. I wanted to keep the Dockerfile in the frontend directory for local development purposes, e.g., `frontend/Dockerfile` uses the Angular server and port 4200.
120121

121122
## Infrastructure
122123

123124
![Infrastructure Diagram](images/Interview-Prep-Infrastructure-v2.drawio.png)
124125

125-
The diagram above illustrates the major parts of the application infrastructure:
126+
The diagram above illustrates the major parts of the application infrastructure. Here are some descriptions and notes:
126127

127128
* **Interview Prep VPC**: "The Virtual Private Cloud (VPC) is a logically isolated network within the AWS cloud where we can launch and manage AWS resources. It provides a secure environment to group and connect related resources and services, such as EC2 instances, RDS databases, and ECS clusters. The VPC allows us to define our own IP address range, create subnets, and configure route tables and network gateways, ensuring that our infrastructure is both secure and scalable." (GitHub Copilot came up with such a great explanation here that I'm just going to use it as-is.)
128129
* **Availability zones A and B**: `us-east-1a` and `us-east-1b`. These zones, along with their corresponding public and private subnets, enhance the app's resilience. Currently, one task each for the ECS frontend and backend is deployed, but this can be scaled to distribute tasks across both availability zones.
@@ -134,13 +135,20 @@ The diagram above illustrates the major parts of the application infrastructure:
134135
* **Public route table**: The public routing table is associated with the public subnets and directs traffic to the internet through the Internet Gateway. This allows resources in the public subnets, such as the load balancer and bastion host, to communicate with the internet.
135136
* **Private route table**: The private routing table is associated with the private subnets and directs traffic to the internet through the NAT Gateway. This allows resources in the private subnets, such as the ECS services and RDS database, to access the internet for updates and patches while keeping them isolated from direct internet access.
136137
* **Internet gateway**: Allows resources within the VPC to communicate with the internet.
137-
* **NAT gateway**: The NAT gateway is in public subnet A but both private subnets can use it via the private route table. We *could* add a NAT gateway to public subnet B to ensure higher availability and fault tolerance.
138+
* **NAT gateway**: The NAT gateway is in public subnet A but both private subnets can use it via the private route table. We *could* add a NAT gateway to public subnet B to ensure higher availability and fault tolerance. The NAT gateway is used, for example, by ECS tasks to pull Docker images from the ECR. It's also used by the migrate Lambda function to get values from AWS Systems Manager Parameter Store.
138139
* **API gateway**: This serves as a proxy that forwards the path, data, method, and other request details to the backend API server. This allows the backend to handle the actual processing of the requests. The API gateway is set up to handle CORS, limiting web browser requests to pages served from our frontend host. In the future we'll add an API key and authorization to restrict usage of the API.
139140
* **ECR for hosting frontend and backend Docker images**: Our GitHub workflow builds and pushes Docker images of our frontend and backend apps to the AWS Elastic Container Registry, tagging the latest build as... "latest."
140141
* **ECS cluster with frontend and backend ECS services**: The GitHub workflow updates the backend and frontend services in the AWS Elastic Container Service. These services are where our frontend and backend servers actually run, providing the necessary environments for our applications to operate.
141142
* **RDS-hosted Postgres database instance**: The application uses an instance of Postgres hosted by the AWS Relational Database Service. It runs in the private subnets.
142143
* **Migrate Lambda function**: This is a function run by the GitHub workflow. A workflow step packages up the migration files with the Lambda function itself and then invokes the function.
143144
* **Route 53-hosted domains**: `dev.interviewprep.onyxdevtutorials.com` and `api.dev.interviewprep.onyxdevtutorials.com`. The DNS configuration in Route 53 connects the frontend and backend domains to the load balancer. This is achieved using alias records that point to the load balancer's DNS name and zone ID.
145+
* **CIDR Blocks**: CIDR (Classless Inter-Domain Routing) blocks are used to define IP address ranges within the VPC.
146+
* **VPC CIDR Block**: This is set to `10.0.0.0/16`, allowing for 65,536 possible IP addresses -- which is plenty for this project.
147+
* **Subnet CIDR Blocks**: Each subnet gets 256 IP addresses:
148+
* **Public Subnet A**: 10.0.1.0/24 provides 256 IP addresses.
149+
* **Public Subnet B**: 10.0.2.0/24 provides 256 IP addresses.
150+
* **Private Subnet A**: 10.0.3.0/24 provides 256 IP addresses.
151+
* **Private Subnet B**: 10.0.4.0/24 provides 256 IP addresses.
144152

145153
## Costs
146154

@@ -260,6 +268,16 @@ Once you're connected to the bastion host, you can directly access the database:
260268

261269
`psql -h <db-endpoint> -U <username> -d <db-name>`
262270

271+
Once connected to the database, you can type `\l` to list all the databases managed by the PostgreSQL server you are connected to.
272+
273+
Type `\dt` to list all the tables in the current database.
274+
275+
Type `\d products` or `\d users` to get information about each of these tables.
276+
277+
## Add New Migration File
278+
279+
Assuming CWD is `backend`, `npx knex migrate:make <migration-file-name> --knexfile ./src/knexFile.ts --migrations-directory ../migrations`.
280+
263281
## Version History
264282

265283
### 0.1.0

backend/migrate-lambda/src/migrate.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
55
const ssmClient = new SSMClient({ region: 'us-east-1' });
66

77
const getSSMParameter = async (name: string): Promise<string> => {
8-
const command = new GetParameterCommand({ Name: name, WithDecryption: true });
9-
const response = await ssmClient.send(command);
10-
return response.Parameter?.Value || '';
8+
if (!name) {
9+
throw new Error('SSM parameter name is required');
10+
}
11+
const command = new GetParameterCommand({ Name: name, WithDecryption: true });
12+
const response = await ssmClient.send(command);
13+
return response.Parameter?.Value || '';
1114
};
1215

1316
export const runMigrations = async () => {
@@ -16,11 +19,11 @@ export const runMigrations = async () => {
1619
const config = knexConfig[env];
1720

1821
config.connection = {
19-
host: await getSSMParameter(`/interview-prep/${env}/DB_HOST`),
20-
user: await getSSMParameter(`/interview-prep/${env}/DB_USERNAME`),
21-
password: await getSSMParameter(`/interview-prep/${env}/DB_PASSWORD`),
22-
database: await getSSMParameter(`/interview-prep/${env}/DB_NAME`),
23-
port: parseInt(await getSSMParameter(`/interview-prep/${env}/DB_PORT`), 10),
22+
host: await getSSMParameter(process.env.DB_HOST_PARAM || ''),
23+
user: await getSSMParameter(process.env.DB_USER_PARAM || ''),
24+
password: await getSSMParameter(process.env.DB_PASS_PARAM || ''),
25+
database: await getSSMParameter(process.env.DB_NAME_PARAM || ''),
26+
port: parseInt(await getSSMParameter(process.env.DB_PORT_PARAM || ''), 10),
2427
ssl: { rejectUnauthorized: false },
2528
};
2629

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Knex } from "knex";
2+
3+
4+
export async function up(knex: Knex): Promise<void> {
5+
return knex.schema.table('users', (table) => {
6+
table.string('favorite_color').nullable();
7+
});
8+
}
9+
10+
11+
export async function down(knex: Knex): Promise<void> {
12+
return knex.schema.table('users', (table) => {
13+
table.dropColumn('favorite_color');
14+
});
15+
}
16+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Knex } from "knex";
2+
3+
4+
export async function up(knex: Knex): Promise<void> {
5+
return knex.schema.table('users', (table) => {
6+
table.string('favorite_song').nullable();
7+
});
8+
}
9+
10+
11+
export async function down(knex: Knex): Promise<void> {
12+
return knex.schema.table('users', (table) => {
13+
table.dropColumn('favorite_song');
14+
});
15+
}
16+

terraform/environments/development/main.tf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ module "rds" {
4343
db_username = var.db_username
4444
db_password = var.db_password
4545
db_sg_id = module.security_groups.db_sg_id
46-
lambda_sg_id = module.security_groups.lambda_sg_id
4746
environment = var.environment
4847
}
4948

terraform/environments/development/outputs.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ output "ecs_task_execution_role_arn" {
4848
value = module.iam.ecs_task_execution_role_arn
4949
}
5050

51+
output "ecs_task_role_arn" {
52+
description = "The ARN of the ECS task role"
53+
value = module.iam.ecs_task_role_arn
54+
}
55+
5156
output "lambda_exec_role_arn" {
5257
description = "The ARN of the Lambda execution role"
5358
value = module.iam.lambda_exec_role_arn

terraform/modules/api_gateway/main.tf

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,24 @@ resource "aws_api_gateway_rest_api" "api" {
66
resource "aws_api_gateway_resource" "proxy" {
77
rest_api_id = aws_api_gateway_rest_api.api.id
88
parent_id = aws_api_gateway_rest_api.api.root_resource_id
9-
path_part = "{proxy+}"
9+
path_part = "{proxy+}" # Path part that acts as a catch-all proxy for any request path.
1010

11-
depends_on = [ aws_api_gateway_rest_api.api ]
11+
depends_on = [ aws_api_gateway_rest_api.api ] # Ensure the API is created before creating the resource.
1212
}
1313

1414
resource "aws_api_gateway_method" "proxy_method" {
1515
rest_api_id = aws_api_gateway_rest_api.api.id
1616
resource_id = aws_api_gateway_resource.proxy.id
17-
http_method = "ANY"
18-
authorization = "NONE"
19-
api_key_required = false
17+
http_method = "ANY" # Handle every type of HTTP request
18+
authorization = "NONE" # No authorization required (yet)
19+
api_key_required = false # No API key required (yet)
2020
request_parameters = {
2121
"method.request.path.proxy" = true
2222
}
23+
# This configuration allows the API Gateway to serve as a proxy for my actual backend application, handling all types of HTTP requests and forwarding them to the backend.
2324
}
2425

26+
// Define the OPTIONS method for the proxy resource (for CORS preflight requests)
2527
resource "aws_api_gateway_method" "proxy_options" {
2628
rest_api_id = aws_api_gateway_rest_api.api.id
2729
resource_id = aws_api_gateway_resource.proxy.id
@@ -30,12 +32,14 @@ resource "aws_api_gateway_method" "proxy_options" {
3032
api_key_required = false
3133
}
3234

35+
# Define the integration between the proxy resource and the backend application. Basically, the API Gateway will forward all requests to the backend application.
3336
resource "aws_api_gateway_integration" "proxy_integration" {
3437
rest_api_id = aws_api_gateway_rest_api.api.id
3538
resource_id = aws_api_gateway_resource.proxy.id
3639
http_method = aws_api_gateway_method.proxy_method.http_method
3740
type = "HTTP_PROXY"
3841
integration_http_method = "ANY"
42+
# Load balancer knows that port 3000 is the backend application
3943
uri = "http://${var.lb_dns_name}:3000/{proxy}"
4044
request_parameters = {
4145
"integration.request.path.proxy" = "method.request.path.proxy"
@@ -66,6 +70,7 @@ resource "aws_api_gateway_integration" "health_integration" {
6670
uri = "http://${var.lb_dns_name}:3000/health"
6771
}
6872

73+
# Defines how API Gateway should handle the OPTIONS method for the proxy resource. In this case, it uses a MOCK integration to generate a mock response.
6974
resource "aws_api_gateway_integration" "proxy_options_integration" {
7075
rest_api_id = aws_api_gateway_rest_api.api.id
7176
resource_id = aws_api_gateway_resource.proxy.id
@@ -76,6 +81,9 @@ resource "aws_api_gateway_integration" "proxy_options_integration" {
7681
}
7782
}
7883

84+
# In Amazon API Gateway, an aws_api_gateway_method_response specifies the possible responses from an API Gateway, while an aws_api_gateway_integration_response maps the response from an integration to the API Gateway response.
85+
86+
# This resource specifies the response parameters (headers) that the integration should return. It is part of the integration setup and tells API Gateway what to include in the response when the OPTIONS method is called.
7987
resource "aws_api_gateway_integration_response" "proxy_options_integration_response" {
8088
rest_api_id = aws_api_gateway_rest_api.api.id
8189
resource_id = aws_api_gateway_resource.proxy.id
@@ -88,6 +96,7 @@ resource "aws_api_gateway_integration_response" "proxy_options_integration_respo
8896
}
8997
}
9098

99+
# This resource specifies the method response parameters (headers) that the method should return. It is part of the method setup and ensures that the headers specified in the integration response are actually included in the final response sent to the client.
91100
resource "aws_api_gateway_method_response" "proxy_options_response" {
92101
rest_api_id = aws_api_gateway_rest_api.api.id
93102
resource_id = aws_api_gateway_resource.proxy.id
@@ -109,15 +118,18 @@ resource "aws_api_gateway_deployment" "api_deployment" {
109118
]
110119
rest_api_id = aws_api_gateway_rest_api.api.id
111120

121+
# This effectively triggers a redeployment whenever I do `terraform apply`, even if there are no actual changes to the configuration. I need to experiment with this setting.
112122
triggers = {
113123
redeployment = "${timestamp()}"
114124
}
115125

126+
# Minimize downtime by creating the new deployment before destroying the old one. And... because I don't think AWS would let me destroy the API given that it's in use by the load balancer.
116127
lifecycle {
117128
create_before_destroy = true
118129
}
119130
}
120131

132+
# An API Gateway stage is a logical reference to a lifecycle state of your API (for example, dev, test, prod). Stages are used to manage and deploy different versions of your API, allowing you to test changes in a development environment before promoting them to production.
121133
resource "aws_api_gateway_stage" "api_stage" {
122134
deployment_id = aws_api_gateway_deployment.api_deployment.id
123135
rest_api_id = aws_api_gateway_rest_api.api.id
@@ -132,11 +144,11 @@ resource "aws_api_gateway_stage" "api_stage" {
132144
resource "aws_api_gateway_method_settings" "api_method_settings" {
133145
rest_api_id = aws_api_gateway_rest_api.api.id
134146
stage_name = aws_api_gateway_stage.api_stage.stage_name
135-
method_path = "*/*"
147+
method_path = "*/*" # The path and method for which these settings apply. The format is HTTP_METHOD/RESOURCE_PATH. You can use */* to apply the settings to all methods and resources.
136148
settings {
137-
metrics_enabled = true
138-
logging_level = "INFO"
139-
data_trace_enabled = true
149+
metrics_enabled = true # Enable CloudWatch metrics for the method.
150+
logging_level = "INFO" # E.g., INFO, ERROR
151+
data_trace_enabled = true # Can generate a large volume of log data, especially for APIs with high traffic or large payloads.
140152
}
141153
}
142154

@@ -145,6 +157,10 @@ resource "aws_cloudwatch_log_group" "api_gateway_log_group" {
145157
retention_in_days = 7
146158
}
147159

160+
# IAM stuff should probably be in IAM module.
161+
162+
# It could be to have a root-level configuration to enable logging for the various modules.
163+
148164
resource "aws_iam_role" "api_gateway_cloudwatch_role" {
149165
name = "${var.environment}-interview-prep-api-gateway-cloudwatch-role"
150166
assume_role_policy = jsonencode({
@@ -185,16 +201,18 @@ resource "aws_iam_role_policy_attachment" "api_gateway_cloudwatch_policy_attachm
185201
role = aws_iam_role.api_gateway_cloudwatch_role.name
186202
}
187203

204+
# custom_domain_name and custom_domain_zone_id are output and used in the dns module.
188205
resource "aws_api_gateway_domain_name" "custom_domain" {
189206
domain_name = "api.dev.interviewprep.onyxdevtutorials.com"
190207

191208
endpoint_configuration {
192-
types = ["EDGE"]
209+
types = ["EDGE"] # The endpoint type (EDGE, REGIONAL, or PRIVATE)
193210
}
194211

195-
certificate_arn = var.certificate_arn
212+
certificate_arn = var.certificate_arn # The ARN of the SSL certificate to use for the custom domain.
196213
}
197214

215+
# Used to map the custom domain to the API Gateway stage.
198216
resource "aws_api_gateway_base_path_mapping" "custom_domain_mapping" {
199217
api_id = aws_api_gateway_rest_api.api.id
200218
stage_name = aws_api_gateway_stage.api_stage.stage_name

terraform/modules/bastion/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ resource "aws_instance" "bastion" {
33
instance_type = var.instance_type
44
subnet_id = var.public_subnet_id
55
vpc_security_group_ids = [var.bastion_sg_id]
6-
key_name = var.key_name
6+
key_name = var.key_name # SSH key pair name
77

88
tags = {
99
Name = "${var.environment}-interview-prep-bastion"

terraform/modules/ecr/main.tf

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
resource "aws_ecr_repository" "frontend" {
22
name = "interview-prep-frontend"
3-
image_tag_mutability = "MUTABLE"
3+
image_tag_mutability = "MUTABLE" # Allows using a tag like "latest" to refer to the latest version of the image.
4+
# Allows you to enable or disable automatic scanning of container images for vulnerabilities when they are pushed to the repository.
45
image_scanning_configuration {
5-
scan_on_push = true
6+
scan_on_push = true # Images are automatically scanned for vulnerabilities when they are pushed to the repository.
67
}
78

89
tags = {
@@ -14,8 +15,9 @@ resource "aws_ecr_repository" "frontend" {
1415
resource "aws_ecr_repository" "backend" {
1516
name = "interview-prep-backend"
1617
image_tag_mutability = "MUTABLE"
18+
# Allows you to enable or disable automatic scanning of container images for vulnerabilities when they are pushed to the repository.
1719
image_scanning_configuration {
18-
scan_on_push = true
20+
scan_on_push = true # Images are automatically scanned for vulnerabilities when they are pushed to the repository.
1921
}
2022

2123
tags = {

0 commit comments

Comments
 (0)