Link Search Menu Expand Document

Week 12: Conclusion

Homework

Today, we put together a bit of the last few weeks to show what a full workflow looks like. We’ll be building a website using Python Flask and deploying to AWS.

What we’re doing is not industry-standard, but it’s a good foundation for everything that is done in industry. This means you shouldn’t go about doing this for a professional project, but it’s quite nice for your own projects to learn what works and what doesn’t. In industry, everything will build on top of these concepts (and therefore be a lot more complicated), but we hope that having this foundation will allow you to understand which pieces go where.

If you’re curious, read the footnotes. I’ve tried to note areas that are nonstandard.

First, Some Theory

In our networks week, we talked about how computers are wired together. Today, we’ll simplify that by only considering the client and server. Since we’ll be making a simple web application, the client will be accessing port 80 on the server:

┌──────┐
│client├─┐     ┌───────────┐
├──────┤ │     │           │
│client├─┴┬───►│:80 server │
├──────┤  │    │           │
│client├──┘    └───────────┘
└──────┘

So far, we have also explored the concept of pushing code to a central repository (ie. Github) to collaborate with others:

┌───┐
│dev├─┐     ┌───────────┐
├───┤ │     │           │
│dev├─┴┬───►│:22 git    │
├───┤  │    │           │
│dev├──┘    └───────────┘
└───┘

But how do we get code on Github to a server somewhere so that others can talk to it? The solution is some kind of checkout mechanism. There are various ways organizations do this, but I’ll illustrate one of the more common ones:

  • Work on a codebase typically happens on a branch. (for example, feature/implement-hello-world)
  • At some point, this work is completed and is merged into the main branch.
  • At this point, when a new commit is added to main, it is autodeployed to a server.

This last step usually consists of a server hook on the Git side that triggers the server to redeploy itself. To explore this, we’ll be doing the deployment manually first to understand the process, and then we’ll explore automated options. At the end, it will look something like this:

┌──────┐                                            ┌───┐
│client├─┐     ┌───────────┐     ┌────────┐       ┌─┤dev│
├──────┤ │     │           │     │        │       │ ├───┤
│client├─┴┬───►│:80 server │◄─── │ git :22│ ◄────┬┴─┤dev│
├──────┤  │    │           │     │        │      │  ├───┤
│client├──┘    └───────────┘     └────────┘      └──┤dev│
└──────┘                                            └───┘

To make our life easier, we’ll be making our own git server on the web server. This means it will end up looking like this:

┌──────┐                                           ┌───┐
│client├─┐     ┌─────────────────────────┐       ┌─┤dev│
├──────┤ │     │                         │       │ ├───┤
│client├─┴┬───►│:80 server  ◄───  git :22│ ◄────┬┴─┤dev│
├──────┤  │    │                         │      │  ├───┤
│client├──┘    └─────────────────────────┘      └──┤dev│
└──────┘                                           └───┘

Creating Our Flask Application

In app.py, put the following content. This starts a server that returns “Hello World!” when visited.

from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

In your terminal, run the following to start the server:

FLASK_APP=app flask run

When you go to localhost:5000 (or whatever the link says in your terminal), you should see Hello, World!.

(Manually) Deploying Flask Application With EC2

Creating an EC2 Instance

Launch an EC2 instance with the following configuration (all stock options unless otherwise noted):

  • AMI: Amazon Linux 2 (should be default option)
  • Key pair: be sure to create one so you can log in later. This should download something like cs198.pem.
  • Network settings: Allow traffic from all of SSH, HTTPS, HTTP

When your instance is provisioned, you should see a Public IPv4 DNS. You should be able to SSH in via the following command:

ssh -i cs198.pem ec2-user@<YOUR_PUBLIC_IPV4_DNS>

Let’s change it so that we can SSH in with our own machine’s private keys. Append the line in ~/.ssh/id_rsa.pub (on your local machine) to ~/.ssh/authorized_keys on the EC2 instance.

Now you should be able to SSH in directly:

ssh ec2-user@<YOUR_PUBLIC_IPV4_DNS>

To make our lives easier, let’s add this config to our ~/.ssh/config to remember this configuration. Add the following to that file so you can SSH in later with just ssh ec2:

Host ec2
	Hostname <YOUR_PUBLIC_IPV4_DNS>
	User ec2-user

Installing Dependencies

On the EC2 instance, execute the following directions to install dependencies:

  1. sudo yum install git
  2. pip3 install flask gunicorn

Setting Up a Git Server

Now we will set up a Git server on the EC2 instance so we can push our code. 1

  1. Make a new folder that will house the code and cd into it:
     mkdir git_server
     cd git_server
    
  2. Create a bare git repository (that acts as the master):
     git init --bare
    

And that’s it!

Pushing to the Git Server

Now we go back to our local machine and push our code to the remote server.

  1. First, we initalize our git repo and add the commits. (You should know how to do this by now!)
  2. Then, we add the remote SSH server as a git remote:
     git remote add origin ec2:git_server
    
  3. Now we should be able to push our code!
     git push --set-upstream origin master
    

The code has been pushed to EC2!

Manually Checking Out Code and Deploying

Now, we manually clone the code on the remote server and start up Flask to see if it all works. On the remote server:

  1. Clone the server code directly to a new folder:
     git clone git_server website
    

    You should now have a folder website that should contain your app.py!

  2. Start the Flask instance:
     FLASK_APP=app flask run --host=0.0.0.0 --port=8080
    

    Here, we need to add the --host=0.0.0.0 flag since we want to allow people from anywhere to connect.

  3. Before we can connect, we’ll need to mess with some port settings. Here, we choose to redirect all web traffic (going to port 80) to port 8080, and we’ll run Flask listening on port 80802.
     sudo iptables -A INPUT -i eth0 -p tcp --dport 80 -j ACCEPT
     sudo iptables -A INPUT -i eth0 -p tcp --dport 8080 -j ACCEPT
     sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
    
  4. Now you should be able to see your website at <YOUR_PUBLIC_IPV4_DNS>! (Go to your browser and put that link in your browser directly!)

Quick Recap

A quick breather to make sure we’re all on the same page; that was a lot!

  1. We created a Git Server
  2. We add the server from our local repository, and we pushed our code
  3. We checked out the code on the server in another folder.
  4. We started up the Flask server.

New Feature + Autodeployment

Autodeployment

Ok, that was quite a lot of work to deploy the application. It would be a pain if we had to this incantation every time we change something in our code! Let’s automate that so that we can just push our code to the server and have Flask automatically restart itself.

Before we start this, make sure Flask is stopped.

  1. On the server, we’ll make a systemd service that we can start and stop. (Think of this as a job manager.) We’ll also use a ~ production-grade ~ server for Python with gunicorn3. In the file /etc/systemd/system/website.service, put the following content:
    [Unit]
    Description=Flask website
    After=network.target
    [Service]
    Type=simple
    Restart=always
    RestartSec=1
    User=ec2-user
    WorkingDirectory=/home/ec2-user/website
    ExecStart=/usr/local/bin/gunicorn app:app -p 0.0.0.0:8080
    
    [Install]
    WantedBy=multi-user.target
    
  2. Reload systemd:
     sudo systemctl daemon-reload
    
  3. Enable and start the service
     sudo systemctl enable website.service
     sudo systemctl start website.service
    
  4. You should be able to visit your website again!

Notably, we can now do the following:

  1. Change code on our server
  2. Restart it with
     sudo systemctl restart website.service
    

which we’ll automate after every change to the master branch.

Autodeploying On Master Change

Now, we’ll make changes to the master branch autodeploy. To do this, we will take advantage of git hooks. In git_server/hooks/post-receive, put the following:

#!/bin/bash
TARGET="/home/ec2-user/website/"
GIT_DIR="/home/ec2-user/git_server/"
BRANCH="master"

while read oldrev newrev ref
do
        # only checking out the master (or whatever branch you would like to deploy)
        if [ "$ref" = "refs/heads/$BRANCH" ];
        then
                echo "Ref $ref received. Deploying ${BRANCH} branch to production..."
                unset GIT_DIR
                cd $TARGET && git pull
                sudo /bin/systemctl restart website
        else
                echo "Ref $ref received. Doing nothing: only the ${BRANCH} branch may be deployed on this server."
        fi
done

This will

  1. git pull your code to website, and
  2. Restart the server.

If you try this now, it won’t entirely work! This is because systemctl requires root privileges. To fix this, we make a quick edit to our sudoers file to not require a password when we use sudo for restarting the website. Execute sudo visudo, scroll to the bottom, and change the file so it looks like this:

...
%wheel  ALL=(ALL)       ALL

%ec2-user ALL= NOPASSWD: /bin/systemctl restart website

## Same thing without a password
...

(Note the new line in the middle.)

Now our server should deploy itself!

Adding a New Feature

Time to see if this thing actually works. In your local computer, make a change to the code. (For example, change “Hello, World” to “Goodbye, World”.) Now, git commit and push this, and reload the website. It should be live!

Footnotes

  1. In a real-world scenario, you would deploy to somewhere like Github and have your application pull code from Github. We’re doing this to save time. Please don’t do this in a day job! 

  2. Why? Essentially, only privileged applications (ie. with root) can bind on port 80, the port that serves websites. This means that we have two options: run Flask as root, or redirect traffic from port 80 to some other port. We choose to do the second method, redirecting all traffic to port 8080

  3. The essential change here is that gunicorn will create worker processes that handle client requests, allowing us to ~ scale ~. If you’re curious, read more about it on their website. For our purposes, just think of it as making our life a bit easier.