Jenkins i Docker – Jak budować i testować mikroserwisy

Opublikowane przez Dariusz Grabowski w dniu

Trudno mówić o mikroserwisach w kontekście nowinki. Jest to powszechny sposób dostarczania oprogramowania. Aby efektywnie je rozwijać potrzebny jest potok ciągłej integracji i ciągłego dostarczania. Jak opracować taki potok i od czego zacząć? Pokażę Ci jak to zrobić dla Jenkinsa i Dockera na przykładzie prostej aplikacji w pythonie.

Z tego artykułu dowiesz się jak krok po kroku stworzyć pipeline w którym:

  1. uruchomisz testy aplikacji i zapiszesz ich wyniki,
  2. zbudujesz obraz Docker dla Twojego mikroserwisu,
  3. uruchomisz testy obrazu z użyciem DGoss,
  4. opublikujesz swój gotowy obraz w repozytorium.

Poznajmy nasz mikroserwis

Mikroserwis, czyli bezstanowa aplikacja realizująca jedno zadanie biznesowe. Zwykle jest opakowana w kontener, co ułatwia jej wdrożenie. Na potrzeby tego artykułu przygotowałem prostą aplikację w pythonie. Na początku zapoznamy się z aplikacją, a następnie przejdziemy do opakowania jej w potok ciągłej integracji.

Zrzut ekranu z testowanej aplikacji

Aplikacja jest typowym hello world, z tą jedną różnicą, że w wersji webowej. Do obsługi http użyłem tutaj wbudowanego w pythona modułu SimpleHTTPRequestHandler. Aplikacja parsuje parametry GET i na podstawie przekazanego name wita użytkownika. Rzuć okiem na kod poniżej.

class GreetingsGenerator:
    def __init__(self, query_components):
        if 'name' in query_components:
            self._name = query_components["name"][0]
        else:
            self._name = "world"

    def generate(self):
        return "hello " + self._name


class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()

        generator = GreetingsGenerator(parse_qs(urlparse(self.path).query))
        html = f"<html><head></head><body><h1>{generator.generate()}</h1></body></html>"

        self.wfile.write(bytes(html, "utf8"))
        return

if __name__ == '__main__':
    handler_object = MyHttpRequestHandler
    PORT = int(os.getenv('SERVER_PORT', '8000'))

    my_server = socketserver.TCPServer(("", PORT), handler_object)
    print("Starting server...")
    my_server.serve_forever()

Funkcjonalność generowania powitania wydzieliłem do osobnej klasy GreetingsGenerator, aby móc napisać do niej testy jednostkowe. Są one bardzo proste. Na przykładzie ich wyników nauczymy się jak generować raport dla Jenkinsa. Testy zamieściłem poniżej.

from .server import GreetingsGenerator

def test_greetings_with_name():
    values = {'age': ['12'], 'name': ['Jan']}
    generator = GreetingsGenerator(values)
    assert generator.generate() == "hello Jan"

def test_greetings_without_name():
    values = {'age': ['12']}
    generator = GreetingsGenerator(values)
    assert generator.generate() == "hello world"

W kolejnych krokach stworzymy potok ciągłej integracji, w którym przetestujemy aplikację i przygotujemy ją do wdrożenia.

Cały kod dla tego projektu umieściłem na repozytorium:
https://github.com/grabowski-d/python-microservice

Przygotowanie joba Jenkins Pipeline

Przed przystąpieniem do tworzenia joba musimy upewnić się, czy nasze środowisko jest dobrze skonfigurowane. Aby pomyślnie uruchomić ten projekt potrzebne będą wtyczki:

Warto przyjrzeć się też maszynie, na której uruchomimy naszego joba. Powinien być tam zainstalowany Docker. Najłatwiej sprawdzić to logując się do niej i wywołując polecenie docker --version. Jeśli do uruchomienia Jenkinsa użyłeś mojego poradnika, niczym się nie musisz przejmować, wszystko jest poprawnie uruchomione.

