├── .gitignore ├── README.md ├── containers ├── app │ ├── Dockerfile │ ├── blog │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── forms.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── static │ │ │ └── css │ │ │ │ └── blog.css │ │ ├── templates │ │ │ └── blog │ │ │ │ ├── base.html │ │ │ │ ├── post_detail.html │ │ │ │ ├── post_edit.html │ │ │ │ └── post_list.html │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── manage.py │ ├── mysite │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── requirements.txt └── database │ ├── Dockerfile │ └── docker-entrypoint.sh └── kubernetes ├── app ├── replication-controller-maroon.yaml ├── replication-controller-orange.yaml └── service.yaml └── database ├── persistent-volume-claim.yaml ├── persistent-volume.yaml ├── replication-controller.yaml └── service.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | /venv 2 | db.sqlite3 3 | containers/app/static 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scalable and resilient Django with Kubernetes 2 | 3 | This repository contains code and notes to get a sample Django 4 | application running on a Kubernetes cluster. It is meant to go along 5 | with a [related blog post][blog-post] that provides more context and 6 | explains some of the theory behind the steps that follow. 7 | 8 | ## Preliminary steps 9 | 10 | 1. Fetch the source code for this example. 11 | ```` 12 | git clone https://github.com/hnarayanan/kubernetes-django.git 13 | ```` 14 | 15 | 2. [Install Docker][docker-install]. 16 | 17 | 3. Take a look at and get a feel for the [example Django 18 | application][example-app] used in this repository. It is a simple blog 19 | that’s built following the excellent [Django Girls 20 | Tutorial][django-girls-tutorial]. 21 | 22 | 4. [Setup a cluster managed by Kubernetes][kubernetes-install]. The 23 | effort required to do this can be substantial, so one easy way to get 24 | started is to sign up (for free) on Google Cloud Platform and use a 25 | managed version of Kubernetes called [Google Container Engine][GKE] 26 | (GKE). 27 | 28 | 1. Create an account on Google Cloud Platform and update your 29 | billing information. 30 | 31 | 2. Install the [command line interface][gcp-sdk]. 32 | 33 | 3. Create a project (that we'll refer to henceforth as 34 | `$GCP_PROJECT`) using the web interface. 35 | 36 | 4. Now, we're ready to set some basic configuration. 37 | ```` 38 | gcloud config set project $GCP_PROJECT 39 | gcloud config set compute/zone europe-west1-d 40 | ```` 41 | 42 | 5. Then we create the cluster itself. 43 | ```` 44 | gcloud container clusters create demo 45 | gcloud container clusters list 46 | ```` 47 | 48 | 6. Finally, we configure `kubectl` to talk to the cluster. 49 | ```` 50 | gcloud container clusters get-credentials demo 51 | kubectl get nodes 52 | ```` 53 | 54 | ## Create and publish Docker containers 55 | 56 | For this example, we'll be using [Docker Hub](https://hub.docker.com/) 57 | to host and deliver our containers. And since we're not working with 58 | any sensitive information, we'll expose these containers to the 59 | public. 60 | 61 | ### PostgreSQL 62 | 63 | Build the container, remembering to use your own username on Docker 64 | Hub instead of `hnarayanan`: 65 | 66 | ```` 67 | cd containers/database 68 | docker build -t hnarayanan/postgresql:9.5 . 69 | ```` 70 | 71 | You can check it out locally if you want: 72 | 73 | ```` 74 | docker run --name database -e POSTGRES_DB=app_db -e POSTGRES_PASSWORD=app_db_pw -e POSTGRES_USER=app_db_user -d hnarayanan/postgresql:9.5 75 | # Echoes $PROCESS_ID to the screen 76 | docker exec -i -t $PROCESS_ID bash 77 | ```` 78 | 79 | Push it to a repository: 80 | 81 | ```` 82 | docker login 83 | docker push hnarayanan/postgresql:9.5 84 | ```` 85 | 86 | ### Django app running within Gunicorn 87 | 88 | Build the container: 89 | 90 | ```` 91 | cd containers/app 92 | docker build -t hnarayanan/djangogirls-app:1.2-orange . 93 | ```` 94 | 95 | Push it to a repository: 96 | 97 | ```` 98 | docker push hnarayanan/djangogirls-app:1.2-orange 99 | ```` 100 | 101 | We're going to see how to perform rolling updates later in this 102 | example. For this, let's create an alternative version of our app that 103 | simply has a different header colour, build a new container app and 104 | push that too to the container repository. 105 | 106 | ```` 107 | cd containers/app 108 | emacs blog/templates/blog/base.html 109 | 110 | # Add the following just before the closing tag 111 | 116 | 117 | docker build -t hnarayanan/djangogirls-app:1.2-maroon . 118 | docker push hnarayanan/djangogirls-app:1.2-maroon 119 | ```` 120 | 121 | ## Deploy these containers to the Kubernetes cluster 122 | 123 | ### PostgreSQL 124 | 125 | Even though our application only requires a single PostgreSQL instance 126 | running, we still run it under a (pod) replication controller. This 127 | way, we have a service that monitors our database pod and ensures that 128 | one instance is running even if something weird happens, such as the 129 | underlying node fails. 130 | 131 | ```` 132 | cd kubernetes/database 133 | kubectl create -f replication-controller.yaml 134 | 135 | kubectl get rc 136 | kubectl get pods 137 | 138 | kubectl describe pod 139 | kubectl logs 140 | ```` 141 | 142 | Now we start a service to point to the pod. 143 | 144 | ```` 145 | cd kubernetes/database 146 | kubectl create -f service.yaml 147 | 148 | kubectl get svc 149 | kubectl describe svc database 150 | ```` 151 | 152 | ### Django app running within Gunicorn 153 | 154 | We begin with three app pods (copies of the orange app container) 155 | talking to the single database. 156 | 157 | ```` 158 | cd kubernetes/app 159 | kubectl create -f replication-controller-orange.yaml 160 | kubectl get pods 161 | 162 | kubectl describe pod 163 | kubectl logs 164 | ```` 165 | 166 | Then we start a service to point to the pod. This is a load-balancer 167 | with an external IP so we can access the site. 168 | 169 | ```` 170 | cd kubernetes/app 171 | kubectl create -f service.yaml 172 | kubectl get svc 173 | ```` 174 | 175 | Before we access the website using the external IP presented by 176 | `kubectl get svc`, we need to do a few things: 177 | 178 | 1. Perform initial migrations: 179 | ```` 180 | kubectl exec -- python /app/manage.py migrate 181 | ```` 182 | 183 | 2. Create an intial user for the blog: 184 | ```` 185 | kubectl exec -it -- python /app/manage.py createsuperuser 186 | ```` 187 | 188 | 3. Have a CDN host static files since we don't want to use Gunicorn 189 | for serving these. This demo uses Google Cloud storage, but you're 190 | free to use whatever you want. Just make sure `STATIC_URL` in 191 | `containers/app/mysite/settings.py` reflects where the files are. 192 | ```` 193 | gsutil mb gs://demo-assets 194 | gsutil defacl set public-read gs://demo-assets 195 | 196 | cd django-k8s/containers/app 197 | virtualenv --distribute --no-site-packages venv 198 | source venv/bin/activate 199 | pip install Django==1.9.5 200 | export DATABASE_ENGINE='django.db.backends.sqlite3' 201 | ./manage.py collectstatic --noinput 202 | gsutil -m cp -r static/* gs://demo-assets 203 | ```` 204 | 205 | At this point you should be able to load up the website by visiting 206 | the external IP for the app service (obtained by running `kubectl get 207 | svc`) in your browser. 208 | 209 | Go to `http://app-service-external-ip/admin/` to login using the 210 | credentials you setup earlier (while creating a super user), and 211 | return to the site to add some blog posts. Notice that as you refresh 212 | the site, the name of the app pod serving the site changes, while the 213 | content stays the same. 214 | 215 | ## Play around to get a feeling for Kubernetes' API 216 | 217 | Now, suppose your site isn't getting much traffic, you can gracefully 218 | *scale* down the number of running application pods to one. (Similarly 219 | you can increase the number of pods if your traffic starts to grow!) 220 | 221 | ```` 222 | kubectl scale rc app-orange --replicas=1 223 | kubectl get pods 224 | ```` 225 | 226 | You can check *resiliency* by deleting one or more app pods and see it 227 | respawn. 228 | 229 | ```` 230 | kubectl delete pod 231 | kubectl get pods 232 | ```` 233 | 234 | Notice Kubernetes will spin up the appropriate number of pods to match 235 | the last known state of the replication controller. 236 | 237 | Finally, to show how we can migrate from one version of the site to 238 | the next, we'll move from the existing orange version of the 239 | application to another version that's maroon. 240 | 241 | First we scale down the orange version to just one copy: 242 | 243 | ```` 244 | kubectl scale rc app-orange --replicas=1 245 | kubectl get pods 246 | ```` 247 | 248 | Then we spin up some copies of the new maroon version: 249 | 250 | ```` 251 | cd kubernetes/app 252 | kubectl create -f replication-controller-maroon.yaml 253 | kubectl get pods 254 | ```` 255 | 256 | Notice that because the app service is pointing simply to the label 257 | `name: app`, both the one orange and the three maroon apps respond to 258 | http requests to the external IP. 259 | 260 | When you're happy that the maroon version is working, you can spin 261 | down all remaining orange versions, and delete its replication 262 | controller. 263 | 264 | ```` 265 | kubectl scale rc app-orange --replicas=0 266 | kubectl delete rc app-orange 267 | ```` 268 | 269 | ## Cleaning up 270 | 271 | After you're done playing around with this example, remember to 272 | cleanly discard the compute resources we spun up for it. 273 | 274 | ```` 275 | gcloud container clusters delete demo 276 | gsutil -m rm -r gs://demo-assets 277 | ```` 278 | 279 | ## And coming in the future 280 | 281 | Future iterations of this demo will have additional enhancements, such 282 | as using a Persistent Volume for PostgreSQL data and learning to use 283 | Kubernetes' Secrets API to handle secret passwords. Keep an eye on 284 | [the issues for this project][issues] to find out more. And you're 285 | free to help out too! 286 | 287 | [blog-post]: https://harishnarayanan.org/writing/kubernetes-django/ 288 | [docker-install]: https://docs.docker.com/engine/installation/ 289 | [kubernetes-install]: http://kubernetes.io/docs/getting-started-guides/ 290 | [example-app]: https://github.com/hnarayanan/kubernetes-django/tree/master/containers/app 291 | [django-girls-tutorial]: http://tutorial.djangogirls.org 292 | [GKE]: https://cloud.google.com/container-engine/ 293 | [gcp-sdk]: https://cloud.google.com/sdk/ 294 | [issues]: https://github.com/hnarayanan/kubernetes-django/issues 295 | -------------------------------------------------------------------------------- /containers/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5.1 2 | 3 | RUN apt-get -q update && apt-get install -y -q \ 4 | sqlite3 --no-install-recommends \ 5 | && apt-get clean && rm -rf /var/lib/apt/lists/* 6 | 7 | ENV LANG C.UTF-8 8 | 9 | RUN pip install --upgrade pip virtualenv 10 | 11 | RUN virtualenv /venv 12 | ENV VIRTUAL_ENV /venv 13 | ENV PATH /venv/bin:$PATH 14 | 15 | RUN mkdir -p /app 16 | WORKDIR /app 17 | 18 | ADD requirements.txt /app/requirements.txt 19 | RUN pip install --no-cache-dir -r /app/requirements.txt 20 | 21 | ADD . /app 22 | 23 | CMD gunicorn -b :8000 mysite.wsgi 24 | -------------------------------------------------------------------------------- /containers/app/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hnarayanan/kubernetes-django/95f794f164167e6eb66156f58e84149e5a8178b6/containers/app/blog/__init__.py -------------------------------------------------------------------------------- /containers/app/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Post 4 | 5 | 6 | admin.site.register(Post) 7 | -------------------------------------------------------------------------------- /containers/app/blog/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Post 4 | 5 | 6 | class PostForm(forms.ModelForm): 7 | 8 | class Meta: 9 | model = Post 10 | fields = ('title', 'text',) 11 | -------------------------------------------------------------------------------- /containers/app/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.utils.timezone 6 | from django.conf import settings 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Post', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('title', models.CharField(max_length=200)), 21 | ('text', models.TextField()), 22 | ('created_date', models.DateTimeField(default=django.utils.timezone.now)), 23 | ('published_date', models.DateTimeField(null=True, blank=True)), 24 | ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /containers/app/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hnarayanan/kubernetes-django/95f794f164167e6eb66156f58e84149e5a8178b6/containers/app/blog/migrations/__init__.py -------------------------------------------------------------------------------- /containers/app/blog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | 4 | 5 | class Post(models.Model): 6 | 7 | author = models.ForeignKey('auth.User') 8 | title = models.CharField(max_length=200) 9 | text = models.TextField() 10 | created_date = models.DateTimeField( 11 | default=timezone.now) 12 | published_date = models.DateTimeField( 13 | blank=True, null=True) 14 | 15 | def publish(self): 16 | self.published_date = timezone.now() 17 | self.save() 18 | 19 | def __str__(self): 20 | return self.title 21 | -------------------------------------------------------------------------------- /containers/app/blog/static/css/blog.css: -------------------------------------------------------------------------------- 1 | .page-header { 2 | background-color: #ff9400; 3 | margin-top: 0; 4 | padding: 20px 20px 20px 40px; 5 | } 6 | 7 | .page-header h1, .page-header h1 a, .page-header h1 a:visited, .page-header h1 a:active { 8 | color: #ffffff; 9 | font-size: 36pt; 10 | text-decoration: none; 11 | } 12 | 13 | .content { 14 | margin-left: 40px; 15 | } 16 | 17 | h1, h2, h3, h4 { 18 | font-family: 'Lobster', cursive; 19 | } 20 | 21 | .date { 22 | float: right; 23 | color: #828282; 24 | } 25 | 26 | .save { 27 | float: right; 28 | } 29 | 30 | .post-form textarea, .post-form input { 31 | width: 100%; 32 | } 33 | 34 | .top-menu, .top-menu:hover, .top-menu:visited { 35 | color: #ffffff; 36 | float: right; 37 | font-size: 26pt; 38 | margin-right: 20px; 39 | } 40 | 41 | .post { 42 | margin-bottom: 70px; 43 | } 44 | 45 | .post h1 a, .post h1 a:visited { 46 | color: #000000; 47 | } -------------------------------------------------------------------------------- /containers/app/blog/templates/blog/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | Django Girls 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 |
18 |
19 |
20 | {% block content %} 21 | {% endblock %} 22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /containers/app/blog/templates/blog/post_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "blog/base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% if post.published_date %} 6 |
7 | {{ post.published_date }} 8 |
9 | {% endif %} 10 | 11 |

