ium/IUM_04.Konteneryzacja.ipynb
2024-04-09 09:46:32 +02:00

24 KiB

Inżynieria uczenia maszynowego

27 marca 2024

4. Konteneryzacja

Plan na dziś

  1. Konteneryzacja i Docker
  2. Integracja Docker-Jenkins

1. Konteneryzacja

  • Konteneryzacja to metoda tworzenia lekkich, odizolowanych środowisk uruchomieniowych
  • Różnica między konteneryzacją a maszynami wirtualnymi:
    • Wirtualizacja na poziomie systemu operacyjnego, a nie sprzętu
    • Konteneryzacja jest lżejsza: kontener używa tego samego jądra (_kernel) co system hosta - mniejszy narzut
    • Konteneryzacja zapewnia gorszy stopień izolacji od hosta
    • Dzięki dostępnym narzędziom i infrastrukturze kontenery są łatwiejsze w utrzymywaniu i bardziej przenośne
  • Kontenery ułatwiają rozwój, uruchamianie i dostarczanie aplikacji.
  • Konteneryzacja to nie tylko Docker:
    • chroot
    • Solaris Containers
    • LXC (Linux Containers)
    • OpenVZ
    • Windows containers

1.1 Docker

  • Najpopularniejszy obecnie system konteneryzacji
  • Działa w m.in. na systemach Linux, Windows i Mac OS
  • Udostępnia narzędzia i serwisy ułatwiające korzystanie, zarządzanie i dzielenie się kontenerami
  • Docker umożliwia stworzenie paczki zawierającej sam program jak i środowisko, w którym ma być on uruchomiony
  • Dokumentacja: https://docs.docker.com/get-started/

1.1 Terminologia

  • _Container (kontener) - instancja obrazu. Może być uruchomiona, zatrzymana. Stan kontenera można zapisać tworząc nowy obraz. Uruchomienie kontenera zazwyczaj zajmuje mało czasu.
  • _Image (obraz) - niezmienny (readonly) przepis na stworzenie kontenera. Obrazy można ściągnąć lub udostępnić za pośrednictem rejestru. Budowanie obrazu trwa zazwyczaj długo.
  • _Dockerfile - plik tekstowy zawierający przepis na zbudowanie obrazu
  • _Docker registry - rodzaj repozytorium, przechowującego obrazy dockera
  • _Docker Hub - publiczne Docker registry, z którego każdy może pobrać potrzebne mu obrazy oraz publikować swoje

1.2 Jak zacząć?

1.3 Podstawowe polecenia

  • docker help [polecenie] - wyświetla listę dostępnych poleceń dockera lub opis podanego polecenia. Rozbudowana dokumentacja poleceń: https://docs.docker.com/engine/reference/commandline/docker/

  • docker run - uruchamia istniejący obraz tworząc kontener. Przykładowe wywołania:

    • docker run -i -t ubuntu
      Uruchamia kontener z obrazu "ubuntu", alokuje terminal (-t) i ustawia tryb "interaktywny" (-i), dzięki czemu dostajemy terminal "wenątrz" kontenera i możemy wywoływać w nim polecenia

    • docker run -p 8080:8080 jenkins/jenkins:lts
      Uruchomi kontener z Jenkinsem, w wersji "LTS". Jeśli obraz Jenkins nie był wcześniej zbudowany/pobrany na lokalną maszynę, automatycznie zostanie pobrany z Docker Hub. Port 8080 konenera zostanie powiązany z portem 8080 hosta. Dzięki temu będziemy mogli w przeglądarce dostać się do Jenkinsa pod adresem http://localhost:8080

  • docker build [OPTIONS] PATH | URL | - - buduje obraz na podstawie pliku Dockerfile i kontekstu (plików dostępnych podczas budowania). Przykład:
    • docker build -t tzietkiewicz/helloworld:1.0 .
      buduje obraz przekazując bieżący katalog (.) jako kontekst i korzystając z pliku Dockerfile znajdującego się tamże. Obraz zostanie otagowany (-t) tagiem tzietkiewicz/helloworld z wersją 1.0
  • docker images - listuje dostępne lokalnie obrazy
  • docker ps - listuje uruchomione/zatrzymane kontenery
  • docker stop CONTAINER_ID - zatrzymuje kontener. Uruchomione w nim procesy zostają wyłączone (SIGTERM -> SIGKILL)
  • docker start CONTAINER_ID - uruchamia ponownie kontener
  • docker system df - pokazuje miejsce zajęte przez obrazy, kontenery i woluminy
  • docker system prune - usuwa nieużywane obrazy i kontenery, zazwyczaj zwalniając sporo miejsca na dysku
  • docker exec - uruchamia polecenie wewnątrz działającego kontenera

1.4 Interakcja/komunikacja z kontenerem

  • Poprzez terminal
  • Poprzez docker exec - wywołanie polecenia w działającym kontenerze
  • Poprzez port zmapowany przez flagę -p (np. REST API albo interfejs w przegląrce)
  • Przez system plików:

Zadanie 1 [na zajęciach]

  1. Zainstaluj Docker (lub skorzystaj z https://laboratoria.wmi.amu.edu.pl/problemy/docker)
  2. Uruchom obraz ubuntu w trybie interaktywnym z terminalem (docker run -ti ubuntu)

Poniższe punkty wywołujemy wewnątrz kontenera (w otwartym właśnie terminalu):

  1. Jaka wersja Ubuntu działa w kontenerze? (polecenie cat /etc/issue)
  2. Jaki użytkownik wywołuje polecenia? (polecenie whoami)
  3. Jaki jest bieżący katalog? (polecenie pwd)
  4. Jaki procesor jest widoczny w środku kontenera? Czy jest to procesor Twojej maszyny? (lscpu)
  5. Ile wolnego miejsca na dysku jest widoczne wewnątrz kontenera (df -h)? Czy jest to zgodne z tym, co pokazuje maszyna hosta?
  6. Sprawdź, ile dostępnej pamięci RAM widać w konenerze (free -h). Czy zgadza się to z wynikiem na maszynie hosta?
  7. Spróbuj uruchomić jedno z popularnych narzędzi Linuksowych, dostępnych na maszynie hosta (np. vim, less, htop). Czy udało się je uruchomić?
  8. Zainstaluj w kontenerze jedno z brakujących narzędzi, np: apt update; apt install htop
  9. Uruchom następujące polecenie:
    while true; do date > time.log; sleep 1; done będzie ono co sekundę zapisywać bieżący timestamp w pliku time.log

Poniższe polecenia wykonujemy na zewnątrz kontenera (otwórz w tym celu nową konsolę):

  1. Sprawdź ID uruchomionego kontenera (docker ps) i zatrzymaj go (docker stop)
  2. Sprawdź co się stało w oknie z konsolą kontenera
  3. Uruchom kontener jeszcze raz (docker start). Gdzie jest nasza konsola?!!
  4. Żeby otrzymać konsolę, wykonaj jedno z poniższych:
    • zatrzymaj kontener i uruchom jeszcze raz z flagą -i
    • uruchom polecenie "bash" w kontenerze za pomocą polecenia docker exec -ti CONTAINER_ID bash - pozwala ono uruchomić dowolne polecenie w działającym kontenerze.
  5. Czy polecenie while true; do date > time.log; sleep 1; done wciąż działa? Sprawdź ostatni timestamp zapisany do pliku time.log

1.4 Dockerfile

  • Dokumentacja: https://docs.docker.com/engine/reference/builder/
  • Dockerfile składa się z serii poleceń.
  • Polecenia dockera są pisane WIELKIMI LITERAMI.
  • Wywołanie każdego polecenia tworzy nową warstwę (_layer).
  • Jeśli zbudowaliśmy obraz, a potem zmieniliśmy jedno z poleceń lub dodaliśmy nowe, to przebudowane zostaną tylko warstwy od zmienionej w dół (oszczędność czasu i zasobów).

Przykładowy Dockerfile:

# Nasz obraz będzie dziedziczył z obrazu Ubuntu w wersji latest
FROM ubuntu:latest

# Instalujemy niezbędne zależności. Zwróć uwagę na flagę "-y" (assume yes)
RUN apt update && apt install -y figlet

# Stwórzmy w kontenerze (jeśli nie istnieje) katalog /app i przejdźmy do niego (wszystkie kolejne polecenia RUN, CMD, ENTRYPOINT, COPY i ADD będą w nim wykonywane)
WORKDIR /app

# Skopiujmy nasz skrypt do katalogu /app w kontenerze
COPY ./figlet-loop.sh ./

# Domyślne polecenie, które zostanie uruchomione w kontenerze po jego starcie
CMD ./figlet-loop.sh

Zawartość pliku figlet-loop.sh:

#!/bin/bash
while read line; do
    figlet "$line"
done

Budujemy obraz:

docker build -t figlet-loop .
Sending build context to Docker daemon  3.072kB
Step 1/5 : FROM ubuntu:latest
 ---> 94e814e2efa8
Step 2/5 : RUN apt update && apt install -y figlet

 ---> Running in ba8b14deeeca
Get:1 http://archive.ubuntu.com/ubuntu bionic InRelease [242 kB]
[...]
Get:18 http://security.ubuntu.com/ubuntu bionic-security/universe amd64 Packages [1402 kB]
Fetched 22.3 MB in 3s (8343 kB/s)
Reading package lists...
Building dependency tree...
Reading state information...
[...]
Reading package lists...
Building dependency tree...
Reading state information...
The following NEW packages will be installed:
  figlet
0 upgraded, 1 newly installed, 0 to remove and 50 not upgraded.
Need to get 133 kB of archives.
After this operation, 752 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu bionic/universe amd64 figlet amd64 2.2.5-3 [133 kB]
debconf: delaying package configuration, since apt-utils is not installed
Fetched 133 kB in 0s (605 kB/s)
Selecting previously unselected package figlet.
(Reading database ... 4039 files and directories currently installed.)
Preparing to unpack .../figlet_2.2.5-3_amd64.deb ...
Unpacking figlet (2.2.5-3) ...
Setting up figlet (2.2.5-3) ...
update-alternatives: using /usr/bin/figlet-figlet to provide /usr/bin/figlet (figlet) in auto mode
update-alternatives: warning: skip creation of /usr/share/man/man6/figlet.6.gz because associated file /usr/share/man/man6/figlet-figlet.6.gz (of link group figlet) doesn't exist
Removing intermediate container ba8b14deeeca
 ---> 30470dc0bd47
Step 3/5 : WORKDIR /app
 ---> Running in 47ca74217790
Removing intermediate container 47ca74217790
 ---> 0f352dfc965d
Step 4/5 : COPY ./figlet-loop.sh ./
 ---> 450ba60dc50d
Step 5/5 : CMD ./figlet-loop.sh
 ---> Running in 38f83a71b1a9
Removing intermediate container 38f83a71b1a9
 ---> c6d81a065621
Successfully built figlet-loop:latest

Uruchamiamy:

docker run -ti figlet-loop
Hello World!
 _   _      _ _        __        __         _     _ _ 
| | | | ___| | | ___   \ \      / /__  _ __| | __| | |
| |_| |/ _ \ | |/ _ \   \ \ /\ / / _ \| '__| |/ _` | |
|  _  |  __/ | | (_) |   \ V  V / (_) | |  | | (_| |_|
|_| |_|\___|_|_|\___/     \_/\_/ \___/|_|  |_|\__,_(_)

Zamiast domyślnego polecenia (figlet-loop.sh) uruchommy bash, żeby sprawdzić, co jest w naszym kontenerze:

docker run -ti figlet-loop:latest bash
root@a44c68ce835e:/app# pwd
/app
root@a44c68ce835e:/app# ls -l
total 4
-rwxrwxr-x 1 root root 53 Mar 28 22:33 figlet-loop.sh
root@a44c68ce835e:/app# 

Zadanie 2. [10 pkt]

  1. Napisz prosty Dockerfile, który zdefinuje środowisko potrzebne do wywołania poleceń stworzonych na zajęciach "2. Dane". Przypuszczalnie wystarczy, że zainstalujesz zależności takie jak kaggle czy pandas, np:

    pip install --user kaggle
    pip install --user pandas
    

    Niech zdefiniowany przez Ciebie obraz dziedziczy z jakiegoś popoularneg obrazu, np. ubuntu Umieść Dockerfile w głównym katalogu repozytorium ze skryptami stworzonymi na zajęciach 2.

  2. Zbuduj obraz korzystając z polecenia docker build . Na końcu otrzymasz id obrazu, który powstał

  3. Uruchom kontener z terminalem docker run -ti IMAGE_ID podając ID obrazu, które otrzymałeś w poleceniu 2.

  4. Spróbuj uruchomić jeden z twoich skryptów w kontenerze. Jeśli brakuje jakiś zależności: możesz spróbować zainstalować je interaktywnie w konsoli. W ten sposób będziesz wiedział jakie polecenia dodać do Dockerfile.

  5. Plik Dockerfile dodaj do repozytorium

  6. Utwórz bezpłatne konto na https://hub.docker.com/

  7. Po zalogowaniu stwórz nowe repozytorium (https://hub.docker.com/repository/create), możesz nazwać je np. ium

  8. W ustawieniach docker hub wygeneruj "Access token" (https://hub.docker.com/settings/security)

  9. Uruchom docker login --username TWÓJ_DOCKER_ID i podaj stworzony w poprzednim kroku "Access token"

  10. Dodaj tag do obrazu stworzonego 2 punkcie 3. Dzięki temu będzie można go opublikować: docker tag IMAGE_ID TWÓJ_DOCKER_ID/ium:NUMER_WERSJI. Mogliśmy otagować obraz na etapie jego budowania (flaga -t), ale wtedy nie znaliśmy jeszcze nazwy użytkownika i repozytorium z Docker Hub.

  11. Teraz możesz wypchnąć swój otagowany obraz do repozytorium: docker push TWÓJ_DOCKER_ID/ium:NUMER_WERSJI

  12. Twój obraz powinien być widoczny na Docker Hub. Inni mogą go teraz znaleźć na Docker Hub, np.:
    https://hub.docker.com/r/tzietkiewicz/ium
    oraz uruchomić wywołując: docker run TWÓJ_DOCKER_ID/ium:NUMER_WERSJI, np.:
    docker run tzietkiewicz/ium

  13. Link do obrazu w Docker Hub wklej do arkusza ze zbiorami danych link

2. Integracja Docker-Jenkins

2.1 Jenkins

Jenkins może działać na wielu systemach operacyjnych:

  • Linux
  • Windows
  • MacOS

2.1 Jenkins - Terminologia (https://www.jenkins.io/doc/book/glossary/):

  • Controller: The central, coordinating process which stores configuration, loads plugins, and renders the various user interfaces for Jenkins
  • Agent: An agent is typically a machine, or container, which connects to a Jenkins controller and executes tasks when directed by the controller
  • Master: A deprecated term, synonymous with Controller.
  • Node: A machine which is part of the Jenkins environment and capable of executing Pipelines or Projects. Both the Controller and Agents are considered to be Nodes.
  • Executor: A slot for execution of work defined by a Pipeline or Project on a Node. A Node may have zero or more Executors configured which corresponds to how many concurrent Projects or Pipelines are able to execute on that Node.

Główna instancja Jenkinsa (tzw. _Controller aka. Master) może mieć podłączonych kilka węzłów ("nodes") typu "slave".

  • _Controller jest odpowiedzialny za interakcję z użytkownikiem, rozdzielanie zadań dla między agentów
  • Zadania ("builds") są wykonywane na jednym z "Agentów". Node, na którym jest uruchomiony "Controller" może działać również jako "Agent"
  • Każdy węzeł może działać pod kontrolą innego systemu operacyjnego. Dzięki temu możemy wykonywać zadania (albo ich części) w różnych środowiskach
  • Do definiowana gdzie może być wykonana dana część pipeline, służy sekcja agent / node. Poniższy kod wywoła sie tylko na węźle/węzłach "myAgent":
    node("myAgent") {
      stage("One"){
          echo 'hello'
      }
    }
    

2.2 Docker w Jenkins

  • Jenkins posiada wygodną intergrację z Dockerem
  • Umożliwia ona uruchamianie kroków (steps) wewnątrz kontera
  • Obrazy mogą być automatycznie pobrane albo zbudowane na podstawie Dockerfile
  • Więcej informacji: https://www.jenkins.io/doc/book/pipeline/docker/

2.2 Przykłady pipeline

Używanie gotowego obrazu:

  • Scrippted pipeline:
    node {
        docker.image('ubuntu:latest').inside {
            stage('Test') {
                sh 'cat /etc/issue'
            }
        }
    }
    
  • Declarative:
    pipeline {
        agent {
            docker { image 'ubuntu:latest' }
        }
        stages {
            stage('Test') {
                steps {
                    sh 'cat /etc/issue'
                }
            }
        }
    }
    

2.2 Przykłady pipeline

Budowanie obrazu z Dockerfile:

  • Scrippted pipeline:

    node {
        checkout scm
        //Pierwszy argument to tag, który zostania nadany zbudowanemu obrazowi
        //Jeśli chcemy użyć Dockerfile z innej ścieżki niż ./Dockerfile, możemy ją podać jako drugi argument
        def testImage = docker.build("test-image", "./dockerfiles/test") 
    
        //Wszystkie polecenia poniżej wykonają się w kontenerze, z podmontowanym Workspace Jenkinsa
        testImage.inside {
            sh 'make test'
        }
    }
    
  • Declarative:

    pipeline {
    agent { 
        dockerfile true 
    }
    stages {
        stage('Test') {
            steps {
                sh 'cat /etc/issue'
            }
        }
    }
    }
    

Zadanie 3. [5 pkt]

  1. Zmodyfikuj stworzony na poprzednich (3.) zajęciach Jenkinsfile opisujący pipeline "s123456-create-dataset" tak, żeby wywoływać w nim skrypty stworzone na zajęciach 2., w środku kontenera stworzonego w zadaniu 2.
    Skorzystaj z mechanizmu tworzącego kontener bezpośrednio na Jenkinsie wprost z pliku Dockerfile.

  2. Zmodyfikuj stworzony na poprzednich (3.) zajęciach Jenkinsfile opisujący pipeline "s123456-dataset-stats" tak, żeby wywoływać w nim skrypty stworzone na zajęciach 2., w środku kontenera stworzonego w zadaniu 2.
    Skorzystaj z mechanizmu wykorzystującego gotowy obraz (ściągany z Docker Hub)