Создаем приложение: Docker, VueJs и Python-Sanic. Часть 3

Сегодня завершающая часть трилогии (предыдущие: Часть 1, Часть 2) о создании мультисервисного web-приложения на базе технологии Docker. В этой статье мы окончательно «сложим пазлы» в единую картину работающего приложения.

Уточним, что нам осталось сделать согласно постановки задачи:

  1. Реализовать WebSocket сервер на http://localhost/ws; (используем Python-Sanic).
  2. Написать простейший чат http://localhost/ (используя VueJs) который будет авторизироваться через http://localhost/api, реализованный в Части 2. Получим некий token, при помощи которого можно будет подключиться к чату на базе WebSocket-сервера из п. 1.

Этап 1. WebSocket server на Sanic

Небольшое отступление. Асинхронный фреймворк Sanic позволяет реализовать WS-сервер на базе созданного еще во 2-й части API. Я решил создать отдельный процесс, чтобы, с одной стороны, не смешивать код из разных частей статьи, с другой, чтобы наглядно продемонстрировать простоту микросервисной архитектуры.

Итак:

# Из корня проекта выполняем:
  mkdir ws

Добавляем файл ws/Dockerfile

FROM python:3.6.7-slim-stretch
WORKDIR /app
RUN apt-get update
RUN apt-get -y install gcc
COPY requirements.txt /tmp
RUN pip install -r /tmp/requirements.txt
VOLUME [ "/app" ]
EXPOSE 8000
CMD ["python", "run.py"]

Здесь мы «просим» docker создать нам контейнер, взяв за основу образ с python:3.6.7. Нужно доустановить в него некоторые системные библиотеки и необходимые для работы приложения python-пакеты из ws/requirements.txt:

# содержимое ws/requirements.txt
sanic
asyncio_redis

Кроме того, мы указали, что запуск приложения будет осуществляться на 8000 порту внутри самого контейнера, а команда реализация сервера находиться внутри run.py:

import os
from time import time
from sanic import Sanic
import ujson
import asyncio_redis
from websockets.exceptions import ConnectionClosed

app = Sanic('websocket')

conn = {}
CONN_CACHE_TIME = 10 # sec

@app.listener('before_server_start')
async def start(app, loop):
    app.redis = await asyncio_redis.Pool.create(host='redis', poolsize=10)


@app.listener('after_server_stop')
async def stop(app, loop):    
    app.redis.close()


async def check_token(request):
    token = request.args['token']   
    

async def checkTokenAlive(ws, token):
    if time() - conn.get(ws, 0) > CONN_CACHE_TIME:
        token_exists = await app.redis.exists(token)
        if token_exists:
            conn[ws]=time()            
        else:
            return False
    return True
    

@app.websocket('/')
async def feed(request, ws):                      
    token = request.args['token'].pop()
    if token:
        isAlive = await checkTokenAlive(ws, token)
        while isAlive:
            try:
                data = await ws.recv()            
                if data:                
                    if data=="/out":
                        await ws.close()
                    await ws.send(f'I\'ve received: {data}')                            
            except  ConnectionClosed:
                pass
            isAlive = await checkTokenAlive(ws, token)
    await ws.close()


if __name__ == "__main__":                
    debug_mode =  os.getenv('API_MODE', '') == 'dev'   

    app.run(
        host='0.0.0.0',
        port=8000,
        debug=debug_mode, 
        access_log=debug_mode
    )

В сервере предусмотрена периодическая (10 секунд) проверка token на «наличие» в Redis. Напомню, данный токен — это то, что получит наше SPA в случае успешной авторизации на localhost/api/v1.0/user/auth (реализация в Часть 2). Дополнительно реализована инструкция «/out» для самого чата, которая при отправке с клиента, закрывает текущее WebSocket-соединение.

Дополняем docker-compose.yml новым сервисом:

services:    
  ws: 
    container_name: test_ws
    build: 
      context: ./ws
    tty: true
    restart: always
    volumes: 
      - "./ws:/app"    
    links:      
      - "redis"     
    networks:      
      - internal
    env_file:
      - .env

Корректируем конфигурацию nginx сервера таким образом, чтобы все запросы, которые приходят на localhost/ws, проксировались к контейнеру «ws»:

services:    
       location /ws {            
        rewrite /ws$     /    break;  
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";        
        proxy_redirect     off;
        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-Proto    $scheme;
        proxy_set_header Host $http_host;
        proxy_pass http://ws:8000;
    }

