Deploy xkcdpass as a service

In this blog post, we will extend our previous xkcdpass package by developping an API around xkcdpass. The goal is to generate a new password on each API request. The API will be in plain text for the moment.

Let's code

Here is our API code, we are using uvicorn as a web server and xkcdpass package. let's write this in a xkcdpassapi.py: import os import uvicorn from xkcdpass import xkcd_password as xp wordfile = xp.locate_wordfile() mywords = xp.generate_wordlist(wordfile=wordfile, min_length=5, max_length=8) def main(): uvicorn.run("xkcdpassapi:app", host=os.environ.get("HOST", "0.0.0.0"), port=int(os.environ.get("PORT", "5000")), log_level="info") async def app(scope, receive, send): assert scope['type'] == 'http' await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], }) await send({ 'type': 'http.response.body', 'body': xp.generate_xkcdpassword(mywords).encode(), }) if __name__ == "__main__": main() The app server will be listening on 0.0.0.0:5000 by default, but can be overriden by HOST and PORT environment variables. Next, let's create a Dockerfile to package the app: FROM python:3.9-alpine AS BUILD RUN apk --update add gcc build-base RUN mkdir -p /xkcdpass/dist RUN pip install shiv RUN pip install xkcdpass==1.17.4 uvicorn[standard] --target /xkcdpass/dist ADD xkcdpassapi.py /xkcdpass/dist RUN cd /xkcdpass && shiv -o xkcdpass.pyz --site-packages dist -e xkcdpassapi:main --compressed FROM python:3.9-alpine COPY --from=BUILD /xkcdpass/xkcdpass.pyz /bin/ CMD xkcdpass.pyz Again we're using multi-stage build:
  1. First step we install dependencies and package eht whole app and dependencies in a shiv xkcdpass.pyz
  2. Then we load xkcdpass.pyz in the final image

Build and run

