Εικόνα Docker για διανομή Εφαρμογής Ενιαίας Σελίδας

Η Μονοσελίδα Εφαρμογή (SPA) είναι ένα σύνολο στατικών αρχείων JavaScript και HTML, καθώς και εικόνων και άλλων πόρων. Επειδή δεν αλλάζουν δυναμικά, η δημοσίευσή τους στο διαδίκτυο είναι πολύ εύκολη. Υπάρχει ένας μεγάλος αριθμός φθηνών και ακόμη και δωρεάν υπηρεσιών για αυτό, ξεκινώντας από απλές σελίδες GitHub (και για μερικούς ακόμη και με narod.ru) και τελειώνοντας με ένα CDN όπως το Amazon S3. Ωστόσο, χρειαζόμουν κάτι άλλο.

Χρειαζόμουν μια εικόνα Docker με SPA ώστε να μπορεί εύκολα να κυκλοφορήσει τόσο στην παραγωγή ως μέρος ενός συμπλέγματος Kubernetes όσο και στο μηχάνημα ενός προγραμματιστή back-end που δεν έχει ιδέα τι είναι το SPA.

Έχω καθορίσει τις ακόλουθες απαιτήσεις εικόνας για τον εαυτό μου:

  • ευκολία χρήσης (αλλά όχι συναρμολόγηση).
  • ελάχιστο μέγεθος τόσο από άποψη δίσκου όσο και RAM.
  • ρύθμιση παραμέτρων μέσω μεταβλητών περιβάλλοντος έτσι ώστε η εικόνα να μπορεί να χρησιμοποιηθεί σε διαφορετικά περιβάλλοντα.
  • την πιο αποτελεσματική διανομή αρχείων.

Σήμερα θα σας πω πώς:

  • έντερο nginx?
  • Δημιουργία brotli από πηγές.
  • διδάσκουν στατικά αρχεία για την κατανόηση μεταβλητών περιβάλλοντος.
  • και φυσικά πώς να συναρμολογήσετε μια εικόνα Docker από όλα αυτά.

Ο σκοπός αυτού του άρθρου είναι να μοιραστώ την εμπειρία μου και να προκαλέσω τα έμπειρα μέλη της κοινότητας σε εποικοδομητική κριτική.

Δημιουργία εικόνας για συναρμολόγηση

Για να κάνετε την τελική εικόνα Docker μικρή σε μέγεθος, πρέπει να τηρείτε δύο κανόνες: ένα ελάχιστο επίπεδο επιπέδων και μια μινιμαλιστική εικόνα βάσης. Μία από τις μικρότερες βασικές εικόνες είναι η εικόνα του Alpine Linux, οπότε αυτό θα επιλέξω. Κάποιοι μπορεί να υποστηρίξουν ότι το Alpine δεν είναι κατάλληλο για παραγωγή και μπορεί να έχουν δίκιο. Αλλά προσωπικά δεν είχα ποτέ κανένα πρόβλημα μαζί του και δεν υπάρχουν επιχειρήματα εναντίον του.

Για να έχω λιγότερα επίπεδα, θα συναρμολογήσω την εικόνα σε 2 στάδια. Το πρώτο είναι ένα προσχέδιο· όλα τα βοηθητικά προγράμματα και τα προσωρινά αρχεία θα παραμείνουν σε αυτό. Και στο τελικό στάδιο θα γράψω μόνο την τελική έκδοση της εφαρμογής.

Ας ξεκινήσουμε με τη βοηθητική εικόνα.

Για να μεταγλωττίσετε μια εφαρμογή SPA, χρειάζεστε συνήθως το node.js. Θα πάρω την επίσημη εικόνα, η οποία συνοδεύεται επίσης από τους διαχειριστές πακέτων npm και νήματος. Για λογαριασμό μου, θα προσθέσω το node-gyp, που χρειάζεται για την κατασκευή μερικών πακέτων npm, και τον συμπιεστή Brotli από την Google, που θα μας φανεί χρήσιμος αργότερα.

Dockerfile με σχόλια.

# Базовый образ
FROM node:12-alpine
LABEL maintainer="Aleksey Maydokin <[email protected]>"
ENV BROTLI_VERSION 1.0.7
# Пакеты, которые нужны, чтобы собрать из исходников Brotli
RUN apk add --no-cache --virtual .build-deps 
        bash 
        gcc 
        libc-dev 
        make 
        linux-headers 
        cmake 
        curl 
    && mkdir -p /usr/src 
    # Исходники Brotli скачиваем из официального репозитория
    && curl -LSs https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src 
    && cd /usr/src/brotli-$BROTLI_VERSION 
    # Компилируем Brotli
    && ./configure-cmake --disable-debug && make -j$(getconf _NPROCESSORS_ONLN) && make install 
    # Добавляем node-gyp
    && yarn global add node-gyp 
    # Убираем за собой мусор
    && apk del .build-deps && yarn cache clean && rm -rf /usr/src