Так как мы пишем SPA-приложение, работающее в браузере, нам необходимо получить самый что ни есть «классический» JavaScript, который гарантированно будет выполняться на подавляющем большинстве браузеров, установленных у пользователей. В силу очевидных причин, развитие JavaScript как языка программирования (ES5, ES6 и т. д.) сильно ушло вперед по сравнению с тем, что могут предложить существующие браузеры. По этой причине для того, чтобы воспользоваться всей мощью языка на клиенте, нам необходимо «преобразовать» наш «современный» синтаксис в («старый») понятный для браузера код.

Для этой цели как нельзя лучше подходит пакетный статический анализатор кода Webpack, к которому в качестве плагинов можно подключить конвертеры: Browserify, TypeScript, Sass, Less, css-minify, js-minify и многие другие, облегчающие web-разработку клиента. В общем случае — Webpack представляет собой демон (работающий процесс), который в зависимости от конфигурации отслеживает изменение кода и «налету» преобразовывает «удобный и современный» js-код в аналогичный по функционалу, но подходящий для работы в большинстве браузеров. В общем случае текст кода, который мы пишем, последовательно преобразовывается подключаемыми к демону плагинами и сохраняется в виде одного/двух файлов в директории /dist.

Настройка Webpack (установка и конфигурирование плагинов) — весьма муторное занятие, но существуют «утилиты-помощники», позволяющие выполнить эту затратную по времени операцию. Так как мы пишем фронтенд на VueJs, то в качестве такого «помощника» воспользуемся утилитой vue-cli, которая, кроме разворачивания Webpack, создаст базовое тестовое приложение на Vue, которое мы изменим под свои цели.

Этап 2. Создаем «SPA demo» при помощи vue-cli

Итак, нам нужно:

  • запустить контейнер с Node (версии LTS 10.5);
  • сгенерировать при помощи vue-cli demo-приложение;
  • запустить его в контейнере;
  • настроить маршрутизацию запросов сервера nginx.

Выполним из корня нашего проекта команды:

    # Переменной GID присваиваем идентификатор группы хост-машины
    echo "GID=$(id -g)" >> .env
    # Переменной GID присваиваем идентификатор текущего пользователя
    echo "UID=$(id -u)" >> .env

Cоздаем файл app/Dockerfile:

# За основу выбераем последний стабильный (LTS) образ версии Node
FROM node:10.15.0-alpine
WORKDIR /app
VOLUME ["/app"]
# инсталируем vue-cli согласно https://cli.vuejs.org/guide/installation.html
RUN npm install -g @vue/cli

В docker-compose.yml добавляем:

services:
  app:
    build: ./app
    tty: true
    user: "${UID}:${GID}"
    container_name: test_app
    volumes:
      - "./app:/app"
    networks:
    - internal    
    env_file:
      - .env

Хочу обратить внимание на 5-ю строчку в сервисе app. Предварительно мы сохранили в файл .env ID пользователя и группы хостмашины в силу особенности устройства ядра Linux. Даём указание docker запустить контейнер от лица этого юзера и группы. Это сделано для того, чтобы файлы, которые мы будем сейчас создавать внутри контейнера, можно было редактировать/создавать извне, то есть из хостмашины, не меняя владельца (chown) всех файлов.

Далее выполняем:

# из корня нашего проекта, перестраиваем и перезапускаем контейнеры
make upb
# или, если нравится традиционный способ, то:
docker-compose up -d --force-recreate --build

Мы запустили контейнер с Node версии 10.15.0, c предварительно установленным vue-cli.

Теперь:

  # подключаемся к sh-консоли работающего контейнера c Node  (в docker-compose его имя test_app)
docker exec -it test_app /bin/sh
# в консоли контейнера переходим в корневой каталог
cd /
# создаем demo приложение при помощи уже установленной во время создания контейнера vue-cli
vue create app
# Мастер, сообщаем что директория не пуста.. Выбираем "Merge"
# Затем в  меню "Manually select features" выбираем пакеты которые бы мы хотели установить для нашего приложения
# Выбираем нужные пакеты, я оставил:
# ◉ Babel
# ◉ Router
# ◉ CSS Pre-processors
# ◉ Linter / Formatter
# Далеее, на все вопросы установщика, можем соглашаться по умолчанию.
# В конце установки пакетов, мастер выдает сообщение
# $ cd app
# $ npm run serve

Выполнив последние 2 команды, мы увидим, что webpack по умолчанию запустился на 8080 порту.

