Uruchomienie Cloud Run do uruchamiania Chrome Headless przy ograniczonym budżecie

Cloud Run to nowa usługa zarządzana na Google Cloud Platform, która umożliwia uruchamianie aplikacji w kontenerach bezpośrednio z obrazu Dockera. Ogromną zaletą tej usługi jest to, że opłata naliczana jest tylko wtedy, gdy Twoja aplikacja jest rzeczywiście używana!

Wraz z pojawieniem się rozwiązań cloud hostowanie web aplikacji stało się jeszcze łatwiejsze i bardziej dostępne. Nie znaczy to jednocześnie że stało się tańsze. Patrząc na ceny instancji VPS w chmurze i tych od dostawców takich jak OVH nie trudno doliczyć się, że różnica w kosztach utrzymania najmniejszych choćby serwerów wirtualnych jest spora. Wraz ze wzrostem rozmiaru instacji, rośnie także różnica w cenie.

Należy pamiętać, że to co obiecuje nam chmura to przede wszystkim elastyczność i w tym sprawdza się ona najlepiej. W tym poście chciałbym przybliżyć czytelnikom w jaki sposób możemy użyć nowego serwisu dostępnego w chmurze Google a mianowcie Cloud Run.

Co to jest Cloud Run?

Cloud Run to nic innego jak środowisko uruchomieniowe operujące skonteneryzowanymi aplikacjami. Google chce uprościć jak najbardziej model aplikacji i ukryć warstwę hardware pod abstrakcją. W Cloud Run zarządzamy kontenerami które podczas tworzenia mogą mieć jedynie przypisaną dostępną pamięć RAM. Sam serwis dba pod spodem o to aby aplikacja miała wystarczająco mocy obliczeniowej do zaspokojenia obciążenia. Google steruje także automatycznie skalowaniem kontenerów dbając o to aby niezależnie od aktualnego zapotrzebowania praca działa się nie przerwanie.

Wywołać pracę w kontenerze można na dwa sposoby. Cloud Run tak czy inaczej wymaga od nas postawienia procesu webserwera na porcie 8080 który będzie obsługiwał przychodzące żądania. Pojawić się one mogą w sposób tradycyjny, wysyłając żądanie na wygenerowany przez serwis endpoint HTTP (Google sam zajmie się load balancingiem między instancjami kontenera, dla nas widoczny jest zawsze tylko jeden działający kontener) lub poprzez wysłanie wiadomości na topic w PubSub, Cloud Run posiada integrację z PubSub którą można ustawić w taki sposób aby wiadomości z tematu automatycznie kierowane były do kontenera.

Wycena usługi Cloud Run jest podobna jak w przypadku Cloud Functions. Płacimy tylko za faktyczny czas działania aplikacji w zaokrągleniu do sekundy, przy czym płacimy tylko za łączny czas pracy kontenera, to co odróżnia Cloud Run od Cloud Functions (oprócz działania w oparciu o kontener Dockera) to to, że kontener w Cloud Run może obsługiwać wiele requestów jednocześnie. W takim wypadku zostaniemy obciążeni za czas od rozpoczęcia pierwszego i zakończenia ostatniego requestu. Jest to odmienny model jak w przypadku Cloud Functions gdzie obciążani jesteśmy za czas trwania ale także ilość inwokacji danej funkcji.

Przechodząc już do konkretów i tematu tego posta. Cloud Run oferując uruchomienie dowolnego kontenera tak naprawdę otwiera nam drzwi do urzuchamiania dowolnej aplikacji w modelu cennika on-demand. Poniżej pokażę w jaki sposób możemy uruchomić przykładowe wdrożenie kontenera zawierającego przeglądarkę Chrome Headless która będzie non-stop w gotowości i oczekiwaniu na nasze requesty. Aby nadać temu postowi więcej konktekstu wyobraźmy sobie projekt.

Przykładowe wdrożenie Cloud Run wraz z Chrome Headless

Klient przychodzi do nas z konkretną prośbą: "mam stronę X.com i chciałbym pobierać z niej dane na żądanie, problem w tym, że strona to SPA (single page app) i tradycyjny scraping nie zdaje egzaminu. Chciałbym rozwiązanie które pozwoli uruchomić Chrome Headless na żądanie (prawdopodobnie kilka razy dziennie), ale żeby także nie było zbyt kosztowne w utrzymaniu."

Z pomocą przychodzi właśnie Cloud Run, struktura projektu wygląda następująco:

  • 1. Stworzenie obrazu Docker zawierającego zainstalowany Chrome Headless wraz z prostą aplikacją w Node.js która przy użyciu biblioteki Puppeteer łączy się z lokalną przeglądarką i wykorzystując protokół CDP (Chrome DevTools Protocol) wykona odpowiedni zestaw akcji przez przeglądarkę.
  • 2. Zbudowanie oraz wysłanie obrazu do Google Container Registry
  • 3. Uruchomienie usługi Cloud Run z naszym wcześniej stworzonym kontenerem
  • 4. Aplikacja gotowa możemy zacząć jej używać. Rachunku pozostają niskie, mimo w pełni działającemu deploymentowi Chrome Headless

Deployment obrazu Dockera z zainstalowanym Chrome do Cloud Run

Pierwszą rzecz jaką musimy wykonać to stworzenie naszej aplikacji na bazie obrazu Dockera zawierającego zainstalowany Chrome. W tym przykładzie będziemy bazować na aplikacji napisanej w Node.js

FROM node:10-slim