Ήδη εδώ παλεύω για τον μινιμαλισμό, οπότε η εικόνα συντάσσεται από μια μεγάλη ομάδα.

Την ολοκληρωμένη εικόνα μπορείτε να τη βρείτε εδώ: https://hub.docker.com/r/alexxxnf/spa-builder. Αν και συνιστώ να μην βασίζεστε σε εικόνες άλλων ανθρώπων και να συλλέγετε τις δικές σας.

nginx

Μπορείτε να χρησιμοποιήσετε οποιονδήποτε διακομιστή ιστού για τη διανομή στατικού περιεχομένου. Έχω συνηθίσει να δουλεύω με το nginx, οπότε θα το χρησιμοποιήσω τώρα.

Το Nginx έχει επίσημη εικόνα Docker, αλλά έχει πάρα πολλές μονάδες για απλή στατική διανομή. Ποια από αυτά περιλαμβάνονται στην παράδοση μπορεί να τα δει μια ειδική ομάδα ή στο επίσημο αρχείο Docker.

$ docker run --rm nginx:1-alpine nginx -V

nginx version: nginx/1.17.9
built by gcc 8.3.0 (Alpine 8.3.0) 
built with OpenSSL 1.1.1d  10 Sep 2019
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --with-perl_modules_path=/usr/lib/perl5/vendor_perl --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-Os -fomit-frame-pointer' --with-ld-opt=-Wl,--as-needed

Θα χρησιμοποιήσω το Dockerfile ως βάση, αλλά θα αφήσω σε αυτό μόνο ό,τι χρειάζεται για τη διανομή στατικού περιεχομένου. Η έκδοσή μου δεν θα μπορεί να λειτουργήσει μέσω HTTPS, δεν θα υποστηρίζει εξουσιοδότηση και πολλά άλλα. Αλλά η έκδοσή μου θα μπορεί να διανέμει αρχεία συμπιεσμένα με τον αλγόριθμο Brotli, ο οποίος είναι ελαφρώς πιο αποτελεσματικός από το gzip. Θα συμπιέσουμε τα αρχεία μία φορά· δεν χρειάζεται να το κάνετε αυτό αμέσως.

Αυτό είναι το Dockerfile με το οποίο κατέληξα. Τα σχόλια στα ρωσικά είναι δικά μου, στα αγγλικά - από το πρωτότυπο.

Dockerfile

# Базовый образ снова Alpine
FROM alpine:3.9
LABEL maintainer="Aleksey Maydokin <[email protected]>"
ENV NGINX_VERSION 1.16.0
ENV NGX_BROTLI_VERSION 0.1.2
ENV BROTLI_VERSION 1.0.7
RUN set -x 
    && addgroup -S nginx 
    && adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx 
# Устанавливаем пакеты, которые нужны чтобы собрать nginx и модуль ngx_brotli к нему
    && apk add --no-cache --virtual .build-deps 
            gcc 
            libc-dev 
            make 
            linux-headers 
            curl 
    && mkdir -p /usr/src 
# Скачиваем исходники
    && curl -LSs https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz | tar xzf - -C /usr/src 
    && curl -LSs https://github.com/eustas/ngx_brotli/archive/v$NGX_BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src 
    && curl -LSs https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src 
    && rm -rf /usr/src/ngx_brotli-$NGX_BROTLI_VERSION/deps/brotli/ 
    && ln -s /usr/src/brotli-$BROTLI_VERSION /usr/src/ngx_brotli-$NGX_BROTLI_VERSION/deps/brotli 
    && cd /usr/src/nginx-$NGINX_VERSION 
    && CNF="
            --prefix=/etc/nginx 
            --sbin-path=/usr/sbin/nginx 
            --modules-path=/usr/lib/nginx/modules 
            --conf-path=/etc/nginx/nginx.conf 
            --error-log-path=/var/log/nginx/error.log 
            --http-log-path=/var/log/nginx/access.log 
            --pid-path=/var/run/nginx.pid 
            --lock-path=/var/run/nginx.lock 
            --http-client-body-temp-path=/var/cache/nginx/client_temp 
            --http-proxy-temp-path=/var/cache/nginx/proxy_temp 
            --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp 
            --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp 
            --http-scgi-temp-path=/var/cache/nginx/scgi_temp 
            --user=nginx 
            --group=nginx 
            --without-http_ssi_module 
            --without-http_userid_module 
            --without-http_access_module 
            --without-http_auth_basic_module 
            --without-http_mirror_module 
            --without-http_autoindex_module 
            --without-http_geo_module 
            --without-http_split_clients_module 
            --without-http_referer_module 
            --without-http_rewrite_module 
            --without-http_proxy_module 
            --without-http_fastcgi_module 
            --without-http_uwsgi_module 
            --without-http_scgi_module 
            --without-http_grpc_module 
            --without-http_memcached_module 
            --without-http_limit_conn_module 
            --without-http_limit_req_module 
            --without-http_empty_gif_module 
            --without-http_browser_module 
            --without-http_upstream_hash_module 
            --without-http_upstream_ip_hash_module 
            --without-http_upstream_least_conn_module 
            --without-http_upstream_keepalive_module 
            --without-http_upstream_zone_module 
            --without-http_gzip_module 
            --with-http_gzip_static_module 
            --with-threads 
            --with-compat 
            --with-file-aio 
            --add-dynamic-module=/usr/src/ngx_brotli-$NGX_BROTLI_VERSION 
    " 
