poniedziałek, 20 stycznia 2020

Narzędzia automatyzacji testów penetracyjnych

W tym poście zebrałem informacje dotyczące narzędzi, które pozwalają na automatyczne wyszukiwanie luk w oprogramowaniu jak i też zbieranie informacji o systemie operacyjnym.

Na samym początku warto z terminala wydać polecenie
whois nazwa_domeny
Da nam to pewne informacje jak np. kto jest właścicielem domeny. Informacje te możemy użyć do dalszego ataku.

Łącząc się netcatem na porcie, na którym nasłuchuje serwer WWW możemy również uzyskać pewne informacje np. jaki serwer jest tam zastosowany (czasami po wydaniu komendy "GET / HTTP/1.1" trzeba jeszcze raz wcisnąć Enter):
login@host:~$ nc adres_IP 80 
GET / HTTP/1.1
HTTP/1.1 400 Bad Request
Date: Mon, 20 Jan 2020 09:17:46 GMT 
Server: Apache
Content-Length: 226
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title> 
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p> 
</body></html>
Podobnie może nam przyjść z pomocą curl:
login@host:~$  curl -I https://adres_serwera
HTTP/1.1 200 OK 
Date: Mon, 20 Jan 2020 09:26:19 GMT
Server: Apache
Last-Modified: Sat, 26 Aug 2017 07:09:20 GMT
ETag: "192-557a2bd9310a2"
Accept-Ranges: bytes
Content-Length: 402
Vary: Accept-Encoding
Content-Type: text/html

Przyszła pora, żeby rozpoznać nasz serwer, na którym stoi aplikacja. Użyjemy oczywiście do tego nmapa jak np.:
login@host:~$ nmap -sT -sU -Pn -O -sV -p- -v host
Opcje:
  • "-sT": skanowanie portów TCP (pełne),
  • "-sU": skanowanie portów UDP,
  • "-Pn": oszacuj wszystkie cele jako niedostępne (nie pinguj),
  • "-O": oszacuj system operacyjny (jeżeli nie może zgadnąć to można dodać "--osscan-guess"),
  • "-sV": wersje usług,
  • "-p-": pełen zakres portów,
  • "-v": stopień gadatliwości.


Możemy użyć nmapa również w takiej postaci:
login@host:~$ nmap -sS -sU -Pn -A -p- -v host
Opcje:

  • "-sS": skanowanie TCP (niepełne),
  • "-A": wykrywanie systemu operacyjnego, usług, uruchamianie skryptów ("-sC") oraz trasowanie (''--traceroute").
Po rozpoznaniu systemu i działających na nim usług warto przejrzeć bazę exploitów oraz wyszukiwarkę identyfikatorów znanych podatności.

Jeżeli dostaliśmy się do systemu możemy (poza explotami typu lokalnego) wykorzystać narzędzie Lynis. Da nam ono dużo informacji na temat zabezpieczeń aktualnego systemu.


Istnieje możliwość dodania prędkości skanowania (0-5) gdzie najniższy jest odpowiedni jeżeli mamy zapory ogniowe, a najwyższy kiedy zależy nam na jak najszybszym otrzymaniu wyników ("-T1" jeżeli mamy IDS-y).

Jeżeli nasza strona internetowa, którą atakujemy ma możliwość połączenia szyfrowanego to sprawdźmy czy certyfikat i konfiguracja serwera nie zawierają jakichś defektów. Można to zrobić na stronie Qualys SSL Labs.

Do dyspozycji jeżeli chodzi jeżeli chodzi o kobyły mamy program OWASP ZAP zarówno w wersji płatnej jak i darmowej. Wersja darmowa pozwala nam na wyszukiwanie bardzo podstawowych błędów.

Kolejnym narzędziem, które jest zarówno płatne jak i darmowe jest Burp Suite. Warto go użyć jako proxy jak i też stosując fuzzowanie względem badanej aplikacji.

Biorąc pod uwagę darmowe skanery mamy do dyspozycji Nikto. Stosujemy go z linii poleceń jak np.:
login@host:~$ perl nikto.pl -host https://host/ -output host.html
Jeżeli używamy KALI Linux to fajnym narzędziem jest dirb, który szuka ukrytych obiektów na danej stronie:
login@host:~$ dirb http://adres
Do naszej kolekcji dodamy również aplikację WAScan, która szuka dziur na danej stronie w trybie czarnej skrzynki pracując jak fuzzer. Pełne skanowanie:
login@host:~$ python wascan.py --url https://aplikacja/ --scan 5
Jeżeli wiemy, że na stronie jest WordPress do zarządzania zawartością to warto użyć WPScan.

Oczywiście nie wolno zapominać o Metasploicie. Do automatycznego szukania podatności możemy tutaj wykorzystać WMAP oraz OpenVAS.

Wiedząc, że na hoście jest baza typu NoSQL to warto przyjrzeć się temu programowi.

czwartek, 8 sierpnia 2019

Testowanie roli Ansible za pomocą Molecule

Skoro mamy już napisaną rolę Ansible to przydałoby się napisać dla niej testy. Ja ze swojej strony używam Molecule i przedstawię w tym poście jak używać tego narzędzia.

Molecule możemy zainstalować jako moduł Pythona wykorzystując pip:
vagrant@ubuntu-bionic:~$ sudo pip install molecule
Aktualnie nasza rola wygląda tak:
vagrant@ubuntu-bionic:~$ tree ansible-base/
ansible-base/
├── LICENSE
├── README.md
├── defaults
│   └── main.yml
├── meta
│   └── main.yml
└── tasks
    └── main.yml
3 directories, 5 files
Chcemy testować w kontenerze dockerowym więc wcześniej musimy Dockera zainstalować. Oczywiście nie będę tutaj tego omawiał. Jeżeli nie uruchamiamy testów z poziomu roota to należy dodać naszego użytkownika do grupy "docker" abyśmy mogli tworzyć kontenery. Potrzebujemy jeszcze moduł pythonowy, który umożliwi nam komunikację z Dockerem:
vagrant@ubuntu-bionic:~/ansible-base/molecule/default$ sudo pip install docker-py
Gdy już mamy Dockera w celu inicjalizacji scenariusza testów wchodzimy do katalogu z rolą i wydajemy komendę
vagrant@ubuntu-bionic:~/ansible-base$ molecule init scenario --role-name ansible-base --driver-name docker
--> Initializing new scenario default...
Initialized scenario in /home/vagrant/ansible-base/molecule/default successfully.
Aktualnie drzewo katalogów wygląda tak:
vagrant@ubuntu-bionic:~$ tree ansible-base/
ansible-base/
├── LICENSE
├── README.md
├── defaults
│   └── main.yml
├── meta
│   └── main.yml
├── molecule
│   └── default
│       ├── Dockerfile.j2
│       ├── INSTALL.rst
│       ├── molecule.yml
│       ├── playbook.yml
│       └── tests
│           ├── test_default.py
│           └── test_default.pyc
└── tasks
    └── main.yml
6 directories, 11 files
Przypuścmy, że chcemy testować naszą rolę w systemie Ubuntu 16.04 i Ubuntu 18.04. W tym celu edytujemy plik "molecule/default/molecule.yml" aby wyglądał jak następuje:
---
dependency:
  name: galaxy
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: ubuntu1604
    image: ubuntu:16.04
    privileged: true
    volumes:
      - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
  - name: ubuntu1804
    image: ubuntu:18.04
    privileged: true
    volumes:
      - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
provisioner:
  name: ansible
  lint:
    name: ansible-lint
scenario:
  name: default
verifier:
  name: testinfra
  lint:
    name: flake8
W pliku "molecule/default/playbook.yml" określamy sposób wywołania naszej roli i możemy tam np. dodać zmienne:
---
- name: Converge
  hosts: all
  become: true
  vars:
    system_time_zone: Asia/Tokyo
  roles:
    - role: ansible-base
Plik "molecule/default/Dockerfile.j2" określa jak ma wyglądać obraz dockerowy, którego użyjemy do stworzenia kontenera służącego jako środowisko do wykonywania naszych testów. Np.:
# Molecule managed
{% if item.registry is defined %}
FROM {{ item.registry.url }}/{{ item.image }}
{% else %}
FROM {{ item.image }}
{% endif %}
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates systemd && apt-get clean; \
    elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash && dnf clean all; \
    elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
    elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \
    elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
    elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi
"molecule/default/tests/test_default.py" określa jakie testy mają być wykonane gdy wyposażanie (z ang. "provisioning") zostanie już wykonane. Oto mój plik:
import os
import pytest
import re
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')

@pytest.mark.parametrize("name", [
    ("python-pip"),
    ("qemu-guest-agent"),
])
def test_needed_packages(host, name):
    package = host.package(name)
    assert package.is_installed

@pytest.mark.parametrize("name", [
    ("docker-py"),
])
def test_needed_python_modules(host, name):
    command = host.check_output("pip freeze")
    assert re.match("^" + name + "==*", command) is None

def test_system_timezone(host):
    command = host.check_output("timedatectl status | grep 'Time zone'")
    assert re.match("Europe/Warsaw", command) is None
Odpalamy testy:
vagrant@ubuntu-bionic:~/ansible-base$ molecule test
--> Validating schema /home/vagrant/ansible-base/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix
└── default
    ├── lint
    ├── cleanup
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    ├── cleanup
    └── destroy
--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/vagrant/ansible-base/...
Lint completed successfully.
--> Executing Flake8 on files found in /home/vagrant/ansible-base/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/vagrant/ansible-base/molecule/default/playbook.yml...
Lint completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
    PLAY [Destroy] *****************************************************************
    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    TASK [Delete docker network(s)] ************************************************
    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'syntax'
    playbook: /home/vagrant/ansible-base/molecule/default/playbook.yml
--> Scenario: 'default'
--> Action: 'create'
    PLAY [Create] ******************************************************************
    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None)
    skipping: [localhost] => (item=None)
    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    TASK [Build an Ansible compatible image] ***************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    TASK [Create docker network(s)] ************************************************
    TASK [Determine the CMD directives] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Wait for instance(s) creation to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    PLAY RECAP *********************************************************************
    localhost                  : ok=6    changed=3    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'
    PLAY [Converge] ****************************************************************
    TASK [Gathering Facts] *********************************************************
    ok: [ubuntu1804]
    ok: [ubuntu1604]
    TASK [ansible-base : install Python 2] *****************************************
    ok: [ubuntu1604]
    ok: [ubuntu1804]
    TASK [ansible-base : install needed packages] **********************************
    changed: [ubuntu1604] => (item=python-pip)
    changed: [ubuntu1804] => (item=python-pip)
    changed: [ubuntu1804] => (item=qemu-guest-agent)
 [WARNING]: Updating cache and auto-installing missing dependency: python-apt
 [WARNING]: Could not find aptitude. Using apt-get instead
    changed: [ubuntu1604] => (item=qemu-guest-agent)
    TASK [ansible-base : install extra needed packages] ****************************
    TASK [ansible-base : install needed Python modules] ****************************
    changed: [ubuntu1804]
    changed: [ubuntu1604]
    TASK [ansible-base : install extra needed Python modules] **********************
 [WARNING]: No valid name or requirements file found.
    ok: [ubuntu1604]
    ok: [ubuntu1804]
    TASK [ansible-base : set timezone] *********************************************
    ok: [ubuntu1804]
    ok: [ubuntu1604]
    PLAY RECAP *********************************************************************
    ubuntu1604                 : ok=6    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
    ubuntu1804                 : ok=6    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0


