Ghost cms to rozwiązanie które napędza tego bloga. Jest minimalistyczne do bólu, szybkie, a posty pisane są w markdownie, przez co nie muszę odrywać rąk od klawiatury – co jest dla mnie bardzo istotne. Stawiam dopiero pierwsze kroki z tym systemem, ale już mam przeczucie, że większość funkcjonalności już poznałem. Jak jednak skonfigurowany jest ten blog? Spieszę z wyjaśnieniem, bo musiałem go delikatnie dostosować.
Docker swarm
Jak zapewne zauważyliście, bardzo duży nacisk kładę na docker swarm. Technologia ta odpowiada za dystrybucję i utrzymanie środowiska składającego się z kontenerów dockera. Mnie jako administratora nie interesuje zatem który worker podniesie dany serwis, lecz czy jest on osiągalny w ramach klastra. Jedyne rozróżnienie jakie mam, to czy dany node jest managerem czy workerem. Manager posiada zainstalowany load-balancer który przekazuje ruch do kontenerów w ramach sieci overlay. W związku z tym, to właśnie manager powinien mieć jak najwięcej wolnych zasobów, aby jego praca była płynna. Niejednokrotnie uruchamiając apt update
blokowałem cały ruch który przechodzi właśnie przez load-balancer. Bardzo nieładnie. Workery natomiast mogą przyjąć na klatę faktyczne kontenery które chcę uruchomić. Jest tam testowa jak i produkcyjna wersja sklepu, blog i inne mniej lub bardziej przydatne kontenery. Gdzie faktycznie jest uruchomiony blog lub sklep nie ma najmniejszego znaczenia dopóki trzymam się pewnych zasad. O nich właśnie nie pomyślałem w tym przypadku.
W poszukiwaniu plików

