dpzc-4/03_IaaS/03_04.ipynb
nlitkowski e248075771 a
2022-01-05 01:08:54 +01:00

27 KiB

Zadania do realizacji w czasie zajęć

W trakcie tych zajęć zautomatyzujemy instalację Wordpresa. Celem jest osiągnięcie tej samej konfiguracji co poprzednio, jednak tak aby wszystko zostało zainstalowane i uruchomione przy wykonaniu jednego skryptu.

Instalacja CLI

  1. Pobierz najnowsze wydanie _hcloud CLI: https://github.com/hetznercloud/cli/releases/latest/download/hcloud-linux-amd64.tar.gz. Pobrany plik rozpakuj

    tar -xvzf hcloud-linux-amd64.tar.gz
    

    Zweryfikuj działanie CLI:

    ./hcloud version
    

    Wskazówka: Pamiętaj aby plik rozpakować w miejscu, w którym możliwe jest uruchamianie plików wykonywalnych.

  2. Dla łatwiejszego wpisywania poleceń w zalecane jest skonfigurowanie odpowiedniego autouzupełniania składni:

    • bash:
      source <(./hcloud completion bash)
      
    • fish:
      ./hcloud completion fish | source
      
  3. Przed użyciem konieczne jest udzielenie uprawnień naszemu narzędziu konsolowemu do naszego konta. Jedyną dostępną opcją jest wykorzystanie _tokenu autoryzacyjnego.

    ./hcloud context create pzc
    

    Polecenie to zażąda od nas podania tokenu dostępowego. W tym celu należy wygenerować go korzystając z webowego interfejsu (projekt PZC -> zabezpieczenia -> tokeny API). Należy wybrać uprawnienia do odczytu i zapisu, bez tego nie będzie można tworzyć nowych zasobów.

    Uwaga: Należy pilnować bezpieczeństwa tokenu, co prawda nie daje on pełnych uprawnień do konta jak login i hasło, lecz wciaż umożliwia tworzenie dowolnych zasobów.

    Uwaga: Token zostanie zapisany w pliku ~/.config/hcloud/cli.toml. Tam też trafiają pozostałe opcje konfiguracyjne.

    Utworzony kontekst zostanie automatycznie aktywowany, od tego momentu wszystkie polecenia będą wykonywane w jego obrębie.

  4. Spróbujmy utworzyć nową maszynę wirtualną z wiersza poleceń.

    ./hcloud server create --image ubuntu-20.04 --type cx11 --name pzc-test-1 --location hel1
    

    Listę dostępnych lokalizacji, obrazów systemu, i typów instancji możesz uzyskać dzięki następującym poleceniom:

    ./hcloud locations list
    ./hcloud image lsit
    ./hcloud server-type list
    
  5. Przykładowo proces utworzenia maszyny wirtualnej może wyglądać następująco

    $ ./hcloud server create --image ubuntu-20.04 --type cx11 --name pzc-test-1 --location hel1
    4.551s [=================================] 100.00%
    Waiting for server 14328689 to have started
    ... done                                                                                                                                                                                     
    Server 14328689 created
    IPv4: 95.216.211.233
    Root password: 7eN7CihrEvbuViciCmsq
    

    Zweryfikuj w przeglądarce czy maszyna wirtualna została rzeczywiście utworzona.

  6. Dokladne informacje o istniejącej maszynie można uzyskać dzięki poleceniu:

    ./hcloud server describe pzc-test-1
    
  7. Gdy maszyna nie jest już potrzebna można ją usunać:

    ./hcloud server delete pzc-test-1
    

Wskazówka: W zadaniach domowych możesz założyć, że wszystkie te kroki zostały wykonane w katalogu /dev/shm oraz, ze aktywny jest odpowiedni kontekst.

Biblioteka Python

CLI jest bardzo wygodnym klasycznym rozwiązaniem, jednak nie zawsze jest wystarczająco elastyczne. Możliwy jest dostęp do zasobów chmurowych z poziomu języka programowania, większość dostawców dostarcza biblioteki dla najpopuparniejszych jezyków programowania.

Dla chmury Hetzner dostępna jest przykładowo biblioteka dostępowa w języku Python: https://github.com/hetznercloud/hcloud-python). Analogicznie jak w przypadku CLI, konieczne jest posiadanie tokenu dostępowego (może być ten sam co poprzednio). Poniższy fragment kodu tworzy instancję interfejsu dostępowego do chmury:

from hcloud import Client
client = Client(
    token="ftGv4HLU2FsZCD9JEfN8suRh1X4rHImwCOJHP4eKsntvxGyjHR39ADgzYVrbw7m5"
)

Analogicznie jak w CLI możemy utworzyć nową maszynę wirtualną:

from hcloud.images.domain import Image
from hcloud.server_types.domain import ServerType

response = client.servers.create(
    name="pzc-test", server_type=ServerType("cx11"), image=Image(name="ubuntu-20.04")
)
print(f"Utworzono serwer: {response.server.data_model.name} ({response.server.data_model.public_net.ipv4.ip})")
Utworzono serwer: pzc-test (95.217.183.63)

Po zakończeniu pracy można usunąć wszystkie stworzone serwery korzystająć z prostego skryptu:

servers = client.servers.get_all()
print(f"Usuwanie {len(servers)} serwerów")
for s in servers:
    action = client.servers.delete(s)
    print(f"Usuwanie serwera {s.data_model.name} ({s.data_model.public_net.ipv4.ip}): {action.data_model.status}")
Usuwanie 1 serwerów
Usuwanie serwera pzc-test (95.217.183.63): success

Automatyzacja instalacji Wordpress

Przejdziemy teraz do realizacji głównego zadania przewidzianego na dzisiejsze zajęcia. Analogicznie jak na poprzednich zajęciach chcemy uzyskać instalację Wordpresa wraz z konfigurowanym równoważeniem obciążenia, zgodnie z poniższym schematem:

Tworzenie instalacji Wordpress rozpoczynamy od dodania klucza SSH do naszego konta.

ssh_key = client.ssh_keys.create(name="pzc-ssh-key", public_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINXcCzwPDRJm+ICREkvIXRg6A0HEmasC5fLxdqK6P5zm bikol@spire")
print(f"Klucz {ssh_key.data_model.name} został dodany: {ssh_key.data_model.public_key}")
Klucz pzc-ssh-key został dodany: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINXcCzwPDRJm+ICREkvIXRg6A0HEmasC5fLxdqK6P5zm bikol@spire

Teraz przystępujemy do tworzenia właściwych zasobów. Zacznijmy od wirtualnej sieci:

from hcloud.networks.domain import NetworkSubnet

vnet = client.networks.create(
    name="pzc-test-vnet", 
    ip_range="10.10.10.0/24", 
    subnets=[
        NetworkSubnet(ip_range="10.10.10.0/24", network_zone="eu-central", type="cloud")
    ]
)
print(f"Utworzono sieć wirtualną {vnet.data_model.name} ({vnet.data_model.ip_range})")
Utworzono sieć wirtualną pzc-test-vnet (10.10.10.0/24)

Kolejnym etapem jest utworzenie maszyn wirtualnych. Skorzystamy z narzędzia cloud-init aby móc automatycznie nie tylko stworzyć maszynę, ale również zainstalować i uruchomić na niej porządane oprogramowanie. Konfiguracja z wykorzystaniem cloud-init pozwala zdefiniować jakie co ma być wykonane po utworzeniu maszyny wirtualnej. Ważne jest to, że wszystko zostanie wykonane automatycznie bez potrzeby logowania się przez SSH czy innej interakcji z serwerem. cloud-init pozwala na wykonanie dowolnych operacji na serwerze. Służy do tego sekcja runcmd, w której definiujemy polecenia jakie mają zostać wykonane w konsoli. Dla uproszczenia najczęściej wykonywane zadania mają przewidzianą osobną składnie. Przykładowo można

  • tworzyć nowych użytkowników i grupy
  • dodawać i modyfikować pliki
  • instalować nowe oprogramowania i dodawać nowe repozytoria
  • zarządzać zaufanymi certyfikatami
  • zarządzać zamontowanymi zasobami dyskowymi
  • zarządzać kluczami SSH

Na potrzeby tego zadania wykorzystamy tylko część z dostępnych możliwości. Ponadto wykorzystamy tylko jeden z wielu możliwych formatów konfiguracji cloud-init o nazwie cloud config.

cloud_init_db=r'''#cloud-config

# lista podstawowych pakietów, które należy zainstalować
packages:
  - apt-transport-https
  - ca-certificates
  - curl
  - gnupg-agent
  - software-properties-common

# tworzymy plik docker-compose.yml
write_files:
  - path: /root/docker-compose.yml
    content: |
        version: '3.9'

        services:
          db:
              image: mysql:5.7
              restart: always
              ports:
                - "10.10.10.2:3306:3306"
              environment:
                MYSQL_ROOT_PASSWORD: notSecureChangeMe
                MYSQL_DATABASE: wordpress
                MYSQL_USER: wordpress
                MYSQL_PASSWORD: wordpress
              volumes:
                - db_data:/var/lib/mysql

          phpmyadmin:
              image: phpmyadmin
              restart: always
              # przekierowanie portów zostawione tylko dla przykładu, nie należy tak robić na produkcji
              ports:
                - "8080:80"
        volumes:
          db_data: {}

# instalujemy docker, docker-compose a następnie uruchamiamy naszą bazę danych
runcmd:
  - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
  - add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  - apt-get update -y
  - apt-get install -y docker-ce docker-ce-cli containerd.io
  - curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  - chmod +x /usr/local/bin/docker-compose
  - systemctl start docker
  - systemctl enable docker
  - cd /root/ && docker-compose up -d
'''
from hcloud.locations.domain import Location

db_server = client.servers.create(
    name="db", 
    server_type=ServerType("cx11"), 
    image=Image(name="ubuntu-20.04"), 
    ssh_keys=[ssh_key], 
    networks=[vnet], 
    location=Location("hel1"), 
    user_data=cloud_init_db
)

db_server.action.wait_until_finished()
print(f"Tworzenie serwera db: {db_server.action.complete}")
Tworzenie serwera db: True

Serwer został utworzony, możemy pobrać z systemu jego aktualne dane (w tym adres IP).

Uwaga W skrypcie cloud-init przyjeliśmy, że maszyna będzie miała przydzielony adres IP 10.10.10.2, jeśli tak się nie stało to w dalszych krokach Wordpress może mieć problem z kominukacją z bazą danych.

db_server = client.servers.get_by_name("db")
print(f"Serwer: {db_server.data_model.name}\n\tpubliczne IP: {db_server.data_model.public_net.ipv4.ip}\n\tprywatne IP: {db_server.data_model.private_net[0].ip}")
Serwer: db
	publiczne IP: 135.181.201.125
	prywatne IP: 10.10.10.2

Na tym etapie możesz zweryfikować, że wszystko działa jak należy. Odwiedź w przeglądarce http://<publiczny adres IP>:8080.

Uwaga: Utworzenie maszyny wirtualnej trwa zaledwie kilka sekund, jednak wykonanie całego skryptu cloud-init może potrwać nawet kilka minut. Jeśli serwer nadal nie odpowiada pod wskazanym adresem, poczekaj kilka minut.

Wskazówka: Jeśli coś się nie powiodło i chcesz sprawdzić co poszło nie tak, zawsze możesz zalogować się na maszynę wirtualną korzystając z SSH. Warto wtedy sprawdzić zawartość pliku /var/log/cloud-init.log.

Przystępujemy do kolejnego kroku czyli utworzenia pierwszej instancji Wordpress. Skrypt cloud-init różni się tylko treścią pliku docker-compose.yml, pozostała część skryptu pozostaje bez zmian.

cloud_init_wp1=r'''#cloud-config

# lista podstawowych pakietów, które należy zainstalować
packages:
  - apt-transport-https
  - ca-certificates
  - curl
  - gnupg-agent
  - software-properties-common

# tworzymy plik docker-compose.yml
write_files:
  - path: /root/docker-compose.yml
    content: |
        version: '3.9'

        services:
          wordpress:
            image: wordpress:latest
            volumes:
              - wordpress_data:/var/www/html
            ports:
              - "8000:80"
            restart: always
            environment:
              WORDPRESS_DB_HOST: "10.10.10.2:3306"
              WORDPRESS_DB_USER: wordpress
              WORDPRESS_DB_PASSWORD: wordpress
              WORDPRESS_DB_NAME: wordpress
              
        volumes:
          wordpress_data: {}

# instalujemy docker, docker-compose a następnie uruchamiamy naszą bazę danych
runcmd:
  - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
  - add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  - apt-get update -y
  - apt-get install -y docker-ce docker-ce-cli containerd.io
  - curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  - chmod +x /usr/local/bin/docker-compose
  - systemctl start docker
  - systemctl enable docker
  - cd /root/ && docker-compose up -d
'''
wp1_server = client.servers.create(
    name="wordpress-1", 
    server_type=ServerType("cx11"), 
    image=Image(name="ubuntu-20.04"), 
    ssh_keys=[ssh_key], 
    networks=[vnet], 
    location=Location("hel1"), 
    user_data=cloud_init_wp1
)

wp1_server.action.wait_until_finished()
print(f"Tworzenie serwera wordpress-1: {wp1_server.action.complete}")
Tworzenie serwera wordpress-1: True
wp1_server = client.servers.get_by_name("wordpress-1")
print(f"Serwer: {wp1_server.data_model.name}\n\tpubliczne IP: {wp1_server.data_model.public_net.ipv4.ip}\n\tprywatne IP: {wp1_server.data_model.private_net[0].ip}")
Serwer: wordpress-1
	publiczne IP: 65.108.62.231
	prywatne IP: 10.10.10.3

Przejdź teraz w przeglądarce pod http://<publiczny adres IP serwera wordpress-1>:8000 i dokończ instalację Wordpress.

Uwaga: Z pewnością możliwe jest zautoamtyzowanie tego kroku, jednak wykracza to poza zakres tych zajęć.

Teraz musimy utworzyć migawkę tej maszyny wirtualnej a następnie użyć jej do utworzenia nowej maszyny wirtualnej.

response = wp1_server.power_off()
response.wait_until_finished()
print("Serwer wordpress-1 zatrzymany")

response = client.servers.create_image(
    server=wp1_server
)
response.action.wait_until_finished()
wp1_image = response.image
print(f"Utworzono migawkę serwera wordpress-1: {response.action.complete}")


response = wp1_server.power_on()
response.wait_until_finished()
print("Serwer wordpress-1 uruchomiony")
Serwer wordpress-1 zatrzymany
Utworzono migawkę serwera wordpress-1: True
Serwer wordpress-1 uruchomiony
wp2_server = client.servers.create(
    name="wordpress-2", 
    server_type=ServerType("cx11"), 
    image=wp1_image, # tutaj wykorzystujemy utworzoną wcześniej migawkę
    ssh_keys=[ssh_key], 
    networks=[vnet], 
    location=Location("hel1") # tym razem nie ma potrzeby podawać skryptu cloud-init
)
wp2_server.action.wait_until_finished()
print(f"Tworzenie serwera wordpress-2: {wp2_server.action.complete}")
Tworzenie serwera wordpress-2: True
wp2_server = client.servers.get_by_name("wordpress-2")
print(f"Serwer: {wp2_server.data_model.name}\n\tpubliczne IP: {wp2_server.data_model.public_net.ipv4.ip}\n\tprywatne IP: {wp2_server.data_model.private_net[0].ip}")
Serwer: wordpress-2
	publiczne IP: 95.217.167.221
	prywatne IP: 10.10.10.4

Ostatnim krokiem jest konfiguracja mechanizmu równoważenia obciążenia.

from hcloud.load_balancer_types.domain import LoadBalancerType
from hcloud.load_balancers.domain import LoadBalancerAlgorithm, LoadBalancerServiceHttp, LoadBalancerHealthCheck, LoadBalancerService, LoadBalancerHealtCheckHttp, LoadBalancerTarget

lb = client.load_balancers.create(
    name="lb-wp", 
    load_balancer_type=LoadBalancerType(name="lb11"),
    location=Location("hel1"),
    services=[
        LoadBalancerService(
            protocol="http", 
            listen_port=8000, 
            destination_port=8000, 
            proxyprotocol=False, 
            health_check=LoadBalancerHealthCheck(
                protocol="http", 
                port="8000", 
                interval=15, 
                timeout=10, 
                retries=3, 
                http=LoadBalancerHealtCheckHttp(
                    path="/", 
                    status_codes=["2??", "3??"], 
                    tls=False
                )
            ),
            http=LoadBalancerServiceHttp(
                sticky_sessions=True,
                cookie_name="HCLBSTICKY",
                cookie_lifetime=300,
                certificates=[]
            )
        )
    ],
    targets=[
        LoadBalancerTarget(
            type="server",
            server=wp1_server,
            use_private_ip=True
        ), 
        LoadBalancerTarget(
            type="server",
            server=wp2_server,
            use_private_ip=True
        )
    ],
    public_interface=True,
    network=vnet
)
lb.action.wait_until_finished()
print(f"Mechanizm równoważenia obciążenia został utworzony: {lb.action.complete}")
Mechanizm równoważenia obciążenia został utworzony: True
lb = client.load_balancers.get_by_name("lb-wp")
print(f"Publiczny adres IP: {lb.data_model.public_net.ipv4.ip}")
Publiczny adres IP: 95.217.169.62

Zweryfikuj instalację w przeglądarce pod adresem: http://<publiczny adres IP LB>:8000.

Udało się uzyskać konfigurację analogiczną jak poprzednio, tym razem jednak całość została wykonana za pomocą skryptu w języku Python. Pokazuje to jak bardzo elastyczne może być pracowanie w chmurze w modelu IaaS.

Po zakończeniu pracy rekomenduje usunięcie wszystkich utworzonych zasobów.

lbs = client.load_balancers.get_all()
print(f"Usuwanie {len(lbs)} mechanizmów LB")
for s in lbs:
    action = client.load_balancers.delete(s)
    print(f"\tUsuwanie LB {s.data_model.name}: {action}")

images = client.images.get_all(type="snapshot")
print(f"Usuwanie {len(images)} migawek")
for s in images:
    action = client.images.delete(s)
    print(f"\tUsuwanie migawki {s.data_model.name}: {action}")

servers = client.servers.get_all()
print(f"Usuwanie {len(servers)} serwerów")
for s in servers:
    action = client.servers.delete(s)
    print(f"\tUsuwanie serwera {s.data_model.name} ({s.data_model.public_net.ipv4.ip}): {action.data_model.status}")

ssh_keys = client.ssh_keys.get_all()
print(f"Usuwanie {len(ssh_keys)} kluczy SSH")
for s in ssh_keys:
    action = client.ssh_keys.delete(s)
    print(f"\tUsuwanie klucza {s.name}: {action}")

vnets = client.networks.get_all()
print(f"Usuwanie {len(vnets)} sieci wirtualnych")
for s in vnets:
    action = client.networks.delete(s)
    print(f"\tUsuwanie sieci wirtualnej {s.name}: {action}")

volumes = client.volumes.get_all()
print(f"Usuwanie {len(vnets)} wolumenów")
for v in volumes:
    action = client.volumes.delete(v)
    print(f"\tUsuwanie wolumenu {v.name}: {action}")
Usuwanie 0 mechanizmów LB
Usuwanie 0 migawek
Usuwanie 0 serwerów
Usuwanie 0 kluczy SSH
Usuwanie 0 sieci wirtualnych
Usuwanie 0 wolumenów
	Usuwanie wolumenu pb-volume: True
	Usuwanie wolumenu s470627: True
	Usuwanie wolumenu s434804-volume: True
	Usuwanie wolumenu s434704-gitea-volume: True