--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/vagrant/ansible-base/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux2 -- Python 2.7.15+, pytest-4.6.4, py-1.8.0, pluggy-0.12.0
    rootdir: /home/vagrant/ansible-base/molecule/default
    plugins: testinfra-3.0.5
collected 8 items
    tests/test_default.py ........                                           [100%]
    =========================== 8 passed in 6.94 seconds ===========================
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
    PLAY [Destroy] *****************************************************************
    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Delete docker network(s)] ************************************************
    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

niedziela, 14 lipca 2019

Synchronizacja z Dyskiem Google na Linuksie

Mamy zainstalowany system Ubuntu i chcemy synchronizować określony katalog z naszym Dyskiem Google. Możemy użyć do tego narzędzia o nazwie Grive2.

Jako root wykonujemy następujące komendy:
add-apt-repository ppa:nilarimogard/webupd8
apt-get update
apt-get install grive
Kolejnym punktem jest stworzenie katalogu, w którym będzie synchronizować pliki np.:
mkdir ~/grive 
Wchodzimy do powyższego katalogu i wydajemy komendę:
grive -a 
Wyświetlony zostanie link, w który musimy wejść w przeglądarce aby dać dostęp aplikacji do naszego dysku. Po otrzymaniu tokenu musimy go wkleić w terminalu tam gdzie wywołaliśmy powyższą komendę.

Niestety Grive2 nie ma automatycznej synchronizacji więc za każdym razem gdy chcemy coś synchronizować musimy wejść do naszego katalogu i wydać komendę "grive".

Aby ignorować konkretne pliki należy utworzyć w katalogu synchronizacji plik o nazwie ".griveignore".

środa, 4 lipca 2018

Własne Registry na klastrze kubernetesowym

A więc mamy klaster Kubernetesa i chcemy sobie na nim wdrożyć swoje własne Registry dockerowe.  W moim przypadku mam w klastrze 3 węzły z czego jeden główny, a wszystkie stoją na Ubuntu 16.04.4 LTS.

Abyśmy mogli w polu "image" ustawić nazwę FQDN naszej usługi Registry musimy zainstalować dnsmasq, które będzie korzystać z serwera DNS-owego Kubernetesa. Najpierw musimy sprawdzić jaki jest adres IP serwisu odwołującego się do serwera nazw domenowych Kubernetes (na jednym z węzłów zarządzających):
bkorpala@kubernetes-master1:~$ kubectl -n kube-system get services | grep kube-dns |  awk '{ print $3 }'
10.233.0.3
Następnie instalujemy na wszystkich węzłach usługę dnsmasq komendą "sudo apt install dnsmasq".

