Concourse – open-source CI/CD na kontenerach

Concourse – open-source CI/CD na kontenerach

Prawie każdy z dostawców chmury udostępnia narzędzia do automatyzacji dostarczania oprogramowania. Microsoft ma Azure Pipelines, Amazon AWS CodePipeline, a Google Cloud Build. Niekwestionowanym liderem wśród open-source do CI/CD jest oczywiście Jenkinsie. Rozwiązania Jenkinsa polegają na dodawaniu do niego wtyczek w zależności od potrzeb i w ten sposób rozszerzamy jego funkcje. Dzisiaj chciałbym pokazać wam jak można inaczej podejść do automatyzacji z pomocą Concourse. Kod użyty we wpisie znajdziecie na Red-DevOps GitHub.


Poznaj Concourse

Wydaje mi się, że niewiele osób słyszało o tym narzędziu, a jest ono używane przez jedne z największych marek świata. Głównym zadaniem Concourse jest automatyzacja dostarczania oprogramowania za pomocą pipelin-ów. Na stronie projektu https://concourse-ci.org/ możemy znaleźć krótki samouczek oraz wszystkie informację, których potrzebujemy by zainstalować Concourse.

Kluczowymi założeniami tego narzędzia są:
– ekspresyjność przez bycie precyzyjnym
– wszechstronność przez bycie uniwersalnym
– bezpieczeństwo przez bycie odpornym na awarie

Brzmi to dość enigmatycznie dlatego najlepszym sposobem na zrozumienie przewagi tego narzędzia nad innymi będzie sprawdzenie go w praktyce. Po wszystkim sami wyciągniecie wnioski czy powyższe założenia zostały spełnione. Dodam, że początki nauki Concourse nie są proste, ale po paru godzinach powinniśmy być w stanie czytać nawet dość skomplikowane projekty.

Pierwszy pipeline

Pipeline Concourse definiujemy w plikach yaml. Do stworzenia najprostszego pipeline-a potrzebujemy dwóch elementów, jeden jobs i jeden step:

  • jobs – jest to lista zadań jaką ma wykonać nasz pipline; kolejność występowania w yaml nie ma znaczenia
  • steps – kroki są wykonywane w kolejności, jest kilka rodzajów step-s np.
    • task – wykonuje zadanie
    • get – ściąga resource ( o resource za chwilę)
    • put – aktualizuje resource
    • in_parallel – wykonuje krok równolegle
    • i inne…

W sekcji plan określamy kolejność wykonywania kroków.

jobs:
- name: hello-world-job
  plan:
  - task: hello-world-task

Cały pipeline opiera się na krokach. By dostarczyć Concourse informacji jak wykonać step dodajemy do niego config. Step jest tak naprawdę kontenerem stworzonym przez concourse. W config podajemy rodzaj platformy, na której nasz worker/kontener będzie pracować, obraz kontenera, którego użyjemy do jego utworzenia i zadania jakie mają zostać wykonane.

jobs:
- name: hello-world-job
  plan:
  - task: hello-world-task
    config:
      platform: linux
      image_resource:
        type: registry-image
        source:
          repository: busybox
      run:
        path: echo
        args: ["Hello world!"]

W powyższym przykładzie w sekcji config podaliśmy platformę linux i obecnie tylko taką obsługuje Concourse. Obraz busybox zostanie ściągnięty z dockerhub w wersji domyślniej latest. A kontener wykona command echo „Hello world!”

O tym w jaki sposób zainstalować i wystatrować Concourse możecie znaleźć w samouczku na stronie projektu. Ja pokażę jak można zrobić to samo, ale hostując go zdalnie. Zanim do tego przejdę, chcę omówić jeszcze koncepcję inputs/outputs oraz czym są resources.

Przekazywanie danych między krokami

Pipelin-y są ciągiem zautomatyzowanych następujących po sobie zadań. Zatem podstawową funkcją jest możliwość przekazywania danych wykonanych w jednym kroku do następnego. W Concourse przekazujemy foldery. W celu przekazania danych z kroku musimy w nim umieścić klucz outputs. Będę się opierał na poprzednim przykładzie.

jobs:
- name: hello-world-job
  plan:
  - task: hello-world-task
    config:
      platform: linux
      image_resource:
        type: registry-image
        source:
          repository: busybox
      outputs:
      - name: the-artifact
      run:
        path: ls
        args: ["-lF"]

Poprzez dodanie klucza outpust dajemy Concourse znać, że chcemy by w naszym workspace stworzył folder o nazwie podanej w powyższym przykładzie the-artifact. Będzie to lokalizacja, do której będzie można się odwołać nawet po zakończeniu pracy tego workera.

By użyć tego folderu podajemy analogicznie klucz inputs po czym wybieramy odpowiednią nazwę. Dzięki temu Concourse zamontuje tą lokalizację do danego workera. Jeśli nazwa nie będzie dostępna to pipeline wyrzuci błąd.

