week-7: PostgreSQL RDS integration — terraform, backend, pipeline
This commit is contained in:
@@ -278,6 +278,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TF_VAR_aws_account_id: ${{ secrets.TF_VAR_AWS_ACCOUNT_ID }}
|
TF_VAR_aws_account_id: ${{ secrets.TF_VAR_AWS_ACCOUNT_ID }}
|
||||||
TF_VAR_aws_region: ${{ secrets.TF_VAR_AWS_REGION }}
|
TF_VAR_aws_region: ${{ secrets.TF_VAR_AWS_REGION }}
|
||||||
|
TF_VAR_db_name: ${{ secrets.TF_VAR_db_name }}
|
||||||
|
TF_VAR_db_username: ${{ secrets.TF_VAR_db_username }}
|
||||||
|
TF_VAR_db_password: ${{ secrets.TF_VAR_db_password }}
|
||||||
|
TF_VAR_db_host: ${{ secrets.TF_VAR_db_host }}
|
||||||
|
|
||||||
terraform-apply:
|
terraform-apply:
|
||||||
name: Terraform Apply
|
name: Terraform Apply
|
||||||
@@ -309,8 +313,35 @@ jobs:
|
|||||||
- name: Terraform Init
|
- name: Terraform Init
|
||||||
run: terraform init
|
run: terraform init
|
||||||
|
|
||||||
- name: Terraform Apply
|
- name: Terraform Apply — provision RDS
|
||||||
run: terraform apply -auto-approve -lock-timeout=5m
|
run: terraform apply -auto-approve -lock-timeout=5m
|
||||||
env:
|
env:
|
||||||
TF_VAR_aws_account_id: ${{ secrets.TF_VAR_AWS_ACCOUNT_ID }}
|
TF_VAR_aws_account_id: ${{ secrets.TF_VAR_AWS_ACCOUNT_ID }}
|
||||||
TF_VAR_aws_region: ${{ secrets.TF_VAR_AWS_REGION }}
|
TF_VAR_aws_region: ${{ secrets.TF_VAR_AWS_REGION }}
|
||||||
|
TF_VAR_db_name: ${{ secrets.TF_VAR_db_name }}
|
||||||
|
TF_VAR_db_username: ${{ secrets.TF_VAR_db_username }}
|
||||||
|
TF_VAR_db_password: ${{ secrets.TF_VAR_db_password }}
|
||||||
|
TF_VAR_db_host: ${{ secrets.TF_VAR_db_host }}
|
||||||
|
|
||||||
|
- name: Capture RDS hostname
|
||||||
|
id: rds
|
||||||
|
run: |
|
||||||
|
RDS_HOST=$(terraform output -raw rds_hostname)
|
||||||
|
echo "hostname=$RDS_HOST" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Terraform Apply — wire EC2 with RDS hostname
|
||||||
|
run: terraform apply -auto-approve -lock-timeout=5m
|
||||||
|
env:
|
||||||
|
TF_VAR_aws_account_id: ${{ secrets.TF_VAR_AWS_ACCOUNT_ID }}
|
||||||
|
TF_VAR_aws_region: ${{ secrets.TF_VAR_AWS_REGION }}
|
||||||
|
TF_VAR_db_name: ${{ secrets.TF_VAR_db_name }}
|
||||||
|
TF_VAR_db_username: ${{ secrets.TF_VAR_db_username }}
|
||||||
|
TF_VAR_db_password: ${{ secrets.TF_VAR_db_password }}
|
||||||
|
TF_VAR_db_host: ${{ steps.rds.outputs.hostname }}
|
||||||
|
|
||||||
|
- name: Job summary
|
||||||
|
run: |
|
||||||
|
echo "### Terraform Apply - $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- RDS hostname: \`${{ steps.rds.outputs.hostname }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- EC2 re-provisioned with DB connection wired" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
RUN npm ci --only=production
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
@@ -57,11 +57,17 @@ resource "aws_instance" "app" {
|
|||||||
docker pull ${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-backend:latest
|
docker pull ${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-backend:latest
|
||||||
docker pull ${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-frontend:latest
|
docker pull ${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-frontend:latest
|
||||||
|
|
||||||
# Run backend
|
# Run backend — DB credentials passed as environment variables
|
||||||
|
# Values come from Terraform variables which are sourced from GitHub Secrets
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name task-backend \
|
--name task-backend \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
|
-e DB_HOST=${var.db_host} \
|
||||||
|
-e DB_PORT=5432 \
|
||||||
|
-e DB_NAME=${var.db_name} \
|
||||||
|
-e DB_USER=${var.db_username} \
|
||||||
|
-e DB_PASSWORD=${var.db_password} \
|
||||||
${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-backend:latest
|
${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-backend:latest
|
||||||
|
|
||||||
# Wait for backend to be ready
|
# Wait for backend to be ready
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# RDS — PostgreSQL for Scalable Task Platform
|
||||||
|
# Week 7 | infra/terraform/rds.tf
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ── DB Subnet Group ──────────────────────────
|
||||||
|
# RDS needs to know which subnets it can live in.
|
||||||
|
# We use the private subnets so it is never
|
||||||
|
# reachable from the public internet.
|
||||||
|
resource "aws_db_subnet_group" "tasks" {
|
||||||
|
name = "${var.project_name}-db-subnet-group"
|
||||||
|
subnet_ids = aws_subnet.private[*].id
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-db-subnet-group"
|
||||||
|
Project = var.project_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Security Group: RDS ──────────────────────
|
||||||
|
# Only the EC2 instance security group may reach
|
||||||
|
# the database on port 5432. No public access.
|
||||||
|
resource "aws_security_group" "rds" {
|
||||||
|
name = "${var.project_name}-rds-sg"
|
||||||
|
description = "Allow PostgreSQL access from EC2 only"
|
||||||
|
vpc_id = aws_vpc.main.id
|
||||||
|
|
||||||
|
ingress {
|
||||||
|
description = "PostgreSQL from EC2"
|
||||||
|
from_port = 5432
|
||||||
|
to_port = 5432
|
||||||
|
protocol = "tcp"
|
||||||
|
security_groups = [aws_security_group.ec2.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
egress {
|
||||||
|
description = "Allow all outbound"
|
||||||
|
from_port = 0
|
||||||
|
to_port = 0
|
||||||
|
protocol = "-1"
|
||||||
|
cidr_blocks = ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-rds-sg"
|
||||||
|
Project = var.project_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── RDS Parameter Group ──────────────────────
|
||||||
|
# Explicit parameter group gives you a place to
|
||||||
|
# tune Postgres settings later without replacing
|
||||||
|
# the instance.
|
||||||
|
resource "aws_db_parameter_group" "tasks" {
|
||||||
|
name = "${var.project_name}-pg16"
|
||||||
|
family = "postgres16"
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-pg16"
|
||||||
|
Project = var.project_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── RDS Instance ─────────────────────────────
|
||||||
|
resource "aws_db_instance" "tasks" {
|
||||||
|
identifier = "${var.project_name}-db"
|
||||||
|
|
||||||
|
# Engine
|
||||||
|
engine = "postgres"
|
||||||
|
engine_version = "16"
|
||||||
|
instance_class = "db.t3.micro" # Free-tier eligible
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
allocated_storage = 20
|
||||||
|
max_allocated_storage = 100 # Enable storage autoscaling up to 100 GB
|
||||||
|
storage_type = "gp2"
|
||||||
|
storage_encrypted = true
|
||||||
|
|
||||||
|
# Credentials — sourced from Terraform variables
|
||||||
|
# which are populated from GitHub Secrets via the pipeline
|
||||||
|
db_name = var.db_name
|
||||||
|
username = var.db_username
|
||||||
|
password = var.db_password
|
||||||
|
|
||||||
|
# Network
|
||||||
|
db_subnet_group_name = aws_db_subnet_group.tasks.name
|
||||||
|
vpc_security_group_ids = [aws_security_group.rds.id]
|
||||||
|
publicly_accessible = false
|
||||||
|
multi_az = false # Single-AZ is fine for this stage
|
||||||
|
|
||||||
|
# Maintenance
|
||||||
|
parameter_group_name = aws_db_parameter_group.tasks.name
|
||||||
|
backup_retention_period = 7
|
||||||
|
backup_window = "03:00-04:00"
|
||||||
|
maintenance_window = "Mon:04:00-Mon:05:00"
|
||||||
|
deletion_protection = false # Set to true before production hardening (Week 10)
|
||||||
|
|
||||||
|
# Lifecycle — prevent accidental destroy on credential rotation
|
||||||
|
lifecycle {
|
||||||
|
ignore_changes = [password]
|
||||||
|
}
|
||||||
|
|
||||||
|
skip_final_snapshot = true # Change to false and set final_snapshot_identifier for production
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-db"
|
||||||
|
Project = var.project_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Outputs ───────────────────────────────────
|
||||||
|
# The EC2 user-data script and any future modules
|
||||||
|
# that need the DB endpoint will read these.
|
||||||
|
output "rds_endpoint" {
|
||||||
|
description = "RDS instance endpoint (host:port)"
|
||||||
|
value = aws_db_instance.tasks.endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
output "rds_hostname" {
|
||||||
|
description = "RDS hostname only (no port)"
|
||||||
|
value = aws_db_instance.tasks.address
|
||||||
|
}
|
||||||
|
|
||||||
|
output "rds_port" {
|
||||||
|
description = "RDS port"
|
||||||
|
value = aws_db_instance.tasks.port
|
||||||
|
}
|
||||||
|
|
||||||
|
output "rds_db_name" {
|
||||||
|
description = "Database name"
|
||||||
|
value = aws_db_instance.tasks.db_name
|
||||||
|
}
|
||||||
@@ -37,3 +37,25 @@ variable "private_subnet_cidr" {
|
|||||||
type = string
|
type = string
|
||||||
default = "10.0.2.0/24"
|
default = "10.0.2.0/24"
|
||||||
}
|
}
|
||||||
|
variable "db_name" {
|
||||||
|
description = "PostgreSQL database name"
|
||||||
|
type = string
|
||||||
|
default = "tasksdb"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_username" {
|
||||||
|
description = "PostgreSQL master username"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_password" {
|
||||||
|
description = "PostgreSQL master password"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
variable "db_host" {
|
||||||
|
description = "RDS instance hostname — output from aws_db_instance.tasks.address"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user