Skip to content

Commit c54c6d0

Browse files
committed
oo ii aa ii
2 parents cd0dde2 + f29734f commit c54c6d0

6 files changed

Lines changed: 218 additions & 7 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Deploy to EC2
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
jobs:
10+
deploy:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
17+
- name: Export deployment settings
18+
run: |
19+
echo "EC2_HOST=${{ secrets.EC2_HOST }}" >> "$GITHUB_ENV"
20+
echo "EC2_USER=${{ secrets.EC2_USER }}" >> "$GITHUB_ENV"
21+
if [ -n "${{ vars.DEPLOY_PORT }}" ]; then
22+
echo "PORT=${{ vars.DEPLOY_PORT }}" >> "$GITHUB_ENV"
23+
else
24+
echo "PORT=80" >> "$GITHUB_ENV"
25+
fi
26+
27+
- name: Set up SSH key
28+
uses: webfactory/ssh-agent@v0.8.0
29+
with:
30+
ssh-private-key: ${{ secrets.EC2_SSH_KEY }}
31+
32+
- name: Add EC2 host to known_hosts
33+
run: |
34+
mkdir -p ~/.ssh
35+
ssh-keyscan -H "$EC2_HOST" >> ~/.ssh/known_hosts
36+
37+
- name: Sync project to EC2
38+
run: |
39+
rsync -az --delete \
40+
--exclude '.git' \
41+
--exclude '.github' \
42+
--exclude '.venv' \
43+
--exclude '__pycache__' \
44+
--exclude '*.pyc' \
45+
--exclude '.ruff_cache' \
46+
./ "$EC2_USER@$EC2_HOST:/home/$EC2_USER/nasa-sky-app"
47+
48+
- name: Execute remote deployment
49+
run: |
50+
ssh "$EC2_USER@$EC2_HOST" "
51+
set -euo pipefail
52+
cd ~/nasa-sky-app
53+
chmod +x deploy/deploy.sh
54+
APP_ROOT=/opt/nasa-sky-app SERVICE_NAME=nasa-sky-app PORT=${PORT:-80} ./deploy/deploy.sh
55+
"

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
src/__pycache__/
22
.venv/
3-
outputs
3+
.ruff_cache

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ A barebones FastAPI project that serves the landing page for the NASA Sky Explor
2020

2121
3. Open <http://127.0.0.1:8000/> in your browser to view the page titled **“Minimal FastAPI App.”**
2222

23+
## Linting
24+
25+
Install Ruff into your virtual environment and run it against the project source to keep the
26+
codebase clean:
27+
28+
```bash
29+
pip install ruff
30+
ruff check .
31+
```
32+
2333
## Project structure
2434

2535
```
@@ -30,6 +40,36 @@ NASASpaceAppsChallenge2025/
3040
│ └── server.py # FastAPI application serving the HTML page
3141
└── web/
3242
└── index.html # Static HTML served at the root route
43+
└── deploy/
44+
└── deploy.sh # Helper script to install and run the service via systemd
3345
```
3446

3547
Feel free to build on this foundation for richer APIs or interfaces.
48+
49+
## Deployment script
50+
51+
The `deploy/deploy.sh` script provisions the application on a Linux host using `systemd`. It:
52+
53+
- Syncs the repository into `/opt/nasa-sky-app/app` (configurable with `APP_ROOT`).
54+
- Ensures `/opt/nasa-sky-app` exists with the proper ownership, then syncs code and creates a
55+
virtual environment for dependencies.
56+
- Generates a `systemd` unit that runs Uvicorn on port `80` by default.
57+
- Enables and restarts the service.
58+
59+
Run it directly on the target server after cloning or syncing the repository:
60+
61+
```bash
62+
chmod +x deploy/deploy.sh
63+
APP_ROOT=/opt/nasa-sky-app SERVICE_NAME=nasa-sky-app ./deploy/deploy.sh
64+
```
65+
66+
Override `SERVICE_USER`, `SERVICE_GROUP`, `PORT`, or `PYTHON_BIN` to fit your environment. When the
67+
port is below `1024` (the default `80`), the generated unit grants
68+
`CAP_NET_BIND_SERVICE` so the application can bind to the port without running as root.
69+
70+
### GitHub Actions deployment
71+
72+
A workflow at `.github/workflows/deploy.yml` uses the repository secrets `EC2_HOST`, `EC2_USER`,
73+
and `EC2_SSH_KEY` to sync the codebase to an Ubuntu 24.04 EC2 instance and execute the deployment
74+
script remotely. It runs on every push to `main` (and can be triggered manually). Optional
75+
repository variable `DEPLOY_PORT` lets you override the port without editing the workflow.

