Continuous Deployment with Python Packages

Published 12. Jun 2016 by Sam Briesemeister.

So you wrote an application in Python, maybe just an incredibly useful library. Fantastic!

Now, how to get it to users, into environments that require it? The natural choice is Python’s PyPI package distribution infrastructure, of course.

But you’re a fast mover, adherent to the DevOps paradigms, and generally like lean process. Right?

Enter the practices of continuous integration, continuous delivery, and continuous deployment.

I’ve come to love GitLab and their CI features, previously a separate product, but now thoroughly integrated. (There’s room for some UI/UX improvement, but that’s another story.) They’ve constructed a layer of automation that facilitates all kinds of workflows. With a little configuration, it can also handle full testing of Python applications, and deployment in the way of pushing packages to PyPI services.

Package Registration

Peter Downs wrote a wonderful tutorial on preparing your package for distribution, including much of the important administrative components, e.g. package registration. Others have similarly posted relevant tutorials. You’ll want to review these tutorials, and register your package with PyPI accordingly.

For the purposes of continuous deployment, there are a few additional steps:

  • A separate user should be registered with PyPI, whose credentials will be stored in GitLab as well (securely). This should not be your primary account.
  • Once created, add this separate user as a maintainer, in PyPI, so it can push new package versions.
  • Save the credentials somewhere, you’ll need them later.

Configuring the GitLab Project

This isn’t too tricky, but there are a few important parts to get right.

First, you’ll obviously have to have a project repository registerd in GitLab. Once you’ve done that, you’ll configure the Variables used during build/test/etc.

Recent releases of GitLab put the Variables configuration in the “gear icon” menu on the far right, when looking at a repository’s homepage. (One of those UI/UX improvements I mentioned…)

Configure the following Variables:

  • Key PYPI_USER: the username you registered (separately) on PyPI, strictly for automatic package upload.
  • Key PYPI_PASSWORD: the password for that user. Hopefully this is quite unique.

Security note: These values are exposed as environment variables to the runners. They will not be visible unless some part of your build/test/deploy application prints them to console output (stdout, stderr etc). Their configuration can only be managed by users, in GitLab, who are granted Master or Owner permissions in the repository. The credentials used to upload packages should be treated as sensitive data. Please be mindful of whom you’re granting access to, and how your automation handles these values.

Configuring the GitLab Runner

This is best done by code example, I’m afraid, annotated for clarity. This configuration belongs in the file .gitlab-ci.yml in your code repository. (Mine, for example, here.)

# Advise GitLab that these environment vars should be loaded from the Variables config.
variables:
    PYPI_USER: SECURE
    PYPI_PASSWORD: SECURE

stage:
  - test
  - deploy
  - cleanup

unittest:   # I hope you're testing before you push a package...
  stage: test
  script:
    - echo # prepare your environment as needed
    - python -m unittest discover -f -s test/ -t ./
    - echo # any other testing/coverage kit you need to apply...

deploy_pypi:
  stage: deploy
  script:   # Configure the PyPI credentials, then push the package, and cleanup the creds.
    - echo "[server-login]" >> ~/.pypirc
    - echo "username=" ${PYPI_USER} >> ~/.pypirc
    - echo "password=" ${PYPI_PASSWORD} >> ~/.pypirc
    - python setup.py check sdist bdist upload   # This will fail if your creds are bad.
    - echo "" > ~/.pypirc && rm ~/.pypirc  # If the above fails, this won't run.
  only:
    - /^v\d+\.\d+\.\d+([abc]\d*)?$/  # PEP-440 compliant version (tags)
  except:
    - branches


cleanup_pypirc:
   stage: cleanup
   when: always   # this is important; run even if preceding stages failed.
   script:
    - rm -vf ~/.pypirc  # we don't want to leave these around, but GitLab may clean up anyway.

And that’s the bulk of it. Provided your package’s setup.py is configured sensibly, this should “just work.”

A Live Example

It’s always nice to have a functioning sample for reference, isn’t it?

I’ve recently rolled out a package of my own, “webapptitude” that demonstrates all this.

This package is a library to extend Google AppEngine, especially webapp2, and composes many features that I find essential for building applications. As such its test setup is somewhat convoluted (i.e. the AppEngine SDK), but the process works well enough.

The configuration above, and my own project, relies on Git tags explicitly declaring versions to deploy. This requires version-tags to comply with PEP-440, and in my configuration, with a prefixed “v” on the tags. My Makefile also prepares these tags based on the version identified in the package code.

Feel free to throw questions at me on Twitter. :)