jobs:
- name: hello-world-job
  plan:
  - task: hello-world-task
    config:
      platform: linux
      image_resource:
        type: registry-image
        source:
          repository: busybox
      outputs:
      - name: the-artifact
      run:
        path: sh
        args:
        - -cx
        - |
          ls -l .
          echo "hello from another step!" > the-artifact/message
  - task: read-the-artifact
    config:
      platform: linux
      image_resource:
        type: registry-image
        source:
          repository: busybox
      inputs:
      - name: the-artifact
      run:
        path: sh
        args:
        - -cx
        - |
          ls -l .
          cat the-artifact/message

Dzięki takiej konstrukcji drugi task read-the-artifact będzie w stanie odczytać wiadomość z lokalizacji the-artifact/message, która została tam umieszczona w pierwszym kroku.

Resources czyli kluczowy element Concourse


Resources jest bez wątpienia najważniejszym elementem tego CI/CD. Przedstawia on zewnętrzne zasoby i pozwala na interakcje z nimi. Głównym zadaniem resources jest reprezentowanie ich w pipeline, mogą to być obiekty, systemy i inne. Kilka przykładowych rzeczy jakie możemy z nimi zrobić to:

  • time – ustawiać zadania cron
  • git – wyzwalanie piepline w momencie pojawienia się nowego commita
  • s3 – pobieranie danych z Amazon S3
  • terraform – pozwala na modyfikacje infrastruktury za pomocą terraforma
  • maven – wykorzystywanie zadań deploy mavena
  • itd…

Powyższe przykłady to tylko jedna z opcji, którą posiada każdy z wymienionych zasobów. Niektóre z nich mają ich więcej, inne mniej. Typów resources są dziesiątki! Poniżej fragment strony z dokumentacji, na której widać tylko wycinek dostępnych do implementacji zasobów.

Całą listę znajdziecie na https://resource-types.concourse-ci.org/

Concourse stara się ograniczać dług technologiczny będąc niezależny od wybranego stacku. Nieważne jakiego systemu kontroli wersji używamy, jaki język programowania wybraliśmy, czy używamy docker swarm czy kubernetesa. Jeśli jesteśmy w stanie zamknąć dany obiekt, system w zasobach Concourse to ten CI/CD będzie mógł z niego korzystać.

Wersje zasobu

By Concourse mógł rozróżnić stan zewnętrznego zasobu nadaje mu różne wersje, w momencie, w którym wykryje nową wersję wyzwala się zadanie.

Do każdego z zasobów możemy odwołać się specjalnymi stepami. Są to:

  • get – zwraca aktualny stan zasobu
  • in – zwraca wybraną wersję zasobu
  • out – generuje nową wersję zasobu (np. dla git będzie to push nowego commita na repozytorium)

Wartość true przy kluczu trigger oznacza, że w momencie kiedy pojawi się nowy commit piepline rozpocznie wykonywanie planu.

Stawiamy instancje Concourse na AWS

Na stronie Concurse możemy znaleźć quick-start, który pokazuje jak zainstalować go na lokalnym komputerze. Poniżej pokażę jak postawić hosta Concourse na zdalnym serwerze. Wykorzystamy do tego instancje EC2 z chmury AWS.

By przygotować Concurse najwygodniej będzie skorzystać z gotowego pliku dla docker-compose, który możemy pobrać za pomocą komendy curl -O https://concourse-ci.org/docker-compose.yml. Dodatkowo wszelkie interakcje z tym narzędziem z poziomu linii komend wykonujemy za pomocą FLY CLI. Możemy też poszukać AMI, które już posiada wszystkie wymagane softy. My użyjemy standardowego AMI Amazon Linux 2 i wszystko skonfigurujemy ręcznie. Upewnijmy się też, że mamy dostęp do instancji na portach 22 i 8080.

Po połączeniu się z instancją wykonujemy następujący skrypt.

#!/bin/bash
sudo yum install docker
wget https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)
sudo mv docker-compose-$(uname -s)-$(uname -m) /usr/local/bin/docker-compose
sudo chmod -v +x /usr/local/bin/docker-compose
sudo systemctl enable docker.service
sudo systemctl start docker.service
sudo chmod 666 /var/run/docker.sock

curl -O https://concourse-ci.org/docker-compose.yml

Wykonanie powyższego skryptu zajmuje kilka dobrych chwil. Skrypt najpierw instaluje dockera, następnie docker-compose i daje im odpowiednie uprawnienia. Później włączamy dockera i pobieramy plik wykonywalny Concourse dla docker-compose. W dalszej kolejności docker-compose stawia nam potrzebne kontenery i konfiguruje je. Jeśli chcemy zrobić z tej instancji AMI jest to najlepszy moment na zrobienie snapshota. Z tego względu, że za każdym razem gdy zmieni się publiczny adres naszej instancji musimy w pliku docker-compose.yml podstawić jej adres do zmiennej CONCOURSE_EXTERNAL_URL przed postawieniem wszystkich komponentów.

