Skip to content

Commit 23a732d

Browse files
Khai189claude
andcommitted
Proxy ml-service through backend instead of a separate Lightsail service
ml-service goes back to being a second container in the same aspc-backend-v1 deployment (reachable via localhost only, no public endpoint of its own — Lightsail only allows one per service). backend gains /api/ml (status, open) and /api/ml/match (API-key gated), the one public door external callers like p-ickup/frontend use to reach it. This replaces the standalone aspc-ml-service Lightsail service from the previous approach, which needed its own AWS resource and gave external callers an unpredictable, newly-assigned hostname. This way the URL is the backend's already-known public address, and no new AWS resource is needed at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent dbf9fb8 commit 23a732d

5 files changed

Lines changed: 109 additions & 106 deletions

File tree

‎.github/workflows/deploy-backend.yml‎

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
name: Deploy Backend to AWS Lightsail
1+
name: Deploy Backend + ML Service to AWS Lightsail
22
on:
33
pull_request:
44
types: [closed]
55
branches:
66
- main
77
paths:
88
- 'backend/**'
9+
- 'ml_microservice/**'
910
workflow_dispatch: # Add this to allow manual runs
1011
inputs:
1112
environment:
@@ -16,41 +17,49 @@ jobs:
1617
build-and-deploy:
1718
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
1819
runs-on: ubuntu-latest
19-
20+
2021
steps:
2122
- name: Checkout code
2223
uses: actions/checkout@v4
23-
24+
2425
- name: Set up Docker Buildx
2526
uses: docker/setup-buildx-action@v3
26-
27+
2728
- name: Login to DockerHub
2829
uses: docker/login-action@v3
2930
with:
3031
username: ${{ secrets.DOCKERHUB_USERNAME }}
3132
password: ${{ secrets.DOCKERHUB_TOKEN }}
32-
33-
- name: Build and push Docker image
33+
34+
- name: Build and push backend image
3435
uses: docker/build-push-action@v5
3536
with:
3637
context: ./backend
3738
push: true
3839
tags: ${{ secrets.DOCKERHUB_USERNAME }}/aspc-backend:latest
3940
platforms: linux/amd64
40-
41+
42+
- name: Build and push ML service image
43+
uses: docker/build-push-action@v5
44+
with:
45+
context: ./ml_microservice
46+
push: true
47+
tags: ${{ secrets.DOCKERHUB_USERNAME }}/aspc-ml-service:latest
48+
platforms: linux/amd64
49+
4150
- name: Configure AWS credentials
4251
uses: aws-actions/configure-aws-credentials@v4
4352
with:
4453
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
4554
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
4655
aws-region: us-west-2
47-
56+
4857
- name: Install Lightsail Control plugin
4958
run: |
5059
curl "https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl" -o "lightsailctl"
5160
chmod +x ./lightsailctl
5261
sudo mv ./lightsailctl /usr/local/bin/lightsailctl
53-
62+
5463
- name: Create deployment configuration
5564
run: |
5665
cat > containers.json << EOF
@@ -70,7 +79,21 @@ jobs:
7079
"PORT": "5000",
7180
"IDP_METADATA_URL": "${{ secrets.IDP_METADATA_URL }}",
7281
"ENGAGE_API_KEY": "${{ secrets.ENGAGE_API_KEY }}",
73-
"ENGAGE_API_URL": "${{ secrets.ENGAGE_API_URL }}"
82+
"ENGAGE_API_URL": "${{ secrets.ENGAGE_API_URL }}",
83+
"ML_SERVICE_URL": "http://localhost:8000",
84+
"ML_SERVICE_API_KEY": "${{ secrets.ML_SERVICE_API_KEY }}",
85+
"ML_PROXY_API_KEY": "${{ secrets.ML_PROXY_API_KEY }}"
86+
}
87+
},
88+
"ml-service": {
89+
"image": "${{ secrets.DOCKERHUB_USERNAME }}/aspc-ml-service:latest",
90+
"ports": {
91+
"8000": "HTTP"
92+
},
93+
"environment": {
94+
"SUPABASE_URL": "${{ secrets.SUPABASE_URL }}",
95+
"SUPABASE_SECRET_KEY": "${{ secrets.SUPABASE_SECRET_KEY }}",
96+
"SERVICE_API_KEY": "${{ secrets.ML_SERVICE_API_KEY }}"
7497
}
7598
}
7699
}
@@ -90,12 +113,12 @@ jobs:
90113
}
91114
}
92115
EOF
93-
116+
94117
- name: Deploy to Lightsail
95118
run: |
96119
aws lightsail create-container-service-deployment \
97120
--service-name aspc-backend-v1 \
98121
--containers file://containers.json \
99122
--public-endpoint file://public-endpoint.json
100-
123+
101124
echo "Deployment initiated. Check the AWS Lightsail console for status."

