neděle 10. prosince 2017

Vývoj aplikací přes Docker

V dnešní době asi neexistuje vývojář, který by někdy neslyšel o Dockeru. O technologii, která se v posledních letech stala hlavním prostředkem pro moderní vývoj aplikací.

Co je to Docker?

Docker je kontejner, ve kterém běží vaše aplikace. V konečném stavu se jedná o linuxový virtuální server, který obsahuje vše potřebné, co jste si sami definovali. Představte si, že máte Node.js aplikaci, která běží na portu 8080. Takovouto aplikaci můžete velice jednoduše převézt do Dockeru a tu poté nasadit v Cloudu.

Než se pustíme do samotných příkladů, pojďme si nejdříve říci, proč bychom něco takového, jako je Docker vlastně měli chtít.

Proč Docker?

Jednotné prostředí

Vývoj aplikací je často tvořen z několika fází. První fází bude development prostředí, které je určené pro vývojáře. Po určité době se aplikace dostane do stavu testování, kde se provede akceptace, zda je onen softwarový produkt určen pro produkci. Poté dojde k nasazení na prostředí simulující produkci, tedy otestuje se vůči produkčním datům. A nakonec zde máme produkci.

Když to sečteme, zjistíme, že naše aplikace neběží jen na jednom prostředí, ale naopak jsme nuceni, abychom aplikaci jednoduše rozeběhli na více serverech. Každé prostředí by mělo být jednotné a právě s tím nám Docker může pomoci. Díky Dockeru můžeme totiž zajistit, že ono prostředí bude vždy Debian ve verzi x.y, s Node.js ve verzi 8.1 apod.

Lokální vývoj

Samotnou aplikaci musí někdo někde napsat. K tomu nám samozřejmě slouží naše vlastní počítače. A zde je kámen úrazu. Pokud nepoužijeme Docker, tak aplikaci píšeme v prostředí, které je velice často vzdálené produkčnímu prostředí.

Představme si situaci, kdy Pepa používá Windows, Franta má Linux a Jarda jede na MacOS. V takovém případě musíme někam sepsat požadavky typu: "musíte mít Node.js verzi 8.2, musíte mít nainstalovánu technologii A, B, C,....". V případě Dockeru nemusíte. Nemusíte řešit to, kdo jaký má operační systém, kdo jakou má verzi Node.js, apod. A už vůbec nemusíte své vývojáře nutit, aby používali prostředí, které je jim nepřirozené či je složité na nastavení.

Horizontální škálování

Samotná technologie Docker Vám sice neumožňuje používat škálování, ale díky Dockeru se ke škálování můžete snadněji dostat. K tomuto účelu zde existují technologie jako je Kubernetes, Swarm či Mesos. Jedná se o technologie, které běží nad Vašimi dockery a orchestruje jednotlivé kontejnery.

Snažsí nasazení

Pokud přejdeme na myšlenku Dockeru, který říká: "Nasazuj aplikace z předem připravených kontejnerů", tak nám tím současné říká: "Nestarejte se tolik o to, jak konfigurovat server". S tím práve souvisí i to, jak aplikaci nakonec nasadit. V případě, že máte vlastní virtuální či fyzický server, často se z deploymentu stává černá díra, protože nasadit aplikaci umí jen infra tým, popřípadě ten nejšikovnější programátor, který je i administrátorem. Nasazení pomocí Dockeru totiž nakonec může znamenat, že postup se zredukuje na 1 až 2 řádky kódu (docker registr -> server). Většina Cloudových řešení, jako je Azure, AWS či Google Cloud, Vám totiž nabízí možnost, jak jednoduše takové nasazení z Docker image provést.

Node.js a Docker

Nyní se již pojďme podívat na ukázku, jak použít Docker při vývoji Node.js aplikací.

Pro následující ukázky budete potřebovat:
  1. Node.js 8 a vyšší (pro inicializaci projektu, v budoucnu se nejedná o běhové prostředí aplikace)
  2. Docker
  3. Editor kódu (WebStorm, Atom, Visual Studio Code, apod)
Představme si, že máme jednoduchou Node.js aplikaci, která na portu 8080 vrací "Hello World". Jednoduchý návod na vytvoření takove aplikace by byl následující:

Inicizalizace projektu:
npm init

Instalace závislostí:
npm install express

Soubor src/index.js:
const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.send('Hello world!');
});

app.listen(8080, () => {
    console.log('Running on http://localhost:8080');
});

Aplikaci bychom spustili následujícím příkazem:
node src/index.js

V prohlížeči by nyní na adrese http://localhost:8080 měla běžet naše aplikace. Nyní aplikaci zastavme a pojďme ji převést do Dockeru.

Dockerizování

V kořenovém adresáři projektu vytvořte soubor Dockerfile:
FROM node:carbon

MAINTAINER Ales Dostal <a.dostal@apitree.cz>

# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install

# Bundle app source
COPY . /usr/src/app

