Docker i Nginx jako reverse-proxy do obsługi server side tagging

W tym wpisie dowiesz się, jak krok po kroku wdrożyć GTM server side. Poznasz więc sposób tagowania po stronie serwera z wykorzystaniem: serwerów podglądu i tagowania uruchamianych w środowisku dockerowym, a także Nginx jako serwera proxy.

Zagadnienia poruszane w tym artykule:

  • Czym w ogóle jest tagowanie po stronie serwera oraz dlaczego warto je wdrożyć?
  • Konfiguracja po stronie Tag Managera
  • Nginx jako proxy dla kontenerów dockerowych
  • Niestandardowe zachowanie kontenerów uruchomionych na podstawie obrazów przygotowanych przez Google i jak sobie z tym poradzić?

Czym jest i do czego służy tagowanie po stronie serwera, czyli server side tagging w teorii

Przed wprowadzeniem tagowania po stronie serwera dostępne było wyłącznie umiejscawianie i uruchamianie tagów po stronie przeglądarki urządzenia klienckiego. W konsekwencji nie mieliśmy możliwości kontrolowania tego, jakie dane i dokąd są przesyłane. 

Źródło obrazka: Dokumentacja Google → Client-side tagging

Tagowanie po stronie serwera wprowadza dodatkową warstwę kontroli przepływu danych między aplikacją a narzędziami np. do analizy, konwersji, reklam. 

Źródło obrazka: Dokumentacja Google → Server-side tagging

Dodatkowo niezaprzeczalnymi zaletami tagowania po stronie serwera są:

  • poprawa wydajności strony z powodu wykonywania kodu na serwerze, a nie na urządzeniu klienckim,
  • umożliwienie wprowadzenia bardziej restrykcyjnych polityk dotyczących bezpieczeństwa zawartości → Content Security Policy,
  • zwiększenie prywatności, ponieważ można np. usunąć adres IP z danych przesyłanych do końcowych punktów.

Pewnym drobnym minusem są oczywiście koszty, ponieważ musimy posiadać serwer i mieć w zespole osobę kompetentną do zarządzania nim. 

Przygotowania po stronie Tag Managera

W tej części dowiesz się, jak uzyskać “server container id” niezbędny do realizacji wdrożenia na serwerze. 

1. Po otwarciu strony głównej Tag Managera należy wybrać opcję “Utwórz kontener”.

2. W kolejnym widoku jedną z widocznych opcji na samym dole listy jest “Server”. Wskazujemy go i podajemy dowolną nazwę. Placeholder podpowiada, że może to być np. www.mojawitryna.pl – w przypadku ze screena jest podane “test”. Klikamy “Utwórz”. 

3. Pojawia się okno z dwiema opcjami. Jedna z nich daje możliwość samodzielnej konfiguracji serwera tagowania. Wybieramy ją, co w efekcie powoduje pojawienie się dodatkowego pola z długim ciągiem znaków. Jest to właśnie “server container id”, który będzie potrzebny później do konfiguracji kontenerów serwerów tagowania i podglądu w dockerze. 

4. Klikamy przycisk “Zamknij” w powyższym oknie i przechodzimy do zakładki “Administracja” → “Ustawienia kontenerów”.

5. W oknie, które się pojawi, klikamy przycisk “Dodaj adres URL” i wpisujemy subdomenę kontenera, który będzie działał jako serwer tagowania.

6. Następnie klikamy przycisk “Zapisz” znajdujący się w prawym górnym rogu. To wszystko.

Konfiguracja kontenerów Dockera

W tym miejscu przejdziemy do przedstawienia i omówienia konfiguracji docker-compose zawierającej dwa kontenery: serwera podglądu i serwera tagowania.

version: "3.8"
 
services:
  preview-server:
    image: gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable
    container_name: preview-server
    environment:
      CONTAINER_CONFIG: ${CONTAINER_CONFIG}
      RUN_AS_PREVIEW_SERVER: "true"
    ports:
      - "8081:8080"
 
  server-side-tagging-cluster:
    image: gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable
    container_name: server-side-tagging-cluster
    environment:
      CONTAINER_CONFIG: ${CONTAINER_CONFIG}
      PREVIEW_SERVER_URL: ${PREVIEW_SERVER_URL}
    ports:
      - "8082:8080"
    depends_on:
      - preview-server