deploy/deploy.sh

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
APP_ROOT=${APP_ROOT:-/opt/nasa-sky-app}
6+
APP_DIR="${APP_ROOT}/app"
7+
VENV_DIR="${APP_ROOT}/venv"
8+
SERVICE_NAME=${SERVICE_NAME:-nasa-sky-app}
9+
PORT=${PORT:-80}
10+
PYTHON_BIN=${PYTHON_BIN:-python3}
11+
SUDO_BIN=${SUDO_BIN:-sudo}
12+
export DEBIAN_FRONTEND=${DEBIAN_FRONTEND:-noninteractive}
13+
14+
SERVICE_USER=${SERVICE_USER:-$(whoami)}
15+
SERVICE_GROUP=${SERVICE_GROUP:-$(id -gn)}
16+
17+
if [ "${PORT}" -lt 1024 ]; then
18+
echo "Deploying service on privileged port ${PORT}."
19+
fi
20+
21+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
22+
REPO_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
23+
24+
if command -v apt-get >/dev/null 2>&1; then
25+
if ! command -v rsync >/dev/null 2>&1; then
26+
echo "Installing rsync via apt-get"
27+
${SUDO_BIN} apt-get update -y
28+
${SUDO_BIN} apt-get install -y rsync
29+
fi
30+
31+
if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
32+
echo "Installing Python via apt-get"
33+
${SUDO_BIN} apt-get update -y
34+
${SUDO_BIN} apt-get install -y python3 python3-venv python3-pip
35+
else
36+
if ! "${PYTHON_BIN}" -m venv --help >/dev/null 2>&1; then
37+
echo "Installing python3-venv via apt-get"
38+
${SUDO_BIN} apt-get update -y
39+
${SUDO_BIN} apt-get install -y python3-venv
40+
fi
41+
if ! command -v pip3 >/dev/null 2>&1; then
42+
echo "Installing python3-pip via apt-get"
43+
${SUDO_BIN} apt-get update -y
44+
${SUDO_BIN} apt-get install -y python3-pip
45+
fi
46+
fi
47+
fi
48+
49+
if ! command -v rsync >/dev/null 2>&1; then
50+
echo "rsync is required but was not found in PATH" >&2
51+
exit 1
52+
fi
53+
54+
if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
55+
echo "Python interpreter '${PYTHON_BIN}' not found" >&2
56+
exit 1
57+
fi
58+
59+
if [ ! -d "${APP_ROOT}" ]; then
60+
${SUDO_BIN} mkdir -p "${APP_ROOT}"
61+
fi
62+
${SUDO_BIN} mkdir -p "${APP_DIR}" "${VENV_DIR}"
63+
${SUDO_BIN} chown -R "${SERVICE_USER}:${SERVICE_GROUP}" "${APP_ROOT}"
64+
65+
rsync -a --delete \
66+
--exclude ".git" \
67+
--exclude ".venv" \
68+
--exclude "__pycache__" \
69+
--exclude "*.pyc" \
70+
--exclude ".ruff_cache" \
71+
"${REPO_ROOT}/" "${APP_DIR}/"
72+
73+
"${PYTHON_BIN}" -m venv "${VENV_DIR}"
74+
"${VENV_DIR}/bin/pip" install --upgrade pip
75+
"${VENV_DIR}/bin/pip" install -r "${APP_DIR}/requirements.txt"
76+
77+
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
78+
79+
CAPABILITY_LINES=""
80+
if [ "${PORT}" -lt 1024 ]; then
81+
CAPABILITY_LINES=$'AmbientCapabilities=CAP_NET_BIND_SERVICE\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE'
82+
fi
83+
84+
read -r -d '' SERVICE_UNIT <<EOF
85+
[Unit]
86+
Description=NASA Sky Explorer FastAPI Service
87+
After=network.target
88+
89+
[Service]
90+
User=${SERVICE_USER}
91+
Group=${SERVICE_GROUP}
92+
WorkingDirectory=${APP_DIR}
93+
Environment="PATH=${VENV_DIR}/bin"
94+
ExecStart=${VENV_DIR}/bin/uvicorn src.server:app --host 0.0.0.0 --port ${PORT}
95+
Restart=on-failure
96+
${CAPABILITY_LINES}
97+
98+
[Install]
99+
WantedBy=multi-user.target
100+
EOF
101+
102+
${SUDO_BIN} tee "${SERVICE_FILE}" >/dev/null <<<"${SERVICE_UNIT}"
103+
${SUDO_BIN} systemctl daemon-reload
104+
${SUDO_BIN} systemctl enable --now "${SERVICE_NAME}.service"
105+
${SUDO_BIN} systemctl restart "${SERVICE_NAME}.service"
106+
107+
${SUDO_BIN} systemctl status "${SERVICE_NAME}.service" --no-pager

ruff.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
line-length = 120
2+
src = ["src"]
3+
target-version = "py312"
4+
5+
[lint]
6+
select = ["E", "F", "I", "UP", "W"]

src/server.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Minimal FastAPI application that serves a static HTML homepage for the NASA Sky Explorer Prototype."""
1+
"""Minimal FastAPI application that serves the NASA Sky Explorer Prototype website."""
22

33
from pathlib import Path
44

@@ -8,24 +8,27 @@
88
BASE_DIR = Path(__file__).resolve().parent.parent
99
WEB_DIR = BASE_DIR / "web"
1010
INDEX_PATH = WEB_DIR / "index.html"
11+
ALADIN_PATH = WEB_DIR / "aladin.html"
1112

1213
app = FastAPI(title="NASA Sky Explorer Prototype")
1314

1415

16+
def _read_html(path: Path) -> str:
17+
if not path.exists():
18+
raise HTTPException(status_code=404, detail=f"{path.name} not found")
19+
return path.read_text(encoding="utf-8")
20+
21+
1522
@app.get("/", response_class=HTMLResponse)
1623
def read_index() -> str:
1724
"""Return the contents of the bundled ``index.html`` file."""
1825

1926

2027
@app.get("/aladin")
2128
async def aladin() -> FileResponse:
29+
"""Serve the Aladin viewer page."""
2230
return FileResponse("web/aladin.html")
2331

24-
25-
@app.get("/")
26-
async def root() -> JSONResponse:
27-
return JSONResponse({"status": "ok", "message": "Visit /app for the prototype UI."})
28-
2932
if __name__ == "__main__":
3033
import uvicorn
3134

0 commit comments

Comments
 (0)