# Собираем
    && ./configure $CNF 
    && make -j$(getconf _NPROCESSORS_ONLN) 
    && make install 
    && rm -rf /usr/src/ 
# Удаляем динамический brotli модуль, оставляя только статический
    && rm /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so 
    && sed -i '$ d' /etc/apk/repositories 
# Bring in gettext so we can get `envsubst`, then throw
# the rest away. To do this, we need to install `gettext`
# then move `envsubst` out of the way so `gettext` can
# be deleted completely, then move `envsubst` back.
    && apk add --no-cache --virtual .gettext gettext 
    && mv /usr/bin/envsubst /tmp/ 
    && runDeps="$( 
        scanelf --needed --nobanner /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst 
            | awk '{ gsub(/,/, "nso:", $2); print "so:" $2 }' 
            | sort -u 
            | xargs -r apk info --installed 
            | sort -u 
    )" 
    && apk add --no-cache $runDeps 
    && apk del .build-deps 
    && apk del .gettext 
    && mv /tmp/envsubst /usr/local/bin/ 
# Bring in tzdata so users could set the timezones through the environment
# variables
    && apk add --no-cache tzdata 
# forward request and error logs to docker log collector
    && ln -sf /dev/stdout /var/log/nginx/access.log 
    && ln -sf /dev/stderr /var/log/nginx/error.log
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
STOPSIGNAL SIGTERM
CMD ["nginx", "-g", "daemon off;"]

Θα διορθώσω αμέσως το nginx.conf ώστε το gzip και το brotli να είναι ενεργοποιημένα από προεπιλογή. Θα συμπεριλάβω επίσης κεφαλίδες προσωρινής αποθήκευσης, γιατί δεν θα έχουμε ποτέ αλλαγή στατικού. Και η τελευταία πινελιά θα είναι η ανακατεύθυνση και των 404 αιτημάτων στο index.html, αυτό είναι απαραίτητο για την πλοήγηση στο SPA.

nginx.conf

user nginx;
worker_processes  1;
error_log /var/log/nginx/error.log warn;
pid       /var/run/nginx.pid;
load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;
events {
    worker_connections 1024;
}
http {
    include      mime.types;
    default_type application/octet-stream;
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log main;
    sendfile on;
    keepalive_timeout 65;
    gzip_static   on;
    brotli_static on;
    server {
        listen      80;
        server_name localhost;
        charset utf-8;
        location / {
            root html;
            try_files $uri /index.html;
            etag on;
            expires max;
            add_header Cache-Control public;
            location = /index.html {
                expires 0;
                add_header Cache-Control "no-cache, public, must-revalidate, proxy-revalidate";
            }
        }
    }
}

Μπορείτε να κατεβάσετε την ολοκληρωμένη εικόνα εδώ: https://hub.docker.com/r/alexxxnf/nginx-spa. Καταλαμβάνει 10,5 MB. Το αρχικό nginx καταλάμβανε 19,7 MB. Το αθλητικό μου ενδιαφέρον είναι ικανοποιημένο.

Διδασκαλία της στατικής για την κατανόηση μεταβλητών περιβάλλοντος

Γιατί μπορεί να χρειάζονται ρυθμίσεις στο SPA; Για παράδειγμα, για να καθορίσετε ποιο RESTful API θα χρησιμοποιήσετε. Συνήθως, οι ρυθμίσεις για το επιθυμητό περιβάλλον μεταφέρονται στο SPA στο στάδιο της κατασκευής. Εάν πρέπει να αλλάξετε κάτι, θα πρέπει να δημιουργήσετε ξανά την εφαρμογή. Δεν το θέλω. Θέλω η εφαρμογή να δημιουργηθεί μία φορά στο στάδιο CI και να ρυθμιστεί όσο χρειάζεται στο στάδιο του CD χρησιμοποιώντας μεταβλητές περιβάλλοντος.