Poniższy skrypt wyciąga z metadanych instancji jej publiczny adres IP i podmienia go w pliku docker-compose.yaml. Wystarczy dodać go do UserData przy konfiguracji instancji.

#!/bin/bash
chmod 666 /var/run/docker.sock
sed -i 's/localhost/'"$(curl -l http://169.254.169.254/latest/meta-data/public-ipv4)"'/' /home/ec2-user/docker-compose.yml
docker-compose -f /home/ec2-user/docker-compose.yml up -d

Używając adresu instancji przejdźmy do portu 8080. Po kilku chwilach powinniśmy zobaczyć stronę powitalną Concourse.

Logujemy się za pomocą nazwy użytkownika test, hasło test.

Pipeline „Opowiedz mi dowcip”

Stworzymy pipeline, który będzie opowiadać nam dowcip. W przygotowanym przeze mnie przykładzie zamierzam stworzyć automat, który wykryje nowy commit w prywatnym repozytorium na githubie. Następnie wyzwolona zostanie akcja pobierania tego repo do kroku joke-task. W obu krokach wykorzystamy obraz z prywatnego repozytorium AWS ECR, który zawiera python w wersji 3.9. Skrypt będzie dodawał prostego stringa do pliku joke.txt. Plik ten zostanie przekazany do taska answer-joke-task gdzie będzie odczytany.

Użycie prywatnego repozytorium

By pobrać prywatne repozytorium musimy dostarczyć prywatny klucz. W przykładzie poniżej użyję do tego innego pliku yaml, który Concourse będzie podstawiał w miejsce zmiennych. Nie jest to bezpieczne rozwiązanie bo każdy kto miałby dostęp do serwera miałby też dostęp do klucza prywatnego, ale dla uproszczenia użyjemy pliku. W rozwiązaniach produkcyjnych powinniśmy skorzystać z menadżera uprawnień. Concourse posiada implementację do AWS SSM, AWS Secret Manager HashiCorp Vault, Kubernetes Credential Manager i wielu innych.

resources:
- name: repo
  type: git
  source:
    uri: git@github.com:red-devops/ConcurseCI-example-hidden.git
    branch: main
    private_key: ((ID_RDS))

W celu wstrzyknięcia pliku z credentialsami dodajemy odpowiednią flagę -l credentials.yaml przy ustawianiu pipeline.

ID_RDS: |-
  -----BEGIN OPENSSH PRIVATE KEY-----
  xxxxxx
  ...
  ...

  xxxxxx
  -----END OPENSSH PRIVATE KEY-----
ACCESS_KEY_ID: xxxxxxx
SECRET_ACCESS_KEY: xxxxxxxx

Pobranie obrazu kontenera z AWS ECR

W przypadku pobrania prywatnego obrazu AWS ECR postępujemy podobnie jak poprzednio, ale tym razem musimy dostarczyć dwa parametry aws_access_key_id i aws_secret_access_key. Dodatkowo w AWS IAM do tych credential musimy dodać odpowiednie uprawnienia, które pozwolą nam na pobieranie obrazów z AWS ECR.

image_resource:
  type: docker-image
  source:
    repository: xxxxxxxxxxxx.dkr.ecr.eu-central-1.amazonaws.com/python3.9
    aws_access_key_id: ((ACCESS_KEY_ID))
    aws_secret_access_key: ((SECRET_ACCESS_KEY))

Wykorzystanie recources jako wsadu

Na poniższym zdjęciu widzimy, że w kroku get mamy wartość trigger równą true. Oznacza to, że gdy pipeline wyryje zmianę wersji repozytorium to plan zostanie rozpoczęty, czyli następny krok joke-task się rozpocznie. Nie oznacza to jednak, że resources zostanie automatycznie zamontowany do kontenera zadania. By to osiągnąć musimy dodać w config parametr inputs i wskazać nazwę zasobu do zamontowania.

Przekazanie danych między krokami opisywałem już w poprzednich akapitach więc przejdziemy do wykonania pipeline. Poniżej cały plik joke-pipeline.yaml

resources:
- name: repo
  type: git
  source:
    uri: git@github.com:red-devops/ConcurseCI-example-hidden.git
    branch: main
    private_key: ((ID_RDS))