Gdy środowisko jest gotowe wystarczy stworzyć joba typu pipeline. Jak to zrobić dowiesz się z wcześniej wspomnianego artykułu. W konfiguracji jedyne co musisz zrobić to ustawić adres repozytorium, z którego Jenkins pobierze Jenkinsfile. Jeśli skorzystasz z mojego repozytorium, ekran konfiguracji powinien wyglądać jak na poniższej grafice.

Jenkins pipeline konfiguracja

Twój job jest gotowy. Możesz go uruchomić i podziwiać efekty 🙂 W kolejnych akapitach opiszę, z czego składa się nasz pipeline. Aby łatwiej to zrozumieć, przeanalizujemy każdą sekcję z osobna.

Testy aplikacji w kontenerze – Docker jako agent Jenkins

Gdy wiemy już jak wygląda nasza aplikacja, pora uruchomić dla niej testy jednostkowe. Testy przeprowadzamy w oderwaniu od obrazu, w którym docelowo ta aplikacja ma się znaleźć. Na tym etapie sprawdzamy jedynie prawidłowe działanie oprogramowania.

Jenkins pipeline checkout

Pierwszym krokiem będzie pobranie kodu z repozytorium. Jeśli poprawnie zdefiniowałeś URL w konfiguracji joba, to wystarczy użyć polecenia checkout scm, tak jak na poniższym listingu.

pipeline {
  agent any
  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
  }
}

Przeanalizujmy teraz kolejny fragment kodu. Mamy kod pobrany, pora uruchomić testy. Aby to zrobić potrzebne nam środowisko pythonowe. W tym celu wykorzystamy agenta z kontenerem Docker. Jest to obraz pobierany bezpośrednio z Docker Huba, czyli publicznego repozytorium obrazów.

Jenkins test z dockerem

Gdy kontener jest już uruchomiony stworzymy wirtualne środowisko pythona (virtualenv). Następnie instalujemy potrzebne zależności z pliku requirements-dev.txt – w tym przypadku jest to pytest. W ostatnim kroku uruchamiamy testy, a ich wyniki zapisujemy jako XML JUnit.

stage('Test python app') {
  agent {
    docker { image 'python:3.8.8-slim-buster' }
  }
  steps {
    script {
      dir('app') {
        sh '''
          python -m venv env
          env/bin/pip install -r requirements-dev.txt
          env/bin/pytest . --junit-xml=pytest_junit.xml
        '''
      }
    }
  }
  post {
    always {
      junit testResults: '**/*pytest_junit.xml'
    }
  }
}

Po wykonaniu testów nie pozostaje nic innego jak tylko wczytać wyniki do Jenkinsa za pomocą wtyczki JUnit. Dzięki temu możliwy będzie przegląd przebiegu wszystkich przypadków testowych w GUI Jenkinsa. Dodatkowo jeśli któryś test zakończy się z wynikiem negatywnym, job może zakończyć się już na tym etapie. Pozwoli to zaoszczędzić czas pracy maszyny, na której uruchamiane jest to zadanie. Zablokuje też niechciane wdrożenie aplikacji, która zawiera błędy 🙂

Testowanie obrazu Docker w Jenkinsie

Uruchomiliśmy testy jednostkowe, nasza aplikacja działa poprawnie. W kolejnym etapie zbudujemy mikroserwis i poddamy go testom. W tym celu najpierw utworzymy obraz Dockerowy, w którym:

  • skopiujemy kod aplikacji,
  • skonfigurujemy port, na którym aplikacja ma działać,
  • zdefiniujemy sposób jej uruchamiania.

Dockerfile dla tego mikroserwisu przedstawiony jest na poniższym listingu.

FROM python:3.8.8-slim-buster

WORKDIR /app
COPY app/server.py ./

ENV SERVER_PORT=8000
EXPOSE 8000

CMD [ "python", "./server.py" ]
Jenkins docker build step

Przyjrzyjmy się teraz kolejnemu etapowi naszego potoku. Ma on za zadanie:

  • zbudować obraz mikroserwisu,
  • pobrać i zainstalować oprogramowanie do testowania: DGOSS,
  • uruchomić testy kontenera i zapisać ich wyniki.

