Devops

with Django, Docker and Gitlab

whoami

Luke Spademan

  • Student
  • Started A Levels
    • Maths
    • Further maths
    • Computer Science
    • Physics
  • @lukespademan: [twitter, gitlab/hub, etc.]
  • gave a talk on micro:bits & RPis last year

Devops?

What is Devops?

DevOps is the combination of cultural philosophies, practices, and tools that increases an organization's ability to deliver applications and services at high velocity: evolving and improving products at a faster pace than organizations using traditional software development and infrastructure management processes. -- AWS

Benefits

  • Saves you time
  • Stops you deploying broken code
  • Satisfying

Docker

What is Docker

Docker is a computer program that performs operating-system-level virtualization, also known as "containerization". It was first released in 2013 and is developed by Docker, Inc. Docker is used to run software packages called "containers". -- Wikipedia

What is Docker

  • Code in a 'virtual machine'
  • Code run anywhere with docker installed regardless of:
    • OS/Platform
    • Other installed software

Example

Python

Python


  pip install pipenv --user
  mkdir project
  cd project
  mkdir src
  cd src
  pipenv python --3.6
  pipenv install django gunicorn psycopg2-binary
  pipenv shell
  django-admin startproject mysite .
            

Python


# src/mysite/settings.py
# ...
try:
  SECRET_KEY
except NameError:
  SECRET_FILE = os.path.join(BASE_DIR, 'secret.txt')
  try:
    SECRET_KEY = open(SECRET_FILE).read().strip()
  except IOError:
    try:
      import random
      SECRET_KEY = ''.join(
      [random.SystemRandom().choice(
      'abcdefghijklmnopqrstuvxyz0123456789!@#$%^&*(-_=+)')
          for i in range(50)]
                        )
        secret = open(SECRET_FILE, "w")
        secret.write(SECRET_KEY)
        secret.close()
    except IOError:
        Exception(
    "Please create a %s file with random characters."
        % SECRET_FILE
                  )

# ...
            

Python


# src/mysite/settings.py
# ...
# SECURITY WARNING: don't run with debug turned on
# in production!
if os.environ.get('DJANGO_DEBUG'):
    print("Debug is enabled.")
    DEBUG = True
    ALLOWED_HOSTS = ["localhost", "127.0.0.1", "*"]
else:
    DEBUG = False
    ALLOWED_HOSTS = ["example.com"]
# ...
            

Python


# src/mysite/settings.py
# ...
if DEBUG == True:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        }
            

Python



else:
    DATABASES = {
        'default': {
          'ENGINE': 'django.db.backends.postgresql_psycopg2',
          'NAME': 'postgres',
          'USER': 'postgres',
          'HOST': 'db',
          'PORT': 5432,
        }
    }
# ...
            

Python


# src/mysite/settings.py
# ...

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static/")

            

Test


cd src
pipenv shell
DJANGO_DEBUG=True python manage.py makemigrations
DJANGO_DEBUG=True python manage.py migrate
DJANGO_DEBUG=True python manage.py runserver
            

localhost:8000

django default screen

Hello, Pycon UK!

hello_world app


python manage.py startapp hello_world

hello_world app


# src/hello_world/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

hello_world app


# src/hello_world/views.py

from django.shortcuts import render

def index(request):
    return render(request, 'hello_world/index.html')

hello_world app


# src/hello_world/views.py

from django.shortcuts import render

def index(request):
    return render(request, 'hello_world/index.html')

hello_world app


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello, PyCon UK!</title>
  </head>
  <body>
    <h1>Hello, PyCon UK!</h1>
  </body>
</html>

hello_world app


# src/mysite/settings.py

...

INSTALLED_APPS = [
    'hello_world',
    'django.contrib.admin',
    'django.contrib.auth',
    # ...
]

...

hello_world app


# src/mysite/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('hello_world.urls')),
]

Docker


# Dockerfile
FROM python:3.6
RUN mkdir /src
ADD src/Pipfile /src
ADD src/Pipfile.lock /src
RUN pip install pipenv
WORKDIR /src
RUN pipenv install --system --deploy --ignore-pipfile
ADD src /src
            

Docker Compose


# docker-compose.yml
version: '3'
services:
  nginx:
    build: Dockerfile.nginx
    container_name: mysite_nginx
    depends_on:
    - web
    ports:
    - "80:8080"
            