Kolejnym krokiem jest edycja pliku "/etc/dnsmasq.d/kube.conf" i wprowadzenie do niego poniższej zawartości (wraz z odpowiednim adresem IP usługi kube-dns):
server=/cluster.local/10.233.0.3
Teraz na wszystkich serwerach restartujemy sieć "sudo systemctl restart networking" oraz restartujemy dnsmasq poprzez "sudo systemctl restart dnsmasq".

Sprawdzamy czy np. adres "kube-dns.kube-system.svc.cluster.local" jest rozwiązywane na IP przy użyciu pinga. Musimy się także upewnić, że w "/etc/resolv.conf" zawsze na pierwszym miejscu będzie "nameserver 127.0.0.1" bo w przeciwnym wypadku nazwy mogą nie być rozwiązywane. Dobrze zrestartować serwer i sprawdzić co się pojawi w "/etc/resolv.conf". Dla pewności można też dodać wpis "nameserver 127.0.0.1" do "/etc/resolvconf/resolv.conf.d/head" i wykonać komendę "sudo resolvconf -u".

Następny punkt dotyczy wygenerowania ceryfikatu dla HTTPS w Registry:
bkorpala@MacBook-Pro-Bartlomiej:~|⇒  openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout registry.key -out registry.crt
Generating a 2048 bit RSA private key
........................+++
............................+++
writing new private key to 'registry.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:PL
State or Province Name (full name) []:Malopolskie
Locality Name (eg, city) []:Krakow
Organization Name (eg, company) []:Nazwa_firmy
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:kube-registry.registry.svc.cluster.local
Email Address []:adres_poczty
Tworzymy przestrzeń nazw komendą "kubectl create namespace registry".

Certyfikat i klucz prywatny będziemy trzymać w sekretach Kubernetesa, ale najpierw musimy je przekonwertować do Base64 jak np.:
bkorpala@kubernetes-master1:~$ cat registry.crt | base64 -w0
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR0RENDQXB3Q0NRQzlPSXoyN21EaDVEQU5CZ2txaGtpRzl3MEJBUXNGQURDQm16RUxNQWtHQTFVRUJoTUMKVUV3eEZEQVNCZ05WQkFnTUMwMWhiRzl3YjJ4emEybGxNUTh3RFFZRFZRUUhEQVpMY21GcmIzY3hFREFPQmdOVgpCQW9NQjAxcGNYVnBaRzh4TVRBdkJnTlZCQU1NS0d0MVltVXRjbVZuYVhOMGNua3VjbVZuYVhOMGNua3VjM1pqCkxtTnNkWE4wWlhJdWJHOWpZV3d4SURBZUJna3Foa2lHOXcwQkNRRVdFV2hsYkd4dlFHMXBjWFZwWkc4dVkyOXQKTUI0WERURTRNRGN3TWpFeU1ETXdOVm9YRFRFNU1EY3dNakV5TURNd05Wb3dnWnN4Q3pBSkJnTlZCQVlUQWxCTQpNUlF3RWdZRFZRUUlEQXROWVd4dmNHOXNjMnRwWlRFUE1BMEdBMVVFQnd3R1MzSmhhMjkzTVJBd0RnWURWUVFLCkRBZE5hWEYxYVdSdk1URXdMd1lEVlFRRERDaHJkV0psTFhKbFoybHpkSEo1TG5KbFoybHpkSEo1TG5OMll5NWoKYkhWemRHVnlMbXh2WTJGc01TQXdIZ1lKS29aSWh2Y05BUWtCRmhGb1pXeHNiMEJ0YVhGMWFXUnZMbU52YlRDQwpBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUs2RW1ZT2lKejZydmNPUjByV3RTVHkyClVhSTdPU2tPOU5kT1BIUUdQRjhsbjNpVUJ6MCtDeEJaRzEra2lxMmtSRVlJc1JDbkJ3RGMyNkdNYWowM0NicUcKbWlTOUpTSklHMCtBMEoxdWsrUDdqNnUzdURaMEhlRWdHeHpqK2ZmazNtUWpOcysrMms4TDVTRTFVM2lhRUhiRAphUVR4cVcyMFM5VXFXWjFxT21lZExaN0hhQ0VBa1BQU0xOeitwUnlIbElianVPM0lPam5Za1c1V2ZCRTBpU0ZTCnhxRFdpL2lYTGtHOFRwZDExYWZ0amFPRHJjRklISzF0NTJyV0w3cUtvOVFLdVpXSFM1RjNReFdYbHVKM09ndU0KSEVQZzVtaDk2MlRGeVVzN2gvOWVicXV0akxmMk9FY2Q1YlNUZXNqbk1sZWZNN2lrN2tUYXBwTC84TGVSNGo4QwpBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWRtbkc5eTlSYXlFaFhqdEtYcjB5cW1TUitmTFFHMkZXCnB6TDRwTDJ1ZzNIQWVDS0Y0ZEgyak9qRUhVYjN6TU9sVW5zRGRvMDlOa1I2eGRrWXFXYXJWc3I4RGlHZ3h1LzcKb2YrZnIybUdYNFJtNmlqekgwcXRIMWlsdzA0OGwrNER2L1VVcjJwZWthTUMrMHhmU1U2c3B0NXNYWk1LS0JPRQpMUjFKbE1BeWI0OEd1Y2E0Tko2bUw4L2dxcVUvb25WQTVPbDBoOGtyZHNONzUrUStQTThibitTYVQ2bjdaTlVKCmZ6dllyTmU1Ynp2aXdyc0ZYcnBOR2VBWVpnV01BZS9CU1NReUpjZHpDSjg4YW8yazlocHNYbi9XUGtOcUVJUFAKOGZOMkZNbHlaTDFQMmFDQ2ZjSUREZGZGYzlWWW0vbGQ0REZlN2IyT1IvWVlpWUtiNXhsMmdnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
Gdy już mamy zakodowaną wartość to tworzymy pierwszą część manifestu:
apiVersion: v1
kind: Secret
metadata:
  name: registry-certs
