Full Stack Python Guide To Deployments
Full Stack Python Guide To Deployments
Purpose
This book supplements the writing and material found on the Full Stack Python
website with completely new content. Full Stack Python (FSP) contains explanations of
Python programming concepts, recommendations for how to learn and links to
tutorials found around the web. However, FSP does not provide step-by-step
instructions for performing an end-to-end Python web application deployment.
Over the past several years of writing FSP, I’ve received numerous requests from the
programming community for more comprehensive details tutorials. This book is the
result of those requests. My hope is that this guide helps folks as much as the FSP
website.
Revision History
2015-08-26: Typo and link fixes throughout the book.
1
who’s already purchased the book will not have to pay for a new copy or the
difference between the new and old prices.
2. Write new content and chapters to expand the deployment in the book.
Let me know via email at matthew.makai@gmail.com whenever you run into confusing
wording, inaccurate code snippets and general rough patches while going through
the book. Thank you!
The picture for the cover was deliberately taken on the ground level, in contrast to the
book edition of Full Stack Python which has a picture from above the clouds. This
book is a hands-on, step-by-step guide for deployments that is complementary to the
20,000 foot deployments overview given by Full Stack Python.
Thank you
To Char, for constant encouragement and telling me to “just release it already.” To
Luke and my parents, for inspiring me and listening to me say month after month that
2
it’d be released “any day now.” To Twilio and the Developer Evangelism team,
because I’d sure as hell never be able to write technical content at a level of
excellence without what I’ve learned from you all since I started on the crew.
Technical reviewers
Special thanks to my technical reviewers Andrew, Dylan and Kate for their feedback
on numerous drafts of the book throughout many months of writing.
Andrew Baker
Andrew Tork Baker is a software developer based in Washington, DC. He works at
Twilio on their Developer Education team, devising new ways to teach people about
programming. Though his current job exposes him to many different programming
languages, Python and its community will always be his first choice. Andrew also
organizes the Django District meetup in DC.
Andrew is the instructor for O’Reilly's Introduction to Docker video, blogs at http://
www.andrewtorkbaker.com and can be reached on GitHub via atbaker and Twitter
@andrewtorkbaker.
Dylan Fox
Dylan Fox is a software developer at Cisco Systems, where he's part of a new
Innovation team focused on building new collaboration technologies. He got
into programming and Python while working on a startup he founded in college.
Dylan can be reached on GitHub via dylanbfox and Twitter @YouveGotFox.
3
Kate Heddleston
Kate Heddleston is a web applications developer in the Bay Area who has been using
Python and Django since graduation. She received her MS in CS at Stanford with an
undergrad degree in communication. Kate enjoys using open-source tools to build
web applications, and especially likes to build product features that interface with the
user. She believes that open-source technologies are the foundation of our modern
tech-driven world and that automation is one of the core values that technology offers
us. Thus, open-source automation tools are some of Kate’s favorite things in the world,
just below puppies and just above shoe shopping. Kate can be reached on GitHub via
heddle317 and Twitter @heddle317.
4
Table of Contents
Chapter 1: Introduction .............................................................. 8
1.1 Who This Book Is For 10
1.2 Our Deployment 10
1.3 Deployment Automation 12
1.4 How to Use this Book 14
1.5 Services We'll Use 14
1.6 Our Example WSGI Application 15
1.7 Ready for Server Setup 17
Chapter 2: Servers ................................................................... 18
2.1 Hosting Options 20
2.2 What are Virtualized Servers? 21
2.3 Obtain your Virtual Server 22
2.4 Create Public and Private Keys 28
2.5 Boot the Server and Secure It 30
2.6 Upload the Public Key 35
2.7 Restart the SSH Service 36
2.8 Automating Server Setup 37
2.9 Next: Operating System 41
Chapter 3: Operating Systems ......................................................... 42
3.1 Ubuntu 44
3.2 Installing Ubuntu Packages 45
3.3 Enable the Firewall 46
3.4 Ansible 47
3.5 Automating Operating System Setup 47
3.6 Next: Web Server 51
Chapter 4: Web Servers ............................................................... 53
4.1 Web Servers in Our Deployment 55
4.2 Visualizing our Web Server’s Purpose 56
4.3 Nginx 57
4.4 Installing Nginx 58
4.5 Domain Name Service Resolution 58
4.6 Configure Nginx Without HTTPS 62
4.7 Create an SSL Certificate 64
4.8 Install SSL Certificates 66
4.9 Configure Nginx with HTTPS 68
5
4.10 Restarting Nginx 70
4.11 Automating the Nginx Configuration 70
4.12 Next: Source Control 76
Chapter 5: Source Control ............................................................ 78
5.1 Hosted Source Control Services 81
5.2 Creating a Deploy Key 81
5.3 Authorizing Git Clone Access 82
5.4 Pulling Project Code to the Server 85
5.5 Automating Source Control 87
5.6 Next: Databases 88
Chapter 6: Databases .................................................................. 90
6.1 Databases in Our Deployment 92
6.2 PostgreSQL 93
6.3 Installing PostgreSQL 93
6.4 NoSQL Data Stores 95
6.5 Redis 96
6.6 Installing Redis 96
6.7 Testing the Redis Installation 96
6.8 Automating the PostgreSQL and Redis Installations 98
6.9 Next: Application Dependencies 100
Chapter 7: Application Dependencies .................................................. 103
7.1 Application Dependencies in Our Deployment 105
7.2 Isolating Dependencies 106
7.3 Storing Dependencies in requirements.txt 106
7.4 Creating Our Virtualenv 107
7.5 Installing Dependencies 108
7.6 Syncing the Database Schema 110
7.7 Populating Our Application’s Initial Data 110
7.8 Automating App Dependency Installation 111
7.9 Next: WSGI Server 115
Chapter 8: WSGI Servers .............................................................. 117
8.1 What is WSGI? 119
8.2 WSGI Server Implementations 120
8.3 Configuring Green Unicorn 121
8.4 Starting Gunicorn with Supervisor 122
8.5 Our App is Live! 123
8.6 Automating Gunicorn Setup 123
8.7 Next: Task Queue 125
Chapter 9: Task Queues ............................................................... 127
9.1 What Are Task Queues For? 129
9.2 Task Queue Implementations 130
9.3 Configuring Celery 131
6
9.4 Automating Celery Installation 132
9.5 Next: Continuous Integration 137
Chapter 10: Continuous Integration .................................................... 139
10.1 How Will We Use Continuous Integration? 141
10.2 Setting Up Continuous Integration 142
10.3 Provisioning a New Server 143
10.4 Tweaking Our Ansible Automation 145
10.5 Jenkins Package Installation 148
10.6 Securing Jenkins with Authentication 150
10.7 Creating a CI Deploy Key 156
10.8 Creating Our Jenkins Build Job 160
10.9 Go Ahead, Push Some New Code 163
10.10 Next Steps 164
10.11 Other Open Source CI Projects 164
10.12 Hosted CI Services 165
Chapter 11: What's Next? .............................................................. 167
11.1 Deployment Enhancements 168
11.2 Improving Performance 169
11.3 Onward! 169
Appendix A: Glossary .................................................................. 170
Appendix B: More Python Resources ..................................................... 177
Appendix C: Flask Application Code Tutorial ........................................... 184
7
Introduction
You've built the first version of your Python-powered web application. Now you want
anyone around the world with an Internet connection to be able to use what you've
created. However, before it's possible for others to access your application you need
to configure a production server and deploy your code so it’s running properly in that
environment.
How do you go about handling the entire deployment? What about subsequent
deployments when your code changes? Even if you've previously deployed a web
application there are many moving pieces in the process that can be frustratingly
difficult to handle.
That's where The Full Stack Python Guide to Deployments comes in. This book will
guide you step-by-step through every task necessary to deploy a Python web
application.
Each chapter in this book will teach you how to manually configure a part of the
deployment pipeline and explain what you’re accomplishing with that step. With the
knowledge you learn from working through each step manually, we'll then use Fabric
and Ansible to automate the deployment process. At the end of each chapter you’ll
not only understand what you’re doing but you’ll also have the steps automated for
future deployments.
9
Who This Book is For
If you’re a beginner to intermediate Python developer who’s learned the basics of
building a web application and wants to learn more about deploying web apps, then
this is exactly the book for you.
There are many deployment tutorials on the web, many of which are linked to on Full
Stack Python and in the chapters of this book. However, you’d need to piece together
many of those tutorials and read between the lines to get a complete end-to-end
deployment successfully completed. With this book, you won’t need to worry about
guessing what to do next. We will walk through every step and explain it so you know
what you’re doing and why it needs to be done.
If you’ve already deployed numerous Python web applications and are managing
several Python apps in production, this isn’t the book for you because we are starting
with the assumption that you don’t have any pre-existing knowledge about
deployments.
Our Deployment
Throughout this book we'll work through setting up the infrastructure to run a
production version of a Python web application. All code will be deployed on a single
virtual private server.
It’s okay if some of the technical terms, such as production server or WSGI, are
confusing to you! You’ll learn their definitions and how the pieces fit together as we
go along. There's also a technical terms appendix for reference that you can find near
the end of the book.
10
The picture below this paragraph is a deployment concepts map of the book's
content. Each chapter in this book contains this map with a highlight on the concept
and software we will configure in that chapter.
The above map shows concepts such as web servers and web frameworks. These
concepts are abstractions, not specific implementations of Python software projects.
For example, the abstract concept of a web server is implemented the by Nginx
(pronounced "Engine-X”) server, which we’ll install and configure in chapter 4.
The below map has the same structure as the above map but replaces the concepts
with their implementations for our deployment pipeline.
11
Again, don't worry if you’re unfamiliar with the concepts or implementations shown in
the above maps. Each chapter will introduce a concept, explain how to set up the
implementation manually, automate the steps and give additional resources to
continue learning more advanced topics on the subject.
Deployment Automation
This book teaches you how to automate your application deployments even if you've
never done systems administration work before. Once you understand how
deployments work and have automation in place, you'll be able to quickly iterate on
your code in your development environment then immediately get working code out
to your production server.
12
Automation is critical for keeping running web applications up to date with the most
recently developed code. Once your application is live on the production server, users
will give you feedback, request changes and discover bugs. The faster you can fix
those issues and add enhancements the better chance your application will succeed.
Automated deployments provide the speed and reliability to keep the code running
in production up to date even if you make many changes each day.
Every step in the deployment is automated as we work our way through the chapters.
There is an open source repository on GitHub with the automation code, as well as Git
tags for the incremental steps performed in each chapter. The following links take you
to each chapter’s corresponding tag on GitHub:
• 01-introduction: just the README and a stub directory for SSH keys
Tags may be added in the future when new chapters are added to this book.
13
How to Use this Book
If you've never deployed a Python web application before my recommendation is to
go through the book twice. First, go through the manual steps and take your time
understanding the individual components, such as WSGI servers, as well as the overall
picture of how the implementations fit together once they're deployed. Once you
understand the manual deployment process then set up a second, separate server
and work through each chapter's deployment automation steps.
Learning how to automate deployments with Ansible will be most useful for folks who
have already manually deployed web applications and have a grasp on basic systems
administration. If you're coming to this book with prior deployment experience then
skim through the manual steps in each chapter before moving on to the automated
deployment instructions.
GitHub and Twilio can be used with the free trial account for our purposes.
14
We'll also need several free open source projects to handle the deployment,
including
• Nginx: the second most common web server currently deployed that is
very popular with the Python community
You'll also need a local Linux or Mac OS X environment. If you're on Windows you
should stand up VirtualBox on your machine to run Ubuntu Linux 14.04 LTS. Here’s a
handy tutorial for installing VirtualBox on Windows if you need to do that now.
Our example WSGI application in this book is a Flask project which serves up Reveal.js
presentations that allow live audience voting by text messages. The votes are
calculated in the Flask application and immediately displayed in the presentation via a
WebSocket connection. The app is called Choose Your Own Deployment Adventure
15
Presentations. The code was used at DjangoCon US 2014 for a talk named "Choose
Your Own Django Deployment Adventure" given by Kate Heddleston and myself. The
following screenshot shows what the application looks like with the default
presentation styling.
A benefit of using the Choose Your Own Deployment Adventure Presentations code
as our example is that there is a detailed walkthrough for building the application
(part 1, part 2, part 3 and part 4) on the Twilio blog. The full four-part tutorial is also
included as Appendix C in this book if you prefer to work through it that way. The
code is well documented and released under the MIT license.
16
Ready for Server Setup
Let's get started with our web application deployment by obtaining a production
server from Linode.
17
Servers
Your web application must run somewhere other than your development environment
on your desktop or laptop. That location is a separate server (or cluster of servers)
known as a “production environment”. Throughout this book, we’ll use a single server
for our application’s production environment deployment.
In this chapter we'll take a look at several hosting options, obtain a virtual private
server to use as our production environment, boot up the server and secure it against
unauthorized access attempts.
19
As shown in the above diagram, Linode’s cloud offering will provide us with a virtual
private server for our production environment in this deployment.
Hosting Options
Let's consider our hosting options before diving into the deployment on the Linode
virtual private server. There are four general options for deploying and hosting a web
application:
20
• Platform-as-a-service (PaaS): abstracted execution environments such as
Heroku, Python Anywhere or Amazon Elastic Beanstalk
The first three options are similar. The deployer needs to provision one or more
servers with a Linux distribution. System packages, a web server, WSGI server,
database and the Python environment are then installed. The application code can be
pulled from a source controlled repository and configured in the environment.
We’ll deploy our web application on a virtual private server (VPS). There are plenty of
VPS hosting options. I've used Linode for over five years now. They're a stable
company with solid support when issues occur, which is rarely. You typically get what
you pay for in the VPS hosting industryc.
If you're considering a different provider make sure to check the resources section at
the end of this chapter for evaluating VPS alternatives.
21
Let's obtain a Linode account, provision a server and get our deployment started.
A note before we get started. Throughout this book the instructions will follow a
standard format. Each step will explain what to do along with some context, such as a
screenshot or snippet of code you'll need to type in. Within the code snippets there
are lines prefixed with # that are comments. You don't have to type the comments in,
they just provide additional context for what the commands are specifically doing and
why the steps are necessary.
With that note out of the way, let's provision a Linux server so we can begin the
deployment.
1. Point your web browser to Linode.com. Their landing page will look
something like the following image.
22
3. You'll be sent an email for account confirmation. Fill out the appropriate
information and add initial credit to your account. If you want to enter a
referral code, mine is bfeecaf55a83cd3dd224a5f2a3a001fdf95d4c3d. Your account
23
will go for a quick review to ensure you're not a malicious spam bot and
then the account will be fully activated.
4. Once your account is activated refresh the page. You'll be given a page to
add a Linode instance.
5. Select the 1024 option, month-to-month billing and the data center
location of your choice. I chose Newark, NJ because I grew up in northern
NJ and otherwise the location isn’t important for my deployment. If your
most of your users are located in a specific country or region you'll want
to select the data center location closest to them.
6. Click "Add this Linode!" and a dashboard will appear where we’ll see the
Linode is being created.
24
7. Refresh the page and look for the status to change to "Brand New." Write
down or remember the IP address as we’ll need it later to SSH into the
server, then click on the name of the Linode. A page will appear to show
more information about your new virtual private server.
25
9. We're going with Ubuntu 14.04 LTS for our deployment. This version of
Ubuntu is the latest LTS (Long Term Support) release and has a 5 year
support lifecycle. Therefore this version will receive security updates until
April 2019 as shown on the Ubuntu wiki page for LTS releases.
26
10. Select Ubuntu 14.04 LTS and enter a password. Make sure you type the
password in carefully and remember it! We'll need the password again in
a few minutes to log in as our root user. The "Deployment Disk Size" and
"Swap Disk" can be left as they are.
27
11. Linode will send us back to our server’s dashboard when the build
process begins. We'll see the progress bars start to increase and in a
couple of minutes it'll be ready to boot up.
However, don't start up the server just yet. We need to create a public-private key pair
that will be used to harden our new server against unauthorized login attempts.
Once we are using the key pair we can disable password logins for increased security.
In addition, we won't have to type in a password each time we want to access the
server via SSH, which is super convenient for quick logins.
If you already have an SSH key pair you want to use then feel free to skip on to the
next section. However, if your existing key pair has a passphrase on it you'll want to
follow these steps to create a new key pair. This book uses a passphrase-less key pair
to perform the automated deployments as well as minimize the typing during our
manual deployment process.
2. Create a local directory with the following commands to store our key
pair. Remember that there is also a companion GitHub repository with all
28
this code and tags for each chapter in case you do not want to type
everything in yourself. However, even if you use the code in the repository
you'll have to create your own public-private key pair with these steps
since the key pair cannot be shared.
# the mkdir command with the -p argument will recursively
# create the directory and subdirectory
mkdir -p fsp-deployment-guide/ssh_keys
5. When prompted for a file in which to save the keys do not use the default.
Instead, enter the following directory and file into the prompt.
# we are saving the private key in our local ssh_keys directory
./prod_key
6. Press enter twice when prompted for a passphrase. We will not use a
passphrase for the deployer user's keys.
7. The key pair will be generated by ssh-keygen. Now you have two new files: a
private key prod_key and a public key prod_key.pub. The private key should
never be shared with anyone as it will allow access your server once it's
29
setup. The public key can be shared with anyone and does not need to be
kept private.
8. Execute the following command to create one more new file with the
newly created public key as an authorized key.
# the authorized_keys file will allow our server to grant SSH
# access to a connection using the private key
cp prod_key.pub authorized_keys
You now have the required public and private key files as well as an authorized_keys file.
Keep backups of these files in a safe place since once the server is locked down you'll
need them to log in.
With our new key files we can now fire up the server and set up our security
configuration.
Fortunately, with a few steps we can mitigate the most glaring security weaknesses of
a fresh server.
When you're ready to begin the initial lock down steps we can start our machine up.
1. Click the "Boot" button and the Ubuntu 14.04 LTS boot process will get
started. Booting should take less than a minute. Bring up your local
command line as we'll need it to connect to the remote machine.
30
3. You'll likely receive a prompt like the following warning. This prompt is
stating that you've never connected to this server before, so are you sure
that this host's signature matches the server you're intending to connect
to? Enter yes then enter the root password you created during the earlier
Linode server provisioning step.
The authenticity of host '66.175.209.192 (66.175.209.192)' can't be established.
RSA key fingerprint is 51:3c:ba:bc:c3:83:1a:36:b1:2d:e3:f6:6d:f0:11:56.
Are you sure you want to continue connecting (yes/no)? yes
31
4. You will see a "Welcome to Ubuntu 14.04.3 LTS" message followed by a
prompt. Now we can enter commands on the remote machine to get the
server secured and setup.
8. Next we're going to modify the SSH configuration so the root user
account cannot be directly logged into. Only someone using the exact
private key we created earlier can connect to this server.
9. Open the /etc/ssh/sshd_config file in the text editor of your choice. For
example using vim /etc/ssh/sshd_config. That will open the file in Vim, but
you can also use the nano or emacs commands instead. You should see the
following lines of the file in your editor.
32
#ListenAddress ::
#ListenAddress 0.0.0.0
Protocol 2
# HostKeys for protocol version 2
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_dsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
#Privilege Separation is turned on for security
UsePrivilegeSeparation yes
# Logging
SyslogFacility AUTH
LogLevel INFO
# Authentication:
LoginGraceTime 120
PermitRootLogin yes
StrictModes yes
RSAAuthentication yes
PubkeyAuthentication yes
#AuthorizedKeysFile %h/.ssh/authorized_keys
33
ChallengeResponseAuthentication no
14. Next we need to create a non-root group for the new non-root user we'll
create in a few steps. Execute the following commands. You can change
"deployers" to another group name if you want but you have to be
consistent in future steps.
# create the new deployers group
/usr/sbin/groupadd deployers
# back up the sudoers file
mv /etc/sudoers /etc/sudoers-backup
# modify the sudo list so the deployers group has sudo privileges
(cat /etc/sudoers-backup ; echo "%deployers ALL=(ALL) ALL") > /etc/sudoers
# ensure the appropriate permissions are on the sudoers file
chmod 0440 /etc/sudoers
15. Now add a non-root user with the non-root group we just created.
Replace the name Matt Makai with your name (but leave the quotes around
34
the name). You will be prompted to enter the same new password twice
for this account.
# create the new user. be sure to use your own name here
/usr/sbin/useradd -c "Matt Makai" -m -g deployers deployer
# set up a password for the new user. you'll need the
# password to run sudo commands
/usr/bin/passwd deployer
16. Create a directory for the new deployer user to store our public and
private keys.
# add the deployer user to the deployers group
/usr/sbin/usermod -a -G deployers deployer
# create a directory for the deployer's public key and
# authorized_keys file
mkdir /home/deployer/.ssh
# change the owner and group of the .ssh directory to deployer
# and deployers, respectively
chown -R deployer /home/deployer/.ssh
chgrp -R deployers /home/deployer/.ssh
Don't log out of root just yet. We need to upload the public and private keys for the
deployer user so we can log in with that account.
1. Keep the root SSH connection open. We'll need that in a minute.
2. Go back to the command prompt on your local machine where the SSH
keys we created earlier in this chapter are stored.
35
3. Upload the public SSH key and authorized_keys files to our server for the
new deployer user. Again, make sure to replace the IP address with your
server's IP address.
# scp is the 'secure copy' command and will transfer the
# public key along with the authorized_keys file to the server
scp prod_key.pub authorized_keys deployer@{your.server.ip.address}:~/.ssh
4. When prompted by the scp command enter the deployer's password that
was created in the previous section.
2. Now try to SSH in with the new deployer user in the other terminal
window. Again replace the IP address in this command with the IP address
of your server.
# connecting to our new user instead of the root user
ssh -i ./prod_key deployer@{your.server.ip.address}
3. If you get a command prompt on the server, success! We can now close
out our root connection! You won't be able to log directly into root any
36
longer, only into the deployer user. That's a really good thing because
automated malicious bots often try to log into root with their attacks.
1. On your local machine, create a new directory named prod under your
project. prod will store files for handling deployments to our production
server for our application.
2. Within prod create a new file named fabfile.py with the following contents.
from os import environ
from fabric.api import *
from fabric.context_managers import cd
from fabric.contrib.files import sed
"""
Fabric file to upload public/private keys to remote servers
and set up non-root users. Also prevents SSH-ing in with the
root user. Fill in the following blank fields then run this
Fabric script with "fab bootstrap".
"""
37
env.password = ''
"""
The following functions should not need to be modified to
complete the bootstrap process.
"""
def bootstrap():
local('ssh-keygen -R %s' % env.host_string)
sed('/etc/ssh/sshd_config', '^UsePAM yes', 'UsePAM no')
sed('/etc/ssh/sshd_config', '^PermitRootLogin yes', 'PermitRootLogin no')
sed('/etc/ssh/sshd_config', '^#PasswordAuthentication yes',
'PasswordAuthentication no')
_create_privileged_group()
_create_privileged_user()
_upload_keys(env.new_user)
run('service ssh reload')
def _create_privileged_group():
run('/usr/sbin/groupadd ' + env.new_user_grp)
run('mv /etc/sudoers /etc/sudoers-backup')
run('(cat /etc/sudoers-backup ; echo "%' + env.new_user_grp \
+ ' ALL=(ALL) ALL") > /etc/sudoers')
run('chmod 440 /etc/sudoers')
38
def _create_privileged_user():
run('/usr/sbin/useradd -c "%s" -m -g %s %s' % \
(env.new_user_full_name, env.new_user_grp, env.new_user))
run('/usr/bin/passwd %s' % env.new_user)
run('/usr/sbin/usermod -a -G ' + env.new_user_grp + ' ' + \
env.new_user)
run('mkdir /home/%s/.ssh' % env.new_user)
run('chown -R %s /home/%s/.ssh' % (env.new_user,
env.new_user))
run('chgrp -R %s /home/%s/.ssh' % (env.new_user_grp,
env.new_user))
def _upload_keys(username):
local('scp ' + env.ssh_key_dir + \
'/prod_key.pub ' + env.ssh_key_dir + \
'/authorized_keys ' + \
username + '@' + env.host_string + ':~/.ssh')
3. Within fabfile.py, edit line 17 with the root password used while creating
your Linode server.
# this is the root password you created with the Linode server
env.password = 'New root password here'
4. Next edit line 21 with your server IP address that can be found on the
dashboard of your Linode account.
# this IP address can be found on the Linode Manager Linodes list
env.hosts = ['192.168.1.1']
5. Modify line 24 with your full name for the non-root user.
39
Running the Fabric File
Fabric relies on Python having the Fabric library installed. We’ll use a virtualenv, a
Python tool for isolating packages, to create a separate Python installation that keeps
our Fabric package dependency separate from other existing Python packages.
Note that we will have to use Python 2 for now because unfortunately Fabric and
Ansible (which we’ll starting using in the next chapter) do not yet support Python 3.
Create a virtualenv with the following commands. This virtualenv will hold our Python
application dependencies. If you already have a directory where you keep your
virtualenvs, you can skip the first step and place your new virtualenv in that existing
directory.
mkdir ~/Envs/
virtualenv -p python2.7 ~/Envs/fspdeploy
source ~/Envs/fspdeploy/bin/activate
When you enter the last of those commands your prompt will change to look
something like this, which means we’ve activated the virtualenv in our current shell:
(fspdeploy)$
If we try to use the fabric command right now, it’ll fail with the following error
message.
-bash: fabric: command not found
We need to install Fabric into the virtualenv. Use the following pip command to install
Fabric:
pip install fabric==1.10.2
Make sure you’ve plugged in your server settings at the top of the Fabric file as
specified in the previous section. Execute the script from the local command line with
fab bootstrap. You'll be prompted for a password for the new user and then the script
will connect to the server again with that user to complete the steps we walked
through manually earlier this chapter.
40
Note that this script does not install fail2ban like we did in the manual steps above.
We'll install that package as part of the Ansible scripts we're going to create in the
next chapter.
In the next chapter we'll establish the operating system configuration for running
Python web applications. We will also introduce Ansible which will provide the rest of
the automation necessary for the deployment.
• See the deployment and servers pages on Full Stack Python for a slew of
additional resources and explanations on these topics.
• Choosing a low cost VPS reviews the factors that you should weigh when
deciding on hosting providers.
• First 5 Minutes on a Server explains what steps are necessary for a basic
security profile on a new server.
41
Operating Systems
An operating system runs on a server and controls access to computing resources that
our web application will use to run.
An operating system makes many of the computing tasks we take for granted easy.
For example, the operating system enables writing to files, communicating over a
network and running multiple programs at once. Otherwise you'd need to control the
CPU, memory, network, graphics card and many other components with your own
low-level implementation.
In this chapter we’ll set up and configure our operating system, Ubuntu Linux 14.04
Long Term Support (LTS), on our Linode virtual private server.
43
Ubuntu includes a package manager for installing necessary system libraries that we
need to run our Python web application. We will also lock down the operating system
against unauthorized access attempts.
Ubuntu
Ubuntu is a Linux distribution packaged by the Canonical Ltd company. For desktop
versions of Ubuntu, GNOME (until the 11.04 release) or Unity (11.10 through current)
is bundled with the distribution to provide a user interface.
Ubuntu Long Term Support (LTS) releases are the recommended versions to use for
deployments. LTS versions receive five years of post-release updates from Canonical.
Every two years, Canonical creates a new LTS release, which allows for an easy
44
upgrade path as well as flexibility in skipping every other LTS release if necessary. As
of 2015, 14.04 Trusty Tahr is the latest LTS release.
Mac OS X, and to a lesser extent Windows, are fine for developing your Python
application. However, Windows and Mac OS X are not appropriate for our production
deployment.
• nginx is a web server that will answer our incoming HTTP requests.
• supervisor will control the state of our WSGI server and other installed
programs
Let's install these packages that we'll need throughout our deployment.
45
2. Next install the required packages for our Python web application
deployment.
$ sudo apt-get install fail2ban python-virtualenv python-dev
3. It'll take a little while for the packages to get installed since Aptitude is
downloading everything from the central Ubuntu package repositories.
4. When it's done you'll see something like the following line and you'll be
back at the input prompt.
Processing triggers for libc-bin (2.19-0ubuntu6) ...
With these packages in place we now have the basic system dependencies to get our
Python environment running. However, our system still needs a firewall to lock down
ports other than 22 (ssh), 80 (HTTP) and 443 (HTTPS).
The first three above commands tell our operating system to only allow network
connections to go through to ports 22, 80 and 443. We now need to enable the
firewall with the following command:
sudo ufw enable
46
Our firewall is in place, but it was a pain to manually run all the above commands. Let’s
now take a look at the tool Ansible which will help us automate our deployment
process.
Ansible
Ansible is an open source configuration management tool written in Python that will
allow us to automate the steps we performed manually above. We'll also build on the
Ansible scripts, which are called “playbooks” in Ansible terminology, in each
subsequent chapter.
Ansible has hundreds of built-in modules to execute tasks for setting up and running
servers. The source code for every module is freely available on Ansible's GitHub
repository.
1. In the last chapter we created a prod directory for our fabfile.py script. Now
we’re going to add our Ansible files within the same directory. This can
either be under your project or a separate source control repository.
Remember to keep your confidential information such as usernames and
passwords private!
cd prod
47
touch deploy.yml hosts
mkdir roles group_vars
2. We just created a new file for the main YAML playbook named deploy.yml,
which we'll use to coordinate our deployment's various roles. We also
created a hosts file that'll tell Ansible what server IP addresses we should
deploy to.
4. Fill in the hosts file with the following text. Replace 192.168.1.1 with your
Linode server’s IP address.
[common]
192.168.1.1
5. Next we're going to fill in the roles that will automate the steps we
otherwise would have to manually execute. Some of these directories will
be used later to set up the database and web server.
mkdir -p roles/common/tasks
cd roles
mkdir -p common/handlers common/templates
48
7. Create the following YAML within the group_vars/all file.
# Chapter 3: Operating System (Ubuntu)
app_name: cyoa
deploy_user: deployer
deploy_group: deployers
## this is the local directory with the SSH keys and known_hosts
## file do not include a trailing slash
ssh_dir: ~/fsp-deployment-guide/ssh_keys
8. Let's automate the first steps we took on the server where we manually
updated the apt repository and installed required system packages.
Create a file named prod/roles/common/tasks/main.yml and have it contain the
following YAML.
###
# configures the server and installs the web application
##
- include: ubuntu.yml
9. We need to create the ubuntu.yml file we just wrote on the last line of
main.yml. Create a file named roles/common/tasks/ubuntu.yml so that it is in the
same directory as the main.yml file and fill it with the following YAML lines.
##
# updates the APT package cache and install packages
# servers necessary for web. also enables firewall
##
- apt: update_cache=yes
sudo: yes
49
ufw: rule=allow port=22
- name: enable HTTP connections for web server
ufw: rule=allow port=80
- name: enable HTTPS connections for web server
ufw: rule=allow port=443
- name: enable firewall
ufw: state=enabled
10. We need to install Ansible into our virtualenv. If the virtualenv isn’t
activated right now, make sure to activate it first with the following
command. Make sure to specify the location where you created the
virutalenv back in chapter 2.
source ~/Envs/fspdeploy/bin/activate
12. We've created the basic outline of an Ansible playbook that automates
what our manual instructions set up so far. In order to run the playbook I
recommend creating a shell script named deploy_prod.sh in the base
directory of our project with the following contents.
#!/bin/bash
ansible-playbook ./prod/deploy.yml --private-key=\
./ssh_keys/prod_key -K -u deployer -i ./prod/hosts
14. Now we can invoke that file by running it on the command line as follows.
./deploy_prod.sh
50
15. You'll be asked for the sudo password once to kick things off then if
everything's been typed in properly this playbook will automate the
manual steps from this chapter and the previous one.
• What is a Linux distribution and how do I choose the right one? will explain
more about what distributions are.
51
Additional Ansible Resources
Ansible is a powerful configuration management tool. Once you get over the initial
YAML-learning hump, it’s easy to deploy an entire application stack, as we’ll show
throughout the rest of this book. The following resources will help you get more
comfortable with using Ansible.
• The Ansible Briefs email newsletter by Matt Jaynes always has the latest
helpful resources and tutorials on Ansible.
• Ansible vs. Shell Scripts explains why automating with Ansible instead of
shell scripts is a good idea.
• Ansible Text Message Notifications with Twilio SMS is my blog post with a
detailed example for using the Twilio module that's included as part of core
Ansible 1.6+.
• First Five (and a half) Minutes on a Server with Ansible is similar to the first
five minutes on a server post but shows how to automate those steps using
Ansible.
52
Web Servers
Web servers respond to Hypertext Transfer Protocol (HTTP) requests from clients and
send back a response containing a status code and content such as HTML, XML or
JSON as well. In our deployment, the web server handles serving up static files like
our CSS and JS, while serving as a proxy that passes requests to the WSGI server and
the response back to the web browser.
We’ll use the web server Nginx (pronounced “Engine-X”) throughout our deployment.
54
Nginx is used by many of the top websites on the Internet. It’s configuration files are
generally considered easier to read and write than the Apache HTTP server’s config
files.
1. Handle requests for static files and respond with appropriate files
2. Reverse proxy requests and responses to and from our WSGI server
In the first use case our Nginx web server will directly handle requests for static files by
responding with appropriate files from the static/ directory. In the static/ directory
55
there will be subfolders for various types of static files such as JavaScript, CSS and
images.
For the second use case the web server be a reverse proxy to pass on requests to our
WSGI server so it can run our Python code and craft the response. A reverse proxy is
simply a middleman that accepts an incoming HTTP request and passes it along to the
WSGI server so it can process the request and issue a response.
The web server sends files to a web browser based on the browser's requests. In the
first request, the browser asked for the file query.min.js and the server responded with
the JavaScript file. The web browser then accessed site.css and logo.png and again the
server responded with those static files.
56
However, not all requests are for static files. For example, a page from our web
application must be generated by the WSGI server running our Python code. That
scenario is shown in the following image.
In this case the web server is configured to pass requests for non-static files to the
WSGI server. We’ll lay the groundwork for that configuration in this chapter and finish
it off later in the WSGI server chapter.
Nginx
In our deployment we'll use the Nginx web server, the second most commonly used
web server after Apache. While both Nginx and Apache are fantastic pieces of
software, we're going to use Nginx because it's easier to install and configure if you've
never done this type of thing before.
57
Nginx is used at the largest scale by many organizations on the web so you won't have
to worry about swapping it out later for Apache.
Before we install and configure Nginx on our server we'll need to perform two steps:
set a domain name to point to our server and (optionally) create an SSL certificate.
We’ll first take care of setting up the domain name then give you the choice to create
an SSL certificate to provide an HTTPS connection or just use plain old HTTP to serve
the application.
Installing Nginx
We need to use the system package manager to install Nginx just as we did with
fail2ban and our Python packages.
1. First install the nginx package via apt on our remote machine.
sudo apt-get install nginx
Next we need to set up our domain name so we can access the app from a URL
instead of the IP address. We’ll use Namecheap to handle the domain name server
resolution.
58
1. Start by going to https://www.namecheap.com/ in your browser. Sign up for an
account or sign in to your existing account.
59
3. Select the domain name you want to set up for this application.
60
61
5. There are two choices here. If you want the standard www in your URL, set
www with your server’s IP address and specify it as an A (Address) record. If
instead you want a different subdomain, such as cyoa as I’ve done here,
then enter the subdomain with the server’s IP address and select the A
(Address) record in the dropdown.
6. Save the newly entered information. It may take a few minutes to take
effect as the data must be propagated to many Domain Name System
hosts.
Now we can configure Nginx to either use HTTP or HTTPS. Both options are included
in this chapter. If you just want to deploy without encryption provided by HTTPS, read
the next section, but know that your application won’t be secure. If you want security
and encryption provided by the latest version of SSL/TLS then skip down to
“Configure Nginx with HTTPS”.
1. Create a configuration file under /etc/nginx/conf.d/ with your app name and
.conf extension. In this case we'll deploy with the cyoa.conf filename, so the
full path and filename will be /etc/nginx/conf.d/cyoa.conf. Note that we'll
need to use sudo privileges to create and save the file within Nginx's
configuration directory. Add the following lines to the file.
62
upstream app_server_wsgiapp {
server localhost:8000 fail_timeout=0;
}
server {
listen 80;
# make sure to change the next line to your own domain name!
server_name cyoa.mattmakai.com;
access_log /var/log/nginx/cyoa.access.log;
error_log /var/log/nginx/cyoa.error.log info;
keepalive_timeout 5;
# nginx serve up static files and never send to the WSGI server
location /static {
autoindex on;
alias /home/deployer/cyoa/static;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://app_server_wsgiapp;
break;
}
}
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
63
}
That’s all the configuration you need to use Nginx to serve static files and provide a
reverse proxy to the WSGI application. Skip down to the “Restarting with a new
configuration” section to make the configuration take effect.
Next we create a self-signed certificate. That means this certificate will give a warning
on most browsers if you access the web application because the certificate has not
been blessed by a central certificate authority. I recommend buying a certificate from
a Certificate Authority (CA) such as Digicert or StartSSL once you're ready to go live.
Replace this self-signed certificate with the certificate from a valid CA and those
browser warnings will go away.
If you have trouble with the following steps be sure to read this detailed guide on
creating a self-signed certificate.
Execute these commands on your local machine. You'll need OpenSSL installed
which is part of Mac OS X and most Linux distributions.
1. Create a subdirectory from our base directory to hold our SSL certificate
files as we generate them.
mkdir ssl_cert
cd ssl_cert
64
# write a private key file using a 4096 bit key
openssl genrsa -des3 -out ssl.key 4096
3. ssh-keygen will tell you that an RSA private key is being generated. Then
you'll be prompted for a passphrase as shown below. Enter a passphrase.
# enter a pass phrase between 8-16 characters
Enter pass phrase for ssl.key:
# verify the pass phrase by entering it again
Verifying - Enter pass phrase for ssl.key:
4. Now you have an ssl.key file in the current directory. Next we need to
generate a certificate signing request (CSR).
# openssl allows us to create a signing request file
openssl req -new -key ssl.key -out ssl.csr
65
A challenge password []:
An optional company name []:
6. Remove the passphrase from the key file because it's a pain when using
automation to start and stop the web server as the passphrase would
need to be entered for all operations.
# recreate the key file but without the pass phrase
openssl rsa -in ssl.key -out cyoa.key
We'll upload the cyoa.key and cyoa.crt files to the server as we configure our Nginx
server in the next section.
1. Log back into your remote server. Create a directory to store the SSL certs
temporarily on thed remote server.
mkdir /home/deployer/ssl_tmp/
2. Run the following command from your local machine from within the
ssl_cert directory you previously created. From our local machine we need
to copy in the SSL keys we just generated. Within the directory that stores
66
the SSL key files, run the following commands to get them securely
copied to the server. Remember to use the IP address of your server
instead of the one shown in this command.
scp -i ../ssh_keys/prod_key cyoa.key cyoa.crt \
deployer@{your.server.ip.address}:/home/deployer/ssl_tmp/
We now need to set up Nginx's configuration so that it uses the SSL certificates. We'll
also prepare it for passing requests to the WSGI server that will be set up in a later
chapter.
67
Configure Nginx with HTTPS
Now we’ll configure Nginx using a custom configuration file. This configuration file will
be set up for our SSL connection and our WSGI server we’ll set up later in the book.
1. Create a configuration file under /etc/nginx/conf.d/ with your app name and
.conf extension. In this case we'll deploy with the cyoa.conf filename, so the
full path and filename will be /etc/nginx/conf.d/cyoa.conf. Note that we'll
need to use sudo privileges to create and save the file within Nginx's
configuration directory. Add the following lines to the file.
upstream app_server_wsgiapp {
server localhost:8000 fail_timeout=0;
}
server {
listen 80;
# make sure to change the next line to your own domain name!
server_name cyoa.mattmakai.com;
rewrite ^(.*) https://$server_name$1 permanent;
}
server {
# make sure to change the next line to your own domain name!
server_name cyoa.mattmakai.com;
listen 443 ssl;
ssl_certificate /etc/nginx/cyoa/cyoa.crt;
ssl_certificate_key /etc/nginx/cyoa/cyoa.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-
SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-
GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-
ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-
SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-
RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-
SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-
SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-
SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-
RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;
68
access_log /var/log/nginx/cyoa.access.log;
error_log /var/log/nginx/cyoa.error.log info;
keepalive_timeout 5;
# nginx serve up static files and never send to the WSGI server
location /static {
autoindex on;
alias /home/deployer/cyoa/static;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://app_server_wsgiapp;
break;
}
}
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
Now Nginx is configured with the SSL certificate we created in the earlier section and
will only use secure methods to communicate with clients. Before the application
69
works though, we’ll have to restart with a new configuration. We handle the restarting
of Nginx in the next section.
Restarting Nginx
We need to restart the Nginx server to enable our new configuration, regardless of
whether it’s using SSL/TLS or not.
Our web server Nginx is now manually configured to serve up static files such as
JavaScript, CSS and images. Nginx will also pass requests up to the WSGI server that's
running our Python application, including websocket connections. However, before
we get the WSGI server running we will need to grab our web application's source
code. That's the subject of the next chapter.
If you're running through the manual steps first you can now flip to the next chapter
on source control. To learn how to automate the Nginx configuration steps we just
performed, keep reading below.
70
HTTPS, we will create a flag in Ansible that can be flipped to either set up the server
with HTTP or HTTPS.
1. Open prod/group_vars/all and append the following lines to the existing file.
These are new variables we’ll need for the automated deployment.
71
- name: ensure default symbolic linked website is deleted
file: path=/etc/nginx/sites-enabled/default state=absent
sudo: yes
72
server {
listen 80;
server_name {{ fqdn }};
rewrite ^(.*) https://$server_name$1 permanent;
}
server {
server_name {{ fqdn }};
listen 443 ssl;
ssl_certificate /etc/nginx/{{ app_name }}/{{ app_name }}.crt;
ssl_certificate_key /etc/nginx/{{ app_name }}/{{ app_name }}.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-
SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-
GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-
ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-
SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-
RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-
SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-
SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-
SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-
RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/{{ fqdn }}.access.log;
error_log /var/log/nginx/{{ fqdn }}.error.log info;
keepalive_timeout 5;
# nginx serve up static files and never send to the WSGI server
location /static {
autoindex on;
alias {{ app_dir }}/{{ app_name }}/static;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://app_server_wsgiapp;
break;
}
}
73
# this section allows Nginx to reverse proxy for websockets
location /socket.io {
proxy_pass http://app_server_wsgiapp/socket.io;
proxy_redirect off;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
server {
listen 80;
server_name {{ fqdn }};
access_log /var/log/nginx/{{ fqdn }}.access.log;
error_log /var/log/nginx/{{ fqdn }}.error.log info;
keepalive_timeout 5;
# nginx serve up static files and never send to the WSGI server
location /static {
autoindex on;
alias {{ app_dir }}/{{ app_name }}/static;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
74
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://app_server_wsgiapp;
break;
}
}
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
- include: nginx_no_ssl.yml
when: not deploy_ssl
75
8. There is one more file to create before Ansible will have what it needs to
automate the set up of the web server. Create a file named main.yml within
the prod/roles/common/handlers directory. Write the following contents into the
file. This handler allows us to restart Nginx during the playbook execution
when it's necessary such as upon a configuration file modification.
- name: restart nginx
service: name=nginx state=restarted
sudo: yesr
9. Now we can run our Ansible playbook again with our shell script to install
and configure Nginx on our server.
./deploy_prod.sh
Nginx is ready to serve HTTP requests but we need to run the WSGI server and
• The HTTP/1.1 Specification is the official spec for how the current version of
HTTP works. Version 2.0 is slowly replacing 1.1 but for now this is the spec
to read.
76
• Ars Technica has a detailed guide for How to set up a safe and secure Web
server.
• Nginx web server tutorials are groups of tutorials for setting up and
configuring Nginx.
• A reference with the full list of HTTP status codes is provided by W3C.
• A faster Web server: ripping out Apache for Nginx explains how Nginx has
replaced Apache in some environments.
• Mozilla has a tool for creating SSL/TLS certificate configurations for web
servers in case you want to check your configuration against a
recommended one.
77
Source Control
Source control, also known as version control, stores source code files along with
metadata on changes, such as character additions and deletions.
Version control systems allow developers to modify code without worrying about
permanently screwing something up. Unwanted changes can be rolled back to a
previous working code state.
79
Source control also eases developing software for teams. One developer can
combine her code modifications with other developers' code by viewing differences
in the code and merging in appropriate changes.
Note that some developers recommend deployment pipelines package the source
code to deploy it and never have a production environment touch a source control
system directly. However, for small scale deployments it's often easiest to pull from
source code when you're getting started instead of figuring out how to wrap the
Python code in a system installation package.
Our deployment will use the source control deployment approach for getting our
code onto the production server. This method for deploying code via the git clone and
git pull commands will make our deployment process look like the following diagram.
For our deployment we’ll be using Git as our source control system and GitHub as a
remote hosted source control service. In this chapter, we’ll install Git, set up a read-
only deploy key and use Git to transfer our source code and static files onto the server.
80
Hosted Source Control Services
We could install Git on a remote server and use that server to back up our source
control, but it's easier to get started with a hosted version control service. You can
transition away from the service at a later time by moving your repositories to your
own server if your needs change. A couple of recommended hosted version control
services are:
• GitHub is currently the most commonly used Git source control platform.
• BitBucket provides free Git repositories for public projects and private
repositories for up to five users.
As mentioned earlier, we’ll use GitHub during this deployment. Sign up for a free
GitHub account now as we’ll use it throughout this chapter.
81
# number of bits to use
ssh-keygen -t rsa -b 2048
3. When prompted for a file in which to save the keys do not use the default.
Instead, enter the following directory and file into the prompt.
# save the private key in our current directory
./deploy_key
5. Now you again have two new files: a private key deploy_key and a public key
deploy_key.pub. We’ll next put the public key into GitHub so we can clone
our code repository.
These new deploy keys are different than the prod_key and prod_key.pub that we created
in chapter 2. They cannot be used interchangeably. For example, the prod_key is only
used to log into our server, whereas the deploy_key can clone a Git repository from
GitHub but cannot be used to log into our production server.
2. Press the fork button and select where to fork the repository.
82
3. When the repository finishes forking the forked version’s web page will
appear. We need to add the deploy key we just created so that our new
server can pull down the code.
4. Click the settings link on the right navigation bar within the forked
repository's page.
83
7. Two text boxes will appear on the screen. Fill in a title and the contents of
your deploy_key.pub key file. Make sure you use the deploy_key.pub file, not the
private key stored in deploy_key. Do not allow write access.
84
8. Press the "Add Key" button. Now you're all set to clone this forked
repository's code onto your production server.
85
2. Clone the remote Git repository so we have a copy of it on the server. If
the repository is private the deploy key will grant you access. Be sure to
use your username in the below command with the fork we created
earlier.
ssh-agent bash -c 'ssh-add /home/deployer/deploy_key/deploy_key; git clone
git@github.com:makaimc/cyoa.git'
3. You'll see a prompt asking if the authenticity of the host github.com is valid
based on the key fingerprint. Enter 'yes' to continue.
4. If you run into the following error, you need to make sure your newly
created public key is associated with your GitHub account.
Permission denied (publickey).
fatal: Could not read from remote repository.
5. If the clone works text similar to the following output will appear.
Cloning into 'cyoa'...
remote: Counting objects: 224, done.
remote: Total 224 (delta 0), reused 0 (delta 0), pack-reused 224
Receiving objects: 100% (224/224), 535.18 KiB | 0 bytes/s, done.
Resolving deltas: 100% (113/113), done.
Checking connectivity... done.
7. If we want to update the code by pulling the latest from the remote server
we can run the following command.
ssh-agent bash -c 'ssh-add /home/deployer/deploy_key/deploy_key; git pull origin
master'
86
That's all the manual work we need to do in this chapter. If you're ready to automate
these steps within our Ansible playbook then continue reading below, otherwise we
can begin setting up our PostgreSQL and Redis data stores in the next chapter.
87
dest=/home/{{ deploy_user }}/deploy_key/deploy_key
mode=0600 owner={{ deploy_user }} group={{ deploy_group }}
4. We can again run our Ansible playbook again with our shell script, this
time to install Git, copy over our deploy key and clone the remote
repository onto our server.
./deploy_prod.sh
Our Ansible playbook can now clone our Git repository using a separate deploy key.
However, we don’t yet have all the pieces in place that are necessary for our
application. We need to set up our databases on the server to handle data storage.
Next: Databases
Next we're going to install the relational database and get it ready for our application
to connect to it.
88
Additional source control resources
Source control is a necessary tool for every software project. The following resources
are fantastic for a better general understanding of how to use source control systems
properly.
• Staging Servers, Source Control & Deploy Workflows, And Other Stuff
Nobody Teaches You is a comprehensive overview by Patrick McKenzie of
why you need source control.
• A visual guide to version control provides real-life examples for why version
control is necessary in software development.
• Pro Git is a free open source book that walks through all aspects of using
the version control system.
• A Hacker's Guide to Git covers the basics as well as more advanced Git
commands while explaining each step along the way.
• Git Workflows That Work is a helpful post with diagrams to show how teams
can create a Git workflow for their particular development process.
89
Databases
A persistent database is an abstraction on top of an operating system's file system that
makes it easier for applications to create, read, update, and delete persistent data.
Relational databases store data in structured tables that are persisted to the file
system. Databases provide a mental framework for how the data should be saved and
retrieved instead of having to figure out what to do with the data every time you build
a new application.
In this chapter, we will set up two types of databases. The first is a traditional relational
database called PostgreSQL. The second database is a NoSQL data store. It is an in-
memory key-value pair store called Redis.
91
As shown in the above diagram, PostgreSQL will save our persistent data while Redis
will store transient data.
92
PostgreSQL
PostgreSQL is the recommended relational database for working with Python web
applications. PostgreSQL's feature set, active development and stability contribute to
its usage as the backend for millions of applications live on the Web today.
To work with a relational database using Python, you need to use a code library. In our
case we will use psycopg2 for PostgreSQL.
Installing PostgreSQL
We need to install several new packages to get PostgreSQL, along with Python
connection support, up and running.
With explanations of those packages out of the way, let's get the database installed
and configured.
1. PostgreSQL can be installed via the system packages. Run the following
command to kick off the installation.
$ sudo apt-get install postgresql libpq-dev \
postgresql-client-common postgresql-client
93
2. PostgreSQL is now installed but we need create our database, set up a
non-root user, set up our configuration and create our own tables.
4. With the postgres user we can create the database that will hold our
application's tables.
createdb cyoa
5. Next let's set up a non-root database user to handle the table creation
and maintenance.
createuser --superuser deployer
9. Now let's try out our PostgreSQL connection with the deployer user.
psql cyoa
11. Enter the following command at the PostgreSQL prompt and we'll see no
tables have been created yet for the cyoa database.
\dt
94
No relations found.
13. You may have noticed the error that PostgreSQL could not save history a
file. We need to create a history file for Postgres commands. There just
needs to be a blank file in the deployer's home directory. Run the touch
command to create it.
touch ~/.psql_history
Our PostgreSQL database is installed and we can access it from the deployer account,
but there’s nothing in it yet. We’ll populate some initial data for our application in the
next chapter after installing our application dependencies.
1. Key-value pair
2. Document-oriented
3. Column-family table
4. Graph
These persistent data storage representations are commonly used to augment, rather
than completely replace, relational databases.
95
In this section of the chapter, we’ll install the Redis NoSQL data store to support our
application.
Redis
Redis is an open source in-memory key-value pair data store. Redis is often called "the
Swiss Army Knife of web application development." It can be used for caching,
queuing, and storing session data for faster access than a traditional relational
database, among many other use cases. Key-value pair data stores are based on hash
map data structures and when they're stored in memory they are much faster than
accessing data from persistent storage.
Installing Redis
It's possible to install Redis from source control but in this case the it’ll be easiest to
install the Ubuntu 14.04 LTS package.
96
If you already know how to use Redis you can skip this section if you already know
how to use Redis as we're just going to run through a few basic commands.
3. Values can be retrieved from Redis via the get command followed by the
key name. For example:
127.0.0.1:6379> get cyoa
(nil)
4. A value for the key 'cyoa' has not been set so the value returned is
specified as nil. Let's set it now and get the new set value.
127.0.0.1:6379> set cyoa 1
OK
127.0.0.1:6379> get cyoa
"1"d
5. Strings are the basic data type in Redis so we receive back our 1 as a
String. However we can perform operations on the value that are more
integer-like such as increment and decrement. We'll try that real quick
before moving on with our deployment.
127.0.0.1:6379> incr cyoa
(integer) 2
127.0.0.1:6379> get cyoa
"2"
Redis understands the increment operation and increases the value to 2. When we
retrieve the value through a get though Redis still returns the value back as the basic
String data type.
97
Redis is operating with default settings which will suffice for our web application
deployment. We can now continue our manual set up in the next chapter on
application dependencies or automate this part of the deployment in the remainder
of this chapter.
98
- python-psycopg2
- postgresql-client
- postgresql-client-common
- include: nginx_ssl.yml
when: deploy_ssl
- include: nginx_no_ssl.yml
when: not deploy_ssl
- include: git.yml
99
- include: postgresql.yml
- include: redis.yml
- include: postgresql.yml
- include: redis.yml
5. Make sure to again run our Ansible playbook again with our shell script to
install and configure the databases on our server.
./deploy_prod.sh
100
• Databases integration testing strategies covers a difficult topic that comes
up on every real world project.
• There is no such thing as total security but this IBM article covers hardening
a PostgreSQL database.
101
• NoSQL Weekly is a free curated email newsletter that aggregates articles,
tutorials, and videos about non-relational data stores.
• How To Install and Use Redis provides a guide for getting up and running
with the extremely useful in-memory data store.
• Getting started with Redis and Python is a walkthrough for installing and
playing around with the basics of Redis.
• Scaling Redis at Twitter is a from the trenches story of Redis usage at the
social media company.
102
Application
Dependencies
Application dependencies are the external libraries your project’s code requires to
execute your application. For example, our Choose Your Own Adventure
Presentations web application relies on the Flask microframework, so a reference to
that library is stored in the project’s requirements.txt file and will be installed as an
application dependency.
In this chapter we will install our application dependencies via the pip command,
which downloads the libraries from PyPI into a virtualenv.
104
Let’s take a look at how we’ll use application dependencies and then dive into getting
them set up for our production environment.
105
1. Create a virtualenv for a fresh Python installation environment
Before we jump into our manual steps let’s take a quick look at why we need to isolate
dependencies, how a requirements.txt file is used and explain dependency pegging.
Isolating Dependencies
Dependencies are installed separately from system-level packages to prevent library
version conflicts. The most common isolation method for Python application is using a
virtualenv. Each virtualenv is its own copy of the Python interpreter and dependencies
in the site-packages directory. To use a virtualenv it must first be created with the
virtualenv command and then activated.
106
dependencies means attaching a specific version number after the name of the
dependency.
For example, in our Choose Your Own Adventure Presentations application the
requirements.txt file with pegged dependencies looks like this:
Flask==0.10.1
Flask-Script==2.0.5
Flask-SocketIO==0.4.1
Flask-Login==0.2.11
Flask-SQLAlchemy==2.0
Flask-WTF==0.10.3
gunicorn==19.3.0
redis==2.10.3
twilio==3.7.2
psycopg2==2.5.4
celery==3.1.18
Pegged dependencies with precise version numbers or Git tags are important
because otherwise the latest version of a dependency will be used. While it may
sound good to always stay up to date, there's no telling if your application actually
works with the latest versions of all dependencies. Developers should deliberately
upgrade and test to make sure there were no backwards-incompatible modifications
in newer dependency library versions.
code. Make sure you’re logged into the remote production server with the deployer
user to execute the following commands.
107
mkdir /home/deployer/envs/
2. We'll call our new virtualenv cyoa, an acronym for “Choose Your Own
Adventure”.
virtualenv /home/deployer/envs/cyoa
4. We should see a prefix to the shell like the following. That prefix helps us
remember which virtualenv is currently active.
(cyoa)$
5. Now the virtualenv is activated for our shell. However, this activation is not
automatic for every shell that is opened. Our virtualenv is only activated
for the shell we explicitly activate it for.
6. We can also use other commands in the bin directory without activating
the virtualenv first though. For example we could run the Python
interpreter within that virtualenv by running the following command.
/home/deployer/envs/cyoa/bin/python
Installing Dependencies
With our virtualenv activated we can install the web application's dependencies.
108
2. Copy the template.env file to a new file named .env.
cp template.env .env
3. Fill in the following necessary environment variables in the new .env file.
#!/bin/bash
export DEBUG=False
export SECRET_KEY='supersecretproductionkeyforapp'
export DATABASE_URL='postgres://username:password@localhost/cyoa'
# Redis settings
export REDIS_SERVER='localhost'
export REDIS_PORT='6379'
export REDIS_DB='1'
# Twilio settings
export TWILIO_ACCOUNT_SID=''
export TWILIO_AUTH_TOKEN=''
export TWILIO_NUMBER=''
# Celery
export CELERY_BROKER_URL='redis://localhost:6379/0'
export CELERY_RESULT_BACKEND='redis://localhost:6379/0'
We can test that the dependencies are properly installed and that the environment
variables were invoked by syncing the web application with the database.
109
Syncing the Database Schema
1. Our database is ready for table creation, so let's run a command from the
manage.py file.
(cyoa)$ python manage.py syncdb
2. No output is likely a good sign that the tables created without a problem,
but let's check PostgreSQL to be certain.
5. Enter the following command at the PostgreSQL prompt and we'll now
see that tables have been created in the database for our application.
cyoa=# \dt
List of relations
Schema | Name | Type | Owner
--------+---------------+-------+----------
public | choices | table | deployer
public | presentations | table | deployer
public | wizards | table | deployer
(3 rows)
110
1. We’ve synced our database tables, but we need the user for our
application. Run the following command to create a user with a username
and password of your choice.
(cyoa)$ python manage.py create_wizard username password
Our database tables and new user are ready to go. If you want to finish off the manual
application deployment and populate some of the initial database data, move on to
the next chapter on WSGI server. For the remainder of this chapter we’ll take a look at
automating the above steps.
111
# Chapter 5: Source Control
local_deploy_key_dir: ~/fsp-deployment-guide/deploy_key
code_repository: ssh://git@github.com/makaimc/choose-your-own-adventure-
presentations.git
# Chapter 6: Databases
db_url: postgresql://{{deploy_user}}:{{db_password}}@localhost/{{app_name}}
db_password: fortheloveofgodpleaseuseagoodpassword
db_name: "{{ app_name }}"
db_user: "{{ deploy_user }}"
venv_python: "{{venv_dir}}/bin/python"
wsgil_env_vars: {
DEBUG: True,
SECRET_KEY: 'jqwpifojqwoifjioqwjfoiqwj',
REDIS_SERVER: 'localhost',
REDIS_PORT: 6379,
REDIS_DB: 1,
TWILIO_ACCOUNT_SID: 'ACcb4c69632458114d8bec0e9837d7dadb',
TWILIO_AUTH_TOKEN: '61a236ab4cc21cf4915ca85a8c71ff2c',
TWILIO_NUMBER: '+12027598445',
CELERY_BROKER_URL: 'redis://localhost:6379/0',
CELERY_RESULT_BACKEND: 'redis://localhost:6379/0',
112
}
app_admin_user: ''
app_admin_password: ''
- name: create .env file in base directory of the app from template
template: src=env.j2
dest={{ app_dir }}/.env
113
{% endfor %}
4. The above template loops through the WSGI environment variables set
for wsgi_env_vars in the group_vars/all file and fills them into the .env file.
5. The .env file won't actually be used for running the application except if we
need to SSH in manually and test the installation. Environment variables
for our deployment are set in the Supervisor configuration file instead.
- include: nginx_ssl.yml
when: deploy_ssl
- include: nginx_no_ssl.yml
when: not deploy_ssl
- include: git.yml
- include: postgresql.yml
- include: redis.yml
-include: dependencies.yml
7. Run our Ansible playbook again with our shell script to install the
application dependencies, set the environment variables and sync the
database schema with the application’s models.
./deploy_prod.sh
114
Next: WSGI Server
In the next chapter we'll run the WSGI server, Gunicorn, which will execute our Python
code and bring up our web application.
• This Stack Overflow question details how to set up a virtual environment for
Python development.
115
Python Package Resources
It's useful to browse through these lists in case you come across a library to solve a
problem by reusing the code instead of writing it all yourself. A few of the best
collections of Python libraries are:
116
WSGI Servers
A Web Server Gateway Interface (WSGI) server runs a Python web application on the
implements the web server side of the WSGI interface for running Python web
applications.
The WSGI server implementation for this chapter is Gunicorn, which is short for
“Green Unicorn”.
118
In this chapter we’ll also populate initial data for our PostgreSQL database.
What is WSGI?
Traditional web servers such as Apache and Nginx do not understand or have any way
to run Python. In the late 1990s, a developer named Grisha Trubetskoy came up with
an Apache module called mod_python to execute arbitrary Python code. For several
years in the late 1990s and early 2000s, Apache configured with mod_python ran
most Python web applications.
119
Therefore the Python community came up with WSGI as a standard interface that
modules and containers could implement. WSGI is now the accepted approach for
running Python web applications.
If you're using a standard web framework that implements the WSGI application side
such as Django, Flask or Bottle then your application can be deployed in the same
way as the Choose Your Own Adventure Presentations application. Likewise, if you're
using a standard WSGI container such as Green Unicorn, uWSGI, mod_wsgi or gevent,
you can get them running without worrying about how they implement the WSGI
standard.
• CherryPy is a pure Python web server that also functions as a WSGI server.
In this chapter, we’re going to use the Green Unicorn WSGI server implementation to
run our application.
120
Configuring Green Unicorn
We're going to use Green Unicorn (gunicorn) as our WSGI server in this
Our application should be up and running now, so let’s test it out. Go to the URL
you’ve set up as your hostname. In my case, the URL to visit is http://cyoa.mattmakai.com/.
If we see the following screen, our application is running properly.
No presentations are found yet since we haven’t loaded any data into the application.
121
Starting Gunicorn with Supervisor
We don’t normally want to run the Gunicorn server ourselves by logging into the
server. Instead, we’ll use the Supervisor tool to start the WSGI server implementation
for us.
Supervisor is not yet installed on the system so we need to obtain it via a system
package. Then we just need to create a configuration file and start the supervisor
process once to get it up and running.
122
Our App is Live!
Our app should be up and running at the URL we specified now. Let’s give it a test
with the “Wizards Only” sign in page. Go to http://cyoa.mattmakai.com/wizard/ and you
should see the following screen (replace the root URL with the server you set up to
test your own application):
If that screen shows up, we’re looking good! There aren’t any users in the application
at the moment though so we can’t log in just yet. We will fix the no users issue in the
next section.
123
###
# Sets up and configures Supervisor which runs Green Unicorn
##
- name: ensure Supervisor is installed via the system package
apt: name=supervisor state=present update_cache=yes
sudo: yes
124
autorestart=true
redirect_stderr=True
- include: nginx_ssl.yml
when: deploy_ssl
- include: nginx_no_ssl.yml
when: not deploy_ssl
- include: git.yml
- include: postgresql.yml
- include: redis.yml
-include: dependencies.yml
- include: wsgi.yml
4. As we’ve done in the last several chapters, we’ll re-run our Ansible
playbook with our shell script from the base directory of our deployment
project.
./deploy_prod.sh
125
our server. We’ll learn more about task queues and the Celery task queue
implementation in the next chapter.
126
Task Queues
Task queues manage background work that must be executed outside the usual HTTP
request-response cycle.
In this chapter we'll set up a particular task queue, Celery, that is commonly used with
Python web applications.
128
We'll connect Celery to Redis, which we previously set up in the databases chapter, for
Celery to use as a message queue broker.
For example, a web application could poll the GitHub API every 10 minutes to collect
the names of the top 100 starred repositories. A task queue would handle invoking
code to call the GitHub API, process the results and store them in a persistent
database for later use.
129
Another example is when a database query would take too long during the HTTP
request-response cycle. The query could be performed in the background on a fixed
interval with the results stored in the database. When an HTTP request comes in that
needs those results a query would simply fetch the pre-calculated result instead of re-
executing the longer query.
• The Celery distributed task queue is the most commonly used Python
library for handling asynchronous tasks and scheduling.
• The RQ (Redis Queue) is a simple Python library for queueing jobs and
processing them in the background with workers. RQ is backed by Redis
and is designed to have a low barrier to entry. The intro post contains
information on design decisions and how to use RQ.
130
• Taskmaster is a lightweight simple distributed queue for handling large
volumes of one-off tasks.
Let's set up Celery manually now. As with previous chapter, later we will automate the
installation with additions to our Ansible playbook.
Configuring Celery
To set up our task queue Celery, we need to create two new Supervisor configuration
files. Supervisor will run both the celery command, which handles the queue, and
celerybeat, which runs periodic tasks.
2. We’re going to create two new templates for Supervisor: celery to process
tasks and celerybeat to run periodic tasks.
131
autostart=true
autorestart=true
redirect_stderr=True
5. Stop Supervisor.
sudo service supervisor stop
Supervisor is now up and running again with both the celery and celerybeat programs
running.
132
Automating Celery Installation
We can add a few files to our Ansible playbook to automate the task queue
configuration.
# Chapter 6: Databases
db_url: postgresql://{{deploy_user}}:{{db_password}}@localhost/{{app_name}}
## make sure to change this password to what you want your
## database password to be!
db_password: fortheloveofgodpleaseuseagoodpassword
db_name: "{{ app_name }}"
133
db_user: "{{ deploy_user }}"
deployment_alert: true
134
alert_number: ""
3. Create two new templates for Supervisor, one each for celery and
celerybeat.
135
and EPUB formats often do not preserve the blank line in output so be
sure to check the tagged file on GitHub for the correct YAML.
[program:celery]
environment={% for k, v in wsgi_env_vars.iteritems() %}{% if not loop.first %},{%
endif %}{{ k }}="{{ v }}"{% endfor %}
136
twilio:
msg: "Deployment complete!"
account_sid: "{{ wsgi_env_vars.TWILIO_ACCOUNT_SID }}"
auth_token: "{{ wsgi_env_vars.TWILIO_AUTH_TOKEN }}"
from_number: "{{ wsgi_env_vars.TWILIO_NUMBER }}"
to_number: "{{ alert_number }}"
delegate_to: localhost
when: deployment_alert
7. Re-run our Ansible playbook with our shell script from the base directory
of our deployment project. This time we won’t have to wait in front of the
computer screen for the deployment to finish. If you’ve set up the
alert_number variable to your cell phone, you’ll receive a text message
stating that the deployment finished when it’s done with all the steps.
./deploy_prod.sh
137
• Queues.io is a collection of task queue systems with short summaries for
each one. The task queues are not all compatible with Python but ones that
work with it are tagged with the "Python" keyword.
• Why Task Queues is a presentation for what task queues are and why they
are needed.
• Asynchronous Processing in Web Applications: Part One and Part Two are
great reads for understanding the difference between a task queue and
why you shouldn't use your database as one.
• Celery - Best Practices explains things you should not do with Celery and
shows some underused features for making task queues easier to work
with.
138
Continuous
Integration
Continuous integration (CI) automates building, testing and deploying applications.
140
Jenkins will listen for HTTP POST requests from GitHub alerting it to when new code is
pushed to our repository.
141
Setting Up Continuous Integration
For our project we're going to set up Jenkins to pull code from GitHub, build and test
it then deploy it to our server. Jenkins is widely used and open source. However, there
is a list of other open source and hosted continuous integration servers listed at the
end of the chapter in case you want to try a different CI server out.
The continuous integration and deployment environment we set up will look like the
following diagram.
When we push changes to our remote source control repository on GitHub, an HTTP
POST request will be sent to our Jenkins server. That POST request informs Jenkins
that a new build is required. Jenkins will pull the latest code from GitHub, make sure
it’s ready then deploy it to our production server using the Ansible playbook we’ve
written throughout the book.
142
In a larger environment continuous integration would typically deploy the newest
code to a test server for some manual checks before going to production. However,
with this application we’re keeping our set up as simple as possible and just
deploying it straight into production.
1. On your local machine, create a new directory at the base directory of our
deployment project named jenkins_ci.
mkdir jenkins_ci
cd jenkins_ci
3. When prompted for a file in which to save the keys do not use the default.
Instead, enter the following directory and file into the prompt.
# we are saving the private key in our local jenkins_ci directory
./jenkins_key
143
4. Press enter twice when prompted for a passphrase. We will not use a
passphrase for the Jenkins server user's keys.
5. Now you have two new files: a private key jenkins_key and a public key
jenkins_key.pub. As with our previous keys, the private key should never be
shared with anyone as it will allow them to access your server. The public
key can be shared with anyone and does not need to be kept secret.
6. Execute the following command to create a new file with the newly
created key as an authorized key.
# the authorized_keys file will allow our server to grant SSH
# access to a connection using the private key
cp jenkins_key.pub authorized_keys
8. We’ll secure the server with the prod/fabfile.py Fabric script from chapter 2.
Copy the fabfile Modify the values in the script with the new IP address.
cp prod/fabfile.py jenkins_ci/fabfile.py
10. Edit line 35 in fabfile.py to specify a different key pair to upload to the
server.
env.ssh_key_dir = '~/fsp-deployment-guide/jenkins_ci'
144
'/jenkins_key.pub ' + env.ssh_key_dir + \
12. Run the copied Fabric file with the following command.
fab bootstrap
13. SSH into the continuous integration server with the new user. Again, make
sure to replace the IP address seen here with the IP address of your new
server.
ssh -i jenkins_key deployer@192.168.1.1
Our server is provisioned, locked down with a public-private key pair and we’ve
logged in for the first time. Now we need to actually get Jenkins up and running on
the server.
1. Modify the prod/hosts file with the ansible_sudo_pass argument after the IP
address of the production server you’re using to host your application.
192.168.1.1 ansible_sudo_pass="{{ deploy_user_password }}"
145
deploy_group: deployers
## this is the local directory with the SSH keys and known_hosts
## file. do not include a trailing slash
ssh_dir: ~/fsp-deployment-guide/ssh_keys
# Chapter 6: Databases
db_url: postgresql://{{deploy_user}}:{{db_password}}@localhost/{{app_name}}
## make sure to change this password to what you want your
## database password to be!
db_password: fortheloveofgodpleaseuseagoodpassword
db_name: "{{ app_name }}"
db_user: "{{ deploy_user }}"
146
## these next two environment variables are found in the
## Twilio account dashboard on twilio.com
TWILIO_ACCOUNT_SID: 'found on the twilio dashboard',
TWILIO_AUTH_TOKEN: 'also found on the twilio dashboard',
## grab a number on Twilio for the application and Ansible
TWILIO_NUMBER: '+15551234567',
CELERY_BROKER_URL: 'redis://localhost:6379/0',
CELERY_RESULT_BACKEND: 'redis://localhost:6379/0',
}
## username you want to use to log into app's admin screen
app_admin_user: ''
## password you want to use to log into app's admin screen
app_admin_password: ''
3. In the above Ansible variables file, we’re now pulling the password from
an environment variable instead of keeping it in the file itself. This is a
much better security practice than keeping plaintext passwords in the
variables file itself.
4. Test out the script locally by setting an environment variable with the
password to the remote deployer user.
147
export REMOTE_DEPLOYER_PASSWORD='my super secret password'
5. If you’re using the deploy_prod.sh shell script, remove the -K argument so the
ansible-playbook command looks like the following:
#!/bin/bash
ansible-playbook ./prod/deploy.yml --private-key=\
./ssh_keys/prod_key -u deployer -i ./prod/hosts
With those minor tweaks in place, we can now continue installing and configuring our
Jenkins CI server.
1. Now that we're logged in make sure the necessary packages are installed
on this server.
sudo apt-get update
sudo apt-get install fail2ban git-core python-virtualenv python-dev
3. Add the new package repository into our source.list file with the following
command.
sudo add-apt-repository "deb http://pkg.jenkins-ci.org/debian binary/"
4. We're ready to update the available packages list and install our jenkins
package.
148
sudo apt-get update
sudo apt-get install jenkins
6. Head to your server’s IP address plus the port number 8080 in the
browser. For example, if your Linode’s IP address is 192.168.1.1, go to
http://192.168.1.1:8080/.
There’s a massive problem with this screen though - if you can see it, so can anyone
else! We need to secure the installation so only we can use it.
149
Securing Jenkins with Authentication
Jenkins is open to the whole world until we secure it. We’ll handle those steps now
with a combination of command-line configuration and modifying settings in the
Jenkins user interface.
150
5. Set up a new application for Jenkins on the GitHub screen. Fill in http://
192.168.1.1:8080/securityRealm/finishLogin under the Authorization
callback URL, where 192.168.1.1 is replaced with your Jenkins server IP
address.
151
6. Move back over to the Jenkins web application now that it’s restarted with
the new plugins.
152
9. Check “Enable Security”, then enter the credentials GitHub generated for
you when you created an application for Jenkins CI.
153
10. Select “Matrix-based Security”, enter your GitHub username, click Add,
select all permissions for your username but none for Anonymous, then
click Apply.
154
11. Log in and out of Jenkins to confirm that a logged out user cannot access
anything inside the application and is simply redirected to GitHub to
determine if she should have access to this Jenkins CI user interface.
155
Creating a CI Deploy Key
We need to create one more deploy key separate from the production server deploy
key and the other private keys we use to log into the server. This deploy key will grant
read-only access from the Jenkins server to the forked repository that we want to use
the commands git clone and git pull to grab our code.
1. Log back into the Jenkins server via SSH. Again, make sure to replace the
IP address seen here with the IP address of your new server.
ssh -i jenkins_key deployer@192.168.1.1
3. Make sure you are in the jenkins home directory to execute the following
commands.
cd ~
4. On the remote server create a new directory for our SSH keys. If the
directory already exists you can ignore the error message.
mkdir ~/.ssh
6. When prompted for a file in which to save the keys you can use the
default values.
156
8. Now you again have two new files: a private key id_rsa and a public key
id_rsa.pub. We just need to put the public key into GitHub so they function
as deploy keys.
10. Click the settings link on the right navigation bar within the forked
repository's page.
157
13. Two text boxes will appear on the screen. Fill in a title and the contents of
the id_rsa.pub key file you just created. Make sure you use the id_rsa.pub file,
not the private key stored in id_rsa. Do not allow write access since we
won’t need that for Jenkins right now.
14. Press the "Add Key" button. Now you're all set to clone this forked
repository's code onto your production server.
15. Back on the server as the Jenkins user, let’s test out our deploy key.
Execute the following command and specify ‘yes’ if it asks you to verify the
host signature.
git ls-remote -h ssh://git@github.com/makaimc/choose-your-own-adventure-
presentations.git HEAD
16. Next we need to load our Ansible playbook onto the Jenkins CI server so
the jenkins user can access them. There are a couple of ways to do this.
17. The first option is to use the secure copy command from your local
machine to put the automation files onto the server. For example, you can
tar up your local Ansible playbook then scp it to the server with the
following commands. Run these commands from your local machine and
make sure to replace the IP address with the IP address of your Jenkins
machine.
tar -cvf deploy.tar ~/fsp-deployment-guide
scp -i
18. Then copy those files from the deployer user’s home directory into the ~
(home directory) of the jenkins user and use the following command to
untar the files.
tar -xvf deploy.tar.gz
158
19. The other option is to clone the Ansible playbook automated deployment
repository. For example, if you want to use the default Ansible playbook
you’d clone the following repository. Important note: you will have to
enter your own values into the configuration and ensure you have the
appropriate SSH keys to get this repository to automate your
deployments.
git clone git@github.com:makaimc/fsp-deployment-guide
20. When you get the Ansible playbook on the continuous integration server,
continue on with these steps to test the playbook out. Create a virtualenv
for the Jenkins account so it can use Fabric and Ansible to perform the
deployment.
virtualenv cyoa
source cyoa/bin/activate
pip install Fabric==1.10.2 Ansible==1.9.2
21. Test out the Ansible playbook while logged into the jenkins user. For
testing purposes, export the REMOTE_DEPLOYER_PASSWORD variable, which is the
sudo password for the deployer user on the production server, so that the
updated Ansible playbook can pick it up.
cd ~/fsp-deployment-guide
export REMOTE_DEPLOYER_PASSWORD='remote deployer password'
./deploy_prod
22. You’ll have to enter the sudo password of the remote deployer user for the
server you’re performing the deployment on.
23. You’ll likely see a message like the following, only with your production
server’s IP address instead of the one shown in this message. Enter ‘yes’
and press enter so the deployment can proceed.
The authenticity of host '66.175.209.129 (66.175.209.129)' can't be established.
ECDSA key fingerprint is 7a:2a:d0:4c:62:5e:19:e9:ee:48:7b:f2:70:f9:05:c7.
159
Are you sure you want to continue connecting (yes/no)? yes
ok: [66.175.209.129]
2. Name it “CYOA” and select “Freestyle Project”, then click the “OK” button.
160
3. Under the “Source Code Management” section, select “Git” project and
add your credentials that were configured on the command line.
161
4. Select “Execute shell” and enter the following command on a single line:
export REMOTE_DEPLOYER_PASSWORD='remote deployer password'; /fsp-deployment-guide/
prod/deploy.yml --private-key=/fsp-deployment-guide/prod/hosts
5. Click Save and the project settings will immediately take effect.
6. We’ve set up the initial settings for the project including pulling the code
from the remote GitHub repository onto our server with a new deploy key.
162
Let’s test it manually now. Click the build button on the dashboard to kick
off a new build.
If the build is successful, you’ll receive a text message a bit later that tells you the
deployment is complete!
163
Woohoo! Full service automated deployment by the Jenkins continuous integration
server is complete!
Next Steps
Note that Jenkins is only running over HTTP right now - your interactions are not
encrypted. There’s more security work that needs to be done to lock Jenkins down.
The next step to take is to secure Jenkins behind a reverse proxy that uses HTTPS as
we do with our WSGI server that runs our Python web application.
If a full Jenkins security tutorial is something you’d like to see as part of this guide,
send me an email at matthew.makai@gmail.com, create a pull request on the fsp-
deployment-guide repository on GitHub. There are many directions this book can
continue to expand into and your feedback is valuable for making sure the right
content for the community is written.
For more future directions, continue reading into the next chapter, “What’s Next?”.
164
• BuildBot is a continuous integration framework with a set of components
for creating your own CI server. It's written in Python and intended for
development teams that want more control over their build and
deployment pipeline. The BuildBot source code is on GitHub as well.
Hosted CI Services
If you want to skip setting up your own continuous integration server, take a look at
the following hosted CI services. They make setting up CI on one or more projects
easier than deploying your own Jenkins instance, but for some projects the monthly
fees may be prohibitive for your budget.
• Travis CI provides free CI for open source projects and has a commercial
version for private repositories.
• Circle CI works with open or closed source projects on GitHub and can
deploy them to Heroku if builds are successful.
• Drone is another CI service that also provides free builds for open source
projects.
• Snap is a CI server and build pipeline tool for both integrating and
deploying code.
165
Additional Continuous Integration Resources
The CI set up we created in this chapter is just the beginning for deployment
automation. These articles should give you further insight into CI and where you could
take the infrastructure from here.
166
What’s Next?
Our deployment is now automated and runs continuously whenever code is
committed and pushed into the remote Git repository. However, there are many
enhancements we could make to this deployment process.
Most large-scale web application deployments use a suite of further techniques that
increase the capabilities of their infrastructure. This chapter is intended as a “future
directions” you may want to take in your own deployment. I’ll also add additional
chapters to this book on any of these topics based on reader feedback.
Deployment Enhancements
Our deployment process right now is simple - it’ll get your application out of source
control and onto a server that can execute the Python code. Yet there are a whole
bunch of ways to make it better by incorporating additional steps into the Ansible
playbook. Here’s a list of potential concepts to research and consider adding to our
deployment:
168
• Deploy through a system package instead of version control
Improving Performance
There are numerous ways the web application’s performance can be improved by
modifying the server configuration. Here are several possibilities for scaling up server
and application performance:
Onward!
Those are a few options for improving the deployment process and upgrading the
application’s performance. What content would you like to see added to the book?
Send me an email at matthew.makai@gmail.com or tweet me @mattmakai or
@fullstackpython and let me know what you think. All updates to this book will be
available for free to current purchasers, so your opinion is meaningful in determining
how the content grows over time!
169
Glossary
This glossary provides plain language definitions of the concepts found throughout
this book. If you’re uncertain about what a word or concept means, this glossary plus
the additional information provided on Full Stack Python should clear up your
confusion. If it doesn’t, email me and I’ll make sure to clarify the concepts in future
version of the book.
Application dependencies
Application dependencies are the libraries other than your own project code that are
required to create and run your application. These dependencies are referenced in a
requirements.txt file, retrieved from PyPI via the pip command and installed into a
virtualenv to isolate the dependencies from other project’s dependencies.
Continuous integration
Continuous integration is the concept of building and testing code upon every
change a developer checks into version control. The theory is that if code is not
integrated together until the end of the project, the integration period will be painful
due to numerous bugs that are created at the edges of where the software
components are pieced together. Continuous integration server implementations
include Jenkins, TeamCity and Go.
171
Deployment
A deployment is the process of packaging up an application’s code and putting it on a
server where it can be run for users. Some deployments are just to a development
server for fellow developers to test their code, while other deployments go to a test
server to make sure the code works properly. The last stage in a deployment is usually
to one or more production servers that host the canonical version of an application.
Flask
Flask is a Python web framework created with a small core and easy-to-extend
philosophy. Flask pieces together several open source projects, including Werkzeug,
Jinja2 and SQLAlchemy (for relational database object-relational mapping) to make it
easier to build WSGI-compatible web applications with the framework. Flask is the
web framework used by the Choose Your Own Adventure Presentations example
application deployed throughout this book.
NoSQL
NoSQL, also referred to as Not Only SQL, is a database type that forgoes one or more
constraints present in traditional relational databases in order to gain some other
attribute - often speed of read or write access. There are many types of NoSQL
database implementations, including as Redis, MongoDB, Cassandra and HBase.
172
pip
Pip is a tool used to install Python packages from one location, often a remote
dependency management server such as PyPI or GitHub, into a local Python
installation directory. Pip is often used with a requirements.txt file so the appropriate
versions of application dependencies for an application can be installed.
PostgreSQL
PostgreSQL is an open source relational database implementation. In this book,
PostgreSQL is the primary data storage backend for the application.
Production server
A production server or production environment hosts the canonical version of the
running code for your application. There can be many environments for development
and testing, but production is the only one that contains the live version of your
application.
PyPI
The Python Package Index, also known as PyPI, is a central remote server that contains
numerous versions of application dependencies for Python packages that were
uploaded by their authors. For example, the Flask web microframework is stored on
PyPI and can be retrieved to a local computer by using the pip command. PyPI is
173
pronounced “Pie-pee-eye” to differentiate it from the PyPy project, which is a separate
concept not covered in this book.
Source control
Source control, also known as version control, stores software code files with a
detailed history of every modification made to those files. Source control systems
allow developers to modify code without worrying about permanently screwing
something up. Unwanted changes can be easily rolled back to previous working
versions of the code, which makes developing in both individual and team
environments easier. Examples of source control implementations include Git,
Mercurial and Subversion.
Task queue
A task queue is a concept for a piece of software that executes work in the
background outside the HTTP request-response cycle of a web application. The work
performed by a task queue would often take too long to process when a HTTP
request came in or would need to be performed on a regular interval and are not
initiated by HTTP requests. Python task queue implementations include Celery, RQ
and Taskmaster.
174
virtualenv
Virtualenv provides application dependency isolation for Python projects. The
virtualenv creates a separate copy of the Python installation that is clean of existing
code libraries and provides a directory for new application dependencies on a per-
project basis (a programmer can technically use a virtualenv for many projects at once
but that practice is discouraged).
Web framework
Web frameworks are code libraries that make it easier to build web applications. Web
frameworks take care of common web development issues, such as HTTP requests
and responses, URL routing, authentication, data handling and manipulation, sessions
and security. The web framework implementation in this book is Flask. Other common
Python-based web frameworks include Django, Bottle, Pyramid and Morepath.
Web server
A web server is an application that receives and responds to Hypertext Transport
Protocol (HTTP) requests. The web server implementation used in this book is Nginx.
Nginx serves static and provides a reverse proxy, which is an intermediate handler
between a web client and the WSGI server running our web application. Other
examples of web server implementations include the Apache HTTP Server, IIS.
175
WSGI
Web Server Gateway Interface (WSGI) is a standard interface between Python web
applications or frameworks and web servers. The interface was created to promote
web application portability between different WSGI server implementations such as
Green Unicorn, uWSGI, wsgiref and mod_wsgi.
176
More Python
Resources
This appendix wraps up the book with a collection of the best Python resources I've
found. Many of these resources are free and the ones that cost money are well worth
the investment to purchase them.
This chapter aggregates the best Python resources with a brief description of its one's
learning purpose.
New to Programming
If you're learning your first programming language these books were written with you
in mind. Developers learning Python as a second or later language should skip down
to the next section for "experienced developers".
178
• Learn Python the Hard Way is a free book by Zed Shaw for learning Python
programming.
• Dive into Python 3 is an open source book provided under the Creative
Commons license and available in HTML or PDF form.
• Python for You and Me is an online book for people unfamiliar with the
Python programming language.
• There's a Udacity course by one of the creators of Reddit that shows how to
use Python to build a blog. It's a great introduction to web development
concepts through coding.
• The O'Reilly book Think Python: How to Think Like a Computer Scientist is
available in HTML form for free on the web.
179
• Learn Python in Y minutes provides a whirlwind tour of the Python
language. The guide is especially useful if you're coming in with previous
software development experience and want to quickly grasp how the
language is structured.
• Google's Python Class contains lecture videos and exercises for learning
Python.
• The Python Subreddit and Learn Python Subreddit roll up great Python links
and have an active community ready to answer questions from beginners
and advanced Python developers alike.
180
• The blog Free Python Tips provides posts on Python topics as well as news
for the Python ecosystem.
• Kate Heddleston gave a talk at PyCon 2014 called "Full-stack Python Web
Applications" with clear visuals for how numerous layers of the Python web
stack fit together. There are also slides available from the talk with all the
diagrams.
• My EuroPython 2014 "Full Stack Python" talk goes over each topic from this
guide and provides context for how the pieces fit together. The talk slides
are also available.
181
Curated Python Packages Lists
The following Git repositories aggregate open source libraries into categories, such as
Natural Language Processing and web application frameworks.
Newsletters
Email newsletters are good for keeping up with the latest open source projects and
articles. Rather than you having to spend your time searching the web for these
helpful resources, you can have someone else aggregate them for you and deliver to
your inbox.
• Python Weekly provides a free weekly roundup of the latest Python articles,
videos, projects and upcoming events.
182
anything.
183
App Code Tutorial
This chapter is a streamlined version of the original code tutorial for the example
application we deployed in this book. This Choose Your Own Adventure Presentations
Flask web app provides a reasonable example for deployment because it is open
source and relies on many widely used web application technologies such as
PostgreSQL, Redis, Celery, WebSockets and Green Unicorn.
Going through this appendix is not required to do the deployment in this book. It’s
included since I wrote the original posts for the Twilio blog.
Note that this tutorial will take you up to the tutorial-step-6 tag but I kept working on
the project so for this book you should use the master branch for the deployment.
185
Choose Your Own Adventure
Presentations with Reveal.js, Python and
WebSockets
You’re preparing a technical talk on your new favorite open source project to present
to your local software meetup group.
—————
How do you proceed? If you choose to create another passe linear slide deck, load up
Microsoft PowerPoint. If you decide to build a childhood nostalgia-packed Choose Your
Own Adventure presentation, continue reading this tutorial.
—————
Good choice! To create our Choose Your Own Adventure presentation we’ll use
Reveal.js, Flask, WebSockets and Twilio SMS. Our final product will have decision
screens like the following screenshot where SMS votes from the audience are counted
and displayed in real-time to determine the next step in the presentation story. Once
186
the votes are tallied the presenter can click the left choice or the right choice to go to
the appropriate next slide in the presentation.
Take a look at the DjangoCon US 2014 ”Choose Your Own Django Deployment
Adventure” video if you want to see an example of a Choose Your Own Adventure
presentation in action.
• Flask
187
• Flask-Script for utility commands such as running our Flask server
• A Twilio account with an SMS number so audiences can vote which path the
presentation should follow
• Ngrok for a secure tunnel to our local server running the presentation
Do you want to skip typing out the code yourself? Check out this open source
repository on GitHub. The Git repository contains the final code as well as
intermediate tags named tutorial-step-1, tutorial-step-2 and tutorial-step-3 for each
section below.
At the base directory of our project, cyoa/, create a file named requirements.txt with the
following content in it. Each line contains a dependency for our project that we’ll
install in a moment.
cyoa/requirements.txt
188
Flask==0.10.1
Flask-Script==2.0.5
Flask-SocketIO==0.4.1
gunicorn==19.1.1
redis==2.10.3
twilio==3.6.8
This application uses Flask to serve up the presentation. The Flask-Script extension will
assist us in creating a manage.py file with helper commands to run the web application.
The Flask-SocketIO extension handles the server-side websockets communication.
Create a new virtualenv outside the base project directory to separate your Python
dependencies from other apps you’re working on.
virtualenv cyoa
To install these dependencies run the following pip command on the command line.
pip install -r requirements.txt
cyoa/manage.py
from gevent import monkey
monkey.patch_all()
import os
from cyoa import app, redis_db, socketio
from flask.ext.script import Manager, Shell
manager = Manager(app)
def make_shell_context():
return dict(app=app, redis_db=redis_db)
manager.add_command("shell", Shell(make_context=make_shell_context))
189
@manager.command
def runserver():
socketio.run(app, "0.0.0.0", port=5001)
if __name__ == '__main__':
manager.run()
The above manage.py file will help us run our Flask application with the python manage.py
Next create a file cyoa/cyoa/config.py (the cyoa/cyoa subdirectory is where most of our
Python code will live other than manage.py) and add the below code to create the
configuration variables we’ll need for our Flask application.
cyoa/cyoa/config.py
import os
# General Flask app settings
DEBUG = os.environ.get('DEBUG', None)
SECRET_KEY = os.environ.get('SECRET_KEY', None)
# Redis connection
REDIS_SERVER = os.environ.get('REDIS_SERVER', None)
REDIS_PORT = os.environ.get('REDIS_PORT', None)
REDIS_DB = os.environ.get('REDIS_DB', None)
# Twilio API credentials
TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID', None)
TWILIO_AUTH_TOKEN = os.environ.get('TWILIO_AUTH_TOKEN', None)
TWILIO_NUMBER = os.environ.get('TWILIO_NUMBER', None)
Here’s a run down of what each of these environment variables specified in config.py is
for:
• DEBUG – True or False for whether Flask should display error messages if
something goes wrong
190
• REDIS_SERVER – in this case likely to be localhost or wherever Redis is running
• REDIS_DB – set to 0
Set the DEBUG and SECRET_KEY environment variables now. The Redis and Twilio
environment variables will be set in the next section.
The next file we need to create is cyoa/cyoa/__init__.py, which will set up the core pieces
for our the Flask app.
cyoa/cyoa/init.py
from flask import Flask
from flask.ext.socketio import SocketIO
import redis
app = Flask(__name__, static_url_path='/static')
app.config.from_pyfile('config.py')
from config import REDIS_SERVER, REDIS_PORT, REDIS_DB
redis_db = redis.StrictRedis(host=REDIS_SERVER, port=REDIS_PORT, db=REDIS_DB)
socketio = SocketIO(app)
from . import views
Next create a file named cyoa/cyoa/views.py and put the following code inside.
191
cyoa/cyoa/views.py
from flask import render_template, abort
from jinja2 import TemplateNotFound
from . import app
@app.route('/<presentation_name>/', methods=['GET'])
def landing(presentation_name):
try:
return render_template(presentation_name + '.html')
except TemplateNotFound:
abort(404)
For now the above file contains a single view named landing. This view landing obtains a
presentation name from the URL and checks the cyoa/cyoa/templates/ directory for an
HTML template file matching that name. If a matching file is found, landing renders the
HTML template. If no file name matches the presentation name in the URL, landing
returns a 404 HTTP response.
We’re awfully close to getting to fire up this presentation to take a look at it. An HTML
template file along with some static files are necessary for displaying the presentation.
Create a Reveal.js presentation in templates directory named cyoa/cyoa/templates/
cyoa.html. Add the following HTML inside this file.
cyoa/cyoa/templates/cyoa.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Choose Your Own Adventure Presentations with Reveal.js!</title>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,
user-scalable=no, minimal-ui">
<link rel="stylesheet" href="/static/css/reveal.css">
<link rel="stylesheet" href="/static/css/default.css" id="theme">
<link rel="stylesheet" href="/static/css/zenburn.css">
<!--[if lt IE 9]>
192
<script src="lib/js/html5shiv.js"></script>
<![endif]-->
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Choose Your Own Adventure!</h1>
<h3>With SMS voting</h3>
</section>
<section>
<div>
<h2>Text "left" or "right" to</h2>
<!-- replace this with your Twilio number -->
<h1>(xxx) 555-1234</h1>
</div>
<br>
<div style="display: inline;">
<div style="float: left;">
<h2><a href="#/2">left</a></h2>
<h1><div id="left">0</div> votes</h1>
</div>
<div style="float: right;">
<h2><a href="#/3">right</a></h2>
<h1><div id="right">0</div> votes</h1>
</div>
</div>
</section>
<!-- linked from first choice -->
<section>
<h1>Left</h1>
</section>
<!-- linked from second choice -->
<section>
<h1>Right</h1>
</section>
</div>
</div>
193
<script type="text/javascript"
src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script type="text/javascript"
src="//cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/socket.io.min.js">
</script>
<script src="/static/js/reveal.js"></script>
<script>
Reveal.initialize({
controls: true,
progress: true,
history: true,
center: true,
theme: Reveal.getQueryHash().theme,
transition: Reveal.getQueryHash().transition || 'default',
dependencies: []
});
</script>
</body>
</html>
As you can see in the HTML there are three locally hosted static CSS files and a
JavaScript file you’ll need for your project. Download these files from GitHub or
download and extract this cyoa.tar.gz archive into the cyoa/cyoa/static directory. The CSS
and JavaScript files we need are the following:
With these files in place we can try out the presentation to make sure everything so far
is in working order. Fire up Flask with the following command.
python manage.py runserver
194
Now we can view the initial version of the presentation by going to http://localhost:
5001/cyoa/ in the browser. Flask will find the cyoa.html file in the templates directory and
serve that up so you can view it.
However, the presentation will only come up on your own computer. To access the
presentation from other computers you’ll need to deploy to a hosting server or create
a localhost tunnel using a tool such as Ngrok. Sign up for Ngrok and download the
Ngrok tunneling application.
Fire up ngrok on port 5001 where our Flask application is running. See this
configuring Ngrok post if you’re running Windows. On Linux and Mac OS X Ngrok can
be run with the following command when you’re in the directory that ngrok is located
in.
./ngrok 5001
You’ll see a screen like the following. Take note of the unique https:// forwarding URL
as we’ll need that again in a minute for the Twilio webhook.
You can now pull the presentation both from the localhost URL as well as the
forwarding URL set up by ngrok.
195
Accepting SMS votes
So far we’ve written the Python code to serve a Reveal.js presentation and exposed it
via a localhost tunnel. Now we need a way for the audience to vote on which path they
want the presentation to go. To do that we’ll show a Twilio phone number on the
screen and use Twilio SMS to let the audience vote. If you’ve already got an account
grab a new or existing phone number, otherwise sign up and upgrade a Twilio
account.
We’ll need to set up the message webhook so that each vote SMS to the Twilio
number is sent to our Flask app. The Twilio number screen should look like the
following.
Paste in your Ngrok forwarding URL, which will look like “https://unique Ngrok
code.ngrok.com”, along with “/cyoa/twilio/webhook/” to the Messaging Request URL
196
text box then hit save. For example, if your Ngrok tunnel is “1d4e144c” your URL to
paste into the webhook text box should look like the following URL.
https://1d4e144c.ngrok.com/cyoa/twilio/webhook/
Now Twilio will send a POST HTTP request to the ngrok port forwarding URL which will
be sent down to your localhost server.
cyoa/cyoa/templates/cyoa.html
<!— update your number here -->
(xxx) 555-1234
However, we’re not yet ready to accept those incoming SMS votes. First we need to
ensure Redis is running and write Python code to persist votes to it.
Ensure that the Redis server is installed on your system since that will be our
temporary vote storage system. There’s a quickstart installation guide available for
your operating system of choice if you’re unfamiliar with Redis. In that quickstart you
just need to go from the beginning through the “Starting Redis” section and you’ll be
set for finishing this project.
Let’s add code in our cyoa/cyoa/views.py file for the Twilio webhook HTTP POST that
occurs on an inbound SMS. We’ll increment a counter for each keyword texted in by
people in the audience.
cyoa/cyoa/views.py
import cgi
197
from twilio.rest import TwilioRestClient
client = TwilioRestClient()
@app.route('/<presentation_name>/', methods=['GET'])
def landing(presentation_name):
try:
return render_template(presentation_name + '.html')
except TemplateNotFound:
abort(404)
@app.route('/cyoa/twilio/webhook/', methods='POST')
def twilio_callback():
if to == TWILIO_NUMBER:
redis_db.incr(cgi.escape(message))
resp = twiml.Response()
return str(resp)
It’d be useful to have a way to clear out votes from Redis in between rehearsals for our
presentation. Update cyoa/manage.py with a new function to clear votes from Redis.
cyoa/manage.py
from gevent import monkey
monkey.patch_all()
198
import os
import redis
@manager.command
def clearredis():
rediscli.delete('left')
rediscli.delete('right')
if __name__ == '__main__':
manager.run()
If Flask and ngrok are still running we can test this out now, otherwise fire them up and
let’s text either “left” or “right” to your Twilio number.
When you test out SMS inbound text messages you should receive the “Thanks for
your vote!” response like in the screenshot below.
199
At this point those inbound text message votes are also being stored in Redis. We’re
so close to wrapping up this code at this point let’s write a bit more Python to test that
the messages are indeed in Redis and propagate the results to the presentation via
websockets.
200
websockets.py
from flask.ext.socketio import emit
The above code in the update_count function allows the websocket clients, in this case
the browser that loads the presentation, to connect to the websockets stream and
receive future messages.
cyoa/cyoa/init.py
from flask import Flask
from flask.ext.socketio import SocketIO
import redis
app = Flask(__name__, static_url_path='/static')
app.config.from_pyfile('config.py')
from config import REDIS_SERVER, REDIS_PORT, REDIS_DB
redis_db = redis.StrictRedis(host=REDIS_SERVER, port=REDIS_PORT, db=REDIS_DB)
socketio = SocketIO(app)
from . import views
201
Update cyoa/cyoa/views.py with the following highlighted lines. We’re adding the
socketio.emit call so votes are passed via websockets to the presentation.
cyoa/cyoa/views.py
import cgi
from flask import render_template, request, abort
from jinja2 import TemplateNotFound
from twilio import twiml
from twilio.rest import TwilioRestClient
from .config import TWILIO_NUMBER
from . import app, redis_db
client = TwilioRestClient()
@app.route('/<presentation_name>/', methods=['GET'])
def landing(presentation_name):
try:
return render_template(presentation_name + '.html')
except TemplateNotFound:
abort(404)
@app.route('/cyoa/twilio/webhook/', methods=['POST'])
def twilio_callback():
to = request.form.get('To', '')
from_ = request.form.get('From', '')
message = request.form.get('Body', '').lower()
if to == TWILIO_NUMBER:
redis_db.incr(cgi.escape(message))
'val': redis_db.get(message)},
namespace='/cyoa')
resp = twiml.Response()
resp.message("Thanks for your vote!")
202
return str(resp)
/cyoa/cyoa/templates/cyoa.html
<script type="text/javascript">
Reveal.initialize({
controls: true,
progress: true,
history: true,
center: true,
theme: Reveal.getQueryHash().theme,
transition: Reveal.getQueryHash().transition || 'default',
dependencies: [
]
});
$(document).ready(function() {
namespace = '/cyoa';
socket.on('msg', function(msg) {
203
checkDiv.html(msg.val);
}
}
});
});
</script>
Wrapping it up
You now have a Flask app that can serve up Reveal.js presentations where the
audience can interact with the slide on screen by texting in their votes.
Now it’s up to you to create the Choose Your Own Adventure content!
—————
204
How do you proceed? If you choose to run from the PyCon challenge, close the
browser window now. If you accept the challenge, prepare yourself for the dangers
ahead with the new Wizard Mode functionality and continuing reading this blog post.
—————
You’re still here, adventurer! Let’s get to work. In this series of three blog posts we’re
going to expand the Choose Your Own Adventure Presentations application with new
a Wizard Mode. If you haven’t yet worked through the original blog post I highly
recommend doing that before working through this series. The section below named
“A Clean Starting Point” will get you set up for this tutorial if you’ve already gone
through the original Choose Your Own Adventure Presentations post and just need a
fresh copy of the code.
205
Once you’re inside the application you’ll be able to manage one or more Choose Your
Own Adventure Presentations with a simple screen like this one:
As we go about building our new Wizard Mode you’ll learn about Flask form handling,
WebSockets and how to persist presentation data to a PostgreSQL database.
206
There are three posts in this tutorial series where we will incrementally build out
functionality:
1. Wizards Only: this blog post, where we’ll create a section of the
application only authorized wizards can access.
2. Even Wizards Need Web Forms: the next blog post where we expand the
wizard only pages to control our presentations
3. Voting with a Wand, or Smartphone: our third and final post where we
add a new magical trick to our presentations – voting via web browser
when poor cell service prevents SMS voting
At the end of each post we’ll be able to test what we just built to make sure it’s
working properly. If something goes wrong while you’re working through the tutorial,
the Git tag tutorial-step-4 tag has the end result for code written in this post.
207
• Flask-WTF for web form handling
Finally, time to roll up the magical cloak sleeves again and dive into coding.
Wizards Only
We’re going to create the wizard’s panel to control multiple presentations and how
our audience can interact with them. First though we need a way to sort wizards from
non-wizards in the form of a sign-in screen.
We’ll use four new Python code libraries in this post so let’s get those added to our
project. These new libraries are highlighted below. Update the requirements.txt file so
it matches the following code.
Flask==0.10.1
Flask-Script==2.0.5
Flask-SocketIO==0.4.1
Flask-Login==0.2.11
Flask-SQLAlchemy==2.0
Flask-WTF==0.10.3
gunicorn==19.1.1
redis==2.10.3
twilio==3.6.8
psycopg2==2.5.4
Make sure to active your project’s virtualenv and install the new dependencies with
pip using these two steps:
source ~/Envs/cyoa/bin/activate
pip install -r requirements.txt
With our new dependencies in place we can create our Wizards Only sign-in page.
208
Create a new file named models.py within the cyoa/cyoa/ subdirectory to store information
about wizards. The file should have the following contents:
cyoa/cyoa/models.py
from flask.ext.login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from . import db
class Wizard(UserMixin, db.Model):
"""
Represents a wizard who can access special parts of the application.
"""
__tablename__ = 'wizards'
id = db.Column(db.Integer, primary_key=True)
wizard_name = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
def __init__(self, wizard_name, password):
self.wizard_name = wizard_name
self.password = password
@property
def password(self):
raise AttributeError('password is not readable')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return '' % self.wizard_name
Next we need to modify the cyoa/cyoa/__init__.py file to create a login manager. The
login manager will allow us to control access to wizard settings.
209
cyoa/cyoa/init.py
import redis
from flask import Flask
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.login_view = 'sign_in'
login_manager.init_app(app)
from . import views
from . import websockets
Our new configuration variable will reside in our config.py file as highlighted below.
import os
210
# General Flask app settings
DEBUG = os.environ.get('DEBUG', None)
SECRET_KEY = os.environ.get('SECRET_KEY', None)
# Redis connection
REDIS_SERVER = os.environ.get('REDIS_SERVER', None)
REDIS_PORT = os.environ.get('REDIS_PORT', None)
REDIS_DB = os.environ.get('REDIS_DB', None)
# Twilio API credentials
TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID', None)
TWILIO_AUTH_TOKEN = os.environ.get('TWILIO_AUTH_TOKEN', None)
TWILIO_NUMBER = os.environ.get('TWILIO_NUMBER', None)
In our original Choose Your Own Adventure Presentations blog post we set the other
environment variables listed in the above config.py file. Set an environment variable for
DATABASE_URL that will be loaded into SQLALCHEMY_DATABASE_URI. The value for this variable will
be a URI that points to your running PostgreSQL database. For example, in my local
development environment the DATABASE_URL is set to postgresql://matt:password123@localhost/
cyoa. And no, that’s not the actual password value I use on my development
environment.
We’ll need a form to handle data coming from a wizard. In our application the Flask-
WTF library is going to handle form generation and input sanitization. Create a new
file named forms.py in your cyoa/ subdirectory.
from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField, \
DateField, IntegerField
from wtforms.validators import Required, Length, Regexp, EqualTo
from wtforms import ValidationError
from .models import Wizard
class LoginForm(Form):
wizard_name = StringField('Wizard Name',
validators=[Required(), Length(1, 32)])
211
password = PasswordField('Password', validators=[Required(),
Length(1, 32)])
def validate(self):
if not Form.validate(self):
return False
user = Wizard.query.filter_by(wizard_name=self.
wizard_name.data).first()
if user is not None and not user.verify_password(self.password.data):
self.password.errors.append('Incorrect password.')
return False
return True
The above code creates a Python representation of the sign-in page form. We add a
custom validator within the validate function to ensure the password entered by the
user matches what’s stored in the database. If the form input passes the validation
function then True is returned and the form processing continues along in our views.py
file.
current_user
212
client = TwilioRestClient()
@login_manager.user_loader
def load_user(userid):
return Wizard.query.get(int(userid))
@app.route('/<presentation_name>/', methods=['GET'])
def landing(presentation_name):
try:
return render_template(presentation_name + '.html')
except TemplateNotFound:
abort(404)
@app.route('/cyoa/twilio/webhook/', methods=['POST'])
def twilio_callback():
to = request.form.get('To', '')
from_ = request.form.get('From', '')
message = request.form.get('Body', '').lower()
if to == TWILIO_NUMBER:
redis_db.incr(cgi.escape(message))
socketio.emit('msg', {'div': cgi.escape(message),
'val': redis_db.get(message)},
namespace='/cyoa')
resp = twiml.Response()
resp.message("Thanks for your vote!")
return str(resp)
@app.route('/wizard/', methods=['GET', 'POST'])
def sign_in():
form = LoginForm()
if form.validate_on_submit():
wizard = Wizard.query.filter_by(wizard_name=
form.wizard_name.data).first()
213
login_user(wizard)
return redirect(url_for('wizard_landing'))
@app.route('/sign-out/')
@login_required
def sign_out():
logout_user()
return redirect(url_for('sign_in'))
@app.route('/wizard/presentations/')
@login_required
def wizard_landing():
return render_template('wizard/presentations.html')
What we’re doing in the code above is including our new login manager we created
in the cyoa/cyoa/__init__.py file. We then implement a login manager callback function
named load_user which reloads a Wizard object from the user ID stored in the session.
There are three additional functions:
214
We have the code to run the app but there are no templates to render yet. Let’s create
those now so we can test this thing out. Under the cyoa/cyoa/templates/ folder create a
new file named base.html with the following template code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Matt Makai">
<title>{% block title %}{% endblock %}CYOA Presentations</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/
bootstrap.min.css">
<link rel="stylesheet" href="/static/css/wizard.css">
{% block css_head %}{% endblock %}
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js">
</script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js">
</script>
<![endif]-->
</head>
<body{% block body_class %}{% endblock %}>
{% block nav %}{% endblock %}
{% block content %}{% endblock %}
{% block js_body %}{% endblock %}
</body>
</html>
The above template serves as a base HTML template file that other templates can
extend. Our sign-in page and our wizard landing page will both extend base.html.
You’ll notice we have a new wizard.css file included in our base.html file. There are a few
extra styles and a background image we will include in our app. Rather than having
you type out all that code you’ll want to download the file cyoa-tutorial-step-4.tar.gz
and extract it under the cyoa/cyoa/static/ directory so those files can be served up when
we run our application.
215
base.html eliminates the boilerplate code we don’t want to write in every template, but
we can go even further with our Don’t Repeat Yourself (DRY) principle by using a
helper to render forms. Make a subdirectory named partials under the cyoa/cyoa/
templates/ directory. Within partials construct a file named _formhelpers.html and put the
following template code inside:
{% macro render_field(field) %}
<dt style="text-align: left;" class="admin-field">{{ field.label }}
{% if field.errors %}
<ul class="errors">
{% for error in field.errors %}
<li style="color: red;">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<dd>{{ field(class_="form-control", **kwargs)|safe }}
</dd>
{% endmacro %}
Every time we render a form field in future templates we can include the above file
and it will insert the boilerplate code for a single field into the template.
216
{% from "partials/_formhelpers.html" import render_field %}
<form method="post" action="{{ url_for('sign_in') }}"
id="login-form">
{{ form.csrf_token }}
{{ render_field(form.wizard_name, required=True) }}
<br>
{{ render_field(form.password, required=True) }}
<div style="margin-top: 10px; text-align: right;">
<input class="btn btn-primary"
type="submit" value="Sign in" />
</div>
</form>
</div>
</div>
</div>
<footer class="footer">
<div class="container">
<p class="text-muted pull-right">
Photo by <a href="http://photos.jdhancock.com/photo/2012-04-16-000233-a-spot-of-
magic.html">JD Hancock</a>
</p>
</div>
</footer>
{% endblock %}
The template renders a page with a nice wizard background image for some extra
flavor. We’re close to running our app, but we need a page to render once the wizard
gets past the sign-in page.
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-10">
<h1>Wizards Only Stuff Here</h1>
<a href="{{ url_for('sign_out') }}">Sign out</a>
</div>
217
</div>
</div>
{% endblock %}
With our application code and templates in place we just have one more file to
update so we can run the application. Change the existing manage.py file with the
following highlighted lines.
from gevent import monkey
monkey.patch_all()
import os
import redis
from cyoa import app, redis_db, socketio
manager.add_command("shell", Shell(make_context=make_shell_context))
@manager.command
def syncdb():
db.create_all()
@manager.command
def runserver():
socketio.run(app, "0.0.0.0", port=5001)
218
@manager.command
def clear_redis():
redis_cli = redis.StrictRedis(host='localhost', port='6379', db='0')
redis_cli.delete('left')
redis_cli.delete('right')
if __name__ == '__main__':
manager.run()
We’re itching to get this application running but we need to make sure our database
is created along with our Wizard table. On the command line create PostgreSQL
database using this command:
createdb cyoa
With our updated manage.py script we can create the database tables necessary for
storing wizards.
python manage.py syncdb
Also we need to populate our first authorized wizard. Start up the shell and enter the
following code to create and save a wizard. You can modify the credentials as you see
fit.
python manage.py shell
>>> db.session.add(Wizard('gandalf', 'thegrey'))
>>> db.session.commit()
>>> exit()
Finally, time to test out our new wizard sign-in screen! Within the base directory of our
application run the built-in server using the following command:
python manage.py runserver
219
After entering our credentials and hitting the Sign in button we’ll see the following
landing page.
Click the Sign out button to test out the sign_out function and ensure that we eliminate
any trace of our wizard-ness to the application.
220
Even Wizards Need Web Forms
The Wizards Only mode interface we’re creating throughout this post will grant us the
ability to see which presentations are available as well as create and edit metadata,
such as whether a presentation is visible or invisible to non-wizards.
Note that if you make a typo somewhere along the way in this section, you can
compare your version with the tutorial-step-5 tag. Time to get coding and create our
Wizards Only mode.
221
To make the Wizards Only mode functional we’ll build a new landing page that lists
every presentation we’ve created through this user interface.
Next, replace what we just deleted in cyoa/cyoa/views.py by creating a new file named
cyoa/cyoa/wizard_views.py and populating it with the following code.
from flask import render_template, redirect, url_for
from flask.ext.login import login_required
from . import app, db
@app.route('/wizard/presentations/')
@login_required
def wizard_list_presentations():
presentations = []
return render_template('wizard/presentations.html',
presentations=presentations)
@app.route('/wizard/presentation/', methods=['GET', 'POST'])
@login_required
def wizard_new_presentation():
pass
@app.route('/wizard/presentation/<int:id>/', methods=['GET', 'POST'])
@login_required
def wizard_edit_presentation(id):
pass
@app.route('/wizard/presentation/<int:pres_id>/decisions/')
@login_required
def wizard_list_presentation_decisions(pres_id):
pass
222
@app.route('/wizard/presentation/<int:pres_id>/decision/',
methods=['GET', 'POST'])
@login_required
def wizard_new_decision(pres_id):
pass
@app.route('/wizard/presentation/<int:presentation_id>/decision/'
'<int:decision_id>/', methods=['GET', 'POST'])
@login_required
def wizard_edit_decision(presentation_id, decision_id):
pass
@app.route('/wizard/presentation/<int:pres_id>/decision/'
'<int:decision_id>/delete/')
@login_required
def wizard_delete_decision(pres_id, decision_id):
pass
With the exception of the wizard_list_presentations function, every function above with
the pass keyword in its body is just a stub for now. We’ll flesh out those functions with
code throughout the remainder of this post and also later in part 3 of the tutorial. For
now we need them stubbed because otherwise the url_for function in our redirects
and templates will not be able to look up the appropriate URL paths.
Go back to the cyoa/cyoa/views.py file. Edit the return redirect line shown below so it calls
the new wizard_list_presentations function instead of wizard_landing.
@app.route('/wizard/', methods=['GET', 'POST'])
def sign_in():
form = LoginForm()
if form.validate_on_submit():
wizard = Wizard.query.filter_by(wizard_name=
form.wizard_name.data).first()
if wizard is not None and wizard.verify_password(form.password.data):
login_user(wizard)
return redirect(url_for('wizard_list_presentations'))
223
Our Flask application needs to access the new wizard_views.py functions. Add this single
line to the end of the cyoa/cyoa/__init__.py file to make that happen:
The templates for our new landing page don’t exist yet, so let’s create them now.
Create a new file cyoa/cyoa/templates/nav.html and insert the HTML template markup
below.
<div class="container">
<div class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="{{ url_for('wizard_list_presentations') }}">Wizards Only</
a>
</div>
<div>
<ul class="nav navbar-nav">
<li><a href="{{ url_for('wizard_list_presentations') }}">Presentations</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="{{ url_for('sign_out') }}">Sign out</a></li>
</ul>
</div>
</div>
</div>
</div>
The above template file is a navigation bar that will be included on logged in Wizard
Only pages. You’ll see the template tag {% include "nav.html" %} in every template that
needs to display the navigation bar at the top of the webpage.
Next up we need to modify the temporary markup in the landing page template file
so it displays the presentations we will create through the Wizards Only user interface.
Replace the temporary code found in cyoa/cyoa/templates/wizard/presentations.html with the
following HTML template markup.
{% extends "base.html" %}
{% block nav %}
{% include "nav.html" %}
224
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-10">
<h1>Presentations</h1>
{% if not presentations %}
No presentations found.
<a href="{{ url_for('wizard_new_presentation') }}">Create your first one</a>.
{% else %}
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Is Visible?</th>
<th>Web browser voting?</th>
</tr>
</thead>
<tbody>
{% for p in presentations %}
<tr>
<td><a href="{{ url_for('wizard_edit_presentation',
id=p.id) }}">{{ p.name }}</a></td>
<td>{{ p.is_visible }}</td>
<td><a href="{{ url_for('wizard_list_presentation_decisions',
pres_id=p.id) }}">Manage choices</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-10">
<div class="btn-top-margin">
<a href="{{ url_for('wizard_new_presentation') }}"
class="btn btn-primary">New Presentation</a>
</div>
</div>
</div>
225
</div>
</div>
{% endblock %}
In the above markup we check the presentations object passed into the template to
determine if one or more presentations exist. If not, Flask renders the template with a
“No presentations found.” message and a link to create the first presentation. If one or
more presentation objects do exist, a table is rendered with the name of the
presentation, whether it’s visible to non-wizard users and whether or not we’ve
enabled web browser voting (which we will code in part 3).
Time to test out the current state of our application to make sure it’s working properly.
Make sure your virtualenv is activated and environment variables are set as we
established in part 1 of the tutorial. From the base directory of our project, start the
dev server with the python manage.py runserver command.
(cyoa)$ python manage.py runserver
* Running on http://0.0.0.0:5001/
* Restarting with stat
If your development server does not start up properly, make sure you’ve executed pip
install -r requirements.txt to have all the dependencies the server requires.
Occasionally there are issues installing the required gevent library the first time the
dependencies are obtained via pip.
When you get into the application, the presentations.html template combined with our
existing base.html and new nav.html will create a new landing screen that looks like this:
226
However, we haven’t written any code to power the “Create your first one” link and
“New Presentation” buttons yet. If we click on the “New Presentation” button, we’ll get
a ValueError like we see in the screenshot below because that view does not return a
response.
227
files themselves. In other words, we’re modifying the presentation metadata, not the
HTML markup within the presentations. As we’ll see later in the post, our application
will use the metadata to look in the cyoa/cyoa/templates/presentations folder for a filename
associated with a visible presentation.
Open up cyoa/cyoa/models.py and append the following code at the end of the file.
class Presentation(db.Model):
"""
Contains data regarding a single presentation.
"""
__tablename__ = 'presentations'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
slug = db.Column(db.String(128), unique=True)
filename = db.Column(db.String(256))
is_visible = db.Column(db.Boolean, default=False)
def __repr__(self):
return '<Presentation %r>' % self.name
The above Presentation class is a SQLAlchemy database model. Just like with our
Wizard model, this model maps a Python object to the database table presentations and
allows our application to create, read, update and delete rows in the database for that
table.
We also need a new form to handle the creating and editing of presentation
metadata. We’ll store this form in the cyoa/cyoa/forms.py file.
class PresentationForm(Form):
name = StringField('Presentation name', validators=[Required(),
Length(1, 60)])
filename = StringField('File name', validators=[Required(),
Length(1, 255)])
slug = StringField('URL slug', validators=[Required(),
Length(1, 255)])
is_visible = BooleanField()
228
Now we need to tie together our new Presentation database model and PresentationForm
form. In the cyoa/cyoa/wizard_views.py, remove the pass keyword from the listed functions
and replace it with the highlighted code. What we’re adding below are two imports
for the Presentation and PresentationFrom classes we just wrote. Now that we have
presentations in the database, we can query for existing presentations in the
wizard_list_presentations function. In the wizard_new_presentation and wizard_edit_presentation
functions, we’re using the PresentationForm class to create and modify Presentation objects
through the application’s web forms.
from . import app, db
@app.route('/wizard/presentations/')
@login_required
def wizard_list_presentations():
presentations = Presentation.query.all()
return render_template('wizard/presentations.html',
presentations=presentations)
@app.route('/wizard/presentation/', methods=['GET', 'POST'])
@login_required
def wizard_new_presentation():
form = PresentationForm()
if form.validate_on_submit():
presentation = Presentation()
form.populate_obj(presentation)
db.session.add(presentation)
db.session.commit()
229
return redirect(url_for('wizard_list_presentations'))
@app.route('/wizard/presentation/<int:id>/', methods=['GET', 'POST'])
@login_required
def wizard_edit_presentation(id):
presentation = Presentation.query.get_or_404(id)
form = PresentationForm(obj=presentation)
if form.validate_on_submit():
form.populate_obj(presentation)
db.session.merge(presentation)
db.session.commit()
db.session.refresh(presentation)
presentation=presentation)
In the above code, make sure you’ve changed the first line within the
wizard_list_presentations function from presentations = [] to presentations =
230
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6">
{% from "partials/_formhelpers.html" import render_field %}
{% if is_new %}
<form action="{{ url_for('wizard_new_presentation') }}"
method="post">
{% else %}
<form action="{{ url_for('wizard_edit_presentation', id=presentation.id) }}"
method="post">
{% endif %}
<div>
{{ form.csrf_token }}
{{ render_field(form.name) }}
{{ render_field(form.filename) }}
{{ render_field(form.slug) }}
<dt class="admin-field">
{{ form.is_visible.label }}
{{ form.is_visible }}
</dt>
</div>
<div>
<input type="submit" class="btn btn-success btn-top-margin"
value="Save Presentation"></input>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
Let’s give our upgraded code another spin. First, since we created a new database
table we need to sync our models.py code with the database tables. Run the following
manage.py command at the command line from within the project’s base directory.
Now the tables in PostgreSQL match our updated models.py code. Bring up the
application with the runserver command.
231
(cyoa)$ python manage.py runserver
Point your web browser to http://localhost:5001/wizard/. You should again see the
unchanged Wizards Only sign in page.
Use your wizard credentials to sign in. At the presentations landing screen after
signing in, click the “New Presentation” button.
232
Create a presentation for the default template that’s included with the project. Enter
“Choose Your Own Adventure Default Template” as the presentation name,
“cyoa.html” for the file name, “cyoa” for the URL slug and check the “Is Visible” box.
Press the “Save Presentation” button and we’ll be taken back to the presentations list
screen where our new presentation is listed.
We can edit existing presentations by clicking on the links within the Name column.
Now we’ll use this presentation information to display visible presentations to users
not logged into the application.
In addition, our presentation retrieval function will look for presentation files only in
the cyoa/cyoa/templates/presentations/ folder. The presentations can still be accessed from
the same URL as before, but it’s easier to remember where presentation files are
located when there’s a single folder for them.
Start by deleting the following code in cyoa/cyoa/views.py as we will not need it any
longer.
233
@app.route('/<presentation_name>/', methods=['GET'])
def landing(presentation_name):
try:
return render_template(presentation_name + '.html')
except TemplateNotFound:
abort(404)
One more step in cyoa/cyoa/views.py. Add the following line as a new import to the top
of the file so that our new code can use the Presentation database model for the queries
in the functions we just wrote:
from .models import Wizard
234
client = TwilioRestClient()
Finally create a new list template for the presentations. Call this file cyoa/cyoa/templates/
list_presentations.html.
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-10">
<h1>Available presentations</h1>
{% for p in presentations %}
<p><a href="{{ url_for('presentation', slug=p.slug) }}">{{ p.name }}</a></p>
{% else %}
No public presentations found.
{% endfor %}
</div>
</div>
</div>
{% endblock %}
mkdir cyoa/templates/presentations/
Now go to the cyoa/cyoa/templates/ directory and run the following move command.
mv cyoa.html presentations/
Check out the simple listing of presentations available to the audience. Go to http://
localhost:5001 and you’ll see the list of visible presentations.
235
236
websocket connections. The new browser-based voting can augment our existing
SMS-based voting for presentations when cell signals aren’t working well.
Don that wizard’s cloak one more time and get ready to write a bit more Python code
as we finish out our Choose Your Own Adventure Presentations application.
237
Decisions can be created and saved via the simple form as seen below.
Let’s get into the new code we need to write to transform the above screenshots from
images into a part of our working application. Crank open the existing cyoa/cyoa/
models.py file in your favorite editor. We’re going to add the first highlighted line to the
Presentation model that represents a foreign key to the new Decision model. The
Decision model will hold the slugs to the webpages that will allow someone in the
audience to vote on a decision. Add the new line to Presentation and create the new
Decision model as shown below.
238
class Presentation(db.Model):
"""
Contains data regarding a single presentation.
"""
__tablename__ = 'presentations'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
slug = db.Column(db.String(128), unique=True)
filename = db.Column(db.String(256))
is_visible = db.Column(db.Boolean, default=False)
def __repr__(self):
return '<Presentation %r>' % self.name
class Decision(db.Model):
"""
"""
__tablename__ = 'choices'
id = db.Column(db.Integer, primary_key=True)
slug = db.Column(db.String(128))
first_path_slug = db.Column(db.String(128))
second_path_slug = db.Column(db.String(128))
def __repr__(self):
239
The new models code above allows us to save and manipulate web browser-based
decisions that can be associated with specific presentations.
Next we need to update the cyoa/cyoa/forms.py file with a new DecisionForm class. Within
cyoa/cyoa/forms.py append the following new form class as highlighted below.
from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField,
DateField, IntegerField
from wtforms.validators import Required, Length, Regexp, EqualTo
from wtforms import ValidationError
from .models import Wizard
class LoginForm(Form):
wizard_name = StringField('Wizard Name',
validators=[Required(), Length(1, 32)])
password = PasswordField('Password', validators=[Required(),
Length(1, 32)])
def validate(self):
if not Form.validate(self):
return False
user = Wizard.query.filter_by(wizard_name=self.
wizard_name.data).first()
if user is not None and not user.verify_password(self.password.data):
self.password.errors.append('Incorrect password.')
return False
return True
class PresentationForm(Form):
name = StringField('Presentation name', validators=[Required(),
Length(1, 60)])
filename = StringField('File name', validators=[Required(),
Length(1, 255)])
slug = StringField('URL slug', validators=[Required(),
Length(1, 255)])
is_visible = BooleanField()
240
class DecisionForm(Form):
Length(1, 128)])
'lowercase. No spaces.',
'with no whitespace.')])
'lowercase. No spaces.',
'with no whitespace.')])
The DecisionForm is used to create and edit decisions through the user interface. We’ve
included some basic validation to ensure proper URL slugs are input by the user.
The next file that’s critical for getting our web browser-based voting up and running is
cyoa/cyoa/wizard_views.py. The changes we’re going to make in this file will allow us to
modify the decisions found in presentations so users can only vote in the web browser
on choices we’ve created for them. In part two of our tutorial, we included stub
functions in this wizard_views.py file knowing that we’d flesh them out in this post. Make
sure to remove the pass keyword from the body of those functions and insert the
highlighted code shown below.
from flask import render_template, redirect, url_for
from flask.ext.login import login_required
241
from . import app, db
@app.route('/wizard/presentations/')
@login_required
def wizard_list_presentations():
presentations = Presentation.query.all()
return render_template('wizard/presentations.html',
presentations=presentations)
@app.route('/wizard/presentation/', methods=['GET', 'POST'])
@login_required
def wizard_new_presentation():
form = PresentationForm()
if form.validate_on_submit():
presentation = Presentation()
form.populate_obj(presentation)
db.session.add(presentation)
db.session.commit()
return redirect(url_for('wizard_list_presentations'))
return render_template('wizard/presentation.html', form=form, is_new=True)
@app.route('/wizard/presentation/<int:id>/', methods=['GET', 'POST'])
@login_required
def wizard_edit_presentation(id):
presentation = Presentation.query.get_or_404(id)
form = PresentationForm(obj=presentation)
if form.validate_on_submit():
form.populate_obj(presentation)
db.session.merge(presentation)
db.session.commit()
db.session.refresh(presentation)
return render_template('wizard/presentation.html', form=form,
presentation=presentation)
242
@app.route('/wizard/presentation/<int:pres_id>/decisions/')
@login_required
def wizard_list_presentation_decisions(pres_id):
presentation = Presentation.query.get_or_404(pres_id)
decisions=presentation.decisions.all())
@app.route('/wizard/presentation/<int:pres_id>/decision/',
methods=['GET', 'POST'])
@login_required
def wizard_new_decision(pres_id):
form = DecisionForm()
if form.validate_on_submit():
decision = Decision()
form.populate_obj(decision)
decision.presentation = pres_id
db.session.add(decision)
db.session.commit()
return redirect(url_for('wizard_list_presentation_decisions',
pres_id=pres_id))
presentation_id=pres_id)f
@app.route('/wizard/presentation/<int:presentation_id>/decision/'
'<int:decision_id>/', methods=['GET', 'POST'])
@login_required
243
def wizard_edit_decision(presentation_id, decision_id):
decision = Decision.query.get_or_404(decision_id)
form = DecisionForm(obj=decision)
if form.validate_on_submit():
form.populate_obj(decision)
decision.presentation = presentation_id
db.session.merge(decision)
db.session.commit()
db.session.refresh(decision)
return redirect(url_for('wizard_list_presentation_decisions',
pres_id=presentation_id))
@app.route('/wizard/presentation/<int:pres_id>/decision/'
'<int:decision_id>/delete/')
@login_required
def wizard_delete_decision(pres_id, decision_id):
presentation = Presentation.query.get_or_404(pres_id)
decision = Decision.query.get_or_404(decision_id)
db.session.delete(decision)
db.session.commit()
return redirect(url_for('wizard_list_presentation_decisions',
pres_id=presentation.id))
244
We added code to list, create, edit and delete decisions in the system. These are
standard operations for an administrative interface that any self-respecting wizard
would expect.
Alright, that’s all the Python code we need at the moment for the new Wizards Only
screens. Now we need to create some new templates to generate the HTML for our
decision screens. Create a new file named cyoa/cyoa/templates/wizard/decisions.html with
the following markup.
{% extends "base.html" %}
{% block nav %}
{% include "nav.html" %}
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-10">
<h1>{{ presentation.name }} Decisions</h1>
{% if decisions|length == 0 %}
No web browser voting enabled for
<em>{{ presentation.name }}</em>.
<a href="{{ url_for('wizard_new_decision', pres_id=presentation.id) }}">Add a
decision point for this presentation</a>.
{% else %}
<table class="table">
<thead>
<tr>
<th>Decision</th>
<th>First story path</th>
<th>Second story path</th>
<th>Delete</th>
<th>View choice</th>
</tr>
</thead>
<tbody>
{% for d in decisions %}
<tr>
245
<td><a href="{{ url_for('wizard_edit_decision',
presentation_id=presentation.id, decision_id=d.id) }}">{{ d.slug }}</a></td>
<td>{{ d.first_path_slug }}</td>
<td>{{ d.second_path_slug }}</td>
<td><a href="{{ url_for('wizard_delete_decision',
pres_id=presentation.id, decision_id=d.id) }}" class="btn btn-danger">X</a></td>
<td>{{ d.slug }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div class="btn-top-margin">
<a href="{{ url_for('wizard_new_decision', pres_id=presentation.id) }}" class="btn
btn-primary">Add decision point</a>
</div>
</div>
</div>
</div>
{% endblock %}
The above template markup loops through existing decisions and displays each one’s
name and first and second story paths along with options to delete or view the
decision’s webpage. If no decisions are returned from the database for the
selected presentation, then a simple explanation will show “No web browser voting
enabled for [presentation name].” There must be decision points created in the user
interface for the browser-based voting to work.
There also needs to be a template file for creating new decisions and editing existing
ones. Create the file cyoa/cyoa/templates/wizard/decision.html and insert the following
markup. Make sure you’ve named this file without an ‘s’ at the end of “decision” so it
doesn’t overwrite the previous template we just created.
{% extends "base.html" %}
{% block nav %}
{% include "nav.html" %}
{% endblock %}
{% block content %}
246
<div class="container">
<div class="row">
<div class="col-md-6">
{% from "partials/_formhelpers.html" import render_field %}
{% if is_new %}
<form action="{{ url_for('wizard_new_decision', pres_id=presentation_id) }}"
method="post">
{% else %}
<form action="{{ url_for('wizard_edit_decision', presentation_id=presentation_id,
decision_id=decision.id) }}" method="post">
{% endif %}
<div>
{{ form.csrf_token }}
{{ render_field(form.slug) }}
{{ render_field(form.first_path_slug) }}
{{ render_field(form.second_path_slug) }}
</div>
<div>
<input type="submit" class="btn btn-success btn-top-margin"
value="Save Decision"></input>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
With the above template we have a way to create and edit decisions. The template is
generated with a form submit to either create a new decision if the “New Decision”
button was clicked or edit the existing decision when modifying an existing decision.
Finally, it’s time to test out our new code! Create the new Decision database table by
running the following command at the base directory of our project.
(cyoa)$ python manage.py syncdb
Now run the development server with the following command as we’ve performed in
previous blog posts.
(cyoa)$ python manage.py runserver
247
Head to http://localhost:5001/wizard to check out the current version of our
application.
So far so good. Sign in using the defaults “gandalf” as a username and “thegrey” as
the password that were created in part 1 of the tutorial.
If there are existing presentations in your application, click on the “Manage choices”
link for one of the presentations, or create a new presentation then click the link.
Unfortunately, upon clicking “Manage choices” we will suddenly get the following
error page…
Uh oh. What’s happening here? It looks like the new foreign key relationship in
thePresentation table is causing a database error. Although we created the new
Decision table with the syncdb command, it did not create the foreign key relationship
column in the Presentation table.
How do we fix this problem? We need to ensure the Presentation table has the
appropriate column in the database. To accomplish that we have to
248
• Use a library to perform a schema migration based on our updated
SQLAlchemy models
• Add the column manually to the database with an ALTER TABLE statement
• Drop and recreate the database and re-sync the database tables.
For simplicity’s sake, in this post we’ll use the third method of dropping and recreating
the database. Note that unfortunately the existing wizards and presentations in the
database will be deleted when we blow away the database so we will also walk
through recreating them.
Kill the development server process with Ctrl-C and execute the following commands
to recreate the PostgreSQL database.
(cyoa)$ dropdb cyoa
(cyoa)$ createdb cyoa
(cyoa)$ python manage.py syncdb
We also need to create a new Wizard user in the database since the previous one was
deleted.
(cyoa)$ python manage.py shell
>>> db.session.add(Wizard('gandalf', 'thegrey'))
>>> db.session.commit()
>>> exit()
249
import cgi
from flask import render_template, abort, request
from flask import redirect, url_for
from flask.ext.login import login_user, logout_user, login_required, \
current_user
from jinja2 import TemplateNotFound
from twilio import twiml
from twilio.rest import TwilioRestClient
from .config import TWILIO_NUMBER
from .forms import LoginForm
from . import app, redis_db, socketio, login_manager
from .models import Presentation
client = TwilioRestClient()
@login_manager.user_loader
def load_user(userid):
return Wizard.query.get(int(userid))
@app.route('/', methods=['GET'])
def list_public_presentations():
presentations = Presentation.query.filter_by(is_visible=True)
return render_template('list_presentations.html',
presentations=presentations)
@app.route('/<slug>/', methods=['GET'])
def presentation(slug):
presentation = Presentation.query.filter_by(is_visible=True,
slug=slug).first()
if presentation:
return render_template('/presentations/' + presentation.filename)
abort(404)
@app.route('/cyoa/twilio/webhook/', methods=['POST'])
250
def twilio_callback():
to = request.form.get('To', '')
from_ = request.form.get('From', '')
message = request.form.get('Body', '').lower()
if to == TWILIO_NUMBER:
redis_db.incr(cgi.escape(message))
socketio.emit('msg', {'div': cgi.escape(message),
'val': redis_db.get(message)},
namespace='/cyoa')
resp = twiml.Response()
resp.message("Thanks for your vote!")
return str(resp)
@app.route('/wizard/', methods=['GET', 'POST'])
def sign_in():
form = LoginForm()
if form.validate_on_submit():
wizard = Wizard.query.filter_by(wizard_name=
form.wizard_name.data).first()
if wizard is not None and wizard.verify_password(form.password.data):
login_user(wizard)
return redirect(url_for('wizard_list_presentations'))
return render_template('wizard/sign_in.html', form=form, no_nav=True)
@app.route('/sign-out/')
@login_required
def sign_out():
logout_user()
return redirect(url_for('sign_in'))
@app.route('/<presentation_slug>/vote/<decision_slug>/', methods=['GET'])
presentations = Presentation.query.filter_by(slug=presentation_slug)
presentation = presentations.first()
251
decision = Decision.query.filter_by(presentation=presentation.id,
slug=decision_slug).first()
decision=decision)
@app.route('/<presentation_slug>/vote/<decision_slug>/<choice_slug>/',
methods=['GET'])
presentations = Presentation.query.filter_by(slug=presentation_slug)
presentation = presentations.first()
decision = Decision.query.filter_by(presentation=presentation.id,
slug=decision_slug).first()
if decision:
votes = redis_db.get(choice_slug)
choice=choice_slug)
def broadcast_vote_count(key):
252
total_votes = 0
if redis_db.get(key):
total_votes += int(redis_db.get(key))
total_votes += len(socketio.rooms['/cyoa'][key])
namespace='/cyoa')
The above code creates two new routes for displaying voting choices and decisions
built within the Wizard interface. These decision pages allow an audience member to
vote by clicking one of two buttons on a page and staying on the voting page. Those
votes are calculated just like SMS votes as long as the user stays on the page.
An active websocket connection increments the vote counter for that choice in Redis
and when a user leaves the page the websocket connection is cleaned up and the
Redis value for that choice is decremented. Note however one downside of the web
browser-based voting is that the websocket connection may not be immediately
recognized as closed. It can take a few seconds before the websocket is cleaned up
and the vote counter decremented.
from . import socketio
253
@socketio.on('connect', namespace='/cyoa')
def ws_connect():
pass
@socketio.on('disconnect', namespace='/cyoa')
def ws_disconnect():
pass
@socketio.on('join', namespace='/cyoa')
def on_join(data):
vote = data['vote']
join_room(vote)
broadcast_vote_count(vote)
The above code handles the websockets connections and determines the decision
chosen by a user.
There’s one small change to the Wizard decisions page that’ll make our lives easier.
We want to be able to immediately view a decision after it’s been created. To
accomplish this task, edit the cyoa/cyoa/templates/decisions.html file and update the
following single highlighted line.
{% extends "base.html" %}
{% block nav %}
{% include "nav.html" %}
{% endblock %}
{% block content %}
<div class="container">
254
<div class="row">
<div class="col-md-10">
<h1>{{ presentation.name }} Decisions</h1>
{% if decisions|length == 0 %}
No web browser voting enabled for
<em>{{ presentation.name }}</em>.
<a href="{{ url_for('wizard_new_decision', pres_id=presentation.id) }}">Add a
decision point for this presentation</a>.
{% else %}
<table class="table">
<thead>
<tr>
<th>Decision</th>
<th>First story path</th>
<th>Second story path</th>
<th>Delete</th>
<th>View choice</th>
</tr>
</thead>
<tbody>
{% for d in decisions %}
<tr>
<td><a href="{{ url_for('wizard_edit_decision',
presentation_id=presentation.id, decision_id=d.id) }}">{{ d.slug }}</a></td>
<td>{{ d.first_path_slug }}</td>
<td>{{ d.second_path_slug }}</td>
<td><a href="{{ url_for('wizard_delete_decision',
pres_id=presentation.id, decision_id=d.id) }}" class="btn btn-danger">X</a></td>
td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div class="btn-top-margin">
<a href="{{ url_for('wizard_new_decision', pres_id=presentation.id) }}" class="btn
btn-primary">Add decision point</a>
</div>
255
</div>
</div>
</div>
{% endblock %}
The above one line change in decisions.html allows a wizard to view the decision she’s
created by opening a new browser window with the decision.
We’re almost there – just one more template file to build! Create one more new
template named cyoa/cyoa/templates/web_vote.html with the following contents.
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6">
<h2>You've chosen <em>{{ choice }}</em>. Stay on
this page until all the votes are counted.</h2>
<h1><span id="vote-counter"></span> votes for
{{ choice }}.</h1>
</div>
</div>
{% endblock %}
{% block js_body %}
<script type="text/javascript"
src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/
socket.io.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
namespace = '/cyoa';
var websocket = io.connect('http://' + document.domain + ':'
location.port + namespace);
websocket.emit('join', {'vote': '{{ choice }}'});
websocket.on('msg', function(msg) {
var voteCounter = $('#vote-counter').html(msg.val);
256
});
});
</script>
{% endblock %}
Let’s give our application a final spin to see the web browser-based voting in action.
Run the development server with the following command as we’ve performed in
previous blog posts.
(cyoa)$ python manage.py runserver
Add a new presentation based with the following data or based on a presentation
you’ve already built.
257
Next create a decision point using the new code we wrote in this tutorial. This decision
will allow the audience to vote with their web browser. Create a decision like the
following and save it.
258
Click the link in the rightmost column to view the new decision.
Select one of the two options on the screen and your vote will be tallied along with
any other browser and SMS-based votes.
259
You’re voting for this choice via websocket just by staying on this page! This type of
voting is a huge help for when cellular service doesn’t work in a room or you’re giving
a presentation internationally where many folks don’t have an active cell phone plan,
such as with PyCon 2015 in Canada.
Now we’ve got the ability to vote both with SMS and web browsing in our Choose
Your Own Adventure presentations! If there was an issue you ran into along the way
that you couldn’t figure out remember that there is a tutorial-step-6 tag that contains
the finished code for this blog post.
Let me know what new Choose Your Own Adventure presentation stories you come
up with or open a pull request to improve the code base. Contact me via:
• Email: makai@twilio.com
260
• Twitter: @mattmakai
261