Całość przedstawia się jak na poniższym listingu. O szczegółach dotyczących testowania przeczytasz poniżej.

stage('Build & test docker image') {
  steps {
    script {
      // budujemy obraz z aplikacją, nazwiemy ją 'greeter'
      appImage = docker.build('greeter:latest')

      // pobieramy i konfigurujemy aplikację DGOSS
      sh label: 'Install dgoss', script: '''
        curl -s -L https://github.com/aelsabbahy/goss/releases/latest/download/goss-linux-amd64 -o goss
        curl -s -L https://github.com/aelsabbahy/goss/releases/latest/download/dgoss -o dgoss
        chmod +rx *goss
      '''
      
      // uruchamiamy testy obrazu
      withEnv(["GOSS_OPTS=-f junit", 'GOSS_PATH=./goss']) {
        sh './dgoss run greeter:latest > goss_junit.xml'
      }
    }
  }
  post {
    always {
      // zapisujemy wyniki testów
      junit testResults: '**/*goss_junit.xml'
    }
  }
}

Testowanie kontenerów Docker w Jenkinsie z użyciem DGoss

DGoss to nakładka dla Dockera na program o nazwie Goss. Program ten umożliwia bardzo szybką weryfikację aktualnej konfiguracji serwera. Pozwala on sprawdzić m.in.:

  • zainstalowane pakiety w systemie,
  • konfigurację sieciową, w tym otwarte porty,
  • istniejące pliki i ich uprawnienia,
  • konta użytkowników i ich grupy,
  • działające procesy,
  • i wiele, wiele innych.

Goss napisany jest w języku Go. Aby go uruchomić, wystarczy pobrać binarkę i nadać jej uprawniania do wykonywania. Bogactwo możliwości i bardzo prosta konfiguracja sprawia, że niskim nakładem pracy możemy zastosować takie narzędzie do testów w swoim projekcie. Scenariusz testowy sprowadza się do pliku w formacie YAML. Dla naszego przykładu wygląda następująco:

port:
  tcp:8000:
    listening: true
    ip:
    - 0.0.0.0
process:
  python:
    running: true
http:
  http://localhost:8000:
    status: 200
    allow-insecure: false
    timeout: 5000
    body: []

Sprawdzamy tutaj udostępniony port 8000, czy aplikacja jest uruchomiona (proces python) i czy możliwe jest jej odpytanie z użyciem protokołu http.

Zwróć raz jeszcze uwagę na to, w jaki sposób został test uruchomiony:

withEnv(["GOSS_OPTS=-f junit", 'GOSS_PATH=./goss']) {
  sh './dgoss run greeter:latest > goss_junit.xml'
}

Za pomocą zmiennej środowiskowej GOSS_PATH przekazujemy aplikacji dgoss (wrapper na goss) informację gdzie znajduje się binarka goss. Natomiast za pomocą zmiennej GOSS_OPTS przekazujemy informację na temat formatu wynikowego testów. W przypadku Jenkinsa najwygodniej jest użyć JUnit.

Same testy uruchamiamy w taki sam sposób jak uruchamialibyśmy obraz z użyciem dockera tj:

docker run greeter:latest
./dgoss run greeter:latest

Wyniki testów, podobnie jak w przypadku testów jednostkowych, zapisujemy z użyciem wtyczki JUnit.

Wdrożenie gotowego mikroserwisu do rejestru Docker

Deploy step w pipeline

Ostatnim krokiem na drodze do pomyślnego wdrożenia naszej aplikacji jest… wdrożenie 🙂 Gdy mikroserwis jest już przetestowany pora umieścić go w repozytorium obrazów. Stamtąd może trafić do dalszych testów integracyjnych/systemowych lub na środowisko wdrożeniowe.

Wcześniejsze etapy dotyczyły Ciągłej Integracji (CI). Ten krok dotyczy już Ciągłego Wdrażania (CD) i jest opcjonalny. Możesz spokojnie poprzestać na wcześniej zrealizowanych elementach, jeśli aktualnie wdrożenie jest u Ciebie niemożliwe.

