Terraform

Czy CI/CD może być fascynujące? Część 1

2021-03-10
Podziel się

Nowoczesny proces CI/CD

Procesy CI/CD bywają trudne. Czasem poziom złożoności procesów danej organizacji jest wysoki. Często również samo rozpoczęcie pracy wymaga zapoznania się z kilkoma tysiącami linii kodu napisanego w Groovym, wstawkami w Bashu oraz obejściami wykorzystującymi natywne mechanizmy Jenkinsa w zaskakujący sposób. Na szczęście, stworzenie dobrego procesu przyśpiesza pracę programistów, administratorów oraz opiekunów systemu. Dodatkowo może być całkiem dobrą zabawą, co postaram się zaprezentować w poniższym tekście. Na chwilę jednak wróćmy do początku i przypomnijmy sobie, czym jest CI/CD, DevOps oraz GitOps?

CI/CD

Tutaj skorzystam z najprostszej moim zdaniem definicji. CI/CD to zbiór procesów wewnątrz organizacji, które znacząco zwiększają jakość kodu, szybkość jego dostarczania oraz zadowolenie z pracy jego twórców. Trudno mi sobie wyobrazić człowieka, który “ręcznie” po każdej zmianie w kodzie uruchamia testy jednostkowe, statyczną analizę kodu, buduje kontener, wykonuje testy integracyjne na niższych środowiskach oraz sprawdza bezpieczeństwo aplikacji, po czym pyta przełożonego o zgodę wdrożenia zmiany na środowisko przejściowe. Moim zdaniem programistom powinno się płacić za pisanie kodu, a nie za rzeczy, które można zautomatyzować.

DevOps

Rzeczą oczywistą jest to, że DevOps to kultura. Ale jak zatem nazwać tego człowieka, który te procesy automatyzuje, dba o integrację usług, współpracuje z deweloperami, testerami oraz administratorami? Czasem napisze trochę kodu, skonfiguruje Nginxa, a innego dnia zbada utratę pakietów na nowym środowisku opartym o chmurę publiczną z wykorzystaniem modnych narzędzi oraz Kubernetesa? Osobiście lubię określenie Inżynier Automatyzacji, niestety nie brzmi tak fascynująco, jak DevOps.

GitOps

Trend, lub może raczej dobra praktyka. Repozytorium jako jedyne źródło prawdy o stanie środowiska. W związku z dynamicznym rozwojem chmur publicznych oraz architektury opartej o mikroserwisy okazało się, że skala zmian, jakie wykonywane są na platformach typu Kubernetes, jest gigantyczna. Coraz ważniejsza stała się konfiguracja setek obiektów w identyczny sposób, najlepiej bez udziału człowieka, który jednak czasem popełnia błędy.

O czym dokładnie będzie artykuł?

Skoro przypomnieliśmy sobie najważniejsze pojęcia, krótko streszczę plan działania oraz technologie wykorzystane w tym oto tutorialu.

Użyte technologie

Jak już wspomniałem, procesy potrafią być fascynujące, głównie dlatego, że możemy wykorzystać wiele różnych narzędzi i określić, co najlepiej pasuje do naszego projektu. W związku z tym poniżej skorzystam z narzędzi:

  • Golang
  • Podman
  • Terraform
  • Google Cloud Platform
  • Kubernetes
  • Kustomize

Poniżej skupimy się na stworzeniu aplikacji w Go, IaC z wykorzystaniem Terraform, a także obiektów Kubernetesowych z wykorzystaniem Kustomiza. Chciałbym oddzielić proces budowania komponentów, od tworzenia przepływów. Pozwoli nam to skoncentrować się na poszczególnych elementach. Jeżeli interesuje Cię prosty proces CI/CD, który będzie łatwy do modyfikacji i utrzymania, zapraszam do lektury drugiej części artykułu.

Diagram infrastruktury

Implementacja rozwiązania

Aplikacja w Go

Przedstawiony program odpowiada za serwer http oraz plik HTML. Całość po skompilowaniu do statycznego pliku binarnego waży ok 8MB. Jak na tak małą aplikację – całkiem sporo. Jednak jest to wybór świadomy. Kod aplikacji umieszczę w repozytorium. Efekt działającego programu prezentuję się następująco i nie jest zbytnio emocjonujący:

Konteneryzacja

Dockerfile

Użyję metody wieloetapowego budowania kontenerów. Jej głównym atutem jest możliwość zmniejszenia rozmiaru kontenera oraz pozbycia się niepotrzebnych zależności.

FROM golang:alpine as builder
WORKDIR /build/app

COPY server.go ./

# Build app
RUN go build -o myapp

FROM alpine:latest
COPY --from=builder /build/app/myapp ./myapp
EXPOSE 8080
CMD ["./myapp"]
Budowa obrazu
podman build -t go-hello-world:1.0.0 .
Testowe uruchomienie kontenera
podman run --rm -p 8080:8080 \
    -e APP_VERSION=1.0.0 \
    -e APP_ENVIRONMENT=prod go-hello-world:1.0.0