Docker Compose



  web:
    build: .
    container_name: mysite_django
    command: bash start_project.sh
    depends_on:
    - db
            

Docker Compose



  db:
    image: postgres:latest
    container_name: mysite_postgres
    volumes:
    - "db:/var/lib/postgresql/data"

volumes:
  db: {}
            

Start Django


# src/start_django.sh
#!/bin/bash

ls
cat Pipfile
pip freeze
python manage.py migrate
python manage.py collectstatic --noinput
gunicorn mysite.wsgi -b 0.0.0.0:8000
            

Dockerfile.nginx


FROM nginx:latest

ADD config/nginx /etc/nginx/conf.d
ADD src /src
EXPOSE 80

Nginx Config


mkdir config
cd config
mkdir nginx
cd nginx
           

Nginx Config


# config/nginx/django.conf
upstream web {
  ip_hash;
  server web:8000;
}
           

Nginx Config


server {
  location /robots.txt {alias /src/static/robots.txt;}
  location /favicon.ico {alias /src/static/favicon.ico;}
  location /static/ {
    autoindex on;
    alias /src/static/;
  }

  location / {
    proxy_pass http://web/;
  }
  listen 8080;
}
           

Test


sudo docker-compose build
sudo docker-compose up
            

localhost

Hello, PyCon UK!


Git

Initializing Repo

git init

.gitignore


# .gitignore
__pycache__
db.sqlite3
secret.txt
            

README.md


# Devops Example
            

Push to repo

gitlab file view

Runner

Runner

  1. Get a server
  2. Setup gitlab Repository
  3. Install gitlab-runner
  4. Register runner

Get a Server

Setup gitlab Repository


curl -L https://packages.gitlab.com/install/repositories/ \
  runner/gitlab-runner/script.deb.sh | sudo bash
            

Install gitlab-runner


sudo apt-get install gitlab-runner
            

Register the Runner


sudo gitlab-runner register
            

This will require a code from your_repo/settings/ci_cd > runners

Runnner settings


# /etc/gitlab-runner/config.toml
...
  volumes = ["cache", "/var/run/docker.sock:/var/run/docker.sock"]
...
            

Login


sudo docker login registry.gitlab.com
            

Start the Runner


sudo gitlab-runner start
            

Start docker swarm


sudo docker swarm init --advertise-addr <server-ip-address>
            

Disable Shared Runners

disableing shared runners

.gitlab-ci.yml


image: tmaier/docker-compose:latest

stages:
  - build
  - test
  - deploy

variables:
  WEB_CONTAINER_IMAGE: registry.gitlab.com/lukespademan/pyconuk18-devops-example:latest
  NGINX_CONTAINER_IMAGE: registry.gitlab.com/lukespademan/pyconuk18-devops-example/nginx:latest

before_script:
  - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
            

.gitlab-ci.yml


test:
  stage: test
  script:
    - docker-compose run web python manage.py test --noinput
            

.gitlab-ci.yml


build:
  stage: build
  script:
    - docker-compose build
    - docker-compose push
    tags:
      - docker
            

.gitlab-ci.yml


deploy:
  stage: deploy
  script:
    - docker stack deploy --compose-file docker-compose.yml stack-name
  tags:
    - docker

  only:
    - master
            

docker-compose.yml


version: '3'

services:
            

docker-compose.yml


services:
  nginx:
    image: registry.gitlab.com/lukespademan/pyconuk18-devops-example/nginx:latest
    build:
        context: .
        dockerfile: Dockerfile.nginx
    container_name: devops_nginx
    depends_on:
    - web
    ports:
    - "80:8080"
            

docker-compose.yml


services:
  ...
  web:
    image: registry.gitlab.com/lukespademan/pyconuk18-devops-example:latest
    build: .
    container_name: mysite_django
    command: bash start_django.sh
    depends_on:
    - db
            

docker-compose.yml


services:
  ...
  db:
    image: postgres:latest
    container_name: devops_postgres
    volumes:
    - "db:/var/lib/postgresql/data"
            

docker-compose.yml


volumes:
  db: {}

            

Push changes then...

gitlab pipelines

Conclusion

Conclusion

  • Reduces deployment issues
  • Makes things easier in the long run
  • Learn about something new

Luke Spademan

lukespademan.com twitter.com/lukespademan gitlab.com/lukespademan github.com/lukespademan lspade.xyz @lukespademan