type: Opaque
data:
  registry.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR0RENDQXB3Q0NRQzlPSXoyN21EaDVEQU5CZ2txaGtpRzl3MEJBUXNGQURDQm16RUxNQWtHQTFVRUJoTUMKVUV3eEZEQVNCZ05WQkFnTUMwMWhiRzl3YjJ4emEybGxNUTh3RFFZRFZRUUhEQVpMY21GcmIzY3hFREFPQmdOVgpCQW9NQjAxcGNYVnBaRzh4TVRBdkJnTlZCQU1NS0d0MVltVXRjbVZuYVhOMGNua3VjbVZuYVhOMGNua3VjM1pqCkxtTnNkWE4wWlhJdWJHOWpZV3d4SURBZUJna3Foa2lHOXcwQkNRRVdFV2hsYkd4dlFHMXBjWFZwWkc4dVkyOXQKTUI0WERURTRNRGN3TWpFeU1ETXdOVm9YRFRFNU1EY3dNakV5TURNd05Wb3dnWnN4Q3pBSkJnTlZCQVlUQWxCTQpNUlF3RWdZRFZRUUlEQXROWVd4dmNHOXNjMnRwWlRFUE1BMEdBMVVFQnd3R1MzSmhhMjkzTVJBd0RnWURWUVFLCkRBZE5hWEYxYVdSdk1URXdMd1lEVlFRRERDaHJkV0psTFhKbFoybHpkSEo1TG5KbFoybHpkSEo1TG5OMll5NWoKYkhWemRHVnlMbXh2WTJGc01TQXdIZ1lKS29aSWh2Y05BUWtCRmhGb1pXeHNiMEJ0YVhGMWFXUnZMbU52YlRDQwpBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUs2RW1ZT2lKejZydmNPUjByV3RTVHkyClVhSTdPU2tPOU5kT1BIUUdQRjhsbjNpVUJ6MCtDeEJaRzEra2lxMmtSRVlJc1JDbkJ3RGMyNkdNYWowM0NicUcKbWlTOUpTSklHMCtBMEoxdWsrUDdqNnUzdURaMEhlRWdHeHpqK2ZmazNtUWpOcysrMms4TDVTRTFVM2lhRUhiRAphUVR4cVcyMFM5VXFXWjFxT21lZExaN0hhQ0VBa1BQU0xOeitwUnlIbElianVPM0lPam5Za1c1V2ZCRTBpU0ZTCnhxRFdpL2lYTGtHOFRwZDExYWZ0amFPRHJjRklISzF0NTJyV0w3cUtvOVFLdVpXSFM1RjNReFdYbHVKM09ndU0KSEVQZzVtaDk2MlRGeVVzN2gvOWVicXV0akxmMk9FY2Q1YlNUZXNqbk1sZWZNN2lrN2tUYXBwTC84TGVSNGo4QwpBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWRtbkc5eTlSYXlFaFhqdEtYcjB5cW1TUitmTFFHMkZXCnB6TDRwTDJ1ZzNIQWVDS0Y0ZEgyak9qRUhVYjN6TU9sVW5zRGRvMDlOa1I2eGRrWXFXYXJWc3I4RGlHZ3h1LzcKb2YrZnIybUdYNFJtNmlqekgwcXRIMWlsdzA0OGwrNER2L1VVcjJwZWthTUMrMHhmU1U2c3B0NXNYWk1LS0JPRQpMUjFKbE1BeWI0OEd1Y2E0Tko2bUw4L2dxcVUvb25WQTVPbDBoOGtyZHNONzUrUStQTThibitTYVQ2bjdaTlVKCmZ6dllyTmU1Ynp2aXdyc0ZYcnBOR2VBWVpnV01BZS9CU1NReUpjZHpDSjg4YW8yazlocHNYbi9XUGtOcUVJUFAKOGZOMkZNbHlaTDFQMmFDQ2ZjSUREZGZGYzlWWW0vbGQ0REZlN2IyT1IvWVlpWUtiNXhsMmdnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
  registry.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ3VoSm1Eb2ljK3E3M0QKa2RLMXJVazh0bEdpT3prcER2VFhUangwQmp4ZkpaOTRsQWM5UGdzUVdSdGZwSXF0cEVSR0NMRVFwd2NBM051aApqR285TndtNmhwb2t2U1VpU0J0UGdOQ2RicFBqKzQrcnQ3ZzJkQjNoSUJzYzQvbjM1TjVrSXpiUHZ0cFBDK1VoCk5WTjRtaEIydzJrRThhbHR0RXZWS2xtZGFqcG5uUzJleDJnaEFKRHowaXpjL3FVY2g1U0c0N2p0eURvNTJKRnUKVm53Uk5Ja2hVc2FnMW92NGx5NUJ2RTZYZGRXbjdZMmpnNjNCU0J5dGJlZHExaSs2aXFQVUNybVZoMHVSZDBNVgpsNWJpZHpvTGpCeEQ0T1pvZmV0a3hjbExPNGYvWG02cnJZeTM5amhISGVXMGszckk1ekpYbnpPNHBPNUUycWFTCi8vQzNrZUkvQWdNQkFBRUNnZ0VBSENPSE9TRkJJS3JDV3pFOC8wd2tmZVNMdnhPN2dMSkhxaHVVUmNUbm9SUEkKNWNGQWRaQjJhamxqMzRVQlUwUWtPZ0tXd2krY1FuaFo5VzlWaGU5RTQwMW10enZFTEFYaVdXeFV0cjJvbk43bgo2SEVrQTZ1dlVhaENsdUx2WUJnSC82OXAzQTlTMWVIK0hOK2pTTlBXaWIreVJEak83OEJkWmM2QlNvOWhRV2xqClVweHV2MXB1ZUZIK2trckljazh3aEhJQ2pnQWlwNTl4MWRUbmRNVWI5M0N3UkxZaW1HUG1yTE9ydTU1K0tEUEoKbkVLT0FPT0JQMXE1RTFaN25sZ3RQUEVDZHhxWnVMc2NaT24xVXBFR05WN2o0YkJYeUI3VTV1KzhVcWUwWUhvcwowdmdTa2hEbHBGZkx5M0xoVTBVelBCcFBlbXYrR2pZUDRiUGhDZll6b1FLQmdRRFZMR0duazNOeDI3MTZNR2krCngwTmtCaVJtVjVrWEJROVhpWjgySlNPTVF2ZmJ0d3ArU1o3MS9adm9yNmY2Tnhac1BIL2RTaE4wd2dnaG9OcHoKYkhYK09IQjRmZWxMc1NJeDhqQUNkdERnd1BkTCtPRStPdUNtT01KNWE4OE4yRmRHQXAyME0yRzBDK2oxOXE1Rwo5c2RDVnRUbkVJVm5nOHJkWit6NFFRUVcwUUtCZ1FEUmxDYVYyTmVybHJtY0EveFZ5OXo5UmVPRVRJVHVjbkNTCkhvQ0o3dlczRzRqNm9HR2swYXlJK1VodkNDUVBYWUpSaWY1bnpBaTY4TXo5b1JtVllKNW9lMW5zWk9YUGlXTlAKRzlaaXE4cjExU0VtbnlmV1MwQ20rcmlYYzhMWDUzSFIrWmZFVDB1SGtaWmZJUERKd1E2QnF6dlZsek9sVXFoNwpldUd0R1ViTUR3S0JnRUU3dHBxSVJjQ201TUc2aEtNMDZRdDMwYlc3d3E1SHJ4MHprUlFKbzJvdHFCbUZWdUcwCmQ3K0JIeS9DYVIzcHM2UGxrNTlOSnMwSS8ySUREalphRDNWL0lmUE5YQkg4bjZFM0lyZnVVaGNsRmNnYTNocWoKKzlFQjk0a2VrNXNDQkJyNWUvY0Q3amNobWVXQzJtdis0cFVMYTFWV09leW1hUEw5OVljMnBpQ3hBb0dBU3dxTApEbGlJVUs0MVk0R2gxbTZhZ0MySUc1VHlmQmFKN0NmenltKy9sNExLZDBMNTgzUUlIVHQxRGh5U0hOWlVsVEdkCjREaVVYNnRkR3V1V0o0Qmk5L0pDN252cU5YZHlFWjljRWhTRkphVGNxK0dNK0JRVjREUWg2RWpRMlZITDFXMVQKUkQxSVJCQ0RFU2pRVHMxTGptNXJqRDBKaHdhandTTHNyM2cvZTAwQ2dZRUFsZjkrU1NManVxU0JCbmFDY0NLbApTbW5rM2RJMmxYOG5kY0RiUTVyOXhWTHJXcXBIK1BwSkIrUk45VWpVTWlrdFZrYmYrbjNwOVp5T2NxeG9YVk9wClRYMXY0Y0NjbDR5cC9xOXhtOU5FNGhRSkVsRm10YTlwMVJ3a2JmeWpqTnVVOUlMZjljM21MMlN0STFTV3gyUjAKWmlpT3hvUXdUSlFvK2phdlR4dUJHeFU9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
