Tek Sayfa Uygulamasının dağıtımı için Docker görüntüsü

Tek Sayfalı Uygulama (SPA), resimler ve diğer kaynakların yanı sıra bir dizi statik JavaScript ve HTML dosyasıdır. Dinamik olarak değişmedikleri için bunları çevrimiçi yayınlamak çok kolaydır. Bunun için basit bir GitHub Sayfalarından (ve hatta bazıları için narod.ru'dan) başlayıp Amazon S3 gibi bir CDN ile biten çok sayıda ucuz ve hatta ücretsiz hizmet var. Ancak başka bir şeye ihtiyacım vardı.

Hem Kubernetes kümesinin bir parçası olarak üretimde hem de SPA'nın ne olduğu hakkında hiçbir fikri olmayan bir arka uç geliştiricinin makinesinde kolayca başlatılabilmesi için SPA'lı bir Docker görüntüsüne ihtiyacım vardı.

Kendim için aşağıdaki görsel gereksinimlerini belirledim:

  • kullanım kolaylığı (ancak montaj değil);
  • hem disk hem de RAM açısından minimum boyut;
  • görüntünün farklı ortamlarda kullanılabilmesi için ortam değişkenleri aracılığıyla yapılandırma;
  • Dosyaların en verimli şekilde dağıtılması.

Bugün size nasıl yapılacağını anlatacağım:

  • bağırsak nginx'i;
  • kaynaklardan brotli oluşturun;
  • statik dosyalara ortam değişkenlerini anlamayı öğretin;
  • ve tabii ki tüm bunlardan Docker görüntüsünün nasıl oluşturulacağı.

Bu makalenin amacı deneyimlerimi paylaşmak ve deneyimli topluluk üyelerini yapıcı eleştiriye teşvik etmektir.

Montaj için bir görüntü oluşturma

Nihai Docker görüntüsünün boyutunu küçük yapmak için iki kurala uymanız gerekir: minimum katman ve minimalist bir temel görüntü. En küçük temel imajlardan biri Alpine Linux imajıdır, o yüzden onu seçeceğim. Bazıları Alplerin üretime uygun olmadığını iddia edebilir ve haklı da olabilirler. Ama kişisel olarak onunla hiçbir zaman bir sorunum olmadı ve ona karşı hiçbir tartışmam yok.

Daha az katmana sahip olmak için görüntüyü 2 aşamada birleştireceğim. Birincisi bir taslaktır; tüm yardımcı programlar ve geçici dosyalar onun içinde kalacaktır. Ve son aşamada başvurunun sadece son halini yazacağım.

Yardımcı görüntüyle başlayalım.

Bir SPA uygulamasını derlemek için genellikle node.js'ye ihtiyacınız vardır. NPM ve iplik paketi yöneticileriyle birlikte gelen resmi resmi alacağım. Kendi adıma bazı npm paketleri oluşturmak için gerekli olan node-gyp'i ve daha sonra işimize yarayacak olan Google'ın Brotli kompresörünü ekleyeceğim.

Yorumları içeren Docker dosyası.

# Базовый образ
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

Zaten burada minimalizm için mücadele ediyorum, bu yüzden görüntü büyük bir ekip tarafından bir araya getiriliyor.

Bitmiş görüntü burada bulunabilir: https://hub.docker.com/r/alexxxnf/spa-builder. Yine de başkalarının resimlerine güvenmemenizi ve kendinizinkileri toplamanızı tavsiye ederim.

nginx

Statik içeriği dağıtmak için herhangi bir web sunucusunu kullanabilirsiniz. Nginx ile çalışmaya alışkınım, o yüzden artık onu kullanacağım.

Nginx'in resmi bir Docker görüntüsü var, ancak basit statik dağıtım için çok fazla modül var. Teslimata hangilerinin dahil olduğu özel bir ekip tarafından veya resmi Docker dosyasında görülebilir.

$ 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'ı temel olarak kullanacağım, ancak içinde yalnızca statik içeriği dağıtmak için gerekenleri bırakacağım. Sürümüm HTTPS üzerinden çalışamayacak, yetkilendirmeyi desteklemeyecek ve çok daha fazlası. Ancak benim sürümüm, gzip'ten biraz daha verimli olan Brotli algoritmasıyla sıkıştırılmış dosyaları dağıtabilecek. Dosyaları bir kez sıkıştıracağız; bunu anında yapmanıza gerek yok.

Bu, sonunda elde ettiğim Docker dosyası. Rusça yorumlar bana aittir, İngilizcedir - orijinalinden.

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;"]

Gzip ve brotli'nin varsayılan olarak etkinleştirilmesi için nginx.conf dosyasını hemen düzelteceğim. Ayrıca önbellek başlıklarını da dahil edeceğim çünkü statik değeri hiçbir zaman değiştirmeyeceğiz. Ve son dokunuş 404 isteğin tamamını index.html'ye yönlendirmek olacaktır, bu SPA'da gezinmek için gereklidir.

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";
            }
        }
    }
}

Bitmiş görüntüyü buradan indirebilirsiniz: https://hub.docker.com/r/alexxxnf/nginx-spa. 10,5 MB yer kaplıyor. Orijinal nginx 19,7 MB yer kaplıyordu. Spor ilgim tatmin edici.

Ortam değişkenlerini anlamak için statiği öğretmek

SPA'da neden ayarlara ihtiyaç duyulabilir? Örneğin hangi RESTful API'nin kullanılacağını belirtmek için. Tipik olarak istenen ortamın ayarları, oluşturma aşamasında SPA'ya aktarılır. Bir şeyi değiştirmeniz gerekiyorsa uygulamayı yeniden oluşturmanız gerekecektir. Onu istemiyorum. Uygulamanın CI aşamasında bir kez derlenmesini, CD aşamasında ise ortam değişkenleri kullanılarak gerektiği kadar yapılandırılmasını istiyorum.

Elbette statik dosyaların kendisi herhangi bir ortam değişkenini anlamaz. Bu nedenle bir numara kullanmanız gerekecek. Son görüntüde, nginx'i değil, ortam değişkenlerini okuyacak, bunları statik dosyalara yazacak, sıkıştıracak ve ancak daha sonra kontrolü nginx'e aktaracak özel bir kabuk komut dosyasını başlatacağım.

Bu amaçla Dockerfile ENTRYPOINT parametresini sağlar. Ona aşağıdaki betiği verelim (örnek olarak Angular'ı kullanarak):

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 "$@"

Scriptin işini yapabilmesi için js dosyalarına ayarların şu şekilde yazılması gerekmektedir: ${API_URL}.

Çoğu modern SPA'nın, oluştururken dosyalarına karma eklediğini belirtmekte fayda var. Bu, tarayıcının dosyayı uzun süre güvenli bir şekilde önbelleğe alabilmesi için gereklidir. Dosya değişirse karma değeri de değişir ve bu da tarayıcıyı dosyayı tekrar indirmeye zorlar.

Ne yazık ki, benim yöntemimde, konfigürasyonu ortam değişkenleri aracılığıyla değiştirmek dosya karmasında bir değişikliğe yol açmıyor; bu, tarayıcı önbelleğinin başka bir şekilde geçersiz kılınması gerektiği anlamına geliyor. Farklı ortamlarda farklı konfigürasyonlar kullanıldığından bu sorunu yaşamıyorum.

Son görüntüyü bir araya getirme

Nihayet.

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;"]

Artık ortaya çıkan görüntü herhangi bir yerde birleştirilip kullanılabilir.

Kaynak: habr.com