Φυσικά, τα ίδια τα στατικά αρχεία δεν κατανοούν καμία μεταβλητή περιβάλλοντος. Επομένως, θα πρέπει να χρησιμοποιήσετε ένα κόλπο. Στην τελική εικόνα, δεν θα εκκινήσω το nginx, αλλά ένα ειδικό σενάριο φλοιού που θα διαβάζει μεταβλητές περιβάλλοντος, θα τις γράφει σε στατικά αρχεία, θα τις συμπιέζει και μόνο μετά θα μεταφέρει τον έλεγχο στο nginx.

Για το σκοπό αυτό, το Dockerfile παρέχει την παράμετρο ENTRYPOINT. Ας του δώσουμε το ακόλουθο σενάριο (χρησιμοποιώντας το Angular ως παράδειγμα):

docker-entrypoint.sh

#!/bin/sh
set -e
FLAG_FILE="/configured"
TARGET_DIR="/etc/nginx/html"
replace_vars () {
  ENV_VARS='$(awk 'BEGIN{for(v in ENVIRON) print "

quot;v}')'
# В Angular ищем плейсхолдеры в main-файлах
for f in "$TARGET_DIR"/main*.js; do
# envsubst заменяет в файлах плейсхолдеры на значения из переменных окружения
echo "$(envsubst "$ENV_VARS" < "$f")" > "$f"
done
}
compress () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttf


quot;); do
# Используем максимальную степень сжатия
gzip -9kf "$i" && brotli -fZ "$i"
done
}
if [ "$1" = 'nginx' ]; then
# Флаг нужен, чтобы выполнить скрипт только при самом первом запуске
if [ ! -e "$FLAG_FILE" ]; then
echo "Running init script"
echo "Replacing env vars"
replace_vars
echo "Compressing files"
compress
touch $FLAG_FILE
echo "Done"
fi
fi
exec "$@"

Για να κάνει το σενάριο τη δουλειά του, οι ρυθμίσεις πρέπει να είναι γραμμένες στα αρχεία js με αυτή τη μορφή: ${API_URL}.

Αξίζει να σημειωθεί ότι τα περισσότερα σύγχρονα SPA προσθέτουν hashes στα αρχεία τους κατά την κατασκευή. Αυτό είναι απαραίτητο ώστε το πρόγραμμα περιήγησης να μπορεί να αποθηκεύσει με ασφάλεια το αρχείο για μεγάλο χρονικό διάστημα. Εάν το αρχείο αλλάξει, τότε ο κατακερματισμός του θα αλλάξει, κάτι που με τη σειρά του θα αναγκάσει το πρόγραμμα περιήγησης να πραγματοποιήσει ξανά λήψη του αρχείου.

Δυστυχώς, στη μέθοδό μου, η αλλαγή της διαμόρφωσης μέσω μεταβλητών περιβάλλοντος δεν οδηγεί σε αλλαγή του κατακερματισμού του αρχείου, πράγμα που σημαίνει ότι η προσωρινή μνήμη του προγράμματος περιήγησης πρέπει να ακυρωθεί με κάποιον άλλο τρόπο. Δεν έχω αυτό το πρόβλημα, επειδή αναπτύσσονται διαφορετικές ρυθμίσεις παραμέτρων σε διαφορετικά περιβάλλοντα.

Συνδυάζοντας την τελική εικόνα

Τέλος.

Dockerfile

# Первый базовый образ для сборки
FROM alexxxnf/spa-builder as builder
# Чтобы эффктивнее использовать кэш Docker-а, сначала устанавливаем только зависимости
COPY ./package.json ./package-lock.json /app/
RUN cd /app && npm ci --no-audit
# Потом собираем само приложение
COPY . /app
RUN cd /app && npm run build -- --prod --configuration=docker

# Второй базовый образ для раздачи
FROM alexxxnf/nginx-spa
# Забираем из первого образа сначала компрессор
COPY --from=builder /usr/local/bin/brotli /usr/local/bin
# Потом добавляем чудо-скрипт
COPY ./docker/docker-entrypoint.sh /docker-entrypoint.sh
# И в конце забираем само приложение
COPY --from=builder /app/dist/app /etc/nginx/html/
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

Τώρα η εικόνα που προκύπτει μπορεί να συναρμολογηθεί και να χρησιμοποιηθεί οπουδήποτε.

Πηγή: www.habr.com