Отключаемся от контейнера (Ctrl+С) и видим, что на хост-машине (ls -la app/ ) в папке фронтенда контейнер сгенерил «кучу» файлов, которые благодаря механизму Volumes, теперь являются общими для хост-машины и для контейнера. Самое интересное здесь то, что благодаря GID и UID сгенеренные из контейнера файлы принадлежат текущему юзеру host-машины, хотя внутри контейнера юзер имеет другое имя. Более исчерпывающую информацию о пользователях/группах в контейнерах можно почерпнуть здесь.

Так как у нас уже есть готовый рабочий код, все, что нам осталось сделать, — это подправить наш app/Dockerfile таким образом, чтобы контейнер при запуске выполнял команду запуска демона, то есть npm run serve.

# окончательный вид app/Dockerfile
FROM node:10.15.0-alpine
RUN apk add --no-cache bash
RUN npm install -g @vue/cli
WORKDIR /app
VOLUME ["/app"]
RUN npm install 
EXPOSE 8080
CMD ["npm", "run", "serve"]

Этап 2.1. Настраиваем nginx для vue-приложения

В конец конфигурационного файла nginx/server.conf вставим:

location / {                    
        rewrite /(.*) /$1  break;          
        proxy_redirect     off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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-Proto    $scheme;
        proxy_set_header Host $http_host;
        proxy_pass http://app:8080;
    }

Теперь перезапускаем все созданные контейнеры:

make upb

и заходим на http://localhost. Должны увидеть demo-страницу vue.

Важно! Demo-приложение, которое генерит vue-cli, по умолчанию для mode dev подключает интегрированный плагин vue-hot-reload-api, который через WebSocket-соединение «узнает» от демона webpack об изменениях на сервере и автоматически перезагружает страницу приложения в браузере, подтягивая новые данные. По этой причине, в конфигурации nginx, заложена поддержка проксирования WebSocket-заголовков.

  # Если нужно видеть вывод консоли работающего WebPack, 
  # цепляемся к контейнеру командой
docker attach test_app

Этап 2.2. Пишем клиентское приложение

Немного поговорим о том, как будет работать наш SPA-клиент:

  1. При заходе на localhost проверяем, есть ли в localStorage значение для ключа «token». Если есть, пробуем установить WebSocket-соединение с ws://localhost/ws?token=xxx с сервером. Если сервер закрыл соединение (token отсутствует в redis), сбрасываем приложение на страницу /login.
  2. На странице /login находится форма авторизации, которая отправляет данные на api (http://localhost/api/v1.0/user/auth), и в случае успеха сохраняет token в localStorage с последующим редиректом на главную.
  3. В случае неудачной авторизации, отрисовываем повторно форму авторизации с отображением ошибки валидации.

К сожалению, объем кода SPA-приложения довольно большой, что неминуемо приведет к тому, что объем статьи будет огромный. Все изменения кода в этой статье, я выполнил в отдельной ветке на GitHub. Их можно посмотреть в виде pull request, которые я выполнил по сравнению с состоянием кода по окончании 2-й части.

Итоговый результат

Если вы дочитали до этого места, то, пожалуй, именно сейчас как раз тот момент, когда вы можете полностью увидеть реализованный вариант рабочего приложения, состоящего из 7 контейнеров.

  cd ~
git clone 
 Данный адрес e-mail защищен от спам-ботов, Вам необходимо включить Javascript для его просмотра.
 :v-kolesov/vue-sanic-spa.git
cd vue-sanic-spa
docker-compose up  -d

После запуска всех контейнеров в браузере вбейте http://localhost. Вы должны увидеть, нечто похожее. В моем случае, в правом нижнем углу — 2 терминала, которые подключены непосредственно к контейнерам test_app, test_ws (нужно в целях отладки и дебагинга).

Читайте также:

Создаем приложение: Docker, VueJs и Python-Sanic. Часть 1

Создаем приложение: Docker, VueJs и Python-Sanic. Часть 2

Похожие статьи:
Протягом усієї повномасштабної війни ми висвітлюємо, як ІТ-індустрія реагує, допомагає та працює в умовах російської агресії....
If you are a busy software developer trying to keep your Web development skills up to date you should be learning new software under the guidance of savvy practitioners who have vast experience in development of the real-world...
Projector Foundation оголошує набір на безкоштовні онлайн-курси в ІТ та креативній індустрії. До навчання можуть долучитися жінки від...
Экономический кризис и пандемия внесли серьезные изменения во все сферы жизни: как справляются со сложившейся ситуацией...
Нещодавно Верховна Рада схвалила два законопроєкти, які мають підтримати military-tech в Україні. Документи передбачають...
Яндекс.Метрика