Watch
1
0
Fork
You've already forked technical_exercise
0
This commit is contained in:
Neil Borromeo 2026-08-27 11:26:22 +08:00
commit b00d69e101
8 changed files with 242 additions and 0 deletions

View file

@ -0,0 +1,48 @@
---
name: Validate and deploy
on:
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
- closed
jobs:
validate-and-deploy:
runs-on: docker
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
steps:
- name: Check out repository
uses: https://data.forgejo.org/actions/checkout@v6
- name: Scan secrets
run: gitleaks detect --source . --redact --no-banner
- name: Check Terraform formatting
run: terraform fmt -check -recursive
- name: Initialize Terraform
run: terraform init -backend=false -input=false
- name: Validate Terraform
run: terraform validate
- name: Plan Terraform
run: terraform plan -input=false -no-color -out=tfplan
- name: Lint Ansible
run: ansible-lint deploy/site.yml
- name: Deploy
if: ${{ forgejo.event.action == 'closed' && forgejo.event.pull_request.merged == true }}
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: bash deploy/deploy.sh

23
NOTES.md Normal file
View file

@ -0,0 +1,23 @@
* Changes to terraform/ansible*
I noticed an extra set of brackets that VS Code didn't flag. Running a quick terraform validate catches small syntax issues like this before they cause deployment errors.
In main.tf, I scoped cidr_blocks down from 0.0.0.0/0 to a sample subnet (180.191.0.0/16). While still somewhat broad to accommodate dynamic ISP leases that reset every 48 hours, it significantly reduces the initial attack surface.
Alternative Access & Security Models
Rather than leaving SSH exposed, the best practice—even for small instances—is to remove port 22 from the security group entirely. A few options to consider:
Tailscale / Headscale (Recommended): Uses Tailscale SSH to manage access securely. Users authenticate via Identity Providers (IdPs) like Google or GitHub, while Headscale handles key rotations, ACLs, and permissions. I personally use this setup in production for small cloud workloads; it provides excellent ACL control and a clean management interface.
AWS SSM Session Manager: Allows secure CLI and console access via the AWS agent without open inbound ports, though executing local tools like Ansible internally requires additional setup.
Cloudflare Tunnels: Useful for routing developer traffic directly to the instance without exposing public IPs, especially in environments requiring DNS over tunnels.
I prefer the Tailscale/Headscale combination as a modern, manageable solution that keeps port 22 completely closed to the internet.
Handling Port 80 Ingress
I added port 80 to the ingress rules to support standard HTTP traffic. While this opens a public surface area, network risks can easily be mitigated at the Linux OS level (e.g., hardening TCP kernel parameters against SYN floods). In practice, Nginx will use port 80 primarily to issue 301 permanent redirects to HTTPS (port 443).
*Scenario Response: TinyCommerce App Intermittent Health Check Failures*
Part 3 — Incident note (~30 min, written)
A Scenario response for the TinyCommerce app with intermittent health check failures. Detection relies on baseline health check monitoring. The issue would likely present as a 5xx HTTP error specifically a 502 Bad Gateway indicating an application-level problem within the TinyCommerce app. Assuming the application has a reliable deployment pipeline, I would start by investigating the error logs. For example, if the app crashes whenever it attempts a file write, I would check system monitoring and host logs for clear indicators like disk space exhaustion. On small instances, unrotated application logs, Docker container logs, or bloated volumes are almost always the root cause of disk exhaustion and subsequent crashes.

8
deploy/deploy.sh Normal file
View file

@ -0,0 +1,8 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
terraform apply -input=false -auto-approve tfplan
ansible-playbook -i inventory deploy/site.yml
echo "Deployed."

34
deploy/site.yml Normal file
View file

@ -0,0 +1,34 @@
---
- name: Deploy TinyCommerce
hosts: app
become: true
vars_files:
- vars/vault.yml
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Write app config
ansible.builtin.template:
src: app.env.j2
dest: /etc/tinycommerce/app.env
mode: "0600"
- name: Write nginx config
ansible.builtin.template:
src: nginx_web1.conf.j2
dest: /etc/nginx/sites-enabled/nginx_web1.conf
mode: "0644"
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: Restart app service
ansible.builtin.service:
name: tinycommerce
state: restarted

View file

@ -0,0 +1 @@
DB_PASSWORD={{ db_password }}

View file

@ -0,0 +1,80 @@
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.tinycommerce.com;
set $maintenance_site_key "tinycommerce.com";
set $base /var/www/html/tinycommerce.com;
root /var/www/html/tinycommerce.com/public;
ssl_certificate /etc/letsencrypt/live/tinycommerce.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tinycommerce.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/tinycommerce.com/chain.pem;
include snippets/cloudflare-realip.conf;
include snippets/security.conf;
include snippets/maintenance-mode.conf;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
index index.php;
location / {
if (-f /etc/nginx/maintenance/enabled) {
return 503;
}
try_files $uri $uri/ /index.php?$query_string;
}
include snippets/general.conf;
include snippets/wordpress.conf;
client_max_body_size 0;
location ~ \.php$ {
if (-f /etc/nginx/maintenance/enabled) {
return 503;
}
fastcgi_pass unix:/var/run/php/php-wordpress.sock;
include snippets/php-wordpress-fastcgi.conf;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name tinycommerce.com;
set $maintenance_site_key "tinycommerce.com";
ssl_certificate /etc/letsencrypt/live/tinycommerce.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tinycommerce.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/tinycommerce.com/chain.pem;
include snippets/cloudflare-realip.conf;
include snippets/maintenance-mode.conf;
location / {
if (-f /etc/nginx/maintenance/enabled) {
return 503;
}
return 301 https://www.tinycommerce.com$request_uri;
}
}
server {
listen 80;
listen [::]:80;
server_name tinycommerce.com www.tinycommerce.com;
set $maintenance_site_key "tinycommerce.com";
include snippets/letsencrypt.conf;
include snippets/cloudflare-realip.conf;
include snippets/maintenance-mode.conf;
location / {
if (-f /etc/nginx/maintenance/enabled) {
return 503;
}
return 301 https://www.tinycommerce.com$request_uri;
}
}

2
deploy/vars/vault.yml Normal file
View file

@ -0,0 +1,2 @@
---
db_password: "{{ lookup('ansible.builtin.env', 'DB_PASSWORD') }}"

46
main.tf Normal file
View file

@ -0,0 +1,46 @@
resource "aws_security_group" "app" {
name = "tinycommerce-app-sg"
description = "Allow app and SSH traffic"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["180.191.0.0/16"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "app" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.small"
security_groups = [aws_security_group.app.name]
tags = {
Name = "tinycommerce-app-01"
}
lifecycle {
prevent_destroy = true
}
}