Jako repozytorium obrazów można użyć m. in.:

  • Artifactory
  • Nexus
  • registry na Dockerhub.

W celach ćwiczeń możesz skorzystać z tego ostatniego. Na tej stronie, możesz bezpłatnie założyć jedno prywatne repozytorium. Wystarczy stworzyć konto. Dane dostępowe do tego konta należy podać jako credentials w Jenkinsie. Następnie wystarczy wykonać poniższy stage. Pamiętaj, aby podmienić adres rejestru i credentials-id.

stage('Deploy') {
  steps {
    script {
      docker.withRegistry('https://registry.example.com', 'credentials-id') {
        appImage.push()
      }
    }
  }
}

I to tyle. Aplikacja została przetestowana i pomyślnie umieszczona w repozytorium. Gratulacje!
Warto nadmienić, że tak przygotowany kontener powinien przejść kolejną serię testów systemowych i wydajnościowych. Jest to jednak temat wykraczający poza prosty przykład przedstawiony w tym artykule.

Pełny Jenkinsfile dla zbudowanego przez nas potoku wygląda tak:

pipeline {
  agent any
  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Test python app') {
      agent {
        docker { image 'python:3.8.8-slim-buster' }
      }
      steps {
        script {
          dir('app') {
            sh '''
              python -m venv env
              env/bin/pip install -r requirements-dev.txt
              env/bin/pytest . --junit-xml=pytest_junit.xml
            '''
          }
        }
      }
      post {
        always {
          junit testResults: '**/*pytest_junit.xml'
        }
      }
    }
    stage('Build & test docker image') {
      steps {
        script {
          appImage = docker.build('greeter:latest')

          sh label: 'Install dgoss', script: '''
            curl -s -L https://github.com/aelsabbahy/goss/releases/latest/download/goss-linux-amd64 -o goss
            curl -s -L https://github.com/aelsabbahy/goss/releases/latest/download/dgoss -o dgoss
            chmod +rx *goss
          '''
          
          withEnv(["GOSS_OPTS=-f junit", 'GOSS_PATH=./goss']) {
            sh './dgoss run greeter:latest > goss_junit.xml'
          }
        }
      }
      post {
        always {
          junit testResults: '**/*goss_junit.xml'
        }
      }
    }
    stage('Deploy') {
      steps {
        script {
          docker.withRegistry('https://registry.example.com', 'credentials-id') {
            appImage.push('latest')
          }
        }
      }
    }
  }
  post {
    cleanup {
      script { cleanWs() }
    }
  }
}

Podsumowanie

W tym artykule pokazałem Ci jak krok po kroku stworzyć Pipeline do wdrożenia mikroserwisu. Użyliśmy projektu w pythonie, ale z powodzeniem możesz przełożyć to na dowolną inną technologię. Powyższy potok można dowolnie rozszerzać o narzędzia do statycznej analizy kodu. Można dodawać też inne klasy testów. Ogranicza nas głównie wyobraźnia. Jenkins ma ponad 1500 wtyczek, które zapewnią integrację nawet z egzotycznymi narzędziami. Jeśli chcesz poznać najważniejsze z nich, zajrzyj do mojego nowego e-booka na ten temat:

Jenkins Docker e-book


Subscribe
Powiadom o
guest
3 Comments
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments
Kamil
Kamil
2 lat temu

Dziękuję!

Kuba
Kuba
2 lat temu

Cześć! Bardzo ciekawy artykuł. Natrafiłem jednak na błąd z którym nie mogę sobie poradzić – uruchomiając dockera (docker run) przez jenkinsa port 8000 nie jest wystawiony tj. nie mogę się połączyć. Ale gdy spróbowałem odpalić docker run -p 8000:8000 greeter:latest z ubuntu (ssh do VMki) to wszystko śmiga. Czy masz pomysł co może być nie tak w ustawieniach jenkinsa/sieci?

3
0
Would love your thoughts, please comment.x