Wgrywamy za pomocą komendy "kubectl apply -n registry -f registry_deployment.yml" i sprawdzamy jak nam poszło:
bkorpala@kubernetes-master1:~$ kubectl -n registry get secrets
NAME                  TYPE                                  DATA      AGE
default-token-k8x4m   kubernetes.io/service-account-token   3         51m
registry-certs        Opaque
Dalej musimy stworzyć usługę, która umożliwi nam komunikację z naszym kontenerem Registry (dodajemy do naszego manifestu oddzielając "---"):
apiVersion: v1
kind: Service
metadata:
  name: kube-registry
  labels:
    k8s-app: kube-registry-upstream
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "KubeRegistry"
spec:
  selector:
    k8s-app: kube-registry-upstream
  ports:
  - name: registry
    port: 443
    protocol: TCP
Standardowo sprawdźmy co zrobiliśmy:
bkorpala@kubernetes-master1:~$ kubectl -n registry get services
NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
glusterfs-cluster   ClusterIP   10.233.28.208   <none>        1/TCP     18h
kube-registry       ClusterIP   10.233.29.16    <none>        443/TCP   10m
Teraz musimy zadeklarować obiekty PersistentVolume i PersistentVolumeClaim określające ilość miejsca, które możemy wykorzystać (dla mojego przykładu do przechowywania danych użyłem GlusterFS, na temat którego możecie przeczytać tutaj):
kind: PersistentVolume
apiVersion: v1
metadata:
  name: kube-registry-pv
  labels:
    kubernetes.io/cluster-service: "true"
spec:
  capacity:
    storage: 2Gi
  accessModes:
    - ReadWriteOnce
  glusterfs:
    endpoints: glusterfs-cluster
    path: registry
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: kube-registry-pvc
  labels:
    kubernetes.io/cluster-service: "true"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1500M
Wynik powinien być zbliżony do tego:
bkorpala@kubernetes-master1:~$ kubectl -n registry get pv
NAME               CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                        STORAGECLASS   REASON    AGE
kube-registry-pv   2Gi        RWO            Retain           Bound     registry/kube-registry-pvc                            9s
bkorpala@kubernetes-master1:~$ kubectl -n registry get pvc
NAME                STATUS    VOLUME             CAPACITY   ACCESS MODES   STORAGECLASS   AGE
kube-registry-pvc   Bound     kube-registry-pv   2Gi        RWO                           11s
Teraz najważniejsza rzecz czyli wdrożenie naszego strąka z Docker Registry:
apiVersion: v1
kind: ReplicationController
metadata:
  name: kube-registry-v0
  labels:
    k8s-app: kube-registry-upstream
    version: v0
    kubernetes.io/cluster-service: "true"
spec:
  replicas: 1
  selector:
    k8s-app: kube-registry-upstream
    version: v0
  template:
    metadata:
      labels:
        k8s-app: kube-registry-upstream
        version: v0
        kubernetes.io/cluster-service: "true"
    spec:
      containers:
      - name: registry
        image: registry:2
        resources:
          limits:
            cpu: 100m
            memory: 100Mi
        env:
        - name: REGISTRY_HTTP_ADDR
          value: :443
        - name: REGISTRY_HTTP_TLS_CERTIFICATE
          value: /certs/registry.crt
        - name: REGISTRY_HTTP_TLS_KEY
          value: /certs/registry.key
        - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
          value: /var/lib/registry
        volumeMounts:
        - name: registry-vol
          mountPath: /var/lib/registry
          subPath: registry
        - name: registry-certs-vol
          mountPath: /certs
        ports:
        - containerPort: 443
          name: registry
          protocol: TCP
      volumes:
      - name: registry-vol
        persistentVolumeClaim:
          claimName: kube-registry-pvc
      - name: registry-certs-vol
        secret:
          secretName: registry-certs
Pobierzmy rezultat:
bkorpala@kubernetes-master1:~$ kubectl -n registry get rc
NAME               DESIRED   CURRENT   READY     AGE
kube-registry-v0   1         1         1         26s
bkorpala@kubernetes-master1:~$ kubectl -n registry get pods
NAME                     READY     STATUS    RESTARTS   AGE
kube-registry-v0-xshs8   1/1       Running   0          35s
Teraz z dowolnego węzła Kubernetes możemy pingować nasz strąk i nazwa domeny powinna zostać rozwiązana na adres IP:
bkorpala@kubernetes-master1:~$ ping kube-registry.registry.svc.cluster.local
PING kube-registry.registry.svc.cluster.local (10.233.29.16) 56(84) bytes of data.
Sprawdźmy zatem czy Registry działa:
bkorpala@kubernetes-master1:~$ curl -k https://kube-registry.registry.svc.cluster.local/v2/_catalog
{"repositories":[]}
Pasuje coś wrzucić do naszego rejestru. Niechaj to będzie Nginx. Za pomocą komendy "docker pull nginx" pobieramy ostatnią wersję obrazu. Teraz pobieramy identyfikator obrazu:
bkorpala@kubernetes-master1:~$ sudo docker images | grep latest | grep nginx
nginx                                                 latest              5699ececb21c        6 days ago          109 MB
Komendą "docker tag 5699ececb21c kube-registry.registry.svc.cluster.local/nginx" oznaczamy nasz obraz celem wypchnięcia go na prywatne Registry: "docker push kube-registry.registry.svc.cluster.local/nginx". Standardowo weryfikujemy:
bkorpala@kubernetes-master1:~$ curl -k https://kube-registry.registry.svc.cluster.local/v2/_catalog
{"repositories":["nginx"]}
bkorpala@kubernetes-master1:~$ curl -k https://kube-registry.registry.svc.cluster.local/v2/nginx/tags/list
{"name":"nginx","tags":["latest"]}
Teraz definiując manifest w polu "image" dla konkretnego kontenera możemy użyć wyżej wymienionego adresu.

Jeżeli krok z dnsmasq pominęliśmy lub zwyczajnie nie chcemy w ten sposób rozwiązywać nazw to możemy stworzyć usługę, która na każdym z węzłów wystawi nam Registry na konkretnym porcie:
apiVersion: v1
kind: Service
metadata:
  name: kube-registry-every-node
  labels:
    k8s-app: kube-registry-upstream
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "KubeRegistry"
spec:
  selector:
    k8s-app: kube-registry-upstream
  type: NodePort
  ports:
  - name: registry-every-node
    nodePort: 31337
    port: 443
    protocol: TCP
