Skip to content

Commit 9366050

Browse files
authored
Merge pull request #71 from openplans/iac-setup
IAC Setup
2 parents f4ab51c + 2915729 commit 9366050

35 files changed

Lines changed: 1256 additions & 72 deletions

.env.template

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Server Configuration
2+
ALLOWED_HOSTS='*'
3+
SECRET_KEY='pbv(g=%7$$4rzvl88e24etn57-%n0uw-@y*=7ak422_3!zrc9+'
4+
15
# This is set up to use the PostGIS container spun up by docker-compose
26
# in the root of this repository. If you are using a different database, you will
37
# need to update the DATABASE_URL variable below.

.github/workflows/test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ master, main ]
6+
pull_request:
7+
branches: [ master, main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-24.04 # Use 24.04 for Podman 4.x (matches local dev environment)
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v6
18+
with:
19+
python-version: '3.12'
20+
21+
- name: Install Podman Compose
22+
run: |
23+
pip install podman-compose
24+
25+
- name: Build Images
26+
run: make build
27+
28+
- name: Run Tests
29+
run: make test

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
db/*.sqlite3
33
log/*.log
44

5-
# tmp
5+
# tmp & cache
66
tmp/
77
.sass-cache/
88
profiling/
99
attachments/
1010
build/
11+
.terraform/
1112

1213
# cover_me generated
1314
coverage
@@ -26,6 +27,7 @@ coverage.data
2627
# environment
2728
env*/
2829
.env*
30+
.auto.tfvars
2931

3032
# Local project settings
3133
src/project/local_settings.py

.travis.yml

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

Containerfile

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,42 @@ FROM ubuntu:24.04
33
# Install Python & GeoDjango dependencies
44
RUN apt update && \
55
apt install -y \
6-
libpq-dev \
7-
libproj-dev \
8-
gdal-bin \
9-
python3 \
10-
python3-pip && \
6+
libpq-dev \
7+
libproj-dev \
8+
gdal-bin \
9+
python3 \
10+
python3-pip \
11+
python3-venv && \
1112
apt clean
1213

14+
# Create a virtual environment
15+
ENV VIRTUAL_ENV=/opt/venv
16+
RUN python3 -m venv $VIRTUAL_ENV
17+
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
18+
ENV PYTHONUNBUFFERED=1
19+
1320
# Install Python dependencies
1421
COPY requirements.txt /tmp/requirements.txt
15-
RUN pip3 install -r /tmp/requirements.txt --break-system-packages
22+
RUN pip install --no-cache-dir -r /tmp/requirements.txt
1623

1724
# Copy the application code to the container
1825
COPY src /app
1926
WORKDIR /app
2027

2128
# Run collectstatic to gather static files
22-
RUN REDIS_URL="redis://temp_value/" \
29+
# We pass dummy values for REDIS_URL and SECRET_KEY to ensure settings.py loads without error
30+
RUN REDIS_URL="redis://dummy:6379/0" \
31+
SECRET_KEY="dummy" \
32+
ALLOWED_HOSTS="*" \
2333
python3 manage.py collectstatic --noinput
2434

25-
# Expose the port the app runs on
35+
# Copy gunicorn config
2636
COPY gunicorn.conf.py /app/gunicorn.conf.py
37+
38+
# Expose the port the app runs on
2739
EXPOSE 8000
40+
41+
# Default command
42+
CMD ["sh", "-c", "gunicorn project.wsgi --pythonpath src --workers 3 --config gunicorn.conf.py --bind 0.0.0.0:${PORT:-8000}"]
43+
44+

Makefile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.PHONY: test-env test test-clean build gcp-push gcp-restart gcp-deploy
2+
3+
# Build the container image
4+
build:
5+
podman build -t shareabouts-api -f Containerfile .
6+
7+
# Push image to GCP Container Registry
8+
# Requires: PROJECT_ID, ENVIRONMENT_NAME environment variables
9+
gcp-push:
10+
@if [ -z "$(PROJECT_ID)" ]; then echo "Error: PROJECT_ID is not set"; exit 1; fi
11+
@if [ -z "$(ENVIRONMENT_NAME)" ]; then echo "Error: ENVIRONMENT_NAME is not set"; exit 1; fi
12+
podman tag shareabouts-api gcr.io/$(PROJECT_ID)/shareabouts-api:latest-$(ENVIRONMENT_NAME)
13+
podman push gcr.io/$(PROJECT_ID)/shareabouts-api:latest-$(ENVIRONMENT_NAME)
14+
15+
# Restart the Cloud Run service with the latest image
16+
# Requires: PROJECT_ID, ENVIRONMENT_NAME, SERVICE_NAME, REGION environment variables
17+
gcp-restart:
18+
@if [ -z "$(PROJECT_ID)" ]; then echo "Error: PROJECT_ID is not set"; exit 1; fi
19+
@if [ -z "$(ENVIRONMENT_NAME)" ]; then echo "Error: ENVIRONMENT_NAME is not set"; exit 1; fi
20+
@if [ -z "$(SERVICE_NAME)" ]; then echo "Error: SERVICE_NAME is not set"; exit 1; fi
21+
@if [ -z "$(REGION)" ]; then echo "Error: REGION is not set"; exit 1; fi
22+
gcloud run services update $(SERVICE_NAME)-$(ENVIRONMENT_NAME) \
23+
--region $(REGION) \
24+
--image gcr.io/$(PROJECT_ID)/shareabouts-api:latest-$(ENVIRONMENT_NAME)
25+
26+
# Full deployment: build, push, and restart
27+
gcp-deploy: build gcp-push gcp-restart
28+
29+
# Stub .env file
30+
test-env:
31+
cp .env.template .env
32+
33+
# Run tests in a clean container environment
34+
test: test-env test-clean
35+
podman-compose run --rm test
36+
37+
# Just clean up containers
38+
test-clean:
39+
podman-compose down --remove-orphans 2>/dev/null || true

compose.yml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
version: '3.8'
2-
31
services:
42
init:
53
build:
64
context: .
75
dockerfile: Containerfile
86
command: >
97
sh -c "
10-
python3 manage.py migrate --noinput &&
8+
python3 manage.py migrate --noinput &&
119
python3 manage.py ensuresuperuser --noinput &&
1210
python3 manage.py createdefaultdataset
1311
"
1412
env_file: .env
13+
environment:
14+
- REDIS_URL=redis://redis:6379/0
1515
depends_on:
1616
db: {"condition": "service_healthy"}
1717
redis: {"condition": "service_healthy"}
@@ -21,8 +21,9 @@ services:
2121
build:
2222
context: .
2323
dockerfile: Containerfile
24-
command: gunicorn project.wsgi --pythonpath src --workers ${WORKERS} --config gunicorn.conf.py --bind 0.0.0.0:8000
2524
env_file: .env
25+
environment:
26+
- REDIS_URL=redis://redis:6379/0
2627
ports:
2728
- "8000:8000"
2829
depends_on:
@@ -38,6 +39,20 @@ services:
3839
env_file: .env
3940
environment:
4041
C_FORCE_ROOT: "true"
42+
REDIS_URL: "redis://redis:6379/0"
43+
depends_on:
44+
db: {"condition": "service_healthy"}
45+
redis: {"condition": "service_healthy"}
46+
init: {"condition": "service_completed_successfully"}
47+
48+
test:
49+
build:
50+
context: .
51+
dockerfile: Containerfile
52+
command: python3 manage.py test .
53+
env_file: .env
54+
environment:
55+
- REDIS_URL=redis://redis:6379/0
4156
depends_on:
4257
db: {"condition": "service_healthy"}
4358
redis: {"condition": "service_healthy"}

doc/DEPLOY.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,93 @@ Deploying to Heroku
8080
5. Connect the app with the repository (add a git remote)
8181
6. Push to Heroku
8282
7. Run database migrations (or copy the database from elsewhere)
83+
84+
Deploying to Google Cloud Platform
85+
----------------------------------
86+
87+
The GCP deployment uses OpenTofu (or Terraform) for infrastructure, Podman for
88+
containerization, and Google Cloud Storage for media assets.
89+
90+
### 1. Prerequisites
91+
92+
- [OpenTofu](https://opentofu.org/) or [Terraform](https://www.terraform.io/)
93+
- [Podman](https://podman.io/) or Docker
94+
- [Google Cloud SDK (gcloud)](https://cloud.google.com/sdk)
95+
96+
### 2. Infrastructure Setup
97+
98+
Initialize and apply the OpenTofu configuration in the `infra/gcp` directory:
99+
100+
cd infra/gcp
101+
tofu init
102+
tofu apply
103+
104+
This will create the Cloud SQL instance, Cloud Run service, GCS bucket, and other necessary resources.
105+
106+
### 3. Database Migration
107+
108+
To import an existing database dump (e.g., from Heroku):
109+
110+
1. **Convert to "Clean" SQL**: Use `pg_restore` with flags to ignore ownership and privileges that won't exist on Cloud SQL.
111+
112+
pg_restore -O -x -f dump.sql input.dump
113+
114+
2. **Upload to GCS**:
115+
116+
gcloud storage cp dump.sql gs://your-migration-bucket/
117+
118+
3. **Grant Permissions**: Ensure the Cloud SQL service account can read from the bucket.
119+
120+
gcloud storage buckets add-iam-policy-binding gs://your-migration-bucket \
121+
--member="serviceAccount:<SQL-SERVICE-ACCOUNT>" \
122+
--role="roles/storage.objectViewer"
123+
124+
*(You can find the service account email using `gcloud sql instances describe <instance-id>`)*
125+
126+
4. **Run Import**:
127+
128+
gcloud sql import sql <instance-id> gs://your-migration-bucket/dump.sql \
129+
--database=<db-name> --user=<db-user>
130+
131+
### 4. Image Deployment
132+
133+
A `Makefile` is provided for common deployment tasks.
134+
135+
1. **Authenticate with Container Registry** (one-time setup):
136+
137+
```bash
138+
gcloud auth configure-docker gcr.io
139+
```
140+
141+
*(For Podman, you may also need to run:)*
142+
143+
```bash
144+
gcloud auth print-access-token | podman login -u oauth2accesstoken --password-stdin https://gcr.io
145+
```
146+
147+
2. **Set Environment Variables**:
148+
149+
```bash
150+
export PROJECT_ID=your-project-id
151+
export SERVICE_NAME=your-service-name
152+
export ENVIRONMENT_NAME=your-environment-name
153+
export REGION=your-region
154+
```
155+
156+
3. **Deploy** (build, push, and restart Cloud Run):
157+
158+
```bash
159+
make gcp-deploy
160+
```
161+
162+
Or run individual steps:
163+
164+
```bash
165+
make build # Build the container image locally
166+
make gcp-push # Push image to GCR
167+
make gcp-restart # Update the Cloud Run service
168+
```
169+
170+
### 5. Static Files
171+
172+
Currently, static files are served directly by the container using `dj_static.Cling`. Ensure `STATIC_URL` and `STATICFILES_STORAGE` in `settings.py` are configured appropriately (local serving is the default if GCS static configuration is commented out).

gunicorn.conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
secure_scheme_headers = {
55
'X-FORWARDED-PROTO': 'https',
66
}
7+
accesslog = '-'
8+
errorlog = '-'
9+
timeout = 120
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Example configuration - update with your actual values
2+
project_id = "example-shareabouts"
3+
load_balancer_name = "custom-domains-abcd"
4+
5+
# Add your domain mappings here
6+
# Each key is a service name, value contains domains and cloud_run_service details
7+
domain_mappings = {
8+
# Example:
9+
# shareabouts-api-dev = {
10+
# domains = ["shareaboutsapi-gcp-dev.example.com"]
11+
# cloud_run_service = {
12+
# name = "shareabouts-api-dev"
13+
# region = "us-central1"
14+
# }
15+
# }
16+
}
17+
18+
# Optional: default backend for unmatched requests
19+
# default_backend_service = "projects/example-shareabouts/global/backendServices/default-backend"
20+
21+
# Optional: redirect host for unmatched requests (used when default_backend_service is not set)
22+
# default_redirect_host = "example.com"
23+
24+
# Optional: Legacy host rules for existing backend services not managed by this project
25+
# legacy_host_rules = {
26+
# my-legacy-service = {
27+
# hosts = ["legacy.example.com"]
28+
# path_matcher = "legacy-example-com"
29+
# backend_service = "https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/my-backend"
30+
# }
31+
# }
32+
33+
# Optional: Group domains into separate SSL certificates
34+
# domains not listed here will be grouped into a "default" certificate
35+
# ssl_certs = {
36+
# mycity-gov = ["suggest.mycity.gov", "suggest-staging.mycity.gov"]
37+
# bikeshare-com = ["suggest.bikeshare.com"]
38+
# }

0 commit comments

Comments
 (0)