Znacie cykl życia kontenera? Byt ten jest tworzony, wstaje, pracuje, umiera, jest usuwany. Ghost CMS pracuje na lokalnym systemie plików który w tym przypadku (bez dodatkowej konfiguracji) istnieje tylko podczas pracy kontenera. Kontener umierając zabiera ze sobą wszystkie pliki które posiadał w lokalnej pamięci. Z góry należy zakładać, że każdy kontener jest bezstanowy, a jeśli chcemy mieć dostęp do tych plików nawet po ubiciu kontenera, to należy zastosować volumeny, lub binding (wskazujemy lokalny katalog który będzie synchronizowany z katalogiem z kontenera). Ghost po wybraniu interesującego motywu zapisał go w lokalnym systemie plików, który nie był ani bindowany, ani nie był volumenem na hoście i korzystał z niego. Pierwszy restart kontenera spowodował wyczyszczenie systemu plików, a co za tym idzie ściągnięty motyw przestał być dostępny. Baza danych miała dalej zapisaną nazwę motywu i właśnie jego nowa instancja próbowała szukać w swoim lokalnym systemie plików. Jak się domyślacie, bezskutecznie.
Co gorsza, obrazki z postów również zapisywały się w lokalnym systemie plików, więc po restarcie chwilę wisiały w cache, lecz po chwili przestały być dostępne. Ten problem jednak zaadresujemy dopiero później.
Początkowo zastosowałem bind /var/ghost/content:/content
, aby na hoście trzymać pliki wybranego motywu, oraz grafiki przesyłane w edytorze. To rozwiązanie działało tylko wtedy kiedy ten sam node podniósł instancję. Każda inna nie miała już u siebie na hoście tych plików, więc problem powracał. Dodałem zatem regułę, że serwis może zostać utworzony tylko na konkretnej nazwie hosta. Zadziałało, lecz nie dawało mi to spokoju. Moją intencją było nauczyć się zarządzania klasterem, a nie ustawianie wszystkiego na sztywno i jak mi się dany node odepnie, lub wykrzaczy spektakularnie, to kaplica. Jak zatem sprawić, aby motyw zawsze był dostępny dla naszego bloga (nie wiedziałem jeszcze wtedy, że problem dotyczy również obrazków)?
Niestandardowy obraz rozwiązaniem wielu problemów
Nie lubię pisać Dockerfile
głównie ze względu na problemy z debuggowaniem procesu budowania obrazu. Chcesz echo
? Pisz do pliku. Chcesz ls
? Odpal kontener i ręcznie sprawdzaj wszystko. Nie miałem jednak wyjścia. Z tyłu głowy jednak wiedziałem, że to mnie nie ominie, bo chciałem przetłumaczyć domyślny motyw. Okazało się jednak, że społeczność już to zrobiła link. Super! Teraz pozostaje pytanie jak to repozytorium sklonować do mojego obrazu (obraz ghost nie posiada zainstalowanego git-a, a nie chciałem tego robić w obrazie, bo nie chcę go powiększać). Z pomocą przyszedł mechanizm multi-stage builds z Dockera. Na czym to polega? Możemy w ramach jednego Dockerfile
wykorzystać kilka obrazów. Każdy zapis FROM image-name as stage-name
może dać nam dostęp do swoich plików i przekopiować je do kolejnego kroku. Super! Plan był prosty. Polegał on na użyciu obrazu git-a, a następnie sklonowaniu repozytorium z motywem i przekazanie ściągniętych plików do kolejnego kroku który już mógł wykorzystać faktyczny obraz ghost.
# Download custom template
FROM alpine/git as theme-downloader
WORKDIR /git
RUN git clone https://github.com/juan-g/WorldCasper2.git casper-2
# Use ghost image
FROM ghost
WORKDIR /var/lib/ghost
# Copy custom template to the target image
COPY --from=theme-downloader /git/casper-2 ./content/themes/casper-2
Multi-stage build pozwala na przekazywanie plików pomiędzy krokami budowania, oraz na wykorzystanie wielu obrazów w zależności od potrzeb. Ostatni obraz będzie użyty jako bazowy w wynikowym obrazie
To rozwiązanie pozwoliło na wykorzystanie domyślnego motywu z lokalizacją i poprawkami społeczności w wynikowym niestandardowym obrazie. Sprawdzam, testuję i wszystko działa pięknie. Język jest wczytany poprawnie, motyw renderuje się bez przeszkód, ciekawie wygląda, a posty nie mają obrazków. Czekaj, co?
Jak mam do niestandardowego obrazu wsadzić obrazki? Przecież to bez sensu!
To pytanie zadałem sobie w pierwszej kolejności. Próbowałem nawet rozwiązań typu docker-s3-volume, aby obrazy były przechowywane na s3 (jak to mam w zwyczaju robić). To rozwiązanie jednak miało problemy z przywracaniem plików do volumenu, gdyż ten nie pozwalał na zapis do tego katalogu. Dodatkowo ponownie musiałbym precyzować nazwę hosta, aby oba kontenery lądowały na tym samym hoście, gdyż docker swarm nie posiada mechanizmu wymuszenia deploy-u kontenerów z serwisu na ten sam node. Rozwiązanie fajne, bo nie generuje dodatkowych kosztów (sync dzieje się w momencie zatrzymania/wznowienia kontenera, a pliki są serwowane lokanie, zatem jeśli kontener długo żyje, to nie wygeneruje kosztów za każdy get-object).
To może zatem sync w ramach multi-stage build? No nie, bo build dzieje się tylko w momencie budowania obrazu, a nie podczas jego uruchamiania, więc assety byłby tylko pobierane z s3 w momencie kiedy robiłbym deploy, a to nie nie zdałoby egzaminu, gdyż nowe grafiki nie znalazłyby się na s3). Ponownie zagłębiłem się w dokumentację ghost (bo przecież musieli ten problem jakoś rozwiązać). Znalazłem adaptery dla storage. Wygląda jakby to było to. Dokumentacja opisała adapter jako rozwiązanie na hostowanie grafik w zewnętrznym źródle. Zastanawiałem się dlaczego tylko grafik, skoro równie dobrze mogłyby być trzymane motywy oraz pliki z lokalizacją. Tu bardziej chodzi o prostą logikę, mianowicie assety które nie są tworzone, przesyłane przez użytkownika nie powinny lądować na s3, a być częścią obrazu. Jak się zastanowić, to ma to sens. Przecież motyw i tak kopiuję w procesie budowania obrazu. Dobra, czas to podłączyć.
# Download custom template
FROM alpine/git as theme-downloader
WORKDIR /git
RUN git clone https://github.com/juan-g/WorldCasper2.git casper-2
# Use ghost image
FROM ghost
WORKDIR /var/lib/ghost
# Copy custom template to the target image
COPY --from=theme-downloader /git/casper-2 ./content/themes/casper-2
# Install storage adapter
RUN npm install ghost-storage-adapter-s3
RUN mkdir -p ./content/adapters/storage
RUN cp -r ./node_modules/ghost-storage-adapter-s3 ./content/adapters/storage/s3
Dodałem krok instalacji adaptera dla s3 zgodnie z dokumentacją
Dodanie adaptera kiedy już istnieją jakieś posty z grafikami oczywiście nie naprawia ich, lecz ręcznie musiałem je zaktualizować i przesłać ponownie. Teraz każda przesyłana grafika została wysłana na s3, a link do niej został osadzony w poście. Jeszcze tylko zaktualizowałem reguły na buckecie i to tyle.
Gdzie jednak trafia ten niestandardowy obraz kontenera?
GitLab jest o tyle przyjemnym miejscem, że dostarcza nam przestrzeń na obrazy. Wystarczy tylko odpowiednio otagować nasz obraz podczas budowania, a następnie zrobić push. Za to już odpowiada nasz CI.
echo "Login to ${CI_REGISTRY} as ${CI_REGISTRY_USER}"
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
echo "Build image ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} .
echo "Push image ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
Skrypt odpowiadający za logowanie, budowanie oraz przesłanie obrazu do serwisu GitLab
Docker swarm wymaga jednak jeszcze jednego kroku. Dostęp do naszych obrazów jest ograniczony tylko dla zalogowanych użytkowników. W związku z tym (tak jak zresztą widać powyżej) musimy każdorazowo się logować do registry. W przypadku budowania obrazu nie jest to problematyczne, bo CI_REGISTRY_USER
jest dostarczany przez GitLab, a CI_REGISTRY_PASSWORD
jest unikalne i generowane dla każdego joba. Problem pojawia się, gdy musimy zrobić docker stack deploy
. Oczywiście w docker-compose
wstrzyknięty zostanie odpowiedni obraz image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
, lecz node nie będzie miał uprawnień, aby go pobrać. Rozwiązaniem tego problemu są wbudowane w GitLab Deploy Tokens. Serwis pozwala na wygenerowanie takich poświadczeń logowania, a następnie na wykorzystanie ich w zadaniu z docker stack deploy
(wcześniej jednak trzeba się zalogować do registry tak jak powyżej, lecz przy użyciu poświadczeń z Deploy Token). Na tym jednak nie koniec, ponieważ docker swarm wymaga, aby te poświadczenia zostały zapisane obok nazwy obrazu. To ważne, ponieważ dzięki temu każdy node podczas tworzenia kontenera, będzie miał token który będzie wykorzystany do autoryzacji dostępu do obrazu.
docker stack deploy -c docker-compose.yml blog --with-registry-auth
–with-registry-auth pozwala na przekazanie poświadczeń do klastra, aby te mogły być wykorzystane podczas tworzenia nowych kontenerów
Słowem zakończenia
W ten oto sposób udało mi się rozwiązać dwa problemy z którymi przyszło mi się zmagać. Muszę przestać się bać tworzenia własnych Dockerfile
jeśli coś nie spełnia moich wymagań. Wymaga to kreatywnego podejścia do problemu i przyznam się, że gdyby nie to repozytorium, to dalej błądziłbym po Internecie szukając rozwiązania swojego problemu.