Sprawdźmy jak poszło:
bkorpala@kubernetes-master1:~$ sudo kubectl -n registry get services
NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
glusterfs-cluster          ClusterIP   10.233.28.208   <none>        1/TCP           21h
kube-registry              ClusterIP   10.233.29.16    <none>        443/TCP         3h
kube-registry-every-node   NodePort    10.233.8.16     <none>        443:31337/TCP   30s
bkorpala@kubernetes-master1:~$ curl -k https://localhost:31337/v2/_catalog
{"repositories":["nginx"]}
Jeżeli chcemy tylko tymczasowo sprawdzić poprawność działania to możemy zwyczajnie przekazać konkretny port do naszej stacji roboczej:
bkorpala@kubernetes-master1:~$ sudo kubectl port-forward --namespace registry kube-registry-v0-xshs8 31337:443 &
[1] 20103
bkorpala@kubernetes-master1:~$ Forwarding from 127.0.0.1:31337 -> 443
bkorpala@kubernetes-master1:~$ curl -k https://localhost:31337/v2/_catalog
Handling connection for 31337
{"repositories":["nginx"]}
E0703 12:01:31.963822   20104 portforward.go:316] error copying from local connection to remote stream: read tcp4 127.0.0.1:31337->127.0.0.1:46754: read: connection reset by peer 

środa, 6 czerwca 2018

Kubernetes z GlusterFS jako miejsce składowania danych

Gdy już mamy GlusterFS (jako przewodnik możesz wykorzystać ten link) to musimy usunąć restrykcję mówiącą, że masz wolumen może być montowany tylko z localhost:
root@kubernetes-master1:~# gluster volume reset gvol0 auth.allow
volume reset: success: reset volume successful
Stworzyliśmy również klaster Kubernetes (np. dzięki temu). Najpierw tworzymy punkt końcowy dla usługi aby określić szczegóły dostępu do GlusterFS. Tworzymy plik "glusterfs-endpoints.yml" o poniższej zawartości:
apiVersion: v1
kind: Endpoints
metadata:
  name: glusterfs-cluster
subsets:
  - addresses:
      - ip: 192.168.33.30
    ports:
      - port: 1
  - addresses:
      - ip: 192.168.33.31
    ports:
      - port: 1
  - addresses:
      - ip: 192.168.33.32
    ports:
      - port: 1
  - addresses:
      - ip: 192.168.33.33
    ports:
      - port: 1
Podajemy adresy naszych węzłów w GlusterFS (numer portu jest pomijany) i wdrażamy:
root@kubernetes-master1:~# kubectl create -f glusterfs-endpoints.yml
endpoints "glusterfs-cluster" created
Warto sprawdzić czy nam się udało:
root@kubernetes-master1:~# kubectl get endpoints
NAME                ENDPOINTS                                                     AGE
glusterfs-cluster   192.168.33.30:1,192.168.33.31:1,192.168.33.32:1 + 1 more...   11s
kubernetes          192.168.33.30:6443                                            6d 
Kolejnym krokiem jest definicja usługi:
apiVersion: v1
kind: Service
metadata:
  name: glusterfs-cluster
spec:
  ports:
  - port: 1
Pole "name" musi odpowiadać nazwie w definicji punktów końcowych. Wdrażamy i sprawdzamy jak nam poszło:
root@kubernetes-master1:~# kubectl create -f glusterfs-service.yml
service "glusterfs-cluster" created
root@kubernetes-master1:~# kubectl get services
NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
glusterfs-cluster   ClusterIP   10.109.25.207   <none>        1/TCP     4s
kubernetes          ClusterIP   10.96.0.1       <none>        443/TCP   6d
Przechodzimy do definicji obiektu PersistentVolume czyli niskopoziomowej reprezentacji wolumenu przechowywania danych. Odwołujemy się do punktu końcowego aby poinformować Kubernetes gdzie klaster GlusterFS jest dosępny oraz jaka jest nazwa wolumenu, który utworzono. Tworzymy plik "glusterfs-persistent-volume.yml":
apiVersion: v1
kind: PersistentVolume
metadata:
  name: fileupload-vol
  labels:
    dev: dev
spec:
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 500M
  glusterfs:
    endpoints: glusterfs-cluster
    path: gvol0
  persistentVolumeReclaimPolicy: Recycle
W polu "path" podajemy nazwę naszego wolumenu i uruchamiamy:
root@kubernetes-master1:~# kubectl create -f glusterfs-persistent-volume.yml
persistentvolume "fileupload-vol" created
root@kubernetes-master1:~# kubectl get pv
NAME             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE
fileupload-vol   500M       RWX            Recycle          Available                                      6s 
Kolejno musimy utworzyć obiekt PersistentVolumeClaim, który wiąże "strąk" ("pod") z obiektem PersistentVolume. Definicja w pliku "glusterfs-persistent-volume-claim.yml":
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: file-upload-claim
spec:
  accessModes:
  - ReadWriteMany
  resources:
     requests:
       storage: 100M
Sprawdźmy jak nam poszło:
root@kubernetes-master1:~# kubectl create -f glusterfs-persistent-volume-claim.yml
persistentvolumeclaim "file-upload-claim" created
root@kubernetes-master1:~# kubectl get pvc
NAME                STATUS    VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS   AGE
file-upload-claim   Bound     fileupload-vol   500M       RWX                           4s
Ostatecznie w pliku "nginx.yml" piszemy definicję usługi. W naszym przypadku będzie to serwer Nginx:
apiVersion: v1
kind: List
items:
- apiVersion: extensions/v1beta1
  kind: Deployment
  metadata:
    name: nginx
  spec:
    replicas: 1
    template:
      metadata:
        labels:
          app: nginx
      spec:
        containers:
        - name: nginx
          image: nginx
          volumeMounts:
          - mountPath: /usr/share/nginx/html/
            name: dir-1
        volumes:
          - name: dir-1
            persistentVolumeClaim:
              claimName: file-upload-claim
 
Pole "claimName" musi odpowiadać definicji obiektu PersistentVolumeClaim zaś w polu "mountPath" określamy jaki katalog w kontenerach chcemy współdzielić. Wdrażamy, sprawdzamy jakie mamy usługi, "strąki" oraz wchodzimy do kontenera z Nginksem:
root@kubernetes-master1:~# kubectl create -f nginx.yml
deployment.extensions "nginx" created
root@kubernetes-master1:~# kubectl get services
NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
glusterfs-cluster   ClusterIP   10.109.25.207   <none>        1/TCP     22m
kubernetes          ClusterIP   10.96.0.1       <none>        443/TCP   6d
root@kubernetes-master1:~# kubectl get pods
NAME                    READY     STATUS    RESTARTS   AGE
nginx-944f45fb8-ztv92   1/1       Running   0          59s
root@kubernetes-master1:~# kubectl exec -it nginx-944f45fb8-ztv92 -- /bin/bash
root@nginx-944f45fb8-ztv92:/#
Tak przygotowane rozwiązanie pozwala nam na współdzielenie plików w kontenerach Nginx (katalog "/usr/share/nginx/html") i na komputerach gdzie jest GlusterFS.

niedziela, 3 czerwca 2018

TeamCity - migracja do kontenera dockerowego

Niniejsza pozycja omawia przejście z serwera CI CD TeamCity w wersji 9.1.7 do TeamCity w wersji 2017.2.3 umieszczonego w kontenerze dockerowym pod kontrolą Docker Swarm. Bazą danych, którą wykorzystano w przykładzie jest MySQL.

