Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#47] Replace deployments with cloud provider options #54

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: test
on: [workflow_call] # allow this workflow to be called from other workflows

env:
PYTHON_VERSION: 3.9
PYTHON_VERSION: 3.11

jobs:
run-tests:
Expand All @@ -23,7 +23,7 @@ jobs:
- name: Install poetry 📚
run: pipx install poetry

- name: Set up Python 3.9 🐍
- name: Set up Python ${{ env.PYTHON_VERSION }} 🐍
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
Expand All @@ -46,10 +46,12 @@ jobs:
script:
- name: Basic
args: ""
- name: Deployments, Example Api, & py3.11
args: "deployments='yes' py_version=3.11"
- name: Deployments, Example Api, & py3.9
args: "deployments='yes' py_version=3.9"
- name: No deploy, Example Api, & py3.11
args: "cloud_provider='none' py_version=3.11"
- name: Fly.io, Example Api, & py3.9
args: "cloud_provider='Fly.io' py_version=3.9"
- name: Fly.io, Example Api, & py3.10
args: "cloud_provider='Fly.io' py_version=3.10"
name: "Docker ${{ matrix.script.name }}"
runs-on: ubuntu-latest
env:
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ cookiecutter https://github.com/nickatnight/cookiecutter-fastapi-backend.git
* :computer: **Production ready** Python web server using [FastAPI](https://fastapi.tiangolo.com/)
* :pencil2: **SQLModel** [Library](https://sqlmodel.tiangolo.com/) for interacting with SQL databases from Python code, with Python objects. It is designed to be intuitive, easy to use, highly compatible, and robust
* :light_rail: **Alembic** Lightweight database migration tool for usage with the [SQLAlchemy](https://alembic.sqlalchemy.org/en/latest/) Database Toolkit for Python
* :globe_with_meridians: **NGINX** High Performance Load Balancer, [Web Server](https://www.nginx.com/), & Reverse Proxy
* :lock: **Let's Encrypt** A free, automated, and open [certificate authority](https://letsencrypt.org/) (CA), provided by the Internet Security Research Group (ISRG)...with automatic cert renewal
* :floppy_disk: **postgresql** Powerful open source [object-relational](https://www.postgresql.org/) database
* :convenience_store: **Redis** In-memory data structure [store](https://redis.io/), used as a distributed, in-memory key–value database, cache and message broker
* :seedling: **Celery** [Asynchronous](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) task or job queue
Expand All @@ -48,10 +46,8 @@ The input variables, with their default values (some auto generated) are:
* `db_container_name`: The name of the database container. Default `db`
* `backend_container_name`: The name of the backend container. Default `backend`
* `use_celery`: Whether to use Celery/Beat for asynchronous/scheduled tasks.
* `nginx_container_name`: The name of the nginx web server container. Default `nginx`
* `doctl_version`: The version name of [DigitalOcean Command Line Interface](https://docs.digitalocean.com/reference/doctl/) to use. Default `1.92.0`
* `github_username`: The username of the GitHub user. Used for badge display in generated project `README.md`
* `deployments`: Include `docker-compose` files needed for deployment step in GitHub Action. Options are `y` or `n`
* `cloud_provider`: Cloud application hosting provider. Supported providers: (Fly.io)[https://fly.io/]


## More Details
Expand Down
5 changes: 1 addition & 4 deletions cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@
"db_container_name": "db",
"backend_container_name": "backend",
"use_celery": ["no", "yes"],
"nginx_container_name": "nginx",

"doctl_version": "1.92.0",

"github_username": "change.me",

"deployments": ["no", "yes"]
"cloud_provider": ["none", "Fly.io"]
}
7 changes: 3 additions & 4 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
"%sworker.py" % BASE_BACKEND_SRC_PATH,
]
DEPLOYMENT_FILES = [
"ops/docker-compose.prod.yml",
"ops/docker-compose.staging.yml",
".github/workflows/build.yml",
".github/workflows/deploy.yml",
"{{ cookiecutter.backend_container_name }}/fly.toml",
]


Expand All @@ -22,7 +21,7 @@ def rename_file(old: str, new: str):
os.rename(in_, out_)


if "{{ cookiecutter.deployments }}" == "no":
if "{{ cookiecutter.cloud_provider }}" == "none":
print("Removing deployment files...")
for p in DEPLOYMENT_FILES:
remove_file(p)
Expand Down
6 changes: 2 additions & 4 deletions tests/test_bake_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
# pytest.skip("skipping slow macOS tests on CI", allow_module_level=True)

SUPPORTED_COMBINATIONS = [
{"deployments": "no"},
{"deployments": "yes"},
{"cloud_provider": "none"},
{"cloud_provider": "Fly.io"},
{"use_celery": "no"},
{"use_celery": "yes"},
{"py_version": "3.9"},
Expand All @@ -40,8 +40,6 @@ def context() -> Dict:
"py_version": "3.10",
"db_container_name": "db",
"backend_container_name": "backend",
"nginx_container_name": "nginx",
"doctl_version": "1.92.0",
"github_username": "yer.a.wizard",
}

Expand Down
7 changes: 0 additions & 7 deletions {{ cookiecutter.project_slug }}/.env_example
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ POSTGRES_HOST={{ cookiecutter.db_container_name }}
POSTGRES_PORT=5432
POSTGRES_URL=

# Nginx
NGINX_HOST=localhost
UPSTREAMS=/:{{ cookiecutter.backend_container_name }}:8000
ENABLE_SSL=
CERTBOT_EMAIL=
DOMAIN_LIST=

# Redis
REDIS_HOST=redis
REDIS_PORT=6379
50 changes: 0 additions & 50 deletions {{ cookiecutter.project_slug }}/.github/workflows/build.yml

This file was deleted.

20 changes: 20 additions & 0 deletions {{ cookiecutter.project_slug }}/.github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Fly Deploy

on: [workflow_call]

jobs:
deploy-backend:
name: Deploy FastAPI Backend
runs-on: ubuntu-latest
concurrency: deploy-group # optional: ensure only one action runs at a time
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Set up Flyctl
uses: superfly/flyctl-actions/setup-flyctl@master

- name: Deploy
run: flyctl deploy --remote-only {{ cookiecutter.backend_container_name }}
{% raw %}env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}{% endraw %}
70 changes: 6 additions & 64 deletions {{ cookiecutter.project_slug }}/.github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,16 @@ on:
tags:
- '*'
branches:
- main

{%- if cookiecutter.deployments == 'yes' %}
env:
DOCTL_VERSION: {{ cookiecutter.doctl_version }}
{%- endif %}
- master

jobs:
lint:
uses: nickatnight/gha-workflows/.github/workflows/pre-commit.yml@main

{%- if cookiecutter.deployments == 'yes' %}
build:
needs: [lint]
uses: ./.github/workflows/build.yml
{% raw %}secrets:
do-token: ${{ secrets.DIGITALOCEAN_TOKEN }}
registry: ${{ secrets.REGISTRY }}{% endraw %}

unit-tests:
needs: [build]
uses: ./.github/workflows/unit-tests.yml
{%- elif cookiecutter.deployments == 'no' %}
unit-tests:
needs: [lint]
uses: ./.github/workflows/unit-tests.yml
{%- endif %}

create-release:
permissions:
contents: write
Expand All @@ -50,51 +33,10 @@ jobs:
{% raw %}outputs:
ReleaseTag: ${{ steps.create_release.outputs.release_tag }}{% endraw %}

{%- if cookiecutter.deployments == "yes" %}
{%- if cookiecutter.cloud_provider == "Fly.io" %}
deploy:
runs-on: ubuntu-latest
name: Deploy
if: github.ref == 'refs/heads/main'
needs: [unit-tests]
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Deploy staging
uses: ironhalik/docker-over-ssh-action@v6
if: github.ref == 'refs/heads/develop'
env:
COMPOSE_FILE: ops/docker-compose.staging.yml
STACK_NAME: {{ cookiecutter.project_slug_db }}-staging
{% raw %}DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
REGISTRY: ${{ secrets.REGISTRY }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
with:
user: ubuntu
host: ${{ secrets.STAGING_HOST_IP }}
key: ${{ secrets.SSH_KEY }}
script: |
wget https://github.com/digitalocean/doctl/releases/download/v${{ env.DOCTL_VERSION }}/doctl-${{ env.DOCTL_VERSION }}-linux-amd64.tar.gz
tar xf ./doctl-${{ env.DOCTL_VERSION }}-linux-amd64.tar.gz
mv ./doctl /usr/local/bin
doctl registry login
docker stack deploy --compose-file ${COMPOSE_FILE} --with-registry-auth --prune ${STACK_NAME}{% endraw %}
- name: Deploy prod
uses: ironhalik/docker-over-ssh-action@v6
if: github.ref == 'refs/heads/master'
env:
COMPOSE_FILE: ops/docker-compose.prod.yml
STACK_NAME: {{ cookiecutter.project_slug_db }}-prod
{% raw %}DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
REGISTRY: ${{ secrets.REGISTRY }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
with:
user: ubuntu
host: ${{ secrets.PROD_HOST_IP }}
key: ${{ secrets.SSH_KEY }}
script: |
wget https://github.com/digitalocean/doctl/releases/download/v${{ env.DOCTL_VERSION }}/doctl-${{ env.DOCTL_VERSION }}-linux-amd64.tar.gz
tar xf ./doctl-${{ env.DOCTL_VERSION }}-linux-amd64.tar.gz
mv ./doctl /usr/local/bin
doctl registry login
docker stack deploy --compose-file ${COMPOSE_FILE} --with-registry-auth --prune ${STACK_NAME}{% endraw %}
uses: ./.github/workflows/deploy.yml
secrets: inherit
{%- endif %}
64 changes: 4 additions & 60 deletions {{ cookiecutter.project_slug }}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,11 @@
<a href="https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/releases"><img alt="Release Status" src="https://img.shields.io/github/v/release/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}"></a>
</p>


# {{ cookiecutter.project_slug }}

## Architecture
<p align="center">
<a href="#">
<img alt="Architecture Workflow" src="https://i.imgur.com/8TEpVZk.png">
</a>
</p>

## Usage
1. `make up`
2. visit `http://localhost:8666/v1/ping` for uvicorn server, or `http://localhost` for nginx server
2. visit `http://localhost:8666/v1/ping` for uvicorn server
3. Backend, JSON based web API based on OpenAPI: `http://localhost/v1/`
4. Automatic interactive documentation with Swagger UI (from the OpenAPI backend): `http://localhost/docs`

Expand Down Expand Up @@ -63,56 +55,8 @@ $ pre-commit install
pre-commit installed at .git/hooks/pre-commit
```

### Nginx
The Nginx webserver acts like a web proxy, or load balancer rather. Incoming requests can get proxy passed to various upstreams eg. `/:service1:8001,/static:service2:8002`

```yml
volumes:
proxydata-vol:
...
{{ cookiecutter.nginx_container_name }}:
image: your-registry/{{ cookiecutter.nginx_container_name }}
# OR you can do the following
# build:
# context: ./{{ cookiecutter.nginx_container_name }}
# dockerfile: ./Dockerfile
environment:
- UPSTREAMS=/:{{ cookiecutter.backend_container_name }}:8000
- NGINX_SERVER_NAME=yourservername.com
- ENABLE_SSL=true
- HTTPS_REDIRECT=true
- CERTBOT_EMAIL=youremail@gmail.com
- DOMAIN_LIST=yourservername.com
- BASIC_AUTH_USER=user
- BASIC_AUTH_PASS=pass
ports:
- '0.0.0.0:80:80'
- '0.0.0.0:443:443'
volumes:
- proxydata-vol:/etc/letsencrypt
```

Some of the environment variables available:
- `UPSTREAMS=/:{{ cookiecutter.backend_container_name }}:8000` a comma separated list of \<path\>:\<upstream\>:\<port\>. Each of those of those elements creates a location block with proxy_pass in it.
- `HTTPS_REDIRECT=true` enabled a standard, ELB compliant https redirect.
- `ENABLE_SSL=true` to enable redirects to https from http
- `NGINX_SERVER_NAME` name of the server and used as path name to store ssl fullchain and privkey
- `CERTBOT_EMAIL=youremail@gmail.com` the email to register with Certbot.
- `DOMAIN_LIST` domain(s) you are requesting a certificate for.
- `BASIC_AUTH_USER` username for basic auth.
- `BASIC_AUTH_PASS` password for basic auth.

When SSL is enabled, server will install Cerbot in standalone mode and add a new daily periodic script to `/etc/periodic/daily/` to run a cronjob in the background. This allows you to automate cert renewing (every 3 months). See [docker-entrypoint]({{ cookiecutter.nginx_container_name }}/docker-entrypoint.sh) for details.

{%- if cookiecutter.deployments == "yes" %}
### Deployments
A common scenario is to use an orchestration tool, such as docker swarm, to deploy your containers to the cloud (DigitalOcean). This can be automated via GitHub Actions workflow. See [main.yml](/.github/workflows/main.yml) for more.

You will be required to add `secrets` in your repo settings:
- DIGITALOCEAN_TOKEN: your DigitalOcean api token
- REGISTRY: container registry url where your images are hosted
- POSTGRES_PASSWORD: password to postgres database
- STAGING_HOST_IP: ip address of the staging droplet
- PROD_HOST_IP: ip address of the production droplet
- SSH_KEY: ssh key of user connecting to server (`ubuntu` in this case)
{%- if cookiecutter.cloud_provider == "Fly.io" %}
#### Fly.io
GitHub Actions are responsible for deploying the fly. Be sure to set `FLY_API_TOKEN` as a Actions repostiroy secret. View ./.github/workflows/deploy.yml for more details, or read the [Fly.io](https://fly.io/docs/python/frameworks/fastapi/)
{%- endif %}
13 changes: 0 additions & 13 deletions {{ cookiecutter.project_slug }}/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
volumes:
base-data:
{{ cookiecutter.db_container_name }}-data:
{{ cookiecutter.nginx_container_name }}-data:
redis-data:

services:
Expand Down Expand Up @@ -38,18 +37,6 @@ services:
- base-data:/data
- ./{{ cookiecutter.backend_container_name }}/:/code

{{ cookiecutter.nginx_container_name }}:
restart: always
ports:
- "0.0.0.0:80:80"
env_file:
- .env
build:
context: ./{{ cookiecutter.nginx_container_name }}
dockerfile: ./Dockerfile
volumes:
- {{ cookiecutter.nginx_container_name }}-data:/etc/letsencrypt

redis:
restart: always
image: redis:latest
Expand Down
Loading
Loading