Бір беттік қолданбаны таратуға арналған докер кескіні

Single-page Application (SPA) — статикалық JavaScript және HTML файлдарының, сондай-ақ кескіндер мен басқа ресурстардың жиынтығы. Олар динамикалық түрде өзгермейтіндіктен, оларды желіде жариялау өте оңай. Бұл үшін қарапайым GitHub беттерінен (және кейбіреулер үшін narod.ru арқылы) бастап және Amazon S3 сияқты CDN-мен аяқталатын көптеген арзан және тіпті тегін қызметтер бар. Дегенмен, маған басқа нәрсе керек болды.

Маған SPA бар Docker кескіні қажет болды, осылайша оны өндірісте де Kubernetes кластерінің бөлігі ретінде де, SPA не екенін білмейтін бэк-энд әзірлеушінің машинасында да оңай іске қосуға болады.

Мен өзіме келесі сурет талаптарын анықтадым:

  • пайдаланудың қарапайымдылығы (бірақ құрастыру емес);
  • дискі бойынша да, жедел жады бойынша да минималды өлшем;
  • кескінді әртүрлі орталарда пайдалануға болатындай орта айнымалылары арқылы конфигурациялау;
  • файлдарды ең тиімді тарату.

Бүгін мен сізге қалай айтайын:

  • ішек nginx;
  • көздерден бротли құрастыру;
  • ортаның айнымалы мәндерін түсінуге статикалық файлдарды үйрету;
  • және, әрине, мұның бәрінен Docker кескінін қалай жинау керек.

Бұл мақаланың мақсаты – өз тәжірибеммен бөлісу және тәжірибелі қауымдастық мүшелерін сындарлы сынға итермелеу.

Құрастыру үшін сурет салу

Соңғы Docker кескінін кішірек ету үшін екі ережені сақтау керек: қабаттардың ең азы және минималистік негізгі кескін. Ең кішкентай негізгі кескіндердің бірі - Alpine Linux кескіні, сондықтан мен таңдайтын боламын. Кейбіреулер Альпі өндіруге жарамсыз деп таласуы мүмкін және олар дұрыс болуы мүмкін. Бірақ жеке өзім онымен ешқашан қиындық көрген емеспін және оған қарсы аргументтер де жоқ.

Қабаттардың аз болуы үшін суретті 2 кезеңде жинаймын. Біріншісі - жоба, онда барлық көмекші утилиталар мен уақытша файлдар қалады. Ал соңғы кезеңде қосымшаның соңғы нұсқасын ғана жазамын.

Көмекші суреттен бастайық.

SPA қолданбасын құрастыру үшін әдетте node.js қажет. Мен npm және жіп пакетінің менеджерлерімен бірге келетін ресми суретті аламын. Мен өз атымнан кейбір npm пакеттерін құру үшін қажет node-gyp және Google ұсынған Brotli компрессорын қосамын, ол бізге кейінірек пайдалы болады.

Түсініктемелері бар докер файлы.

# Базовый образ
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 кескіні бар, бірақ оның қарапайым статикалық тарату үшін тым көп модульдері бар. Олардың қайсысы жеткізілімге енгізілгенін арнайы топ немесе ресми Dockerfile файлынан көруге болады.

$ 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. Орыс тіліндегі пікірлер менікі, ағылшынша - түпнұсқадан.

Докер файлы

# Базовый образ снова 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 МБ алады. Түпнұсқа nginx 19,7 МБ орын алды. Менің спорттық қызығушылығым қанағаттандырылды.

Қоршаған ортаның айнымалыларын түсінуге статиканы үйрету

Неліктен 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-лардың көпшілігі құрылыс кезінде файлдарына хэштерді қосады. Бұл браузер файлды ұзақ уақыт қауіпсіз кэштей алуы үшін қажет. Егер файл өзгерсе, оның хэші өзгереді, бұл өз кезегінде браузерді файлды қайтадан жүктеп алуға мәжбүр етеді.

Өкінішке орай, менің әдісімде ортаның айнымалы мәндері арқылы конфигурацияны өзгерту файл хэшінің өзгеруіне әкелмейді, бұл браузердің кэші басқа жолмен жарамсыз болуы керек дегенді білдіреді. Менде бұл мәселе жоқ, себебі әртүрлі конфигурациялар әртүрлі орталарда орналастырылған.

Соңғы кескінді біріктіру

Ақырында.

Докер файлы

# Первый базовый образ для сборки
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