Next, let's build the container: podman build -t blog/xkcdpass-api . That should give the following output: STEP 1: FROM python:3.9-alpine AS BUILD ✔ docker.io/library/python:3.9-alpine Getting image source signatures Copying blob 1d917aee477c done Copying blob ca3cd42a7c95 done Copying blob 07f5f7d4c6ff done Copying blob b429b6305de5 done Copying blob 747e39ff2433 done Copying config 62acc19195 done Writing manifest to image destination Storing signatures STEP 2: RUN apk --update add gcc build-base fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/main/x86_64/APKINDEX.tar.gz fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/community/x86_64/APKINDEX.tar.gz (1/20) Installing libgcc (10.2.1_pre1-r3) (2/20) Installing libstdc++ (10.2.1_pre1-r3) (3/20) Installing binutils (2.35.1-r1) (4/20) Installing libmagic (5.39-r0) (5/20) Installing file (5.39-r0) (6/20) Installing libgomp (10.2.1_pre1-r3) (7/20) Installing libatomic (10.2.1_pre1-r3) (8/20) Installing libgphobos (10.2.1_pre1-r3) (9/20) Installing gmp (6.2.1-r0) (10/20) Installing isl22 (0.22-r0) (11/20) Installing mpfr4 (4.1.0-r0) (12/20) Installing mpc1 (1.2.0-r0) (13/20) Installing gcc (10.2.1_pre1-r3) (14/20) Installing musl-dev (1.2.2-r0) (15/20) Installing libc-dev (0.7.2-r3) (16/20) Installing g++ (10.2.1_pre1-r3) (17/20) Installing make (4.3-r0) (18/20) Installing fortify-headers (1.1-r0) (19/20) Installing patch (2.7.6-r6) (20/20) Installing build-base (0.5-r2) Executing busybox-1.32.1-r5.trigger OK: 204 MiB in 56 packages --> b0ce731f613 STEP 3: RUN mkdir -p /xkcdpass/dist --> 08ec71cd774 STEP 4: RUN pip install shiv Collecting shiv Downloading shiv-0.4.0-py2.py3-none-any.whl (19 kB) Requirement already satisfied: pip>=9.0.3 in /usr/local/lib/python3.9/site-packages (from shiv) (21.0.1) Collecting click!=7.0,>=6.7 Downloading click-7.1.2-py2.py3-none-any.whl (82 kB) Requirement already satisfied: setuptools in /usr/local/lib/python3.9/site-packages (from shiv) (54.2.0) Installing collected packages: click, shiv Successfully installed click-7.1.2 shiv-0.4.0 --> d47f8bde5e4 STEP 5: RUN pip install xkcdpass==1.17.4 uvicorn[standard] --target /xkcdpass/dist Collecting xkcdpass==1.17.4 Downloading xkcdpass-1.17.4.tar.gz (2.3 MB) Collecting uvicorn[standard] Downloading uvicorn-0.13.4-py3-none-any.whl (46 kB) Collecting h11>=0.8 Downloading h11-0.12.0-py3-none-any.whl (54 kB) Collecting click==7.* Using cached click-7.1.2-py2.py3-none-any.whl (82 kB) Collecting watchgod>=0.6 Downloading watchgod-0.7-py3-none-any.whl (11 kB) Collecting PyYAML>=5.1 Downloading PyYAML-5.4.1.tar.gz (175 kB) Installing build dependencies: started Installing build dependencies: finished with status 'done' Getting requirements to build wheel: started Getting requirements to build wheel: finished with status 'done' Preparing wheel metadata: started Preparing wheel metadata: finished with status 'done' Collecting uvloop!=0.15.0,!=0.15.1,>=0.14.0 Downloading uvloop-0.15.2.tar.gz (2.1 MB) Collecting httptools==0.1.* Downloading httptools-0.1.1.tar.gz (106 kB) Collecting websockets==8.* Downloading websockets-8.1.tar.gz (58 kB) Collecting python-dotenv>=0.13 Downloading python_dotenv-0.17.0-py2.py3-none-any.whl (18 kB) Building wheels for collected packages: xkcdpass, httptools, websockets, PyYAML, uvloop Building wheel for xkcdpass (setup.py): started Building wheel for xkcdpass (setup.py): finished with status 'done' Created wheel for xkcdpass: filename=xkcdpass-1.17.4-py3-none-any.whl size=2325695 sha256=7553f862b63b119c5ea7ca356427293d790521c4b7df00d1d382f170dd7917be Stored in directory: /root/.cache/pip/wheels/5c/59/b1/f022dc79b4977e1da978c895469b6796a98c49ce98e792e050 Building wheel for httptools (setup.py): started Building wheel for httptools (setup.py): finished with status 'done' Created wheel for httptools: filename=httptools-0.1.1-cp39-cp39-linux_x86_64.whl size=105051 sha256=c01116d5c67acd3beee02a26ebd9646dc8603ff0f60692eda12d6aeb940f096d Stored in directory: /root/.cache/pip/wheels/bf/91/65/284ee85517eb6c733119a30257c9c535f795034065a3911b3f Building wheel for websockets (setup.py): started Building wheel for websockets (setup.py): finished with status 'done' Created wheel for websockets: filename=websockets-8.1-cp39-cp39-linux_x86_64.whl size=65602 sha256=0409d4b1f1239b456369479f8225202adebe494e6e76d5ebe17d8836af22747b Stored in directory: /root/.cache/pip/wheels/d8/b9/a0/b97b211aeda2ebd6ac2e43fc300d308dbf1f9df520ed390cae Building wheel for PyYAML (PEP 517): started Building wheel for PyYAML (PEP 517): finished with status 'done' Created wheel for PyYAML: filename=PyYAML-5.4.1-cp39-cp39-linux_x86_64.whl size=45641 sha256=3a2f31bc4826766d19cdb6e7b3e87a79e2b4862fe576cbd23aa3c82be34b26fa Stored in directory: /root/.cache/pip/wheels/b7/a5/c4/504d913c2a55bb09c607541578ec5f844d1ff33467abe93ba5 Building wheel for uvloop (setup.py): started Building wheel for uvloop (setup.py): still running... Building wheel for uvloop (setup.py): finished with status 'done' Created wheel for uvloop: filename=uvloop-0.15.2-cp39-cp39-linux_x86_64.whl size=1444048 sha256=ff338657fbe123cf6de64d7c71de53fb373155cd82ead295b590eb3170a1a655 Stored in directory: /root/.cache/pip/wheels/e2/1a/63/bd18a6050fe7d4a9b180c13874e42a1d2d56f9feca2fc0cd2c Successfully built xkcdpass httptools websockets PyYAML uvloop Installing collected packages: h11, click, websockets, watchgod, uvloop, uvicorn, PyYAML, python-dotenv, httptools, xkcdpass Successfully installed PyYAML-5.4.1 click-7.1.2 h11-0.12.0 httptools-0.1.1 python-dotenv-0.17.0 uvicorn-0.13.4 uvloop-0.15.2 watchgod-0.7 websockets-8.1 xkcdpass-1.17.4 --> 3bd276e1404 STEP 6: ADD xkcdpassapi.py /xkcdpass/dist --> 649e6c6e3fd STEP 7: RUN cd /xkcdpass && shiv -o xkcdpass.pyz --site-packages dist -e xkcdpassapi:main --compressed --> de6b9414bba STEP 8: FROM python:3.9-alpine STEP 9: COPY --from=BUILD /xkcdpass/xkcdpass.pyz /bin/ --> 629fc85d3ef STEP 10: CMD xkcdpass.pyz STEP 11: COMMIT blog/xkcdpass-api --> ef4c287bcc4 ef4c287bcc406e1650c49b4260718f2b151f202155d36dc6017f9d56bdffe5c0 Try the command with: podman run -it blog/xkcdpass-api Gives the following output: INFO: Started server process [1] INFO: Waiting for application startup. INFO: ASGI 'lifespan' protocol appears unsupported. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit) INFO: 10.0.2.100:50252 - "GET / HTTP/1.1" 200 OK Then curl http://localhost:5000

Deploy on Heroku

heroku login heroku container:login heroku create heroku container:push web heroku container:release web heroku open