Sari la conținutul principal

Crearea unei imagini

Până acum, am rulat imagini deja existente, însă acum vom vedea cum putem să ne creăm și publicăm propria noastră aplicație. Vom porni de la nivelul de jos al unei astfel de ierarhii, care este reprezentat de container. Deasupra acestui nivel se află serviciile, care definesc modul în care containerele se comportă în producție, iar la nivelul cel mai sus se află stiva, care definește interacțiunile dintre servicii. Sursele pentru acest exemplu se găsesc în arhiva de laborator.

În acest exemplu, vom crea o aplicație Web folosind Flask (un framework Web scris în Python) care afișează o poză aleatoare la fiecare accesare. Ea va fi scrisă într-un fișier app.py care arată în felul următor:

from flask import Flask, render_template
import random

app = Flask(__name__)

images = [
"https://i.pinimg.com/736x/8f/2a/30/8f2a30993c405b083ba8820ae6803b93.jpg",
"https://images.g2crowd.com/uploads/product/image/large_detail/large_detail_1528237089/microsoft-azure-biztalk-services.jpg",
"https://aptira.com/wp-content/uploads/2016/09/kubernetes_logo.png",
"https://www.opsview.com/sites/default/files/docker.png"
]

@app.route('/')
def index():
url = random.choice(images)
return render_template('index.html', url=url)

if __name__ == "__main__":
app.run(host="0.0.0.0")

După cum se observă în fișierul Python, la baza paginii Web se află un template în fișierul index.html (care trebuie creat într-un director templates):

<html>
<head>
<style type="text/css">
body {
background: black;
color: white;
}
div.container {
max-width: 500px;
margin: 100px auto;
border: 20px solid white;
padding: 10px;
text-align: center;
}
h4 {
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="container">
<h4>Cloud image of the day</h4>

<img src="{{url}}" />
</div>
</body>
</html>

Mai avem nevoie de un fișier requirements.txt unde specificăm pachetele Python care trebuie instalate în imaginea pe care o creăm:

Flask>=2.2.2

O imagine este definită de un fișier numit Dockerfile, care specifică ce se întâmplă în interiorului containerului pe care vrem să îl creăm, unde accesul la resurse (cum ar fi interfețele de rețea sau hard disk-urile) este virtualizat și izolat de restul sistemului. Prin intermediul acestui fișier, putem specifica mapări de porturi, fișiere care vor fi copiate în container când este pornit, etc. Un fișier Dockerfile se aseamănă cu un Makefile, iar fiecare rând din el descrie un strat din imagine. Odată ce am definit un Dockerfile corect, aplicația noastră se va comporta totdeauna identic, indiferent în ce mediu este rulată. Un exemplu de Dockerfile pentru aplicația noastră este următorul:

FROM alpine:edge

RUN apk add --update py3-pip
RUN python3 -m venv /venv

ENV PATH="/venv/bin:$PATH"

COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt

COPY app.py /usr/src/app/
COPY templates/index.html /usr/src/app/templates/

EXPOSE 5000

CMD ["python3", "/usr/src/app/app.py"]

În fișierul de mai sus, avem următoarele comenzi:

  • FROM – specifică o imagine pe care noua noastră imagine este bazată (în cazul nostru, pornim de la o imagine de bază cu Alpine, care se află pe Docker Hub, și în interiorul căreia vom rula aplicația noastră Web scrisă în Python)
  • COPY – copiază fișierele dintr-un director local în containerul pe care îl creăm
  • RUN – rulează o comandă (în exemplul de mai sus, întâi instalează pachetul pip pentru Python, după care instalează pachetele Python enumerate în fișierul requirements.txt)
  • EXPOSE – expune un port în afara containerului
  • CMD – specifică o comandă care va fi rulată atunci când containerul este pornit (în cazul de față, se rulează scriptul Python app.py).
atenție

Atunci când setăm o imagine de bază folosind FROM, se recomandă să specificăm explicit versiunea imaginii în loc să folosim tag-ul latest, pentru că este posibil ca, pe viitor, cea mai recentă versiune să nu mai fie compatibilă cu alte componente din aplicația noastră.

sugestie

Instrucțiunea EXPOSE nu expune propriu-zis portul dat ca parametru, ci funcționează ca un tip de documentație între cine construiește imaginea și cine rulează containerul, în legătură cu ce porturi trebuie publicate. Pentru a publica un port la rularea unui container, se folosește flag-ul -p la comanda de docker run (cum se va vedea mai jos).

În final, vom avea următoarea structură de fișiere:

$ tree
.
├── app.py
├── requirements.txt
└── templates
└── index.html

Pentru a construi aplicația, se rulează comanda de mai jos în directorul curent (flag-ul -t este folosit pentru a da un tag imaginii create):

$ docker build -t testapp .

Sending build context to Docker daemon 6.144kB
Step 1/8 : FROM alpine:edge
[...]
Step 2/8 : RUN apk add --update py3-pip
[...]
Step 3/8 : COPY requirements.txt /usr/src/app/
[...]
Step 4/8 : RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
[...]
Step 5/8 : COPY app.py /usr/src/app/
[...]
Step 6/8 : COPY templates/index.html /usr/src/app/templates/
[...]
Step 7/8 : EXPOSE 5000
[...]
Step 8/8 : CMD python3 /usr/src/app/app.py
[...]

Pentru a verifica dacă imaginea a fost creată cu succes, folosim următoarea comandă:

$ docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
testapp latest 21a2e1b319ac 2 minutes ago 62.9MB

Putem afla mai multe detalii despre imaginea creată cu următoarea comandă:

$ docker image inspect testapp

[
{
"Id": "sha256:d722f3da27d0d0e7d8cf9130738bbdb43a79204cddd4c0a9dba20becb4c0d3eb",
"RepoTags": [
"testapp:latest"
],
"Parent": "sha256:d4f707536bf6b93836d7eda20edc7ccfba5a071e3c8a0d932c020b4c6b23ca00",
"Comment": "",
"Created": " 2019-09-23T14:41:51.204728682Z",
"Container": "d9fec234255480ada84b772eb1e4b722b33fa262bc9481688920cba01f6d7d5d",
"ContainerConfig": {
"Hostname": "d9fec2342554",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"5000/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"python3\" \"/usr/src/app/app.py\"]"
],
"ArgsEscaped": true,
"Image": "sha256:d4f707536bf6b93836d7eda20edc7ccfba5a071e3c8a0d932c020b4c6b23ca00",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": [],
"Labels": {}
},
[...]
"Architecture": "amd64",
"Os": "linux",
"Size": 62900300,
"VirtualSize": 62900300,
[...]
}
]

În acest moment, imaginea se găsește creată în registrul local de imagini Docker. Dacă dorim să o rulăm, folosim următoarea comandă:

$ docker container run -p 8888:5000 testapp

* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
172.17.0.1 - - [23/Sep/2019 14:46:00] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [23/Sep/2019 14:46:01] "GET /favicon.ico HTTP/1.1" 404 -
172.17.0.1 - - [23/Sep/2019 14:46:02] "GET / HTTP/1.1" 200 –
[...]

Accesând dintr-un browser Web adresa http://127.0.0.1:8888, vom vedea aplicația Web pe care am creat-o. Flag-ul -p expune portul 5000 al aplicației și specifică o mapare între el și portul 8888 de pe mașina pe care rulăm. Dacă dorim să rulăm aplicația în modul detașat, o putem face folosind flag-ul -d.