‎.github/workflows/deploy-ml-service.yml‎

Lines changed: 0 additions & 94 deletions
This file was deleted.

‎backend/.env.example‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,16 @@ COURSE_API_KEY=
2121
# --- Campus Labs Engage (events) ---
2222
ENGAGE_API_URL=
2323
ENGAGE_API_KEY=
24+
25+
# --- Ride-matching ML service (ml_microservice/, deployed as a second
26+
# container in this same Lightsail service — reachable at localhost, not
27+
# public on its own). /api/ml/* on this backend is the one public door to
28+
# it, for external callers like p-ickup/frontend. ---
29+
ML_SERVICE_URL=http://localhost:8000
30+
# Sent as X-API-Key when this backend calls ml-service internally. Must
31+
# match SERVICE_API_KEY in ml_microservice/.env.
32+
ML_SERVICE_API_KEY=
33+
# Sent as X-API-Key by external callers (e.g. p-ickup/frontend) hitting
34+
# POST /api/ml/match on *this* backend. Different key from the one above —
35+
# this one guards the public door, that one guards the internal hop.
36+
ML_PROXY_API_KEY=
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import express from 'express';
2+
import axios from 'axios';
3+
4+
const router = express.Router();
5+
6+
// ml-service runs as a second container in the same Lightsail deployment as
7+
// this backend, reachable only via localhost — not publicly exposed on its
8+
// own. This router is the one public door to it, for external callers like
9+
// p-ickup/frontend (which has no other way to reach it).
10+
const ML_SERVICE_URL = process.env.ML_SERVICE_URL || 'http://localhost:8000';
11+
12+
const mlServiceHeaders = () =>
13+
process.env.ML_SERVICE_API_KEY
14+
? { 'X-API-Key': process.env.ML_SERVICE_API_KEY }
15+
: {};
16+
17+
// Status check — intentionally no API key required, mirrors ml-service's own
18+
// open GET / (just reports configured/reachable, nothing sensitive).
19+
router.get('/', async (req, res) => {
20+
try {
21+
const response = await axios.get(ML_SERVICE_URL, {
22+
headers: mlServiceHeaders(),
23+
timeout: 5000,
24+
});
25+
res.json(response.data);
26+
} catch (error) {
27+
console.error('ml-service unreachable:', error);
28+
res.status(502).json({ message: 'Could not reach the matching service.' });
29+
}
30+
});
31+
32+
router.post('/match', async (req, res) => {
33+
const expectedKey = process.env.ML_PROXY_API_KEY;
34+
if (!expectedKey) {
35+
res.status(503).json({ message: 'ML_PROXY_API_KEY is not configured on the server.' });
36+
return;
37+
}
38+
if (req.headers['x-api-key'] !== expectedKey) {
39+
res.status(401).json({ message: 'Missing or invalid X-API-Key header.' });
40+
return;
41+
}
42+
43+
try {
44+
const response = await axios.post(`${ML_SERVICE_URL}/match`, req.body, {
45+
headers: mlServiceHeaders(),
46+
timeout: 30000,
47+
});
48+
res.json(response.data);
49+
} catch (error) {
50+
if (axios.isAxiosError(error) && error.response) {
51+
res.status(error.response.status).json(error.response.data);
52+
return;
53+
}
54+
console.error('ml-service match request failed:', error);
55+
res.status(502).json({ message: 'Failed to reach the matching service.' });
56+
}
57+
});
58+
59+
export default router;

‎backend/src/server.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import instructorsRoutes from './routes/InstructorsRoutes';
1717
import reviewsRoutes from './routes/ReviewsRoutes';
1818
import forumRoutes from './routes/ForumRoutes';
1919
import votingRoutes from './routes/VotingRoutes';
20+
import mlProxyRoutes from './routes/MlProxyRoutes';
2021
import session from 'express-session';
2122
import https from 'https';
2223
import http from 'http';
@@ -131,6 +132,7 @@ app.use('/api/openforum', forumRoutes);
131132
app.use('/api/admin/elections', electionsDashboardRoutes);
132133
app.use('/api/admin/users', adminUsersRoutes);
133134
app.use('/api/voting', votingRoutes);
135+
app.use('/api/ml', mlProxyRoutes);
134136

135137
const PORT = serverConfig.port;
136138

0 commit comments

Comments
 (0)