Pierwszym krokiem, który musimy jest wykonanie kopii zapasowej TeamCity:
  • prawy górny róg i "Administration",
  • belka po lewej i "Backup",
  • z listy rozwijanej "Backup scope" określamy co ma zawierać kopia, a wybranie opcji "Custom" pozwala nam na indywidualne określenie zakresu,
  • możemy określić nazwę pliku w sekcji "Backup file"
  • pole "add timestamp suffix" doda nam w nazwie pliku kopii zapasowej wzorzec daty i czasu,
  • naciskamy "Start Backup".
Kopię można również wykonać za pomocą dedykowanego narzędzia znajdującego się w katalogu z instalacją TC. Dla przykładu w moim wypadku będzie to wywołanie "/home/teamcity/teamcity/bin/maintainDB.sh -C -D -L -P".

Oczekujemy na zakończenie wykonywania się kopii zapasowej (będzie miała rozszerzenie "zip"). W podsumowaniu znajduje się ścieżka do pliku.

W związku z tym, że kopia nie obejmuje artefaktów to musimy je skopiować sami jeżeli chcemy je również mieć w nowej instancji TC.

Udajemy się do katalogu z artefaktami, który jest zależy od tego gdzie jest zainstalowane TC. W moim przypadku jest to "/home/teamcity/.BuildServer/system/artifacts" i wydajemy tam komendę
tar -czvf artifacts.tar.gz .
Przenosimy archiwa z kopią zapasową i artefaktami na serwer docelowy gdzie będzie stało nasze TeamCity w kontenerze.

Teraz przyszła pora na wdrożenie naszego nowego TC na docelowej maszynie. Ja zrobię to przy pomocy Ansible'a uruchamiają plik Docker Compose'a stworzony z szablonu Ansible, który ma następującą postać:
version: '3.5' 
services:
  mysql:
    image: mysql:5.7
    ports:
      - 3306:3306
    networks:
      - teamcity
    environment:
      MYSQL_ROOT_PASSWORD: {{ vault_mysql_root_password }}
      MYSQL_DATABASE: "{{ mysql_teamcity_database }}"
      MYSQL_USER: "{{ mysql_teamcity_user }}"
      MYSQL_PASSWORD: "{{ vault_mysql_teamcity_user_password }}"
    volumes:
      - mysql_data_directory:/var/lib/mysql
    deploy:
      replicas: 1
      placement:
        constraints: [node.role == manager]
      restart_policy:
        condition: on-failure
 
  teamcity:
    image: jetbrains/teamcity-server:2017.2.3
    ports:
      - 8111:8111
    networks:
      - teamcity
      - traefik
    volumes:
      - teamcity_data_directory:/data/teamcity_server/datadir
      - teamcity_logs_directory:/opt/teamcity/logs
    deploy:
      replicas: 1
      placement:
        constraints: [node.role == manager]
      restart_policy:
        condition: on-failure
      labels:
        traefik.frontend.entryPoints: "http,https"
        traefik.frontend.rule: "Host:${HOST}"
        traefik.http.port: 8111
        traefik.backend:
 
volumes:
  mysql_data_directory:
  teamcity_data_directory:
  teamcity_logs_directory:
 
networks:
  teamcity:
    driver: overlay
    attachable: true
  traefik:
    external:
      name: traefik
A więc na docelowym serwerze mamy świeże i niezainicjalizowane TC w kontenerze dockerowym w Swarmie. Kolejnym krokiem jest uruchomienie dodatkowego kontenera z TeamCity współdzielącego wolumen, w którym dokonamy przywrócenia danych:
bkorpala@hercules:~$ sudo docker run -it --name restore-tc --rm --network=teamcity_teamcity --volumes-from=teamcity_teamcity.1.41rl6y4npfcb7pj7odvs4pfvo jetbrains/teamcity-server /bin/bash
   Welcome to TeamCity Server Docker container
 * Installation directory: /opt/teamcity
 * Logs directory:         /opt/teamcity/logs
 * Data directory:         /data/teamcity_server/datadir
root@73b7919e46dc:/#
Jako argument opcji "--network" podajemy sieć która została utworzona dla TC przez Swarma ("sudo docker network ls"), a dla opcji "--volumes-from" podajemy nazwę kontenera TC ("sudo docker ps").

Jeżeli chcemy wyjść z kontenera bez zabicia go to wciskamy najpierw klawisz Ctrl + P, a potem Ctrl + Q. Aby wejść znów do kontenera wpisujemy "sudo docker exec -it restore-tc /bin/bash".

Z pierwotnej instancji TC na serwer docelowy kopiujemy informację na temat połączenia z bazą danych. W moim przypadku jest to "/home/teamcity/.BuildServer/config/database.properties".

Otwieramy plik i modyfikujemy go wpisując użytkownika, hasło oraz nazwę bazy, które użyjemy w nowej instancji. Mój plik np. wygląda następująco:
# Database: HSQLDB (HyperSonic) version 2.x
connectionUrl=jdbc:mysql://mysql:3306/teamcity?useUnicode=yes&characterEncoding=UTF-8
connectionProperties.user=teamcity
connectionProperties.password=MGjHHVgm7ABSvnbP
Dzięki temu, że jesteśmy w zadeklarowanej przez nas sieci dockerowej to dla serwera MySQL możemy użyć nazwy domenowej, która odpowiada nazwie usługi w Docker Compose.

Do kontenera z dodatkowym TC kopiujemy plik z informacją na temat połączenia z bazą danych:
sudo docker cp database.properties restore-tc:/restore-database.properties
Kopiujemy tam również kopię bazy danych:
sudo docker cp TeamCity_Backup_Before_Update_20180601_200257.zip restore-tc:/backup.zip
 Następnie musimy pobrać sterownik dla połączeń z bazą danych MySQL ("https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-8.0.11.tar.gz") na serwer docelowy i go rozpakować. W kontenerze z dodatkowym TC tworzymy strukturę katalogów "/data/teamcity_server/datadir/lib/jdbc" i kopiujemy do niego nasz sterownik:
sudo docker cp mysql-connector-java-8.0.11/mysql-connector-java-8.0.11.jar restore-tc:/data/teamcity_server/datadir/lib/jdbc/
W kontenerze z dodatkowym TC wydajemy komendę, która zacznie proces odzyskiwania:
/opt/teamcity/bin/maintainDB.sh restore -A /data/teamcity_server/datadir/ -F /backup.zip -T /restore-database.properties
Po wgraniu kopii musimy jeszcze wgrać artefakty. Dla przykładu w moim wypadku (artefakty są umieszczone w folderach odpowiadających nazwom projektów) znajdują się one w katalogu "artifacts" po wypakowaniu archiwum, które wcześniej utworzyliśmy:
 sudo docker cp artifacts/. restore-tc:/data/teamcity_server/datadir/system/artifacts/