jobs:
- name: tell-me-a-joke
  plan:
  - get: repo
    trigger: true
  - task: joke-task
    config:
      platform: linux
      image_resource:
        type: docker-image
        source:
          repository: xxxxxxxxxxxx.dkr.ecr.eu-central-1.amazonaws.com/python3.9
          aws_access_key_id: ((ACCESS_KEY_ID))
          aws_secret_access_key: ((SECRET_ACCESS_KEY))
      inputs:
      - name: repo
      run:
        path: sh
        args:
        - -cx
        - |
          ls -l
          cd ./repo
          cat joke.txt
          python3 answer.py
          mv joke.txt ../joke-artifact/joke.txt
      outputs:
      - name: joke-artifact

  - task: answer-joke-task
    config:
      platform: linux
      image_resource:
        type: docker-image
        source:
          repository: xxxxxxxxxxxx.dkr.ecr.eu-central-1.amazonaws.com/python3.9
          aws_access_key_id: ((ACCESS_KEY_ID))
          aws_secret_access_key: ((SECRET_ACCESS_KEY))
      inputs:
      - name: joke-artifact
      run:
         path: cat
         args: ["./joke-artifact/joke.txt"]

Ustawiamy pipeline za pomocą FLY CLI

Po skonfigurowaniu docker-compose i dodaniu plików joke-pipeline.yaml i credentials.yaml musimy zalogować się za pomocą CLI FLY do Concourse. Najpierw zainstalujemy CLI za pomocą poniższego skryptu.

#!/bin/bash
curl 'http://localhost:8080/api/v1/cli?arch=amd64&platform=linux' -o fly
chmod +x ./fly
sudo mv ./fly /usr/local/bin/

Następnie wykonujemy polecenia fly -t tutorial login -c http://localhost:8080 -u test -p test

t – oznacza target, w naszym wypadku nie ma to znaczenia bo używamy tylko jednej instancji Concourse, ale gdybyś mieli ich więcej byłoby to pomoce


fly -t tutorial set-pipeline -p joke -c joke-pipeline.yaml -l credentials.yaml

p – nazwa pipeline jaką chcemy użyć
c – ścieżka/plik do ustawień pipeline
l – ścieżka/plik, w którym Concourse będzie szukał zmiennych

Concourse wyświetli zawartość joke-pipeline.yaml i w miejsce zmiennych podstawi dane z credentials.yaml, na końcu zapyta czy akceptujemy ustawienia, dajemy y.

Postępujemy według instrukcji wykonując polecenie fly -t tutorial unpause-pipeline -p joke. Jeśli przejdziemy do web UI Concourse natychmiast po wykonaniu instrukcji zobaczymy, że nasz plan się wykonuje mimo iż nie wydaliśmy jeszcze odpowiedniego polecenia. Dzieje się tak ponieważ gdy Concourse po raz pierwszy sprawdza zasób resourses nadaje mu wersję, więc status wersji się zmienia i warunek trigger jest spełniony co powoduje odpalenie planu.

Widzimy, że wszystkie trzy kroki zakończyły się powodzeniem.


W kroku get pobraliśmy repozytorium przy użyciu prywatnego klucza.

W kolejnych dwóch zadaniach mamy logi z wykonywanych poleceń.

Zadanie joke-task pokazuje zwartość workspace, widzimy, że Concourse stworzył w kontenerze katalog joke-artifact. Kolejno za pomocą polecenia cat joke.txt odczytywana jest zawartość pliku. Później worker wykonuje skrypt answer.py, który dodaje odpowiedź na pytanie. Za pomocą komendy mv przekazujemy plik do katalogu joke-artifact.

W zadaniu answer-joke-task, wyświetlamy tylko zawartość pliku za pomocą komendy cat ./joke-artifact/joke.txt

Sprawdźmy czy Concourse zareaguje gdy dodamy nowego commita.

Tak, Concourse wykrył zmianę wersji repozytorium, pobrał nową wersję v2 i wykonał pipeline. Domyślnie co minutę wykonywany jest skrypt check, który odpowiedzialny jest za sprawdzanie wszystkich wersji zasobów.

Pipeline możemy też uruchomić ręcznie.

Lub za pomocą FLI CLI i komendy fly -t tutorial trigger-job --job joke/tell-me-a-joke --watch

Zakończenie

Za pomocą jednego wpisu nie sposób pokazać wszechstronność i liczbę opcji jakie posiada Concourse CI. Gorąco zachęcam by samemu zainstalować i zapoznać się jakie daje możliwości. Na pierwszy rzut oka UI może wydawać się dość toporne, ale sami zobaczycie, że użytkowanie jest całkiem przyjemne. Przypominam, że cały kod wpisu możecie znaleźć na Red-DevOps GitHub.

Poniżej wrzucam miejsce, od którego warto zacząć swoją przygodę z tym narzędziem.
Concourse Tutorial by Stark & Wayne – dużo bardziej praktyczne przykłady niż na oficjalnej stronie projektu.
Concurse-CI – oficjalna strona projektu.

Comments are closed.