EXPOSE 8080
CMD ["node", "src/index.js"]

Na prvním řádku definuje image, ze kterého budeme vycházet. V našem případě to je node:carbon, což je Node.js verze 8.x. Pokud chcete definovat přesnou verzi Node.js můžete použít například node:8.9.3. Pokud nevíte, jake obrazy jsou k dispozici, není nic jednoduššího, než se podívat na Docker Hub.

Klauzule MAINTAINER je pouze informativní, nicméně je jistě dobré jí definovat, aby samotný image obsahoval metadata o tom, kdo daný image vytvořil a kdo je jeho vlastníkem.

Poté již konkrétně definujeme, co náš Docker image obsahuje. Nejprve vytvoříme adresář /usr/src/app a ten nastavíme jako výchozí adresář. Výchozí při běhu Dockeru.

Dále do výchozího adresáře překopírujeme package.json a spustíme instalaci závislostí.

Posledním příkazem, který se spouští při vytváření Docker image, je překopírování celého projektu do výchozího adresáře.

Na konci je ještě definování portu, který bude z běžícího Dockeru viditelný a také příkaz, který se spustí vždy, když Docker image spustíme. V tom je hlavní rozdíl mezi klauzulí RUN a CMD. V našem případě je to příkaz node src/index.js.

Poslední částí je vytvoření souboru .dockerignore:
node_modules

Do tohoto souboru definujeme adresáře a soubory, které se mají při vytváření docker image ignorovat. V našem případě je to například node_modules, protože tento adresář, který obsahuje závislosti, si docker vytvoří sám (díky RUN npm install).

Vytvoření Docker image

Nyní již nic nebrání tomu, vytvořit si na lokálním stroji vlastní docker image. K tomuto účelu stačí spustit tento příkaz:
docker build -t apitreecz/testapp .

Pozor, nezapoměnte na konci na onu tečku. Jde o to, že tento příkaz spouštíme v kořenovém adresáři projektu a tou tečkou určujeme adresář, kde příkaz docker bude hledat projekt a hlavně soubor Dockerfile a .dockerignore.

Samotný příkaz začně stahovat image node:carbon a současně spustí příkazy jako npm install a kopírování projektu do /usr/src/app (samozřejmě v Dockeru, nikoli ve vašem stroji).

Poté, co příkaz doběhne, měl by napsat něco jako:
Successfully built 46aaa661cef4
Successfully tagged apitreecz/testapp:latest

Abychom se přesvědčili, že náš docker image skutečně existuje, stačí se podívat pomocí příkazu:
docker images

Výstup by měl vypadat nějak takto:
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
apitreecz/testapp             latest              46aaa661cef4        2 minutes ago       751MB

Díky tomu máme ve svém lokálním repozitáři vytvořen Docker image, který stačí jen spustit.

Spuštění Docker image

Pro spuštění docker image stačí následující příkaz:
docker run -p 8080:8080 -d apitreecz/testapp

Pokud jste vše udělali správně, měla by Vaše aplikace běžet na portu 8080. Takže nezbývá, než v browseru vyzkoušet spustit http://localhost:8080.

Abyste byli schopni vůbec sledovat, co se s Vaší aplikací v Dockeru děje, pojďme se podívat na pár příkazů, které Vám můžou pomoci.

Výpis běžících docker kontejnerů:
docker ps

Výstup by měl vypadat nějak takto:
CONTAINER ID        IMAGE               COMMAND               CREATED             STATUS              PORTS
e9321555c9cc        apitreecz/testapp   "node src/index.js"   6 seconds ago       Up 2 seconds        0.0.0.0:8080->8080/tcp

V této chvíli je pro nás duležitý CONTAINER_ID, kterým se daný bežící Docker kontejner identifikuje.

Logy Docker kontejneru:
docker logs <container id>

Vstup do konzole Docker kontejneru:
docker exec -it <container id> /bin/bash

Zastavení Docker kontejneru:
docker stop <container id>

Pokud jste se úspěšně dostali až sem, dá se říci, že máte první krok k "dokerizaci" za sebou. Naučili jsme se, jak vytvořit Docker image, jak daný Docker image spustit, spravovat a nakonec jak ho i zastavit.

Nyní se pojďme podívat na další bod a tím je vývoj vůči Docker kontejneru.

Vývoj na běžícím Docker kontejneru

Jistě jste si všimli, že pokud bychom chtěli vyvíjet aplikaci pomocí Dockeru, tak současný postup by pro nás znamenal: zastavit kontejner, napsat kód, vytvořit kontejner, spustit kontejner. Tento postup by byl samozřejmě možný, ale zároveň i dost zdlouhavý. Proto si pojďme udělat ukázku, jak toto vyřešit elegantnějším zpusobem.

Docker a Next.js

Pro účely ukázky použiji Next.js, který umožňuje hot reload, díky kterému si lépe můžeme ukázat, jak vyvíjet na běžícím docker kontejneru, který bude reagovat na naše změny.

