Packages are not very exciting but the only sane way to deploy. I've tried to
git clone
and make install
on the servers with Puppet, that was
silly. It's about time I learnt how to build packages.
I run a few Flask applications (including this site) that I would like to package for Debian linux, so packaging a minimal Flask application seemed a good place to start.
I created a very simple Flask application called demosite. The
basic requirement of the site is that python setup.py install
works and that
it provides some mechanism changing the configuration file and logging paths.
The package should install the application, all its dependencies, add the
configuration files to /etc/demosite
, set up the logging to log to
/var/log/demosite
, create a un-privileged user and install an init script in
/etc/init.d/demosite
.
I found this post: Packaging a Flask app in a Debian package very helpful getting me started.
The package is built and packaged on a Debian machine provisioned by Vagrant and run in Virtualbox. The application itself is installed inside a virtualenv environment which is then packaged by fpm. Below are all the steps needed to create a package and install it.
Create a vagrant box and ssh into it:
$ vagrant init chef/debian-7.4
$ vagrant up && vagrant ssh
Switch to the root user, update the system and install build dependencies:
vagrant:~$ sudo su
vagrant:~$ apt-get update
vagrant:~$ apt-get install ruby1.8 rubygems git python-dev python-setuptools python-virtualenv ruby-dev curl
vagrant:~$ gem install fpm pleaserun
Installing the application into a virtualenv
is quite straight forward.
Download the source, create a virtualenv
and install the application in it, a
lot problems with generated paths can be avoided by creating the virtualenv
in the desired install path.
vagrant:~$ git clone https://github.com/tarnacious/demosite.git demosite-src
vagrant:~$ virtualenv /usr/share/demosite
vagrant:~$ cd /home/vagrant/demosite-src
vagrant:~/demosite-src$ /usr/share/demosite/bin/python setup.py install
I used pleaserun to create the init file. The gem requires the ruby-dev package to be installed.
vagrant:~$ pleaserun --user demosite --install --name demosite /usr/share/demosite/bin/gunicorn demosite:app
No platform selected. Autodetecting... {:platform=>"sysv", :version=>"lsb-3.1", :level=>:warn}
Writing file {:destination=>"/etc/init.d/demosite"}
Writing file {:destination=>"/etc/default/demosite"}
The file /etc/default
is sourced using the dot operator in the init script. Environment variables can be added here, for the demosite
application these are the locations of the config file and the logging config
file.
vagrant:~$ cat > /etc/default/demosite << EOF
export DEMOSITE_CONFIG_PATH=/etc/demosite/config.cfg
export DEMOSITE_LOGGING_CONFIG_PATH=/etc/demosite/logging.conf
EOF
Create the config.cfg
expected by the application.
vagrant:~$ mkdir /etc/demosite
vagrant:~$ cat > /etc/demosite/config.cfg << EOF
TAGLINE="this is all so far"
EOF
Create the logging.conf
expected by the application.
vagrant:~$ cat > /etc/demosite/logging.conf << EOF
[loggers]
keys = root, demosite
[handlers]
keys = root, demosite
[formatters]
keys = default
[formatter_default]
format = [%(asctime)s] - %(name)s - %(levelname)s - %(message)s
class = logging.Formatter
[logger_root]
level = DEBUG
qualname = root
handlers = root
[handler_root]
class = logging.handlers.RotatingFileHandler
formatter = default
args = ("/var/log/demosite/root.log",)
[logger_demosite]
level = DEBUG
qualname = demosite
handlers = demosite
[handler_demosite]
class = logging.handlers.RotatingFileHandler
formatter = default
args = ("/var/log/demosite/demosite.log",)
EOF
Debian packages have hooks for a postinst
, preinst
and prerm
. This
postinst
script creates a user demosite
, a directory /var/log/demosite
and changes ownership over to the demosite
user.
vagrant:~$ cat > postconfig << EOF
#!/bin/sh -e
action="\$1"
oldversion="\$2"
. /usr/share/debconf/confmodule
db_version 2.0
if [ "\$action" != configure ]; then
exit 0
fi
if ! getent passwd demosite >/dev/null; then
adduser --quiet --system --no-create-home --home /home/vagrant/demosite --shell /usr/sbin/nologin demosite
fi
mkdir -p /var/log/demosite
chown demosite /var/log/demosite
EOF
In this package the preinst
and prerm
hooks are the same for now: stop the
service if the init script is there.
vagrant:~$ cat >stopservice << EOF
#!/bin/sh
set -e
if [ -f /etc/init.d/demosite ]; then
/etc/init.d/demosite stop
fi
EOF
Package the application into a deb
package with fpm
.
vagrant:~$ fpm -s dir \
-t deb \
-n demosite \
-v 0.1 \
--deb-init /etc/init.d/demosite \
--before-install stopservice \
--after-install postconfig \
--before-remove stopservice \
-d "python" \
/etc/demosite/=/etc/demosite \
/etc/default/demosite=/etc/default/ \
/usr/share/demosite/=/usr/share/demosite
Now copy the package to the host machine, to a build
directory:
vagrant:~$ mkdir -p /vagrant/build
vagrant:~$ cp demosite_0.1_amd64.deb /vagrant/build/
Unless there was any problems with the build we can exit this box and even destroy it.
vagrant:~$ exit
Ok, let's try it out in a new vagrant box!
$ cd build
$ vagrant init chef/debian-7.4
$ vagrant up && vagrant ssh
vagrant:~$ sudo dpkg -i /vagrant/demosite_0.1_amd64.deb
Does it work? Let's try start it and find out.
vagrant:~$ sudo /etc/init.d/demosite start
demosite started.
Looks good, let's see if it works.
vagrant:~$ curl localhost:8000
...
Woohoo!
To automate all this one could use the Vagrant Shell Provisioner or something like Fabric.
Currently the server is not binding to an external network interface and would not be able to bind a privileged port as it is not running as a privileged user. This is OK as I intend to proxy it though nginx, this is quite simple to configure but it would be nice to manage this configuration via Puppet, which I hope to write about in my next post.
Comments
Thanks for the article. I found I had to run:
In order to get pleaserun to work correctly.