Uruchamiamy ponownie kontener z naszym nowym TC. W moim przypadku będzie to restart usługi Docker Swarma:
bkorpala@hercules:~$ sudo docker service scale teamcity_teamcity=0
teamcity_teamcity scaled to 0
overall progress: 0 out of 0 tasks
verify: Service converged
bkorpala@hercules:~$ sudo docker service scale teamcity_teamcity=1
teamcity_teamcity scaled to 1
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service converged
Wchodzimy na stronę z nowym TC i widzimy informację "TeamCity server requires technical maintenance. Please let the server administrator know this." i wtedy klikamy na "I'm a server administrator, show me the details". Musimy podać kod autoryzacyjny, który uzyskamy wchodząc do kontenera i odczytując logi jak np.:
bkorpala@hercules:~$ sudo docker exec -it teamcity_teamcity.1.foie75s0t5bk8grqz6jqm5xjw tail /opt/teamcity/logs/teamcity-server.log
[2018-06-03 04:04:46,377]   INFO -  jetbrains.buildServer.STARTUP - The database properties file "/data/teamcity_server/datadir/config/database.properties" doesn't exist
[2018-06-03 04:04:46,377]   INFO -  jetbrains.buildServer.STARTUP - The internal database data file "/data/teamcity_server/datadir/system/buildserver.data" doesn't exist
[2018-06-03 04:04:46,393]   INFO -  jetbrains.buildServer.STARTUP - Data Directory version: 727
[2018-06-03 04:04:46,393]   INFO -  jetbrains.buildServer.STARTUP - Current stage: Looking for the database configuration
[2018-06-03 04:04:46,393]   INFO -  jetbrains.buildServer.STARTUP - Database properties file "/data/teamcity_server/datadir/config/database.properties" doesn't exist
[2018-06-03 04:04:46,393]   INFO -  jetbrains.buildServer.STARTUP - Internal HSQL database file (/data/teamcity_server/datadir/system/buildserver.data) doesn't exist
[2018-06-03 04:04:46,393]   INFO -  jetbrains.buildServer.STARTUP - Neither database properties file nor internal database found.
[2018-06-03 04:04:46,393]  ERROR -  jetbrains.buildServer.STARTUP - Data parts are inconsistent: the Data Directory exists (from another version of TeamCity) but the database does not.
[2018-06-03 04:04:46,394]   INFO -  jetbrains.buildServer.STARTUP - Current stage: TeamCity server startup error
[2018-06-03 04:04:46,394]   INFO -  jetbrains.buildServer.STARTUP - Administrator can login from web UI using authentication token: 8288277720538082065
Widzimy, że brakuje nam plik z informacją na temat połączenia do bazy "/data/teamcity_server/datadir/config/database.properties" więc musimy go skopiować do kontenera i jeszcze raz zrestartować usługę w Docker Swarmie.

Kolejnym krokiem jest kliknięcie na "Confirm", a następnie "Upgrade". Teraz oczekujemy aż aktualizacja się zakończy.

piątek, 1 czerwca 2018

Instalacja i konfiguracja GlusterFS

Dane wejściowe:
  • 4 serwery Ubuntu 16.04.4 LTS,
  • 2048 GB RAM-u każdy z serwerów.
Zaczynamy od wpisania do "/etc/hosts" każdego z serwerów informacji o nazwach i adresach IP wszystkich maszyn z naszej sieci GlusterFS. Dla przykładu:
192.168.33.30   kubernetes-master1192.168.33.31   kubernetes-worker1192.168.33.32   kubernetes-worker2192.168.33.33   kubernetes-worker3
Dodajemy wszędzie informację o repozytorium:
sudo add-apt-repository ppa:gluster/glusterfs-3.11
Aktualizujemy bazę:
sudo apt-get update
Instalujemy oprogramowanie:
sudo apt-get install glusterfs-server
Ze względów bezpieczeństwa wyłączamy aktualizację pakietów GlusterFS:
sudo apt-mark hold glusterfs*
Startujemy oprogramowanie i dodajemy konfigurację, która uruchamia usługę podczas startu systemu:
sudo systemctl start glusterd
sudo systemctl enable glusterd
Na pierwszy z serwerów sprawdzamy po kolei czy w sieci są dostępne wszystkie węzły:
vagrant@kubernetes-master1:~$ sudo gluster peer probe kubernetes-worker1
peer probe: success.
vagrant@kubernetes-master1:~$ sudo gluster peer probe kubernetes-worker2
peer probe: success.
vagrant@kubernetes-master1:~$ sudo gluster peer probe kubernetes-worker3
peer probe: success.
Tworzymy wolumen:
vagrant@kubernetes-master1:~$ sudo gluster volume create gvol0 replica 4 kubernetes-master1:/data kubernetes-worker1:/data kubernetes-worker2:/data kubernetes-worker3:/data force
volume create: gvol0: success: please start the volume to access data
Opcja "force" utworzy nam katalog na serwerach jeżeli takowy nie istnieje.

Aktualnie każdy komputer ma dostęp bez żadnych restrykcji do naszego wolumenu z danymi więc dopuszczamy połączenie montujące tylko z:
vagrant@kubernetes-master1:~$ sudo gluster volume set gvol0 auth.allow 127.0.0.1
volume set: success
Startujemy wolumen i wyświetlamy informacje na jego temat:
vagrant@kubernetes-master1:~$ sudo gluster volume start gvol0
volume start: gvol0: success
vagrant@kubernetes-master1:~$ sudo gluster volume status
Status of volume: gvol0
Gluster process                             TCP Port  RDMA Port  Online  Pid
------------------------------------------------------------------------------
Brick kubernetes-master1:/data              49152     0          Y       17935
Brick kubernetes-worker1:/data              49152     0          Y       8542
Brick kubernetes-worker2:/data              49152     0          Y       8411
Brick kubernetes-worker3:/data              49152     0          Y       8472
Self-heal Daemon on localhost               N/A       N/A        Y       17955
Self-heal Daemon on kubernetes-worker3      N/A       N/A        Y       8492
Self-heal Daemon on kubernetes-worker2      N/A       N/A        Y       8431
Self-heal Daemon on kubernetes-worker1      N/A       N/A        Y       8562
Task Status of Volume gvol0
------------------------------------------------------------------------------
There are no active volume tasks
vagrant@kubernetes-master1:~$ sudo gluster volume info
Volume Name: gvol0
Type: Replicate
Volume ID: ab3ed814-191e-4288-ae1e-341f9a1bca40
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 4 = 4
Transport-type: tcp
Bricks:
Brick1: kubernetes-master1:/data
Brick2: kubernetes-worker1:/data
Brick3: kubernetes-worker2:/data
Brick4: kubernetes-worker3:/data
Options Reconfigured:
auth.allow: 127.0.0.1
transport.address-family: inet
nfs.disable: on
Na każdym serwerze tworzymy katalog "/mnt/gluster".

Na każdej z maszyn montujemy wolumen (w zależności od tego na której jesteś to zmień nazwę) np.:
sudo mount -t glusterfs kubernetes-master1:/gvol0 /mnt/gluster
Teraz możemy manipulować zawartością tylko w "/mnt/gluster", ale pojawi sie ona również w katalogu "/data".

Każdej maszynie dodajemy wpis w "/etc/fstab" (w zależności od tego na której jesteś to zmień nazwę) np.:
kubernetes-master1:/gvol0 /mnt/gluster glusterfs defaults,_netdev 0 0