Prvním krokem bude vytvoření jednoduché Next.js aplikace.

Nejprve vytvoříme adresář nextjsdemo a do něj přejdeme:
mkdir nextjsdemo
cd nextjsdemo

Dále provedeme inicializaci pomocí npm:
npm init

Po odklikání všech otázek se nám v projektu vytvoří package.json.

Dále nainstalujeme potřebné závislosti:
npm install --save next react react-dom

Nyní vytvoříme soubor pages/index.js:
export default () => <div>Hello from Next.js!</div>

Poslední částí je zápis scripts do package.json:
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }

Teď už nic nebrání tomu, abychom si svojí aplikaci zkusili spustit:
npm run dev

Na portu 3000 poběží naše aplikace. Pro test stačí v browseru spustit: http://localhost:3000. Pokud jste vše udělali správně, měli byste vidět webovou stránku s "Hello from Next.js!". Při přepsání textu v pages/index.js, můžete vidět, že díky Next.js máte k dispozici hot reload a hned v browseru vidíte samotnou změnu. Aplikaci zastavte a pojďme ji "dokerizovat".

Nyní vytvoříme Dockerfile, který nám opět bude definovat docker image:
FROM node:carbon

MAINTAINER Ales Dostal <a.dostal@apitree.cz>

# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install

# Bundle app source
COPY . /usr/src/app

RUN cd /usr/src/app && npm run build

EXPOSE 3000
CMD ["npm", "start"]

Jedná se o velice podobný soubor, který jsme tvořili v první ukázce, pouze zde přidáváme build samotné aplikace a port, na kterém aplikace bude vystupovat je 3000. Samozřejmě port lze změnit i na 8080, pouze bychom doplnili parametr k npm start, na kterém portu se má Next.js aplikace spustit.

Dále opět vytvoříme také soubor .dockerignore:
.next
node_modules

V .dockerignore opět definujeme všechny adresáře a soubory, které nechceme do docker image kopírovat. Tím, že jsme si zkoušeli aplikaci již pustit, můžete si všimnout, že nám Next.js vytvořil adresář .next, který ale z lokálního stroje nechceme kopírovat, protože si ho image vytvoří sám.

Opět vytvoříme docker image:
docker build -t apitreecz/nextjsdemo .

Pro kontrolu si můžeme zkusit náš docker spustit:
docker run -p 8080:3000 -d apitreecz/nextjsdemo

Pokud jste vše udělali správně, měli byste ve webovém prohlížeči, na adrese http://localhost:8080 vidět naší aplikaci. Zde si můžete všimnout, že i když aplikace v Docker kontejneru běží na portu 3000, my jsme jí přemapovali na port 8080 pro náš lokální stroj.

Nicméně nyní jsme ve stavu, kdy nám kontejner spustil aplikaci v produkčním módu (díky npm start) a zároveň nemáme možnost docker image aktualizovat.

Abychom byli schopni za běhu náš běžící Docker kontejner aktualizovat, použijeme k tomu Docker Compose. Nejprve zastavte původní Docker kontejner a pojďme se podívat jak na to.

V root adresáři vytvoříme soubor docker-compose.yml:
version: "2"
services:
  webapp:
    build: .
    command: npm run dev
    ports:
      - "8080:3000"
    volumes:
      - .:/usr/src/app

A nyní spustíme příkaz:
docker-compose up --build

Tento příkaz nám podle Dockerfile a docker-compose.yml nám vytvoří a spustí Docker image, která přepisuje původní chování a tím je spuštění npm start na npm run dev. Současně s tím nám reaguje na změny stejně, jako v případě spuštění z lokálního stroje.

Nyní v browseru stačí přejít znovu na stránku http://localhost:8080, kde byste měli vidět bežící aplikaci. Současně s tím, pokud nyní změníte text v pages/index.js, uvidíte, že Vaše bežící aplikace na toto zareaguje a dojde k hot reloadu.

Výhodou Docker Compose je hlavně v tom, že nemusíte přepisovat Dockerfile pro lokální vývoj vs produkční běh. Docker Compose vám přepíše původní konfiguraci a tím pádem máte k dispozici jak produkční Docker image, tak image pro lokální vývoj.

Závěr

O Dockeru by se dalo napsat spoustu věcí. Jelikož se článek rozrostl, tak jsem nucen ho rozdělit na dvě části. Příště si ukážeme, jak náš Docker image uložit do Docker Hubu a současně, jak ho poté nasadit v Cloudu (Google Cloud, Azure).

I když Docker přidává další abstrakci Vaší aplikaci, tak věřte, že ona abstrakce je nakonec výhodná, protože Vás odstiňuje od nutnosti unifikovat běžící prostředí složitou konfigurací serverů a lokálních strojů.

React a hrátky s TypeScriptem

V minulosti jsem se již několikrát zmiňoval, že používat JavaScript bez statických typů, je stejné jako jezdit na kole poslepu. Nemusí se...