Podsumowanie

Tutaj chciałbym wrócić do wyboru go server zamiast innego popularnego serwera http. W przypadku powyższego kontenera jego całkowita waga wynosi 14.6MB. Popularny nginx:alpine zajmuję 23.9MB. Dodatkowo nie musimy się martwić konfiguracją serwera http, szczególnie gdy aplikacja znajduję się wewnątrz klastra k8s, gdzie naszym ruchem może zarządzać np. Istio.

Zbudowanie infrastruktury

Inicjalna konfiguracja projektu w GCP
  • Zalogowanie się za pomocą narzędzia gcloud
gcloud auth login
  • Stworzenie nowego projektu z włączeniem cloud api
gcloud projects create kuba-linux-polska --enable-cloud-apis

# --enable-cloud-apis
# enable cloudapis.googleapis.com during creation
  • Ustawienie stworzonego projektu jako obszar roboczy
gcloud config set project kuba-linux-polska
  • Stworzenie konta serwisowego oraz nadanie mu odpowiednich ról
gcloud iam service-accounts create kuba-linux-polska-sa \
--description "Service user for GKE and GitHub Action" \
--display-name "kuba-linux-polska SA"

gcloud projects add-iam-policy-binding kuba-linux-polska --member \
serviceAccount:kuba-linux-polska-sa@kuba-linux-polska.iam.gserviceaccount.com \
--role roles/compute.admin

gcloud projects add-iam-policy-binding kuba-linux-polska --member \
serviceAccount:kuba-linux-polska-sa@kuba-linux-polska.iam.gserviceaccount.com \
--role roles/storage.admin

gcloud projects add-iam-policy-binding kuba-linux-polska --member \
serviceAccount:kuba-linux-polska-sa@kuba-linux-polska.iam.gserviceaccount.com \
--role roles/container.admin

gcloud projects add-iam-policy-binding kuba-linux-polska --member \
serviceAccount:kuba-linux-polska-sa@kuba-linux-polska.iam.gserviceaccount.com \
--role roles/iam.serviceAccountUser
  • Jeżeli chcemy sprawdzić uprawnienia możemy użyć
gcloud projects get-iam-policy kuba-linux-polska  \
--flatten="bindings[].members" \
--format='table(bindings.role)' \
--filter="bindings.members:kuba-linux-polska-sa@kuba-linux-polska.iam.gserviceaccount.com"
  • Pozyskanie pliku autentykacyjnego oraz zalogowanie się jako stworzony użytkownik
mkdir -pv ~/.gcp
gcloud iam service-accounts keys create ~/.gcp/kuba-linux-polska-sa.json \
--iam-account kuba-linux-polska-sa@kuba-linux-polska.iam.gserviceaccount.com

gcloud auth activate-service-account \
kuba-linux-polska-sa@kuba-linux-polska.iam.gserviceaccount.com\
--key-file=/home/kuba/.gcp/kuba-linux-polska-sa.json
Stworzenie definicji klastrów z wykorzystaniem Terraforma

Cały kod wykorzystywany w artykule znajduje się na publicznym repozytorium, w związku z tym przedstawię strukturę katalogów.

. 
├── DEV
│   ├── backend.tf
│   ├── main.tf 
│   └── variables.tf 
├── Module 
│   └── GKE 
│       ├── main.tf 
│       └── variables.tf 
└── PROD 
    ├── backend.tf 
    ├── main.tf 
    └── variables.tf

Jako można zauważyć stworzyłem moduł odpowiadający za Google Kubernetes Engine (GKE) oraz definicję dwóch środowisk. Dodatkowo użyję mechanizmu, który pozwala zapisywać stan środowiska we wcześniej stworzonym zasobie dyskowym wewnątrz chmury publicznej. Jest to bardzo wygodne rozwiązanie, jeśli nie tylko my odpowiadamy za infrastrukturę, a chcemy panować nad obecnym stanem środowiska.

Aby zbudować infrastrukturę z wykorzystaniem powyższego rozwiązania, musimy wykonać kilka prostych czynności.

  • Logowanie jako domyślny użytkownik

Uwaga! Jest to czynność do wykonywania na stacji roboczej. Jeżeli chcemy wykorzystać ten mechanizm w procesie CI/CD musimy wewnątrz modułu zdefiniować plik/token autoryzacyjny do dostawcy chmury publicznej.

gcloud auth application-default login
  • Stworzenie zasobu za pomocą narzędzia gsutil
gsutil mb gs://gke-hello-world
  • Inicjalizacja Terraforma na wybranym środowisku
cd DEV # lub PROD 
terraforma init
  • Stworzenie środowiska
terraform apply \ 
-var="path=~/.gcp/kuba-linux-polska-sa.json" \ 
-var="project=kuba-linux-polska"

Jeżeli wszystko się zgadza potwierdzamy operację wpisując ’yes'.

Manifesty