Zwraca uwagę fakt, że oba serwisy – preview-server oraz server-side-tagging-cluster, korzystają z tego samego obrazu. Natomiast różnią się pewnymi detalami. 

Pierwszy z serwisów będzie wykorzystywany w momencie, gdy użytkownik kliknie przycisk “Podgląd” w Menedżerze tagów Google widoczny w prawym górnym rogu obszaru roboczego:

Wymaga on ustawienia zmiennej środowiskowej RUN_AS_PREVIEW_SERVER na wartość “true”. Druga zmienna – CONTAINER_CONFIG jest wspólna dla obu serwisów i zawiera wartość “server container id”. Pobierana jest z odrębnego pliku, do czego wrócimy nieco później. 

Z kolei drugi serwis odpowiada za przesyłanie właściwych danych i wymaga konfiguracji zmiennej PREVIEW_SERVER_URL. Należy go uruchomić dopiero po tym jak “preview-server” będzie w pełni operatywny. Google zaleca, by konfigurować oba serwisy pod odrębnymi subdomenami i wskazuje, by każda z nich znajdowała się w domenie danej aplikacji oraz koniecznie posiadała dostęp wykorzystujący protokół https. 

Każdy z serwisów ma swoją odrębną nazwę, która przydaje się przy wyszukiwaniu na liście uruchomionych kontenerów, szczególnie jeśli wdrożenie tagowania po stronie serwera obejmuje więcej niż jedną aplikację. Wskazane porty będą później wykorzystane w konfiguracji Nginxa.

Zmienne środowiskowe w zewnętrznym pliku

Kontenery wspomniane w poprzedniej sekcji wykorzystują zewnętrzny plik .env znajdujący się w głównym katalogu projektu wraz z plikiem docker-compose.yml. Jego struktura jest następująca:

CONTAINER_CONFIG=identyfikator-server-container-z-tag-managera
PREVIEW_SERVER_URL=https://preview.example.com

Wystawiamy wszystko na zewnątrz, czyli Nginx jako reverse proxy

Mając przygotowany pliki docker-compose.yml oraz .env zaczynamy konfigurować Nginx, który będzie pełnił funkcję proxy dla dwóch omawianych wcześniej serwisów — serwera podglądu i serwera tagowania. 

W jednym z poprzednich artykułów opisywałem przypadek zdokeryzowanego Nginx pełniącego funkcję serwera proxy. Tym razem Nginx występuje jako oddzielna instancja, a nie kontener dockerowy. 

Tworzymy dwa pliki. Nazwijmy je preview.example.com odpowiadający konfiguracji serwera podglądu oraz sst.example.com dla serwera tagowania. Każdy z nich będzie się różnił tylko podaną nazwą subdomeny oraz kolejnym portem przekazanym w dyrektywie “proxy_pass”. Wyróżniamy dwa bloki “server {}”. Poniżej pierwszy z nich:

server {
  server_name preview.example.com www.preview.example.com;
 
  return 301 https://$host$request_uri;
}

Konfiguracja dla portu 80. Zgodnie z dokumentacją Nginx, nie trzeba go jawnie podawać w dyrektywie “listen”:

“If the directive is not present then either *:80 is used if nginx runs with the superuser privileges, or *:8000 otherwise. “

Drugi blok jest bardziej złożony:

server {
  listen 443 ssl http2;
  server_name preview.example.com www.preview.example.com;
 
  # SSL
  ssl_trusted_certificate /etc/letsencrypt/live/preview.example.com/chain.pem;
  ssl_certificate /etc/letsencrypt/live/preview.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/preview.example.com/privkey.pem;
  include snippets/ssl-config.conf;
 
  # CONFIGS
  include snippets/secure-headers.conf;
  
  # CUSTOMIZATIONS
  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Forwarded-Proto $scheme;
 
    proxy_pass http://localhost:8081;
 
    add_header X-Robots-Tag "noindex, nofollow";
  }
}

