Skip to content

Commit

Permalink
Add google-native-ts-k8s-python-postgresql example
Browse files Browse the repository at this point in the history
  • Loading branch information
adriangb committed Feb 14, 2022
1 parent a365c4f commit bf7d9b9
Show file tree
Hide file tree
Showing 29 changed files with 1,146 additions and 0 deletions.
6 changes: 6 additions & 0 deletions google-native-ts-k8s-python-postgresql/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Poetry's virtual env
/.venv
# hidden folders in the root directory
/.*
!/.gitignore
!/.pre-commit-config.yaml
61 changes: 61 additions & 0 deletions google-native-ts-k8s-python-postgresql/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
exclude: "^.venv/.*|.html"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: "v4.0.1"
hooks:
- id: trailing-whitespace
exclude: ^VERSION.txt$
- id: check-yaml
- id: pretty-format-json
args: ["--autofix"]
- id: check-merge-conflict
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.5.1"
hooks:
- id: prettier
name: Run prettier on infra source code
types_or: [ts]
- repo: local
hooks:
- id: lockfile
name: Update poetry.lock
language: system
entry: poetry lock --no-update
files: ^pyproject.toml|poetry.lock$
pass_filenames: false
- id: version
name: Update VERSION.txt -> pyproject.toml
language: system
entry: bash -c 'cat VERSION.txt | xargs poetry version'
files: ^pyproject.toml|VERSION.txt$
pass_filenames: false
- id: requirements.txt
name: Export app dependencies to app/requirements.txt
language: system
entry: poetry export -f requirements.txt --output app/requirements.txt
files: ^pyproject.toml|poetry.lock$
pass_filenames: false
- id: isort
name: Run isort on app source code
language: system
entry: poetry run isort
types: [python]
pass_filenames: true
- id: black
name: Run black on app source code
language: system
entry: poetry run black
types: [python]
pass_filenames: true
- id: flake8
name: Run flake8 on app source code
language: system
entry: poetry run flake8
types: [python]
pass_filenames: true
- id: mypy
name: Run mypy on app source code
language: system
entry: poetry run mypy
types: [python]
pass_filenames: false
37 changes: 37 additions & 0 deletions google-native-ts-k8s-python-postgresql/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.PHONY: install-poetry .clean test test-mutation docs-build docs-serve

GIT_SHA = $(shell git rev-parse --short HEAD)
PACKAGE_VERSION = $(shell poetry version -s | cut -d+ -f1)

.install-poetry:
@echo "---- 👷 Installing build dependencies ----"
deactivate > /dev/null 2>&1 || true
pip install -U pip wheel
poetry -V || pip install -U poetry
touch .install-poetry

install-poetry: .install-poetry

.init: .install-poetry
@echo "---- 📦 Building package ----"
rm -rf .venv
python -m pip install -U pip wheel
poetry install
git init .
poetry run pre-commit install --install-hooks
touch .init

.clean:
rm -rf .init .mypy_cache .pytest_cache
poetry -V || rm -rf .install-poetry

init: .clean .init
@echo ---- 🔧 Re-initialized project ----

lint: .init
@echo ---- ⏳ Running linters ----
@(poetry run pre-commit run --all-files && echo "---- ✅ Linting passed ----" && exit 0|| echo "---- ❌ Linting failed ----" && exit 1)

test: .init
@echo ---- ⏳ Running tests ----
@(poetry run pytest -v --cov --cov-report term && echo "---- ✅ Tests passed ----" && exit 0 || echo "---- ❌ Tests failed ----" && exit 1)
156 changes: 156 additions & 0 deletions google-native-ts-k8s-python-postgresql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Containerized Python Xpresso app, deployed to GKE via Pulumi

This example is a full end to end example of delivering a containerized Xpresso app.

Using an infrastructure as code approach, running this repo will:

- Provision a GKE cluster
- Provisions a fully managed Google Cloud SQL PostgreSQL database
- Builds a containerized Xpresso app, and it to the Google Artifact Registry
- Deploys that container image as a Kubernetes Service inside of the provisioned GKE cluster

## Prerequisites

Before trying to deploy this example, please make sure you have performed all of the following tasks:

- [downloaded and installed the Pulumi CLI](https://www.pulumi.com/docs/get-started/install/).
- [downloaded and installed Docker](https://docs.docker.com/install/)
- [signed up for Google Cloud](https://cloud.google.com/free/)
- [followed the instructions here](https://www.pulumi.com/docs/intro/cloud-providers/gcp/setup/) to connect Pulumi to your Google Cloud account.

This example assumes that you have Google Cloud's `gcloud` CLI on your path.
This is installed as part of the
[Google Cloud SDK](https://cloud.google.com/sdk/).

As part of this example, we will setup and deploy a Kubernetes cluster on GKE.
You may also want to install [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) if you would like to directly interact with the underlying Kubernetes cluster.

## Deploying the Example

### Set up your GCP Project

You'll need to create a new GCP project (or use an existing one).
Enable the following APIs in GCP if they are not already enabled:

- [Artifact Registry](https://cloud.google.com/artifact-registry/docs/enable-service#enable)
- [Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstart#before-you-begin)
- [Cloud SQL](https://cloud.google.com/sql/docs/mysql/admin-api#enable_the_api)

If you've configured `gcloud` locally and pointed it at your project you can run:

```shell
gcloud services enable artifactregistry.googleapis.com
gcloud services enable sqladmin.googleapis.com
gcloud services enable container.googleapis.com
```

### Configure Docker

We'll be pushing a docker image to Artifact Registry, so configure docker for authentication:

```shell
gcloud auth configure-docker
```

### Configure Pulumi

Now you're ready to get started with the repo.
Clone the repo then cd into the infra directory:

```shell
cd infra
```

Now set up the Pulumi stack:

```shell
pulumi stack init dev
```

Set the required configuration variables for this program:

```shell
pulumi config set xpresso-gke-demo:project [your-gcp-project-here]
pulumi config set xpresso-gke-demo:region us-west1 # any valid region
```

### Deploy

Deploy everything with the `pulumi up` command.
This provisions all the GCP resources necessary, including your GKE cluster and database, as well as building and publishing your container image, all in a single gesture:

```shell
pulumi up
```

This will show you a preview, ask for confirmation, and then chug away at provisioning your cluster.

```shell
pulumi destroy
pulumi stack rm
```

## Local Development

This package comes set up with some basic facilities for local development:

- Make targets for bootstrapping, testing and linting
- Git hooks (via [pre-commit](https://pre-commit.com)) to do code formatting and syncing of derived files

To set up locally you'll need to have Python 3.10 installed.
If you're using [pyenv](https://github.com/pyenv/pyenv), remember to select the right Python version.

### Bootstrapping

Run:

```shell
make init
```

This will:

- Create a virtual environment using [Poetry](https://python-poetry.org) and install all of the app's dependencies.
- Install git hooks via pre-commit.

### Testing

Run:

```shell
make test
```

### Linting

Linting will auto-run on each commit.
To disable this for a single commit, run:

```shell
git commit -m "<commit message>" --no-verify
```

To disable this permanently:

```shell
poetry run pre-commit --uninstall
```

To run linting manually (without committing):

```shell
make lint
```

### Versioning

So that we can include info about the project version in our infra (in particular, we want the version in the image tag) we keep the source of truth in a `VERSION.txt` file.
This is also convenient to programmatically check for version bumps (for example in CI).

This version is synced to the Python package version (in `pyproject.toml`) via a pre-commit hook.

### Dependency specification

Dependencies are specified in `pyproject.toml` and managed by Poetry.
But we don't want to have to install Poetry to build the image, so we export Poetry's lockfile to a `app/requirements.txt` via a pre-commit hook.
Then when we build the image we can just `pip install -r app/requirements.txt`.
1 change: 1 addition & 0 deletions google-native-ts-k8s-python-postgresql/VERSION.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.1
4 changes: 4 additions & 0 deletions google-native-ts-k8s-python-postgresql/app/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**

!/app
!/requirements.txt
1 change: 1 addition & 0 deletions google-native-ts-k8s-python-postgresql/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
10 changes: 10 additions & 0 deletions google-native-ts-k8s-python-postgresql/app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.10-slim
RUN mkdir /opt/project
WORKDIR /opt/project
COPY ./requirements.txt .
RUN pip install --no-cache-dir -U pip wheel && \
pip install --no-cache-dir -r requirements.txt && \
rm -rf requirements.txt
COPY app ./app/
ENV PYTHONPATH "${PYTHONPATH}:/opt/project"
CMD ["python", "app/main.py"]
Empty file.
Empty file.
14 changes: 14 additions & 0 deletions google-native-ts-k8s-python-postgresql/app/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Literal

from pydantic import BaseSettings, SecretStr


class Config(BaseSettings):
app_port: int
app_host: str
db_username: str
db_password: SecretStr | None = None
db_host: str
db_port: int
db_database_name: str
log_level: Literal["DEBUG", "INFO"]
11 changes: 11 additions & 0 deletions google-native-ts-k8s-python-postgresql/app/app/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import asyncpg # type: ignore[import]


class ConnectionHealth:
def __init__(self, pool: asyncpg.Pool) -> None:
self.pool = pool

async def is_connected(self) -> bool:
conn: asyncpg.Connection
async with self.pool.acquire() as conn:
return await conn.fetchval("SELECT 1") == 1 # type: ignore
42 changes: 42 additions & 0 deletions google-native-ts-k8s-python-postgresql/app/app/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Annotated, AsyncGenerator

import asyncpg # type: ignore[import]
from xpresso import Depends

from app.config import Config
from app.db import ConnectionHealth as DatabaseConnectionHealth



async def get_pool(config: Config) -> AsyncGenerator[asyncpg.Pool, None]:
# password is optional:
# - cloudsql proxy won't work with it
# - docker run postgres won't work without it
connection_kwargs = dict(
host=config.db_host,
port=config.db_port,
database=config.db_database_name,
user=config.db_username
)
if config.db_password is not None:
connection_kwargs["password"] = config.db_password.get_secret_value()
async with asyncpg.create_pool(**connection_kwargs) as pool: # type: ignore
yield pool


InjectDBConnectionPool = Annotated[asyncpg.Pool, Depends(get_pool, scope="app")]


async def get_connection(pool: InjectDBConnectionPool) -> AsyncGenerator[asyncpg.Connection, None]:
async with pool.acquire() as conn: # type: ignore
yield conn


InjectDBConnection = Annotated[asyncpg.Connection, Depends(get_connection)]


def get_db_health(pool: InjectDBConnectionPool) -> DatabaseConnectionHealth:
return DatabaseConnectionHealth(pool)


InjectDBHealth = Annotated[DatabaseConnectionHealth, Depends(get_db_health, scope="app")]
11 changes: 11 additions & 0 deletions google-native-ts-k8s-python-postgresql/app/app/lifespan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from contextlib import asynccontextmanager
from typing import AsyncGenerator

from app.dependencies import InjectDBHealth


@asynccontextmanager
async def lifespan(db_health: InjectDBHealth) -> AsyncGenerator[None, None]:
if not (await db_health.is_connected()):
raise RuntimeError("Failed to connect to DB")
yield
Loading

0 comments on commit bf7d9b9

Please sign in to comment.