# Create and change to the app directory.
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
# Copying this separately prevents re-running npm install on every code change.
COPY package*.json ./

RUN apt-get update
RUN apt-get install -y gconf-service libasound2 libatk1.0-0 libcairo2 libcups2 libfontconfig1 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libxss1 fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN dpkg -i google-chrome-stable_current_amd64.deb; apt-get -fy install

# Install dependencies.
RUN npm install --production

# Copy local code to the container image.
COPY . .

# Run the web service on container startup.
CMD [ "node", "app.js" ]
                            

Używając takie bazy w pliku Dockerfile mamy pewność że zbudowany obraz zawsze będzie zawierał zainstalowany przeglądarkę Chrome.

Kolejnym krokiem będzie zbudowanie obrazu i wysłanie go do Container Registry w GCP. W tym celu najłatwiej posłyżyć się narzędziem CLI do obsługi GCP, w tym celu odsyłam do instrukcji, należy pamiętać że operację buildowania obrazu będziemy wykonywać dla konkretnego projektu, dlatego przedtem należy uwierzytelnić się w narzędziu.

Sama komenda nie jest skomplikowana i wykonujemy ją w katalogu aplikacji, tam gdzie umieszczony jest również plik Dockerfile

# {PROJECT-ID} - id projektu który możemy znaleźć w konsoli GCP
# {IMAGE-NAME} - nazwa obrazu która będzie jego identyfikatorem
gcloud builds submit --tag gcr.io/{PROJECT-ID}/{IMAGE-NAME}
                            

Poniżej wklejam także przykładowy kod aplikacji w Node.js który możemy wbudować w obraz.

#app.js
const express = require('express');
const puppeteer = require('puppeteer');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());

app.post('/', async (req, res) => {
    
    // puppeteer action code here...

})

let browser

let main = async () => {
    browser = await puppeteer.launch({
        headless: true,
        args: [
            '--disable-gpu',
            '--no-sandbox',
        ],
        executablePath: 'google-chrome'
    });

    app.listen(8080, () =>
        logger.log(`listening on port ${PORT}`)
    );

}

main()
                            

Uruchamiamy obraz z Container Registry jako usługa Cloud Run

Tak jak wcześniej wspomniałem, możemy uruchomić kontener Cloud Run w dwóch trybach - prywatnym i publicznym. Tryb prywatny charakteryzuje się tym, że dostęp do kontenera, a właściwie procesu serwera na porcie 8080 jest chroniony i dostępny tylko poprzez skonfigurowanie kolejki PubSub. Drugi tryb czyli publiczny, przydziela naszemy kontenerowi unikalny adres w domenie run.app który pozwala na wysyłanie żądań HTTP do serwera na porcie 8080. W tym poście skupimy się na tej drugiej konfiguracji z uwagi na prostą konfigurację.

Aby uruchomić usługę kontenera Cloud Run, wchodzimy w usługę Cloud Run w naszym projekcie i przystepujemy do uruchamiania usługi. Sam proces wymaga od nas jedynie garstki konfiguracji.

Przede wszystkim musimy wybrać obraz z Container Registry, nadać nazwę, wybrac region i zdecydować o sposobie dostępu do usługi. Na powyższym zrzucie ekranu widoczna jest konfiguracja przykładowej usługi z publicznym dostępem.

Ponadto Cloud Run pozwala na konfirgurację dostępnej pamięci RAM, portu webserver (jeżeli jest inny niż standardowy), a także ilości zapytań per kontener i ich timeout. Jest to o tyle ważne, że w przypadku osiągnięcia limitu, Cloud Run uruchomi kolejną instancję kontenera automatycznie, co wpłynie na dodatkowy mnożnik w rachunku za usługę.

Po wszystkim, nasz kontener będzie aktywny a wszystkie szczegóły możemy podejrzeć w konsoli. Tam również znajdziemy przydzielony adres URL, a także z tego miejsca możemy wdrożyć nową wersję w przypadku gdy wersja obrazu kontenera zmieni się. Dzięki zakładce "Logs" mamy podgląd do komunikatów wyświetlanych przez proces aplikacji w trybie "tail".

Podsumowując, Cloud Run to świetna usługa kiedy ważny jest dla nas budżet i elastyczność. Dzięki bazowaniu na Dockerze, Cloud Run pozwala na deployment dowolnej aplikacji z wieloma zależnościami i używanie jej tylko wtedy gdy faktycznie jej potrzebujemy. Integracja z PubSub jeszcze bardziej sprzyja wdrożeniom w dużych architekturach. W mojej opinii chmura będzie szła właśnie w kierunky usług uruchamiania kontenerów bez dostepu do hardware, już niebawem oferty na serwery dedykowane i VPS mogą spać niżej w ofercie dostawców chmur a ich miejsce zajmą właśnie usługi typu Cloud Run. Zachęcam do sppróbowania aby przekonać się jakie to łatwe. W Code Fibers jesteśmy już po pierwszym wdrożeniu produkcyjnym dla klienta, więc jeżeli masz pytania dotyczące takich wdrożeń chętnie na nie odpowiemy!

These posts might be interesting for you:

  1. Klaster RabbitMQ na Dockerze z Nodejs w 5 minut
  2. Handling Promise rejections in Express.js (Node.js) with ease
Author: Peter

I'm a backend programmer for over 10 years now, have hands on experience with Golang and Node.js as well as other technologies, DevOps and Architecture. I share my thoughts and knowledge on this blog.