diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml new file mode 100644 index 0000000..7fbb47a --- /dev/null +++ b/.github/workflows/push.yaml @@ -0,0 +1,45 @@ +name: build +on: + schedule: + # Execute at 2am EST every day + - cron: '0 21 * * *' + push: + branches: + - 'main' + pull_request: + types: + - 'opened' + - 'synchronize' + - 'reopened' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Build local image + uses: docker/build-push-action@v2 + with: + context: . + load: true + tags: hello-world:${{ github.sha }} + + - name: PyTest + run: | + docker run hello-world:${{ github.sha }} pytest app.py + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'hello-world:${{ github.sha }}' + format: 'table' + exit-code: '1' + severity: 'CRITICAL' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8b3442d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +ARG PYTHON_TAG=3.9-alpine +FROM python:$PYTHON_TAG + +COPY app/requirements.txt /tmp/requirements.txt +RUN pip3 install -r /tmp/requirements.txt + +RUN addgroup -g 1000 -S app && \ + adduser -u 1000 -S app -G app + +RUN mkdir -p /home/app \ + && chown -R app /home/app + +USER 1000 + +WORKDIR /home/app +COPY app . + +CMD ["uvicorn", "app:app", "--reload", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a0b2b8f --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +IMAGE_REGISTRY := docker.io +IMAGE_NAME := hello-world-timestamp +IMAGE_TAG := $(shell git rev-parse --abbrev-ref HEAD) + +TRIVY_ARGS := --exit-code=1 --severity=CRITICAL +# TRIVY_ARGS := --exit-code=1 --severity=CRITICAL,HIGH + +CONTAINER_PORT := 8000 + +IMAGE_URL := $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG) + +build: + docker build . -t $(IMAGE_URL) + +run: build + docker run -p $(CONTAINER_PORT):8000 $(IMAGE_URL) + +scan: build + trivy image $(IMAGE_URL) + +test: build + docker run $(IMAGE_URL) pytest app.py \ No newline at end of file diff --git a/README.md b/README.md index 6945f6c..a9d39f5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,84 @@ # terragrunt-experiment-demo-app -Hello world api + +A very simple Hello world api with a unix timestamp. + +**Note: The CI/CD intentionally does not push any images.** + +## How to run it + +Simply run `make run` + +``` +➜ terragrunt-experiment-demo-app git:(main) make run +docker build . -t docker.io/hello-world-timestamp:main +Sending build context to Docker daemon 103.4kB +Step 1/10 : ARG PYTHON_TAG=3.9-alpine +Step 2/10 : FROM python:$PYTHON_TAG + ---> 4b0c0ad22230 +Step 3/10 : COPY app/requirements.txt /tmp/requirements.txt + ---> Using cache + ---> e39f71ee5f01 +... (omitted) +Step 10/10 : CMD ["uvicorn", "app:app", "--reload", "--host", "0.0.0.0", "--port", "8000"] + ---> Using cache + ---> 35bada7cdac8 +Successfully built 35bada7cdac8 +Successfully tagged hello-world-timestamp:main +docker run -p 8000:8000 docker.io/hello-world-timestamp:main +INFO: Will watch for changes in these directories: ['/home/app'] +INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) +INFO: Started reloader process [1] using statreload +INFO: Started server process [8] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +## Features + +### FastAPI + +Implemented in FastAPI, a nice rest API framework for python + +### No critical CVEs (at time of writing) + +Dockerized in an alpine python image with (at the tinme of writing) zero critical CVEs. For contrast, try `trivy image python:3` + +``` +2022-03-28T19:47:17.550-0400 INFO Detected OS: debian +2022-03-28T19:47:17.550-0400 INFO Detecting Debian vulnerabilities... +2022-03-28T19:47:17.685-0400 INFO Number of language-specific files: 1 +2022-03-28T19:47:17.685-0400 INFO Detecting python-pkg vulnerabilities... + +python:3 (debian 11.2) +====================== +Total: 25 (CRITICAL: 25) + ++----------------------+------------------+----------+--------------------+-----------------+---------------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++----------------------+------------------+----------+--------------------+-----------------+---------------------------------------+ +| curl | CVE-2021-22945 | CRITICAL | 7.74.0-1.3+deb11u1 | | curl: use-after-free and | +| | | | | | double-free in MQTT sending | +| | | | | | -->avd.aquasec.com/nvd/cve-2021-22945 | +``` + +#### Is Alpine bad for Python? + +There is literature out there saying Alpine is bad for python https://pythonspeed.com/articles/alpine-docker-python/ . This should be taken with a grain of salt. The main issue is that python packages don't usually have wheels compiled against [musl](https://musl.libc.org/), which is the C library Alpine uses. So this means you're often stuck building packages yourself. If you're building FastAPI (as we are) this is fine, however it's pretty noticable when building large projects like `pandas`. + +Altogether though, it's a compromise. In my experience, it's better to install python on-top of `ubuntu` base images rather than using the official `python` debian-based images, which are usually riddled with CVEs. + +### Trivial pytest example + +The pytest example was really only included to be able to show it off in CI. + +## TODO + +This example is by no means the gold-standard of a python app. Notable aspects: + +- It doesn't have a fancy/standard folder structure. + + I don't write python full time, so I'd need to review the `src,docs,test` etc style. +- No fancy virtual environments. + + Python is famously complex with virtual environments, with many competing tools. I am not touching that here, as I find containers are a nice way to avoid those troubles. If I was working in the ecosystem though, I'd look at `conda` (which I have used, and is very useful), or `poetry`. +- I hear `tox` is useful. I have not used it. +- I am not doing any linting of the python code. +- I find that good projects usually use pre-commit hooks for enforcing good python style. I am not doing that here. diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..2214ce1 --- /dev/null +++ b/app/app.py @@ -0,0 +1,36 @@ +#!/bin/python3 + +from time import time +from fastapi import FastAPI + +app = FastAPI() + +def timestamp(form: str = "unix") -> int: + """ + Args: + form: the type of timestamp. At the moment must be "unix". + + Returns: + The current timestamp + + Raises: + NotImplementedError: If unsupported timestamps are requested. + """ + t = time() + if not form == "unix": + raise NotImplementedError("Only Unix format accepted") + return int(t) + +def test_timestamp(): + assert isinstance(timestamp(), int) + +@app.get("/") +async def read_root(): + return { + "message": "Hello World", + "timestamp": timestamp() + } + +@app.get("/healthz") +async def healthz(): + return {"OK": 200} \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..7ab7b86 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,3 @@ +uvicorn==0.17.6 +fastapi==0.75.0 +pytest==7.1.1 \ No newline at end of file