Analizując po kolei strukturę tego bloku na początku wskazujemy, na jakim porcie, dla jakiej subdomeny Nginx ma nasłuchiwać oraz określamy, że ruch ma być szyfrowany i wykorzystywać http2. Następnie w sekcji “# SSL” ustalamy ścieżki do certyfikatów i konfigurujemy ssl, a w “# CONFIGS” bezpieczne nagłówki. 

  • Blok “location {}” zawiera właściwą konfigurację umożliwiającą zadziałanie Nginxowi jako proxy. 
  • Dyrektywa “proxy_set_header Host” ustawia nagłówek “Host” w przekazywanym do backendu zapytaniu HTTP na wartość “$host” przechowującą nazwę hosta z oryginalnego żądania klienta lub nazwę z “server_name” z powyższej konfiguracji.
  • Dyrektywa “proxy_set_header X-Real-IP” przechowuje adres ip klienta, który nawiązał połączenie z serwerem Nginx.
  • Dyrektywa “proxy_set_header X-Forwarded-For” zawiera adres ip klienta łączący się z serwerem Nginx wraz z adresami serwerów proxy.
  • Dyrektywa “proxy_set_header X-Forwarded-Host” zawiera nazwę hosta z oryginalnego zapytania wraz z nazwami serwerów proxy.
  • Dyrektywa “proxy_set_header X-Forwarded-Proto” zawiera protokół http/https, który był użyty w żądaniu klienta.
  • Dyrektywa “proxy_pass” określa gdzie Nginx ma przekierować żądania.
  • Dyrektywa “add_header X-Robots-Tag” dodaje nagłówek do odpowiedzi HTTP wysyłanej do klienta. Wartość “noindex, nofollow” informuje roboty indeksujące, że dana strona nie zezwala na indeksowanie i śledzenie linków.

Nginx – bezpieczne nagłówki, konfiguracja ssl i certyfikaty

Wyżej skupiliśmy się na rozkładaniu na czynniki pierwsze bloków “server {}”, wspomnieliśmy jednak o konfiguracji ssl i bezpiecznych nagłówków. 

Poniżej plik ssl-config.conf:

ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

Plik powstał w oparciu o Mozilla SSL Generator, aby zapewnić odpowiedni poziom bezpieczeństwa zestawionego połączenia. 

Drugi plik – secure-headers.conf zawiera nagłówki mające zwiększyć poziom bezpieczeństwa aplikacji internetowej:

# HSTS
add_header Strict-Transport-Security "max-age=31536000" always;
 
# XSS Protection
add_header X-XSS-Protection "1; mode=block";
 
# Clickjacking
add_header X-Frame-Options "SAMEORIGIN";
 
# X-Content Type Options
add_header X-Content-Type-Options nosniff;
 
# Secure Cookie
add_header Set-Cookie "Path=/; HttpOnly; Secure";

Strict-Transport-Security wymusza użycie przez klienta protokołu https do łączenia z aplikacją.

  • X-XSS-Protection chroni przed atakami typu Cross Site Scripting.
  • X-Frame-Options chroni przed atakami typu Clickjacking.
  • X-Content-Type-Options chroni przed atakami typu MIME sniffing.

Nagłówek Set-Cookie w takiej konfiguracji oznacza, że ciastka są dostępne we wszystkich ścieżkach na stronie, nie są dostępne dla skryptów JS i są przesyłane tylko przez zabezpieczone połączenia protokołem HTTPS.

Mając gotową konfigurację Nginx dla obu subdomen należy najpierw wykomentować linie “listen”, “ssl_trusted_certificate”, “ssl_certificate”, “ssl_certificate_key”, ponieważ certyfikaty będą dopiero generowane, a następnie przeładować konfigurację Nginx poleceniem:

systemctl reload nginx.service

Teraz należy wykonać poniższe polecenie w celu uzyskania certyfikatów Lets Encrypt dla subdomen, pod którymi będą działały kontenery:

certbot --nginx -d preview.example.com -d www.preview.example.com

A następnie odkomentować linie wspomniane wyżej i ponownie przeładować konfigurację Nginx. Kiedy konfiguracja Nginx jest kompletna można uruchomić kontenery dockerowe wykonując z poziomu głównego katalogu projektu polecenie:

docker-compose up -d

Niestandardowe zachowanie kontenerów dockerowych…

Kilkanaście minut od uruchomienia status kontenerów zmienił się na “unhealthy”. Uzyskana informacja od inżynierów Google pozwala twierdzić, że “unhealthy” nie oznacza, że kontener nie działa, a że oprogramowanie sugeruje restart, np. w celu pobrania nowszej wersji dodatkowych modułów. Ta informacja wymusiła utworzenie skryptu bash uruchamianego cyklicznie w celu sprawdzenia czy są jakieś kontenery z ww. statusem i jeśli tak, by je restartował. Poniżej kod:

#!/bin/bash
 
# Script finds out which container has unhealthy status and restarts it
 
for container_id in $(docker ps -q -f health=unhealthy)
do
  echo "Restarting unhealthy container: $container_id"
  docker restart $container_id
done

Po zapisaniu pliku należy mu nadać uprawnienia do wykonania dla danego użytkownika poleceniem:

chmod 700 skrypt.bash

A następnie można do tablicy crona dodać linię, np.

*/1 * * * * /path/to/skrypt.bash

Dzięki czemu skrypt będzie wykonywany co minutę i w razie potrzeby uruchomi ponownie “chory” kontener. ?

Pewność, że wszystko działa jak należy jest bezcenna

To, czy wszystko działa, można sprawdzić na kilka sposobów. Jednym z nich jest sprawdzenie w logu — wyświetlenie logów poleceniem docker compose logs nazwa-serwisu i sprawdzenie, czy pojawiają się linie podobne do poniższych:

server-side-tagging-cluster-poc | Your tagging server is running on the latest version.
server-side-tagging-cluster-poc | ***Listening on {"address":"::","family":"IPv6","port":8080}***
server-side-tagging-cluster-poc | Sending aggregate usage beacon (see https://www.google.com/analytics/terms/tag-manager/): https://www.googletagmanager.com/sgtm/a?v=s1&id=GTM-XXXYYYZZZ&iv=21&bv=3&rv=39k0&ts=ct_http!400*1.d_node-ver!18*1&cu=0.28&cs=0.13&mh=9277824&z
server-side-tagging-cluster-poc | Sending aggregate usage beacon (see https://www.google.com/analytics/terms/tag-manager/): https://www.googletagmanager.com/sgtm/a?v=s1&id=GTM-XXXYYYZZZ&iv=21&bv=3&rv=39k0&ts=ct_http!400*1&cu=0.25&cs=0.02&mh=10881248&z

Drugim sposobem jest uruchomienie polecenia docker-compose ps i sprawdzenie, czy w kolumnie “State” jest widoczne “Up (healthy)”.

Trzecim sposobem jest sprawdzenie endpointa “/healthz”. Jeśli w przeglądarce wyświetli się “ok”, to zdecydowanie oznacza, że jest w porządku. Alternatywnie można skorzystać z narzędzia cURL i polecenia curl https://subdomena.mojastrona.pl/healthz.

A co jeśli chcę mieć klaster obsługujący tagowanie?

Jest to jak najbardziej możliwe. Wtedy wystarczy nawet na odrębnej fizycznej maszynie, uruchomić kolejne (jeden lub więcej) kontenery serwera tagowania, utworzyć dla nich kolejne subdomeny, analogiczną konfigurację Nginx i przypisać do nich ten sam “server container id” użyty w pierwszej instancji. 

Podsumowanie: server side tagging z wykorzystaniem Docker i Nginx – co na pewno musisz wiedzieć?

Wdrożenie tagowania po stronie serwera na dockerze z wykorzystaniem Nginx i Docker Compose może być procesem problematycznym, ale też zaskakującym, szczególnie dla osób, które nie miały z nim wcześniej styczności. Dlatego właśnie w artykule pokazaliśmy, jak zrobić to krok po kroku. Przed rozpoczęciem wdrożenia, warto zrozumieć, czym w ogóle jest tagowanie po stronie serwera i dlaczego jest tak bardzo istotne. Nie bez znaczenia jest także to, jak poradzić sobie z konfiguracją poszczególnych serwisów w obrębie oprogramowania Nginx i Docker Compose. Pewne kwestie mogą nas zaskoczyć także po udanym wdrożeniu, dlatego odpowiednie przygotowanie to w tym przypadku klucz do sukcesu.

 

Autor

Piotr Bracha — pracuje jako administrator systemów. Karierę zaczynał jako młodszy administrator systemów w call center, gdzie miał m.in. możliwość poznania administrowania systemem CentOS i centralą telefoniczną Asterisk. Obecnie rozwija się w technologiach kontenerowych i chmurowych. Fan automatyzacji i porządku – w kodzie, ale też w środowisku pracy. Poza pracą uwielbia motoryzację i strzelectwo. Więcej artykułów autorstwa Piotra znajdziesz w serwisie medium.com.


.

Leave a Comment

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *