Deploying a Python application to Openshift is fairly easy. Write a Dockerfile and run oc new-app /path/to/Dockerfile, that's it!! But if you want implement a full fledged modern CI/CD using Jenkins and openshift, you need to do little more than that. So let's dive into it.

We will explain about the whole process in three blog posts:

  1. Part One: We will explain about deployment structure and preparations.
  2. Part Two: We will write the Jenkins Pipeline.
  3. Part Three: Deploy using Jenkins Pipeline to Openshift and Webhook implementation.

In this blog, we will explain the deployment structure and required preparations for this deployment.

Deployment Structure

In this deployment, our python program will be running inside a container. That program will be served through a Gunicorn server. We will reverse proxy this server from NGINX, running in the same container. The following diagram shows our application structure:
Screen-Shot-2018-08-12-at-3.32.20-PM

The Openshift Cluster will have three project spaces, CI/CD, DEV and STAGE. CI/CD will contain the Jenkins file, running pipelines inside it. DEV and STAGE projects will have applications running inside them. Next diagram will show what we intend to do in this CI/CD project:
Screen-Shot-2018-08-12-at-3.28.36-PM

Preparations:

For implementing this CI/CD project, we need to prepare few things beforehand. Let us go through them one by one.

A Python Application

NB: if you have a python application, which runs through gunicorn and has tests runable through nosetests(or any xml test result exporter) then please skip this section.

Writing A Python Application

For this demo purpose, we will use a small Flask application, which exposes a simple REST api. We will be using the code below:

from flask import Flask, jsonify

app = Flask(__name__)

tasks = [
    {
        'id': 1,
        'title': 'Openshift Jenkins Pipeline Python/Nginx Implementation',
        'description': 'Find the implementation at https://github.com/ruddra/openshift-python-nginx'
    },
    {
        'id': 2,
        'title': 'Openshift Jenkins Pipeline Django Implementation',
        'description': 'Find the implementation at https://github.com/ruddra/openshift-django'
    }
]

@app.route('/', methods=['GET'])
def get_tasks():
    return jsonify({'tasks': tasks})

if __name__ == '__main__':
    app.run(host='0.0.0.0')

First we should create a empty Project directory. Inside that, we will put another folder named app, which will contain the python application. Save the above flask application as flask_app.py. We need to install flask using pip install flask as well.

Adding Gunicorn

We can run this application using python flask_app.py, but its not safe to use a development server in production. So we will use gunicorn to run our application. Let's install that using pip install gunicorn, then write a wsgi.py inside the app directory.

from flask_app import app as application

if __name__ == "__main__":
    application.run()

If we run gunicorn --bind 0.0.0.0:5000 wsgi, we should be able to see our project up and running in 0.0.0.0:5000. The output should look like this:
Screen-Shot-2018-08-11-at-9.22.20-PM

Writing Tests

Writing tests are important, and we are going to store test results in Jenkins. For this purpose, let's create some tests inside app directory.

import unittest 

class MyTestClass(unittest.TestCase): 
  @classmethod
  def setUpClass(cls):
      pass 

  @classmethod
  def tearDownClass(cls):
      pass 

  def setUp(self):
      pass 

  def tearDown(self):
      pass 

  def test_dummy_one(self):
      self.assertEqual(2, 2) 

  def test_dummy_two(self):
      self.assertEqual(True, True) 

For running the tests, we are going to use nosetests, so let's install that using pip install nose. We can run tests using nosetests --with-xunit, it will generate a xml file which will contain the test results.

Our preparation is complete for the python application. Now let's store our requirements using pip freeze > requirements.pip.

Preparing NGINX

In this step, we will write a NGINX configuration file which will proxy pass the gunicorn server.

upstream web {  
  ip_hash;
  server 0.0.0.0:5000;
}

server {
    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://web/;
    }
    listen 8081;
    server_name _;
}

In here, the NGINX will listen to 8081 port. It will proxy pass gunicorn at location "/". Let's save the above code in default.conf file and store in config/nginx directory of the project.

Writing Dockerfile

This is the most important preparation step of our project. For this demo purpose, we will be using a alpine based nginx Dockerfile.

FROM nginx:mainline-alpine

# --- Python Installation ---
RUN apk add --no-cache python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    if [ ! -e /usr/bin/pip ]; then ln -s pip3 /usr/bin/pip ; fi && \
    if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi && \
    rm -r /root/.cache

# --- Work Directory ---
WORKDIR /usr/src/app

# --- Python Setup ---
ADD . .
RUN pip install -r app/requirements.pip

# --- Nginx Setup ---
COPY config/nginx/default.conf /etc/nginx/conf.d/
RUN chmod g+rwx /var/cache/nginx /var/run /var/log/nginx
RUN chgrp -R root /var/cache/nginx
RUN sed -i.bak 's/^user/#user/' /etc/nginx/nginx.conf
RUN addgroup nginx root

# --- Expose and CMD ---
EXPOSE 8081
CMD gunicorn --bind 0.0.0.0:5000 wsgi --chdir /usr/src/app/app & nginx -g "daemon off;"

Let's store this Dockerfile in root directory of Project.
In Python Setup section, we are using a minimal python setup with pip enabled. For our demo purpose, this is just enough. If your project has more dependencies, then install them using apk add python3-dev ... etc.
In NGINX Setup section, we have add the nginx default.conf file to /etc/nginx/conf.d/.
In Expose and CMD section, we have exposed 8081 port and we will be running this command when the docker is running: gunicorn --bind 0.0.0.0:5000 wsgi --chdir /usr/src/app/app & nginx -g "daemon off;"

So far, the project directories should be looking like this:

| - Project
| -- app
| |-- flask_app.py
| |-- wsgi.py
| |-- requirements.pip
| -- config
| |-- nginx
| ||-- default.conf
| - Dockerfile

If you want to run this Dockerfile in project root directory, you can do this like:

docker build -t flask-nginx .
docker run -p 8081:8081 flask-nginx

You can access the application in browser through localhost:8081

Preparing Git Repository

Let's create a repository at your github/bitbucket account. Then push this code to that repository.

Preparing Openshift/Minishift

I would recommend using minishif for this demo purpose. I am not sure if this app will run in free RedHat's openshift cloud services. If you own a private Openshift Server, that is fine too. For using minishift, please follow this guideline.

Preparing Jenkins

Once your openshift/minishift is ready, let's log into that system using:

 oc login --server=server_name --username=your_username --password=your_password

Now, we need to create three project spaces, cicd, dev, stage for this demo purpose.

oc new-project dev   --display-name="Dev"
oc new-project stage --display-name="Stage"
oc new-project cicd --display-name="CI/CD"

Very good. Now we are going to use project cicd and install Jenkins there:

oc project cicd
oc new-app jenkins-persistent

Let's expose jenkin's url using:

oc expose svc/jenkins-persistant

FYI: You can do this step using Openshift UI as well.
Screenshot

Now, we need to give jenkins permission to make changes in dev and stage. Let's do that using:

oc policy add-role-to-user edit system:serviceaccount:cicd:jenkins -n dev
oc policy add-role-to-user edit system:serviceaccount:cicd:jenkins -n stage

Our preparation is completed. Let's continue with Jenkins pipeline in next blog.

If you have any questions regarding this, feel free to comment. Thanks for reading.

Cheers!!