{{ post.title }}

12 |

{{ post.text|linebreaks }}

13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /containers/app/blog/templates/blog/post_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "blog/base.html" %} 2 | 3 | {% block content %} 4 |

New post

5 |
6 | {% csrf_token %} 7 | {{ form.as_p }} 8 | 9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /containers/app/blog/templates/blog/post_list.html: -------------------------------------------------------------------------------- 1 | {% extends "blog/base.html" %} 2 | 3 | {% block content %} 4 | {% for post in posts %} 5 |
6 |
7 | {{ post.published_date }} 8 |
9 |

{{ post.title }}

10 |

{{ post.text|linebreaks }}

11 |
12 | {% endfor %} 13 | {% endblock content %} 14 | -------------------------------------------------------------------------------- /containers/app/blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /containers/app/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = [ 7 | url(r'^$', views.post_list, name='post_list'), 8 | url(r'^post/(?P[0-9]+)/$', views.post_detail, name='post_detail'), 9 | url(r'^post/new/$', views.post_new, name='post_new'), 10 | url(r'^post/(?P[0-9]+)/edit/$', views.post_edit, name='post_edit'), 11 | ] 12 | -------------------------------------------------------------------------------- /containers/app/blog/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404, redirect 2 | from django.utils import timezone 3 | from django.conf import settings 4 | 5 | from .models import Post 6 | from .forms import PostForm 7 | 8 | 9 | def post_list(request): 10 | posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date') 11 | return render(request, 'blog/post_list.html', {'posts': posts, 'pod_name': settings.MY_POD_NAME}) 12 | 13 | def post_detail(request, pk): 14 | post = get_object_or_404(Post, pk=pk) 15 | return render(request, 'blog/post_detail.html', {'post': post, 'pod_name': settings.MY_POD_NAME}) 16 | 17 | def post_new(request): 18 | if request.method == "POST": 19 | form = PostForm(request.POST) 20 | if form.is_valid(): 21 | post = form.save(commit=False) 22 | post.author = request.user 23 | post.published_date = timezone.now() 24 | post.save() 25 | return redirect('blog.views.post_detail', pk=post.pk) 26 | else: 27 | form = PostForm() 28 | return render(request, 'blog/post_edit.html', {'form': form, 'pod_name': settings.MY_POD_NAME}) 29 | 30 | def post_edit(request, pk): 31 | post = get_object_or_404(Post, pk=pk) 32 | if request.method == "POST": 33 | form = PostForm(request.POST, instance=post) 34 | if form.is_valid(): 35 | post = form.save(commit=False) 36 | post.author = request.user 37 | post.published_date = timezone.now() 38 | post.save() 39 | return redirect('blog.views.post_detail', pk=post.pk) 40 | else: 41 | form = PostForm(instance=post) 42 | return render(request, 'blog/post_edit.html', {'form': form, 'pod_name': settings.MY_POD_NAME}) 43 | -------------------------------------------------------------------------------- /containers/app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /containers/app/mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hnarayanan/kubernetes-django/95f794f164167e6eb66156f58e84149e5a8178b6/containers/app/mysite/__init__.py -------------------------------------------------------------------------------- /containers/app/mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'pnen6oa247)zwqwx3sui^ng)&%m#*a!ug0+ir1f=%n(s=#ux1x' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'blog', 41 | ) 42 | 43 | MIDDLEWARE_CLASSES = ( 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | 'django.middleware.security.SecurityMiddleware', 52 | ) 53 | 54 | ROOT_URLCONF = 'mysite.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'mysite.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': os.getenv('DATABASE_ENGINE', 'django.db.backends.postgresql_psycopg2'), 81 | 'NAME': os.getenv('DATABASE_NAME', 'db'), 82 | 'USER': os.getenv('DATABASE_USER', 'user'), 83 | 'PASSWORD': os.getenv('DATABASE_PASSWORD', 'password'), 84 | 'HOST': os.getenv('DATABASE_SERVICE_HOST', '127.0.0.1'), 85 | 'PORT': os.getenv('DATABASE_SERVICE_PORT', 5432) 86 | } 87 | } 88 | 89 | # Internationalization 90 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 91 | 92 | LANGUAGE_CODE = 'en-us' 93 | 94 | TIME_ZONE = 'Europe/London' 95 | 96 | USE_I18N = True 97 | 98 | USE_L10N = True 99 | 100 | USE_TZ = True 101 | 102 | 103 | # Static files (CSS, JavaScript, Images) 104 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 105 | 106 | STATIC_URL = 'https://storage.googleapis.com/demo-assets/' 107 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 108 | 109 | MY_POD_NAME = os.getenv('MY_POD_NAME', 'local') 110 | MY_POD_NAMESPACE = os.getenv('MY_POD_NAMESPACE', 'local') 111 | MY_POD_IP = os.getenv('MY_POD_IP', 'localhost') 112 | -------------------------------------------------------------------------------- /containers/app/mysite/urls.py: -------------------------------------------------------------------------------- 1 | """mysite URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import include, url 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', include(admin.site.urls)), 21 | url(r'', include('blog.urls')), 22 | ] 23 | -------------------------------------------------------------------------------- /containers/app/mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /containers/app/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.9.5 2 | gunicorn==19.4.5 3 | psycopg2==2.6.1 4 | wheel==0.24.0 5 | -------------------------------------------------------------------------------- /containers/database/Dockerfile: -------------------------------------------------------------------------------- 1 | # vim:set ft=dockerfile: 2 | FROM debian:jessie 3 | 4 | # explicitly set user/group IDs 5 | RUN groupadd -r postgres --gid=999 && useradd -r -g postgres --uid=999 postgres 6 | 7 | # grab gosu for easy step-down from root 8 | ENV GOSU_VERSION 1.7 9 | RUN set -x \ 10 | && apt-get update && apt-get install -y --no-install-recommends ca-certificates wget && rm -rf /var/lib/apt/lists/* \ 11 | && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" \ 12 | && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" \ 13 | && export GNUPGHOME="$(mktemp -d)" \ 14 | && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ 15 | && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \ 16 | && rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc \ 17 | && chmod +x /usr/local/bin/gosu \ 18 | && gosu nobody true \ 19 | && apt-get purge -y --auto-remove ca-certificates wget 20 | 21 | # make the "en_US.UTF-8" locale so postgres will be utf-8 enabled by default 22 | RUN apt-get update && apt-get install -y locales && rm -rf /var/lib/apt/lists/* \ 23 | && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 24 | ENV LANG en_US.utf8 25 | 26 | RUN mkdir /docker-entrypoint-initdb.d 27 | 28 | RUN apt-key adv --keyserver ha.pool.sks-keyservers.net --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 29 | 30 | ENV PG_MAJOR 9.5 31 | 32 | RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list 33 | 34 | RUN apt-get update \ 35 | && apt-get install -y postgresql-common \ 36 | && sed -ri 's/#(create_main_cluster) .*$/\1 = false/' /etc/postgresql-common/createcluster.conf \ 37 | && apt-get install -y \ 38 | postgresql-$PG_MAJOR \ 39 | postgresql-contrib-$PG_MAJOR \ 40 | && rm -rf /var/lib/apt/lists/* 41 | 42 | # make the sample config easier to munge (and "correct by default") 43 | RUN mv -v /usr/share/postgresql/$PG_MAJOR/postgresql.conf.sample /usr/share/postgresql/ \ 44 | && ln -sv ../postgresql.conf.sample /usr/share/postgresql/$PG_MAJOR/ \ 45 | && sed -ri "s!^#?(listen_addresses)\s*=\s*\S+.*!\1 = '*'!" /usr/share/postgresql/postgresql.conf.sample 46 | 47 | RUN mkdir -p /var/run/postgresql && chown -R postgres /var/run/postgresql 48 | 49 | ENV PATH /usr/lib/postgresql/$PG_MAJOR/bin:$PATH 50 | ENV PGDATA /var/lib/postgresql/data 51 | VOLUME /var/lib/postgresql/data 52 | 53 | COPY docker-entrypoint.sh / 54 | 55 | ENTRYPOINT ["/docker-entrypoint.sh"] 56 | 57 | EXPOSE 5432 58 | CMD ["postgres"] 59 | -------------------------------------------------------------------------------- /containers/database/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ "${1:0:1}" = '-' ]; then 5 | set -- postgres "$@" 6 | fi 7 | 8 | if [ "$1" = 'postgres' ]; then 9 | mkdir -p "$PGDATA" 10 | chmod 700 "$PGDATA" 11 | chown -R postgres "$PGDATA" 12 | 13 | chmod g+s /run/postgresql 14 | chown -R postgres /run/postgresql 15 | 16 | # look specifically for PG_VERSION, as it is expected in the DB dir 17 | if [ ! -s "$PGDATA/PG_VERSION" ]; then 18 | eval "gosu postgres initdb $POSTGRES_INITDB_ARGS" 19 | 20 | # check password first so we can output the warning before postgres 21 | # messes it up 22 | if [ "$POSTGRES_PASSWORD" ]; then 23 | pass="PASSWORD '$POSTGRES_PASSWORD'" 24 | authMethod=md5 25 | else 26 | # The - option suppresses leading tabs but *not* spaces. :) 27 | cat >&2 <<-'EOWARN' 28 | **************************************************** 29 | WARNING: No password has been set for the database. 30 | This will allow anyone with access to the 31 | Postgres port to access your database. In 32 | Docker's default configuration, this is 33 | effectively any other container on the same 34 | system. 35 | 36 | Use "-e POSTGRES_PASSWORD=password" to set 37 | it in "docker run". 38 | **************************************************** 39 | EOWARN 40 | 41 | pass= 42 | authMethod=trust 43 | fi 44 | 45 | { echo; echo "host all all 0.0.0.0/0 $authMethod"; } >> "$PGDATA/pg_hba.conf" 46 | 47 | # internal start of server in order to allow set-up using psql-client 48 | # does not listen on external TCP/IP and waits until start finishes 49 | gosu postgres pg_ctl -D "$PGDATA" \ 50 | -o "-c listen_addresses='localhost'" \ 51 | -w start 52 | 53 | : ${POSTGRES_USER:=postgres} 54 | : ${POSTGRES_DB:=$POSTGRES_USER} 55 | export POSTGRES_USER POSTGRES_DB 56 | 57 | psql=( psql -v ON_ERROR_STOP=1 ) 58 | 59 | if [ "$POSTGRES_DB" != 'postgres' ]; then 60 | "${psql[@]}" --username postgres <<-EOSQL 61 | CREATE DATABASE "$POSTGRES_DB" ; 62 | EOSQL 63 | echo 64 | fi 65 | 66 | if [ "$POSTGRES_USER" = 'postgres' ]; then 67 | op='ALTER' 68 | else 69 | op='CREATE' 70 | fi 71 | "${psql[@]}" --username postgres <<-EOSQL 72 | $op USER "$POSTGRES_USER" WITH SUPERUSER $pass ; 73 | EOSQL 74 | echo 75 | 76 | psql+=( --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" ) 77 | 78 | echo 79 | for f in /docker-entrypoint-initdb.d/*; do 80 | case "$f" in 81 | *.sh) echo "$0: running $f"; . "$f" ;; 82 | *.sql) echo "$0: running $f"; "${psql[@]}" < "$f"; echo ;; 83 | *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${psql[@]}"; echo ;; 84 | *) echo "$0: ignoring $f" ;; 85 | esac 86 | echo 87 | done 88 | 89 | gosu postgres pg_ctl -D "$PGDATA" -m fast -w stop 90 | 91 | echo 92 | echo 'PostgreSQL init process complete; ready for start up.' 93 | echo 94 | fi 95 | 96 | exec gosu postgres "$@" 97 | fi 98 | 99 | exec "$@" 100 | -------------------------------------------------------------------------------- /kubernetes/app/replication-controller-maroon.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: app-maroon 5 | labels: 6 | name: app 7 | spec: 8 | replicas: 3 9 | selector: 10 | name: app 11 | colour: maroon 12 | template: 13 | metadata: 14 | labels: 15 | name: app 16 | colour: maroon 17 | spec: 18 | containers: 19 | - name: app 20 | image: hnarayanan/djangogirls-app:1.2-maroon 21 | env: 22 | - name: MY_POD_NAME 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.name 26 | - name: MY_POD_NAMESPACE 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.namespace 30 | - name: MY_POD_IP 31 | valueFrom: 32 | fieldRef: 33 | fieldPath: status.podIP 34 | - name: DATABASE_NAME 35 | value: app_db 36 | - name: DATABASE_USER 37 | value: app_db_user 38 | - name: DATABASE_PASSWORD 39 | value: app_db_pw 40 | ports: 41 | - containerPort: 8000 42 | -------------------------------------------------------------------------------- /kubernetes/app/replication-controller-orange.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: app-orange 5 | labels: 6 | name: app 7 | spec: 8 | replicas: 3 9 | selector: 10 | name: app 11 | colour: orange 12 | template: 13 | metadata: 14 | labels: 15 | name: app 16 | colour: orange 17 | spec: 18 | containers: 19 | - name: app 20 | image: hnarayanan/djangogirls-app:1.2-orange 21 | env: 22 | - name: MY_POD_NAME 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.name 26 | - name: MY_POD_NAMESPACE 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.namespace 30 | - name: MY_POD_IP 31 | valueFrom: 32 | fieldRef: 33 | fieldPath: status.podIP 34 | - name: DATABASE_NAME 35 | value: app_db 36 | - name: DATABASE_USER 37 | value: app_db_user 38 | - name: DATABASE_PASSWORD 39 | value: app_db_pw 40 | ports: 41 | - containerPort: 8000 42 | -------------------------------------------------------------------------------- /kubernetes/app/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: app 5 | labels: 6 | name: app 7 | spec: 8 | ports: 9 | - port: 80 10 | targetPort: 8000 11 | selector: 12 | name: app 13 | type: LoadBalancer 14 | -------------------------------------------------------------------------------- /kubernetes/database/persistent-volume-claim.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: pg-data-claim 5 | labels: 6 | name: pg-data-claim 7 | spec: 8 | accessModes: 9 | - ReadWriteOnce 10 | resources: 11 | requests: 12 | storage: 50Gi -------------------------------------------------------------------------------- /kubernetes/database/persistent-volume.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: pg-data-disk 5 | labels: 6 | name: pg-data-disk 7 | spec: 8 | capacity: 9 | storage: 50Gi 10 | accessModes: 11 | - ReadWriteOnce 12 | persistentVolumeReclaimPolicy: Retain 13 | gcePersistentDisk: 14 | pdName: pg-data-disk 15 | fsType: ext4 16 | -------------------------------------------------------------------------------- /kubernetes/database/replication-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: database 5 | labels: 6 | name: database 7 | spec: 8 | replicas: 1 9 | selector: 10 | name: database 11 | template: 12 | metadata: 13 | labels: 14 | name: database 15 | spec: 16 | containers: 17 | - name: postgres 18 | image: hnarayanan/postgresql:9.5 19 | env: 20 | - name: POSTGRES_DB 21 | value: app_db 22 | - name: PGDATA 23 | value: /var/lib/postgresql/data/pgdata 24 | # TODO: Use Kubernetes Secret objects to hold the following credentials 25 | - name: POSTGRES_USER 26 | value: app_db_user 27 | - name: POSTGRES_PASSWORD 28 | value: app_db_pw 29 | ports: 30 | - containerPort: 5432 31 | # TODO: User persistent disks, volumes and claims for the following 32 | # volumeMounts: 33 | # - mountPath: /var/lib/postgresql/data 34 | # name: pg-data 35 | # volumes: 36 | # - name: pg-data 37 | # persistentVolumeClaim: 38 | # claimName: pg-data-claim 39 | -------------------------------------------------------------------------------- /kubernetes/database/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: database 5 | labels: 6 | name: database 7 | spec: 8 | ports: 9 | - port: 5432 10 | targetPort: 5432 11 | selector: 12 | name: database 13 | --------------------------------------------------------------------------------