Esli Heyvaert
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    3
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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 ![](https://i.imgur.com/6Ofexdz.png) 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 ``` ![](https://i.imgur.com/yzsufM9.png) ### 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. ![](https://i.imgur.com/9D1YMxZ.png) #### Web App maken Nu je een plan hebt, kan je apps "maken". Maak een app aan als volgt: ![](https://i.imgur.com/7REh73E.png) 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)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully