# Labo 05 – Microsoft Azure App Service (Containers)
| | |
| -------- | -------- |
| **Opgave** | Jullie zijn intussen allemaal (hopelijk) ietwat vertrouwd met de basisprincipes van Microsoft Azure. Jullie container en Docker kennis werd opgefrist en uitgebreid tijdens de theorieles. <br><br>In dit labo gaan jullie leren hoe je op een efficiënte manier een Docker container kan uitwerken alsook hoe je deze "in de cloud krijgt". |
| **Deadline** | Indienen tegen de start van het volgende labo. |
| **Indienen** | Dien je verslag in via git, kopieer de tekst en sla op als `jouw-naam.md`. |
| | |
## Inhoudsopgave
[TOC]
## Doelstelling
---
De bedoeling van dit labo is **niet** om na te gaan hoe snel je kan lezen, commando’s kan invoeren en de output kan plakken in een document.
De bedoeling van dit labo is **wel** dat je de tijd neemt om rustig op verkenning te gaan en bij te leren over het systeem waarmee we werken. De opgave dient enkel als leidraad voor jouw zoektocht. Maak er dus geen race tegen de klok van.
---
* Uitwerken van een efficiënt Docker image
* Basis handelingen met Docker en Docker Compose kunnen uitvoeren
* Een Docker applicatie in Azure Cloud hosten
* Gebruik maken van Azure App Service
* Gebruik maken van Azure Container Registry
* Gebruiken van git, meer bepaald via GitHub
## Indienen opdracht
**Plaats al je code in de daarvoor voorziene Git repository!** Je kan dit GitHub classroom via volgende link joinen: https://classroom.github.com/a/0_uqwDJf
Vergeet niet regelmatig te committen (en te pushen)!
## Installatie Docker, Docker-Compose en Azure CLI.
Zoals aangehaald in de theorieles, is de installatie van Docker en Docker-compose erg eenvoudig. Je kan de "dev/build" omgeving best opzetten in een lokale virtuele machine (ik ga uit van Ubuntu Server 20.04).
Volg in [deze tutorial](https://docs.docker.com/engine/install/ubuntu/#install-using-the-convenience-script) "Install using the convenience script" om Docker te installeren. Volg daarna de eerste stap in [deze tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-20-04#step-1-%E2%80%94-installing-docker-compose) om Docker-Compose te installeren.
De Azure CLI installeren is eveneens eenvoudig, volg daarvoor [deze tutorial](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt).
## Flask web app
In dit eerste deel ga je een simpele Flask applicatie maken. Je zorgt er vooral dat deze applicatie "production-ready is".
Maak volgende directory structuur:
```
mkdir docker-labo
cd !$
mkdir web
cd !$
```
### Hello world app
Volgend stukje code is alles wat we nodig hebben om een Flask webapplicatie te runnen. Maak dit bestand aan, noem het `main.py`.
```
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello world!'
if __name__ == '__main__':
app.run()
```
### Flask webserver
Flask heeft een zogenaamde [build-in development webserver](http://flask.pocoo.org/docs/1.0/server/). Deze server is prima zolang je aan het ontwikkelen bent, maar is niet bedoeld voor productiedoeleinden. Deze server kan bijvoorbeeld maximaal 1 request tegelijk behandelen.
Bij webapplicaties geschreven in Python is het gebruikelijk om de ["Web Server Gateway Interface"](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) (kortweg WSGI) te implementeren. [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) houdt zich aan de opgegeven conventies. Deze webserver is wel geschikt om in productie te gebruiken. Doorgaans wordt daar echter nog een "echte" webserver voor geplaatst, maar daarover later meer!
> uWSGI wordt meer en meer ingehaald door ASGI. Dit is een wat modernere oplossing, deze kan bv. overweg met asynchrone calls.
> Voel je vrij om met ASGI aan de slag te gaan. Als je dit en al de rest perfect werkend krijgt, geeft dat recht op een bonuspuntje op het examen.
Maak volgend configuratie bestand aan, met als naam `app.ini`. Je hoeft niet elke lijn perfect te begrijpen.
```
[uwsgi]
; See docs: https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html
; Main app
module = main:app
; Enable uWSGI master process
master = true
processes = 4
; uWSGI socket
socket = 0.0.0.0:5000
; Try to remove all of the generated files/sockets (UNIX sockets and pidfiles) upon exit
vacuum = true
; Exit instead of brutal reload on SIGTERM.
die-on-term = true
; Force the specified protocol (uwsgi, http, fastcgi) for default sockets
protocol = http
```
### Docker image builden
Volgend `Dockerfile` is al wat we nodig hebben om een production-ready image te bouwen. Bekijk elke lijn aandachtig, achterhaal elke lijn.
```
FROM python:3.10.0-alpine
WORKDIR '/app'
RUN apk add --no-cache linux-headers g++
RUN pip install Flask
RUN pip install uwsgi
COPY ./ ./
RUN addgroup -S uwsgi && adduser -S uwsgi -G uwsgi
USER uwsgi
CMD ["uwsgi", "--ini", "app.ini"]
```
Omschrijf wat elke lijn precies doet.
> Antwoord
> ...
> ...
Om een image te bekomen, moeten we een zogenaamde build maken. De basis hiervoor is een `Dockerfile`. Met welk commando kan je dit (voer eveneens uit)?
> Antwoord
> ...
> ...
De output zou er ongeveer als volgt moeten uitzien:
```
Sending build context to Docker daemon 4.096kB
Step 1/9 : FROM python:3.10.0-alpine
3.10.0-alpine: Pulling from library/python
a0d0a0d46f8b: Pull complete
ba51967de001: Pull complete
5ed3eaf4d331: Pull complete
8fec21e4ed42: Pull complete
5cb87e8bc1a2: Pull complete
Digest: sha256:78604a29496b7a1bd5ea5c985d69a0928db7ea32fcfbf71bbde3e317fdd9ac5e
Status: Downloaded newer image for python:3.10.0-alpine
---> c9e1987b6bc6
Step 2/9 : WORKDIR '/app'
---> Running in 5e625f3089c7
Removing intermediate container 5e625f3089c7
---> dea0eed8af3b
Step 3/9 : RUN apk add --no-cache linux-headers g++
---> Running in 24b2834d3d4e
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar. gz
(1/15) Installing libgcc (10.3.1_git20210424-r2)
(2/15) Installing libstdc++ (10.3.1_git20210424-r2)
(3/15) Installing binutils (2.35.2-r2)
(4/15) Installing libgomp (10.3.1_git20210424-r2)
(5/15) Installing libatomic (10.3.1_git20210424-r2)
(6/15) Installing libgphobos (10.3.1_git20210424-r2)
(7/15) Installing gmp (6.2.1-r0)
(8/15) Installing isl22 (0.22-r0)
(9/15) Installing mpfr4 (4.1.0-r0)
(10/15) Installing mpc1 (1.2.1-r0)
(11/15) Installing gcc (10.3.1_git20210424-r2)
(12/15) Installing musl-dev (1.2.2-r3)
(13/15) Installing libc-dev (0.7.2-r3)
(14/15) Installing g++ (10.3.1_git20210424-r2)
(15/15) Installing linux-headers (5.10.41-r0)
Executing busybox-1.33.1-r3.trigger
OK: 205 MiB in 51 packages
Removing intermediate container 24b2834d3d4e
---> 633007b911bf
Step 4/9 : RUN pip install Flask
---> Running in 2d96d8ff781e
Collecting Flask
Downloading Flask-2.0.2-py3-none-any.whl (95 kB)
Collecting Jinja2>=3.0
Downloading Jinja2-3.0.2-py3-none-any.whl (133 kB)
Collecting click>=7.1.2
Downloading click-8.0.3-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.0
Downloading itsdangerous-2.0.1-py3-none-any.whl (18 kB)
Collecting Werkzeug>=2.0
Downloading Werkzeug-2.0.2-py3-none-any.whl (288 kB)
Collecting MarkupSafe>=2.0
Downloading MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl (30 kB)
Installing collected packages: MarkupSafe, Werkzeug, Jinja2, itsdangerous, click, Flask
Successfully installed Flask-2.0.2 Jinja2-3.0.2 MarkupSafe-2.0.1 Werkzeug-2.0.2 click-8.0.3 itsdangerous-2.0.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
WARNING: You are using pip version 21.2.4; however, version 21.3.1 is available.
You should consider upgrading via the '/usr/local/bin/python -m pip install --upgrade pip' command.
Removing intermediate container 2d96d8ff781e
---> 96833f8d6497
Step 5/9 : RUN pip install uwsgi
---> Running in 9b93215d1c72
Collecting uwsgi
Downloading uwsgi-2.0.20.tar.gz (804 kB)
Building wheels for collected packages: uwsgi
Building wheel for uwsgi (setup.py): started
Building wheel for uwsgi (setup.py): finished with status 'done'
Created wheel for uwsgi: filename=uWSGI-2.0.20-cp310-cp310-linux_x86_64.whl size=511927 sha256=5dbe03207f257cf138f27e34827ad697501ffd3b5737ddd7613504ebfb99c2bd
Stored in directory: /root/.cache/pip/wheels/06/05/96/5ee3e21875a5cab911fdbb7d8100b24fbd639c55a65b5b6ccb
Successfully built uwsgi
Installing collected packages: uwsgi
Successfully installed uwsgi-2.0.20
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
WARNING: You are using pip version 21.2.4; however, version 21.3.1 is available.
You should consider upgrading via the '/usr/local/bin/python -m pip install --upgrade pip' command.
Removing intermediate container 9b93215d1c72
---> e88472abe151
Step 6/9 : COPY ./ ./
---> 2449ce482b7d
Step 7/9 : RUN addgroup -S uwsgi && adduser -S uwsgi -G uwsgi
---> Running in 55dd1de87bc9
Removing intermediate container 55dd1de87bc9
---> 05929f2582b9
Step 8/9 : USER uwsgi
---> Running in de487e9fa30f
Removing intermediate container de487e9fa30f
---> 9fda076a4af4
Step 9/9 : CMD ["uwsgi", "--ini", "app.ini"]
---> Running in da5ed2c7e405
Removing intermediate container da5ed2c7e405
---> c7eecf619a55
Successfully built c7eecf619a55
```
Met het commando `docker history <image | container id>` krijg je een inzicht uit welke layers het image is opgebouwd (laatste lijn bij build is altijd container id). Voer dit commando uit, hoeveel layers zijn er in totaal? Denk je dat het aantal layers eventueel omlaag kan?
> Antwoord
> ...
> ...
Indien je bij het vorige commando gebruik hebt gemaakt van een `container id` mag je nog even uitzoeken hoe je het image kan taggen. Dat kan op twee manieren. Wat is het voordeel van tagging? Geeft als tag `jouw-naam/web`, je hoeft geen versie op te geven (default wordt dat dan latest).
> Antwoord
> ...
> ...
### Docker container starten
Je kan nu een container starten op basis van het gemaakte image. Dit kan met `docker run -p 5000:5000 -it <image>`
Wat doet de `-p` parameter, en de `-it` parameter (kan ook geschreven worden als `-i -t`)?
> Antwoord
> ...
> ...
Surf naar je applicatie, als alles goed gaat zie je `Hello world` verschijnen in je browser. Bekijk zeker ook is de output op je terminal.
### Werken met package installers
Het gebruikte `Dockerfile` is ver van perfect opgesteld. Je hebt wellicht zelf al wat pijnpunten gevonden.
De uitgewerkte applicatie is op deze moment nog erg eenvoudig, m.a.w. nog zeer weinig dependencies zijn vereist. Indien je deze applicatie verder gaat uitwerken, ga je (als je dezelfde manier van opbouwen gebruikt) ontzettend veel `RUN pip install <package>` commando's schrijven. Uiteraard, zou je die verschillende RUN commando's kunnen samenvoegen.
Door het samenvoegen van de `RUN`'s zal het aantal layers gereduceerd worden, en bijgevolg ook de build-time evenals de start-time van je container. Anderzijds, hoe meer je in een layer steekt, hoe minder herbruikbaar die is. Er is dus geen "perfecte" oplossing.
In ieder geval, wanneer je gebruik maakt van een hele hoop dependencies zal het Dockerfile sowieso erg slordig, moeilijk leesbaar worden. Een best practice is om je vereiste dependencies in een text-file op te nemen en via een package manager (bv. pip, npm, composer, ...) deze te installeren.
Maak een bestand `requirements.txt` aan met volgende inhoud:
```
Flask==2.0.2
uWSGI==2.0.20
```
Je Dockerfile kan je aanpassen als volgt:
```
FROM python:3.10.0-alpine
WORKDIR '/app'
RUN apk add --no-cache linux-headers g++
COPY ./ ./
RUN pip install -r requirements.txt
RUN addgroup -S uwsgi && adduser -S uwsgi -G uwsgi
USER uwsgi
CMD ["uwsgi", "--ini", "app.ini"]
```
Rebuild je image, en run je container ter verificatie. Pas daarna je `main.py` file aan, voeg volgend stukje code toe.
```
@app.route('/about')
def about():
return 'This app is powered by Docker!'
```
Rebuild opnieuw je image, run je container en controleer of je kan surfen naar je about page. Gaat het builden snel?
> Antwoord
> ...
> ...
Pas het Dockerfile aan om dit efficiënter te maken (tip: theorieles!). Rebuild daarna opnieuw je image.
> Antwoord
> ...
> ...
Pas opnieuw `main.py` aan, voeg volgende toe:
```
@app.route('/info')
def info():
return 'Some important information!'
```
Rebuild opnieuw, dit zou nu hoogstens een paar seconden in beslag mogen nemen. Indien dat niet het geval is, denk even na, en vraag indien nodig raad aan je docent.
### Multi-stage builds
Bekijk nogmaals de layers van je image: `docker history jouw-naam/web`. Welke layer heeft de grootste omvang? Wat heb je gedaan om die layer te bekomen? Waarom is deze layer noodzakelijk (tip: zet `RUN` commando in commentaar `#` en rebuild)
> Antwoord
> ...
> ...
We kunnen jammer genoeg niet zonder deze layer, maar we hebben deze layer zeker en vast niet nodig in productie. Om dit op te lossen kunnen we gebruik maken van zogenaamde [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/).
Er zijn verschillende mogelijkheden om dit te gaan uitwerken, een daarvan is werken met [wheel](https://pythonwheels.com/).
Je nieuwe `Dockerfile` gaat eigenlijk de commando's bevatten om twee images te builden.
Normaal gezien zou volgende code geen verrassingen mogen bevatten (buiten de eerste lijn):
```
FROM python:3.10.0-alpine as build-image
WORKDIR '/app'
RUN apk add --no-cache linux-headers g++
COPY ./requirements.txt ./
RUN pip wheel --wheel-dir=/root/wheels -r requirements.txt
```
In je `Dockerfile` kan je nu als volgt een prodction-ready image beschrijven door gebruik te maken van het intermediair `build-image`:
```
FROM python:3.10.0-alpine as production-image
WORKDIR '/app'
COPY --from=build-image /root/wheels /root/wheels
COPY --from=build-image /app/requirements.txt ./
RUN pip install --no-index --find-links=/root/wheels -r requirements.txt
RUN addgroup -S uwsgi && adduser -S uwsgi -G uwsgi
USER uwsgi
COPY ./ ./
CMD ["uwsgi", "--ini", "app.ini"]
```
Bekijk nogmaals de `docker history`, je zal zien dat je eigen toegevoegde layers nu veel compacter zijn. Merk op dat je `requirements.txt` eventueel zou kunnen kopiëren naar `/root/wheels` en in je `production-image` de `COPY` verwijderen, dat spaart een layer uit.
## Applicatie uitbreiden
Tot dusver is de opgebouwde applicatie bijster simpel. In deze sectie ga je de applicatie herschrijven. Je bouwt aan een applicatie waarbij gebruikers zich kunnen registreren en bijgevolg dus ook kunnen inloggen en uitloggen.
Verwijder in je `web` directory `main.py`. Maak daarna een nieuwe folder aan, genaamd `webapp`.
### User model
Het Python package `Flask-SQLAlchemy` biedt een makkelijke manier om met verschillende type databases aan de slag te gaan. Het is een zogenaamde [object-relational mapping (kortweg ORM)](https://en.wikipedia.org/wiki/Object-relational_mapping) laag. Bekijk eventueel de [documentatie](https://flask-sqlalchemy.palletsprojects.com/en/2.x/).
In dit labo maak je gebruik van [PostgreSQL](https://www.postgresql.org/). Dat impliceert dat `Flask-SQLAlchemy` (eigenlijk onderliggend het package `SQLAlchemy`) verwacht dat je gebruikt maakt van `psycopg2`, een package om met PostgreSQL aan de slag te gaan.
Pas je `requirements.txt` file aan als volgt:
```
Flask==2.02
uWSGI==2.0.20
Flask-SQLAlchemy==2.5.1
psycopg2==2.9.1
```
Om gebruik te maken van `psycopg2` heb je nog Alpine packages nog. Pas je `Dockerfile` aan, vervang de huidige `RUN apk add ...` lijn door:
```
RUN apk add --no-cache linux-headers g++ postgresql-dev gcc python3-dev musl-dev
```
Maak daarna in je `webapp` directory `models.py` met volgende inhoud:
```
from . import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
def __init__(self, username, password):
self.username = username
self.password = password
def __repr__(self):
return '<User {}>'.format(self.username)
```
Zorg dat je de code begrijpt. Je luie docent heeft nog geen comments toegevoegd aan de code, **misschien niet onverstandig om dit even te doen**.
### Routering
Onderstaande code zou weinig verrassingen mogen bevatten. Merk op dat er gebruik gemaakt is van `Flask-Login`, een package dat het mogelijk maakt om gebruikers te authentiseren. Vergeet dit niet te voegen aan je `requirements.txt`: `Flask-Login==0.5.0`. **Voeg comments toe.**
Sla volgende code op in een bestand genaamd `webapp/routes.py`.
```
from flask import url_for, render_template, request, redirect, session
from flask import current_app as app
from .models import db, User
@app.route('/', methods=['GET'])
def home():
if not session.get('logged_in'):
return render_template('index.html')
else:
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
username = request.form['username']
password = request.form['password']
try:
data = User.query.filter_by(username=username, password=password).first()
if data is not None:
session['logged_in'] = True
return redirect(url_for('home'))
else:
return render_template('index.html', data={'username': username, 'password': password})
except Exception as e:
return "Some very good exception handling!"
@app.route('/registration', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
new_user = User(username=request.form['username'], password=request.form['password'])
db.session.add(new_user)
db.session.commit()
return render_template('login.html')
return render_template('register.html')
@app.route('/logout')
def logout():
session['logged_in'] = False
return redirect(url_for('home'))
```
### Init applicatie
Volgende code, `webapp/__init__.py` zou je grotendeels moeten herkennen uit vorige secties. De nodige code om een database te werken is toegevoegd. Alsook leest de applicatie `enviroment variables` in, je gaat die (straks) dus nog moeten instellen. Vergeet de comments niet ;-)!
```
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
pg_user = os.getenv('PG_USER', 'postgres')
pg_host = os.getenv('PG_HOST', 'localhost')
pg_port = os.getenv('PG_PORT', '5432')
pg_database = os.getenv('PG_DATABASE', 'postgres')
pg_password = os.getenv('PG_PASSWORD', 'postgres_password')
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] \
= 'postgresql://' + pg_user + ':' + pg_password + '@' + pg_host + ':' + pg_port + '/' + pg_database
app.config['SECRET_KEY'] = 'super secret key'
# app.config['DEBUG'] = True
# app.config['ENVIRONMENT'] = 'development'
db.init_app(app)
with app.app_context():
# Imports
from . import routes
db.create_all()
return app
```
### Templates
Maak een nieuwe directory `templates` aan, onder `webapp`, m.a.w. `~/docker-labo/web/webapp/templates`.
Flask heeft standaard ondersteuning voor de Jinja2 template engine. Dit maakt het makkelijk om enerzijds templates aan te maken en anderzijds hierin data te injecteren.
Je krijgt opnieuw alles cadeau. De templates zijn opgemaakt op basis van [Twitter Bootstrap](https://getbootstrap.com/). Merk op dat er hier en daar `if-else` structuren terug te vinden zijn. Maak volgende bestanden met bijhorende inhoud aan.
**base.html**
```
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Coolest Docker Example Ever!</title>
<link rel="canonical" href="https://getbootstrap.com/docs/4.0/examples/album/">
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark box-shadow">
<div class="container d-flex justify-content-between">
<a href="/" class="navbar-brand d-flex align-items-center">
<strong>Some cool Docker App</strong>
</a>
</div>
</div>
</header>
<main role="main">
<section class="jumbotron text-center">
<div class="container">
{% block content %}{% endblock %}
<p>
{% if session['logged_in'] %}
<a href="/logout" class="btn btn-danger">Logout</a>
{% else %}
{% if request.path!="/login" and request.path!="/registration" %}
<a href="/login" class="btn btn-primary">Login</a>
<a href="/registration" class="btn btn-secondary">Register</a>
{% endif %}
{% endif %}
</p>
</div>
</section>
</main>
</body>
</html>
```
**index.html**
```
{% extends "base.html" %}
{% block content %}
{% if session['logged_in'] %}
<h1 class="jumbotron-heading">Index - Logged in!</h1>
<p class="lead text-muted">Some inspiring text. Give it another <a href="/logout">try</a>?</p>
{% else %}
<h1 class="jumbotron-heading">Index</h1>
<p class="lead text-muted">Some inspiring text.</p>
{% if data %}
<div class="alert alert-danger" role="alert">
<strong>You must be retarded!</strong> No user with username <strong>{{ data.username }}</strong> and password <strong>{{ data.password }}</strong>!
</div>
<a href="/login" class="btn btn-warning">Try again!</a>
<a href="/registration" class="btn btn-success">Register!</a>
{% endif %}
{% endif %}
{% endblock %}
```
**login.html**
```
{% extends "base.html" %}
{% block content %}
{% if session['logged_in'] %}
<h1 class="jumbotron-heading">Nothing to do here...</h1>
<p class="lead text-muted">Seems like you're already logged in!</p>
{% else %}
<h1 class="jumbotron-heading">Login</h1>
<p class="lead text-muted">This is a login form!</p>
<form action="/login" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="username" class="form-control" id="username" name="username"
placeholder="Enter username">
</small>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" placeholder="Password" name="password">
</div>
<button type="submit" class="btn btn-primary">Login!</button>
</form>
{% endif %}
{% endblock %}
```
**register.html**
```
{% extends "base.html" %}
{% block content %}
{% if session['logged_in'] %}
<h1 class="jumbotron-heading">Nothing to do here...</h1>
<p class="lead text-muted">Seems like you're already logged in!</p>
{% else %}
<h1 class="jumbotron-heading">Register</h1>
<p class="lead text-muted">You won't regret, coolest app ever!</p>
<form action="/registration" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="username" class="form-control" id="username" name="username"
placeholder="Enter username">
</small>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" placeholder="Password" name="password">
</div>
<button type="submit" class="btn btn-success">Register!</button>
</form>
{% endif %}
{% endblock %}
```
### WSGI applicatie
In principe heb je nu een vrij complete app. Hier en daar zeker wel nog voor verbetering vatbaar. Ga gerust je gang, ongetwijfeld *bijna* zo nuttig én leuk als gamen in het forum.
Het enige wat nog ontbreekt is code om je WSGI app, meer bepaald Flask, te starten.
Maak in `~/docker-labo/web` een bestand `wsgi.py` aan met volgende code:
```
from webapp import create_app
app = create_app()
if __name__ == "__main__":
app.run()
```
Vergeet niet je `app.ini` aan te passen, de module is nu uiteraard `wsgi:app`.
Je directory structuur zou er als volgt moeten uitzien:
```
student@docker-student-00:~/docker-labo$ tree
.
└── web
├── app.ini
├── Dockerfile
├── requirements.txt
├── webapp
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ └── templates
│ ├── base.html
│ ├── index.html
│ ├── login.html
│ └── register.html
└── wsgi.py
```
### PostgreSQL dependency
Om er voor te zorgen dat je container gebruik kan maken van een PostgreSQL heb je nog een extra dependency nodig. Voeg aan je `production-image` volgende lijn toe (vlak na installeren van pip packages):
```
RUN pip install ...
RUN apk add --no-cache postgresql-dev
```
### Imperfecties
Merk op dat deze applicatie slechts een voorbeeld is, dus om met Docker te leren werken. Wachtwoorden worden opgeslagen in plain-text, foutafhandeling is zeer beperkt, geen form validation, ... .
## Docker compose
### Postgres database
Je hebt nu een geweldige, fantastische, prachtige, ... applicatie. Je hebt echter nog geen database. In dit labo ga je zelf een database opzetten, via Docker. Indien je deze app online zou zetten is het misschien interessanter om te kiezen voor een hosted oplossing, denk aan AWS RDS, Azure SQL Database.
Je kan je applicatie best al even testen, zonder `docker-compose`. Een PostgreSQL server kan je runnen als volgt:
```
docker run -itd -p 5432:5432 -e POSTGRES_PASSWORD=postgres_password postgres:14.0-alpine
```
Geef de database even de tijd om te starten. In de theorieles zag je een manier om logs te bekijken, de ideale moment om dit eens te gebruiken. Hoe bekijk je de logs van deze container?
> Antwoord
> ...
> ...
Je ziet eveneens dat poort 5432 geopend wordt. Je docent is benieuwd: is dit al dan niet verstandig?
> Antwoord
> ...
> ...
Build je image, en run op basis hiervan een container als volgt (op voorwaarde dat je SQL server actief is!):
```
docker run -e PG_HOST=IP_VAN_JOUW_VM -e PG_PORT=5432 -p 5000:5000 -it jouw-naam/web
```
Merk op dat we slechts twee `environment variables` meegeven. De reden hiervoor is dat we in de applicatie de overige (onveilige) defaults (ingebakken in postgres image) hebben opgegeven, bekijk `__init.py__` nog maar eens goed! De app zou nu moeten werken, test maar uit.
Na het testen, vergeet niet je database en app te stoppen.
### docker-compose.yml opstellen
Zoals gezien in de theorieles heeft `docker-compose` twee grote voordelen:
1. Eenvoudiger runnen van containers
2. Eenvoudiger om meerdere containers te combineren
Navigeer naar je `docker-lab` directory en maak `docker-compose.yml` aan, met volgende inhoud:
```
version: '3.9'
services:
web:
build: ./web
ports:
- 5000:5000
environment:
- PG_PASSWORD=student_password
- PG_USER=student_user
- PG_DATABASE=labo
- PG_PORT=5432
- PG_HOST=postgres
postgres:
image: 'postgres:14.0-alpine'
environment:
- POSTGRES_PASSWORD=student_password
- POSTGRES_USER=student_user
- POSTGRES_DB=labo
```
Probeer bovenstaand bestand volledig te begrijpen, bekijk zeker en vast eens de [documentatie](https://docs.docker.com/compose/). Merk ook op dat bij de Postgres component geen poort wordt opgegeven (noch via expose, noch via ports). Waarom is dit niet nodig ([tip](https://github.com/docker-library/postgres/blob/master/13/alpine/Dockerfile))? Wat is het verschil / nut van expose vs ports?
> Antwoord
> ...
> ...
### Meerdere containers runnen
Je kan nu je build process en run process, voor beide containers, met volgend commando uitvoeren:
```
docker-compose up --build
```
Uiteraard dien je `--build` enkel op te geven indien je aanpassingen hebt doorgevoerd aan je images.
Ooh nee! De applicatie werkt (wellicht) niet! Wat loopt er mis?
Tips:
1. `docker-compose ps`
2. `docker-compose logs <naam van de service>`
> Antwoord
> ...
> ...
Je zou kunnen denken dat dit op te lossen is met `depends_on`. Jammer genoeg is het complexer dan dat, `depends_on` zorgt er enkel voor dat een bepaalde service pas kan starten van zodra de desbetreffende service gestart is. Gestart betekent in dit geval niet per se dat de databaseserver klaar is om connecties af te handelen.
Het idee is dat elke service onafhankelijk kan werken. Wanneer je applicatie een connectie verwacht naar een database, en die is er niet, moet je dat opvangen in je applicatie. Dus niet via een of andere Docker-manier.
Een mogelijke oplossing is om gebruik te maken van zogenaamde database pools. Dit is echter niet de focus van dit labo, kijk [https://docs.sqlalchemy.org/en/13/core/pooling.html#disconnect-handling-pessimistic](hier) voor meer informatie.
#### Simpele 'hack'
Je start de services op, d.m.v. `docker-compose up` (e.v.t. `-d` voor background). Daarna (e.v.t. in een andere terminal) kan je `docker-compose restart web` uitvoeren. Uiteraard, enkel en alleen indien je databaseserver in tussentijd beschikbaar is geworden.
#### Entrypoint
Een iets betere oplossing is een zogenaamd entrypoint te voorzien. Dit is een script dat je gebruikt om een aantal zaken uit te voeren vooraleer je feitelijke commando wordt uitgevoerd. Dit kan bv. prima dienen om te controleren of de databaseconnectie reeds beschikbaar is.
Maak in `web` een bestand `entrypoint.sh` aan en plaats volgende hierin de volgende code:
```
#!/bin/sh
while ! nc -z "$PG_HOST" "$PG_PORT" ; do
sleep 10
printf "Database %ss:%s not ready" "$PG_HOST" "$PG_PORT"
done
exec "$@"
```
Om dit script uitvoerbaar te maken voer je `chmod +x entrypoint.sh` uit. `COPY` zal zorgen dat dit script in je container eveneens uitvoerbaar is (bestandsrechten worden automatisch gekopieerd).
Wat doet dit script?
> Antwoord
> ...
> ...
Pas je `Dockerfile` aan, net voor `CMD` plaats je volgende regel:
```
ENTRYPOINT ["/app/entrypoint.sh"]
```
### Environment files
Typisch gezien ga je alle bovenstaande code op een git repository plaatsen. Dat impliceert dus ook de gevoelige gegevens opgegeven in je `docker-compose.yml`. Om dit te oplossen kan je gebruik maken van Docker secrets, bekijk gerust de [documentatie](https://docs.docker.com/compose/compose-file/).
Dit is (meestal) vrij omslachtig. Een alternatief is werken met `enviroment files`. Je verwijst ernaar in `docker-compose.yml`. Via `.gitignore` kan je er voor zorgen dat je `environment files` niet worden gecommit.
Zoek dit zelf uit, en uiteraard, implementeer dit. Tip [documentatie](https://docs.docker.com/compose/environment-variables/)
Hoe heb je dit opgelost?
> Antwoord
> ...
> ...
## Nginx reverse proxy
uWSGI is een relatief goede webserver, maar heeft zijn beperkingen. Denk bijvoorbeeld aan gzip-compressie, aanbieden van statische content, eventuele load balancing, security instellingen, ... .
Daarom is het vrij gebruikelijk om "bovenop" uWSGI gebruik te maken van Nginx. Dit laatste product heeft een zeer brede community, het project wordt goed onderhouden (altijd interessant met het oog op security).
Als dit soort materie je kan boeien, bekijk dan zeker eens wat Traefik allemaal voor jou zou kunnen beteken. Super interessant, krachtig project, gebruikt eveneens Nginx!
Maak een extra directory aan, genaamd `nginx`. Hierin maak je een nieuw `Dockerfile` aan:
```
FROM nginx:1.21.3-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
```
Maak daarnaast nog config file aan, genaamd `nginx.conf`. We houden deze in dit labo erg eenvoudig, maar weet dat dit dus een stuk uitgebreider kan.
```
server {
listen 80;
location / { try_files $uri @app; }
location @app {
include uwsgi_params;
uwsgi_pass web:5000;
}
}
```
Pas tot slot je `docker-compose.yml` file aan:
```
nginx:
build: ./nginx
ports:
- 80:80
depends_on:
- web
```
Bij je `web` service ga je logischerwijs niet langer poort 5000 openen naar buiten toe. Past dit aan naar `expose`.
Tot slot kun je ervoor zorgen dat Nginx rechtstreeks het uwsgi protocol zal gebruiken. Daarvoor dien je `app.ini` aan te passen:
```
...
; Force the specified protocol (uwsgi, http, fastcgi) for default sockets
protocol = uwsgi
```
Start alle services op, nu kan je normaal gezien surfen naar je app (op poort 80). Kijk dit even na.
## Azure Container Registry & Azure App Service
Jullie hebben ooit wel al eens gewerkt met een Azure Web App. Wellicht heb je toen een bepaalde software stack gekozen en je code naar Azure geüpload. Zoals we tijdens de theorie hebben gezien, wordt dit hoogstwaarschijnlijk omgezet naar een (Docker) container.
De service biedt echter ook de mogelijkheid om te werken met eigen Docker containers. Er kan eveneens gebruik gemaakt worden van "multi-containers", dit is weliswaar (nog steeds) in preview. We gaan de app nu stap-voor-stap online beschikbaar maken!
### Waarom niet Kubernetes? Of Azure Container Instances?
We kunnen hier uren over doorgaan. Kubernetes brengt sowieso extra complexiteit met zich mee. Je zal nog wat extra kennis nodig hebben. Daarnaast is Kubernetes vooral interessant op ietwat grotere schaal: veel apps, veel load, ... Voor een kleinschalige en eenvoudigere set-up zal de App Service doorgaans goedkoper uitkomen.
Je bent misschien ook wel al eens Azure Container Instances tegengekomen. Dit is ook een interessante service, maar is eerder bedoeld om kortstondige zaken te gaan uitvoeren. Vergelijk het een beetje met Azure Functions. Dit komt overigens ook meestal duurder uit dan gebruik te maken van de App Service.
### Azure Container Registry (ACR)
Zoals gezien in de theorie is het mogelijk om gebruik te maken van bv. Docker Hub of je kan zelf een registry opzetten of die van een derde partij gebruiken. In dit geval gaan we onze images (onze build) private beschikbaar maken via Azure Container Registry.
#### Inloggen Azure CLI
`az login`
Copy-paste de link en de code in je browser.
#### Maak een registry aan

Je kan dit ook via de CLI: `az acr create --resource-group <resourceGroup> --name <acrName> --sku Basic`
#### Inloggen registry
```
az acr login --name <acrName>
```
#### Images pushen
Je zal je images correct moeten taggen. Pas daarom `docker-compose.yml` aan.
Bij de webapp voeg je het volgende toe:
```
image: <acrName>.azurecr.io/webapp:latest
```
Bij Nginx voeg je het volgende toe:
```
image: <acrName>.azurecr.io/nginx:latest
```
Test nog eens even de applicatie, en zorg dat alles wordt gebruild:
```
docker-compose up -d --build
```
Doordaat je reeds ingelogd bent op Azure, zal de Docker CLI in staat zijn je images te pushen naar ACR:
```
docker-compose push
```
Kijk na of je images effectief correct "gepusht" zijn, doe dit via de portal of via de CLI:
```
az acr repository show --name <acrName> --repository webapp
az acr repository show --name <acrName> --repository nginx
```
Voor sommige zaken zal je het admin account van je respository moeten enabelen. Je kan dat via de portal (zie screenshot) of via de CLI.
```
az acr update -n <acrName> --admin-enabled true
```

### Azure App Service
Je zal eerst en vooral een Azure App Service Plan moeten aanmaken. Dit bepaalt hoeveel resources je toegewezen krijgt en uiteraard hoeveel je ervoor betaalt. Eigenlijk komt het er technisch gezien op neer dat je een stukje van een Kubernetes cluster afneemt (of wie weet: een andere clustermanagement systeem van Microsoft).
Maak een plan aan, kies voor Dev / Test, Basic B1.

#### Web App maken
Nu je een plan hebt, kan je apps "maken". Maak een app aan als volgt:

Bij de 2de stap kies je voor Docker Compose (preview), als image source kies je uiteraard voor Azure Container Registry. Upload daarna je docker-compose.yml bestand.
#### Environment vars
Als je gewerkt hebt met `env_file`, heeft Azure geen toegang tot je environment variables. Want, je hebt er voor gezorgd dat je `.env` files niet op GitHub beschikbaar zijn. Je kan gelukkig in Azure eveneens env vars gaan instellen.
Ga hiervoor naar je app en klik op "Configuration". Onder "Application settings" kan je een nieuwe "Application Setting" toevoegen. Dit zijn dan ook meteen je env vars.
#### Naar je app surfen - debuggen
Het zal een tijdje duren vooraleer je app effectief online beschikbaar is. Je kan de logs (beperkt) bekijken onder "Monitoring", "Log stream".
## Continuous deployment - GitHub Actions
Stel dat het jouw taak is om telkens om deze app te onderhouden. Het vrolijke developper-team brengt voortdurend nieuwe versies uit... Je kan dat uiteraard manueel gaan oplossen, of je kan dit als goede infrastructure engineer automatiseren.
Ik kan je verzekeren dat de documentatie van Azure je serieus in te steek laat hoe je dit doet voor een "multi-container" app. Gelukkig geeft je docent niet snel op, en heeft hij dit voor jou uitgezocht :-).
### Authenticatie
Je dient eerst en vooral een security principal aan te maken. Dit zal ervoor zorgen dat je GitHub actions straks gemachtigd is om nieuwe containers te deployen.
```
az ad sp create-for-rbac --name "<appName>" --role contributor \
--scopes /subscriptions/<subscriptionID>/resourceGroups/<rg>/providers/Microsoft.Web/sites/<appName> \
--sdk-auth
```
Je `subscriptionID` kan je terugvinden in de portal (Subscriptions) of je deze bv. als volgt achterhalen:
```
az account list --query '[].{Name: name, SubscriptionID:id}' --output table
```
Als output krijg je een stukje JSON, hou dit bij.
Je zal GitHub eveneens toegang moeten geven tot je Container Registry. De credentials kan je als volgt opvragen:
```
az acr credential show --name <acrName>
```
Ga nu naar je GitHub repository, meer bepaald naar settings -> secrets.
Maak volgende secrets aan:
| Key | Value |
| -------- | -------- |
| AZURE_CREDENTIALS | JSON output sevice principal |
| REGISTRY_PASSWORD | Zie outpout acr credentials |
| REGISTRY_USERNAME | Zie outpout acr credentials |
Je paste de values erin zonder quotes rond. Je mag kiezen welk van beide wachtwoorden je gebruikt.
### GitHub Actions
Met GitHub Actions kunnen we allerlei leuke zaken gaan doen. We kunnen bv. automatisch images gaan builden en deze als release beschikbaar maken. In dit geval gaan we images gaan builden, gaan pushen en Azure App Service dwingen de nieuwste versie te deployen.
Maak een directory `.github` aan, met daarin nog een directory: `workflows`. Herin plaatjes je een YML-bestand, bv. `deployment.yml` met volgende inhoud:
```
on: [push]
name: Workflow webapp deployment
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# checkout the repo
- name: 'Checkout GitHub Action'
uses: actions/checkout@master
- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- uses: azure/docker-login@v1
with:
login-server: <acrName>.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
docker build nginx/ -t <acrName>.azurecr.io/nginx:latest
docker push <acrName>.azurecr.io/nginx:latest
docker build web/ -t <acrName>.azurecr.io/webapp:latest
docker push <acrName>.azurecr.io/webapp:latest
- uses: azure/webapps-deploy@v2
with:
app-name: '<appName>'
configuration-file: ./docker-compose.yml
images: |
'<acrName>.azurecr.io/nginx:latest'
'<acrName>.azurecr.io/webapp:latest'
- name: Azure logout
run: |
az logout
```
Probeer dit te begrijpen en pas aan waar nodig. Wanneer je nu nieuwe zaken pusht naar GitHub zullen je containers automatisch vervangen worden.
### Deployments kunnen falen ... traag zijn
Er kan een foutje in je deployment sluipen. Het kan even duren vooraleer de nieuwe versie daadwerkelijk (correct) beschikbaar is. Hoe zou je er voor kunnen zorgen dat je de impact zo beperkt mogelijk houdt?
> Antwoord
> ...
> ...
## Uitgewerkt voorbeeld
Een uitgewerkt voorbeeld van de code kan je [hier](https://github.com/Eslih/kant-en-klaar-web-app) raadplegen. Een meer uitgebreide versie, met meer best-practices, betere GUI, error handling, microservices, ... kan je [hier](https://github.com/Eslih/basic-webapp) vinden.
## Uitbreidingen (extra)
### Integratie labo03
Probeer eens labo03 (deels) opnieuw te maken m.b.v. wat je zonet hebt geleerd. Gebruik het labo document daarvoor en dien dit eveneens in.
### PostgreSQL service (container) vervangen
Als je deze app in productie wil gaan gebruiken, zal de database je wellicht het meeste stress bezorgen. Hoe ga je het aanpakken als je bv. stevig online dient te schalen? Voor de webapplicatie kan je simpelweg extra containers gaan gebruiken. Voor de database lukt dat niet: er wordt locking toegepast op storage niveau. Als er meerdere PostgeSQL instanties dezelfde sotrage gaan gebruiken resulteert dit sowieso in ernstige problemen.
Natuurlijk kan je een database cluster gaan opzetten. Maar, dat is niet zo eenvoudig. Dit vergt veel kennis en zal extra onderhoud vereisen. Dus waarom niet het voordeel van de cloud gebruiken? Pas de applicatie aan zodanig dat deze van een "cloud database" gebruik maakt.
## Mogelijke examenvragen
Verzin minstens 3 goede examenvragen over de inhoud van deze en vorige les (theorie & labo). Deze vragen moeten peilen naar kennis, kunde of beide over het onderwerp in kwestie. Ze moeten doenbaar zijn, maar ook niet te gemakkelijk.
> Antwoord
> 1)
> 2)
> 3)