Skoro mamy już gotową infrastrukturę, możemy umieścić tam naszą aplikację. Zdecydowałem się tutaj na użycie Kustomize, jako narzędzia dość praktycznego i zapewniającego łatwiejsze zarządzanie małymi zmianami. Architektura aplikacji nie jest skomplikowana, nie tworzymy wielu zaawansowanych obiektów, dlatego odrzuciłem użycie Helma.

Porada! W przypadku nauki nowych rozwiązań polecam narzędzie Kind. Pozwala ono na uruchomienie małego klastra z użyciem Dockera. Konfiguracja jest prosta, a samo narzędzie jest niedużą binarką.

Struktura katalogu

Tutaj jak w przypadku Terraforma cały kod umieszczę w repozytorium. Omówię tylko najważniejsze elementy.

. 
├── base 
│   ├── deployment.yaml 
│   ├── kustomization.yaml 
│   ├── namespace.yaml 
│   └── service.yaml 
├── prod 
│   ├── increase_replicas.yaml 
│   └── kustomization.yaml 
└── dev 
    └── kustomization.yaml

Jak widać, struktura nie jest skomplikowana. Mamy zdefiniowane obiekty podstawowe. W zależności od środowiska dokonujemy niedużych zmian. Dodatkowo w przypadku produkcji zwiększamy domyślną liczbę replik naszej aplikacji.

Przygotowanie środowiska

Jako można było zauważyć, jeszcze nie umieściłem mojego kontenera w rejestrze gcr.io. Aby to zrobić, należy:

  • Zalogować się do rejestru
gcloud auth print-access-token | podman login \
-u oauth2accesstoken \ 
--password-stdin https://gcr.io
  • Wypchnąć nasz obraz do prywatnego rejestru
podman push localhost/go-hello-world:1.0.0 docker://gcr.io/kuba-linux-polska/go-hello-world:1.0.0
  • Zalogować się do klastra

Aby zalogować się Kubectlem do naszego klastra, możemy użyć narzędzia gcloud. Po uzyskaniu nazwy właściwego klastra oraz zony możemy bez przeszkód korzystać z Kubectl.

gcloud container clusters list 
gcloud container clusters get-credentials <claster-name> --zone <cluster-zone>
Uruchomienie aplikacji
kubectl kustomize prod | kubectl apply -f -
Test rozwiązania
k get svc hello-world -n hello-world -o json | jq '.status.loadBalancer.ingress[0].ip'

Otrzymany adres IP otwieramy w przeglądarce. Wynik nie powinien się różnić od tego, jaki wygenerował nasz test przy użyciu Podmana.

Wyłączenie infrastruktury

Należy nie zapominać o najważniejszym elemencie korzystania z chmury publicznej – kosztach. Jeżeli skończyliśmy testy naszego rozwiązania i nie zależy nam na korzystaniu z niego w sposób ciągły, usługę należy wyłączyć, aby nie generować niepotrzebnych kosztów. Wykorzystamy do tego ponownie Terraforma.

cd DEV 
terraform destroy \ 
-var="path=~/.gcp/kuba-linux-polska-sa.json" \ 
-var="project=kuba-linux-polska"

Jeżeli wszystko się zgadza, potwierdzamy operację, wpisując `yes`. Oczywiście rejestr obrazów oraz konta serwisowe nie zostaną skasowane. Terraform zarządza jedynie elementami, które stworzył.

Podsumowanie i wnioski

Chmury publiczne jak i konteneryzacja w znacznym stopniu ułatwiają proces developmentu. Aplikacje, spakowane i gotowe do wdrożenia znacznie skracają czas potrzebny na konfigurację środowiska. Jak można było zauważyć, zarówno konfigurowane środowisko, jak i wdrażana aplikacja nie byłby zbyt skomplikowane. Artykuł nie miał tego na celu. Jeżeli wdrażana aplikacja jest tak prosta, jak ta przedstawiona w przykładzie równie dobrze możemy wykorzystać inne usługi dostarczane przez Google Cloud Platform. Np. Cloud Run, który jest zdecydowanie mniej kosztowny, niż instancja Kubernetesa. Zawsze warto przemyśleć, czy zastosowane przez nas rozwiązanie pasuje do specyfiki problemu, który obecnie rozwiązujemy. Celem powyższego ćwiczenia było natomiast zapoznanie czytelnika z możliwościami, jakie daje chmura publiczna oraz narzędzia open source, a dodatkowo pokazanie dobrej zabawy przy nauce rozwiązań, których może nie wszyscy używają na co dzień. Chciałem, aby przedstawiony przykład był łatwy zarówno do odtworzenia, jak i zrozumienia przez każdego. Mam nadzieję, że mi się to udało. Jeżeli tak, to zachęcam do przeczytania drugiej części artykułu poświęconej wyłącznie procesowi CI/CD.

Kod źródłowy

Został umieszczony w trzech opisanych repozytoriach.

Aplikacja

Manifesty

Zobacz również