company_banner

Пакуем приложения ASP.NET Core с помощью Docker

    Приложения ASP.NET Core по-настоящему кросс-платформенны и могут запускаться в «никсах», а соответственно, и в Docker. Посмотрим, как их можно упаковать, чтобы развертывать на Linux и использовать в связке с Nginx. Подробности под катом!



    Примечание: мы продолжаем серию публикаций полных версий статей из журнала Хакер. Орфография и пунктуация автора сохранены.


    О докере


    О микросервисной архитектуре слышали практически все. Сам концепт разбития приложения на части не сказать чтобы новый. Но, новое – это хорошо забытое и переработанное старое.


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


    Для того, чтобы описать что такое Docker очень часто упрощенно используют термин «виртуальная машина». Сходство определенно есть, но говорить так неправильно. Проще всего это различие понять, посмотрев на следующие изображения с официальной документации докера:




    Контейнеры используют ядро текущей операционной системы и делят его между собой. В то время как виртуальные машины с помощью hypervisor используют аппаратные ресурсы.
    Образ/Image докера это read-only объект, который, по сути, хранит в себе шаблон для построения контейнера. Контейнер — это среда в которой выполняется код. Образы хранятся в репозиториях. Например, официальный репозиторий Docker Hub позволяет хранить только один образ приватно. Впрочем, это бесплатно, поэтому даже за это нужно их поблагодарить.


    INFO


    Докер не является единственным представителем контейнеризации. Кроме его существуют и другие технологии. Например:


    rkt (произносится как 'рокет') от CoreOS


    LXD (произносится как ‘лексди’) от Ubuntu


    Windows Containers — ни за что не угадаете от кого.


    Теперь, когда мы ознакомились с теорией, давайте перейдем к практике.


    Установку докера особого смысла разбирать нет, ведь его можно установить на множество операционных систем. Укажу только, что скачать его под свою платформу можно из Docker Store. Если вы устанавливаете Docker под Windows, то необходимо чтобы в BIOS и в ОС была включена виртуализация. О том как включить ее в 10-ке можно прочитать в следующем артикуле: Установка Hyper-V вWindows10


    Создание проекта с поддержкой докера


    Докер это, конечно Linux-овый продукт, но при необходимости можно его использовать при разработке под Mac или под Windows. При создании проекта в Visual Studio для добавления поддержки докера достаточно поставить флажок Enable Docker Support.


    Поддержку докера можно добавить и в существующий проект. Добавляется она в проект таким же образом, как и добавляются различные новые компоненты. Контекстное меню Add – Docker Support.


    В случае, если на вашей машине установлен и запущен докер, будет автоматически открыта консоль и выполнена команда


    docker pull microsoft/aspnetcore:2.0

    которая запускает процесс скачивания образа. Этот образ фактически является заготовкой на основе которого будет создан ваш образ. ASP.NET Core 2.1 использует уже другой образ – microsoft/dotnet:sdk


    В директории с решением для вас будут созданы автоматически следующие файлы:
    .dockerignore (исключение файлов и директорий из образа докера), docker-compose.yml (с помощью этого файла можно сконфигурировать выполнение нескольких сервисов), docker-compose.override.yml (вспомогательная конфигурация docker-compose), docker-compose.dcproj (файл проекта для Visual Studio).


    В директории с проектом создастся файл Dockerfile. Собственно, с помощью этого файла мы и создаем свой образ. По умолчанию (в случае, если проект называется DockerServiceDemo) он может выглядеть примерно так:


    FROM microsoft/aspnetcore:2.0 AS base
    WORKDIR /app
    EXPOSE 80
    
    FROM microsoft/aspnetcore-build:2.0 AS build
    WORKDIR /src
    
    COPY DockerServiceDemo/DockerServiceDemo.csproj DockerServiceDemo/
    RUN dotnet restore DockerServiceDemo/DockerServiceDemo.csproj
    COPY . .
    WORKDIR /src/DockerServiceDemo
    RUN dotnet build DockerServiceDemo.csproj -c Release -o /app
    
    FROM build AS publish
    RUN dotnet publish DockerServiceDemo.csproj -c Release -o /app
    
    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app .
    ENTRYPOINT ["dotnet", "DockerServiceDemo.dll"]

    Начальная конфигурация для .NET Core 2.0 не позволит вам сразу построить образ с помощью команды docker build. Она настроена на то, что будет запущен файл docker-compose из директории уровнем выше. Для того чтобы построение происходило успешно Dockerfile можно привести к подобному виду:


    FROM microsoft/aspnetcore:2.0 AS base
    WORKDIR /app
    EXPOSE 80
    
    FROM microsoft/aspnetcore-build:2.0 AS build
    WORKDIR /src
    COPY DockerServiceDemo.csproj DockerServiceDemo.csproj
    RUN dotnet restore DockerServiceDemo.csproj
    COPY . .
    WORKDIR /src
    RUN dotnet build DockerServiceDemo.csproj -c Release -o /app
    
    FROM build AS publish
    RUN dotnet publish DockerServiceDemo.csproj -c Release -o /app
    
    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app .
    ENTRYPOINT ["dotnet", "DockerServiceDemo.dll"]

    Все что я сделал, это убрал лишнюю директорию DockerServiceDemo.


    Если вы используете Visual Studio Code, то файлики вам придется генерировать вручную. Хотя в VS Code и имеется вспомогательный функционал в виде расширения Docker Добавлю ссылку на мануал о том как работать с докером из VS Code – Working with Docker. Да, статья на английском, но она ведь с картинками


    «Три аккорда» докера


    Для ежедневной работы с докером достаточно помнить всего лишь несколько команд.


    Самая главная команда это, конечно, построение образа. Для того чтобы это сделать необходимо с помощью bash/CMD/PowerShell зайти в директорию, где находится Dockerfile и выполнить команду:


    docker build -t your_image_name . 

    Здесь после параметра -t задается имя вашего образа. Внимание — в конце команды после пробела точка. Эта точка означает что используется текущая директория. Образ можно пометить образ каким-нибудь тэгом (номером или названием). Для этого после имени поставить двоеточие и указать тэг. Если тег не указать, то по умолчанию он будет задан с наименованием latest. Для того, чтобы отправить образ в репозиторий, необходимо, чтобы имя образа включало в себя имя репозитория. Примерно так:


    docker build -t docker_account_name/image_name:your_tag . 

    Здесь your_docker_account_name это имя вашего аккаунта в docker hub.


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


    docker tag image_name docker_account_name/image_name:your_tag

    Для того чтобы отправить изменения в хаб теперь необходимо выполнить следующую команду:


    docker push docker_account_name/image_name:your_tag

    Перед этим необходимо зайти в ваш аккаунт докера. На Windows это делается из UI приложения, а вот на *nix это делается командой:


    docker login

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


    docker run -it -p 5000:80 image_name

    Параметр -it создаст псевдо-TTY и ваш контейнер станет отвечать на запросы. После запуска команды сервис станет доступным по адресу http://localhost:5000/


    -p 5000:80 связывает порт 5000 контейнера с портом 80 хоста.


    Кроме того, есть такие команды:


    docker ps –a

    Покажет вам список контейнеров. Так как добавлен ключ -a, то будут отображены все контейнеры, а не только те, которые запущены на данный момент.


    docker rm container_name

    Эта команда удалит контейнер с именем container_name. rm – сокращение от remove


    docker logs container_name

    Отобразит логи контейнера


    docker rmi image_name

    Удалит образ с именем image_name


    Запуск контейнера через реверс прокси-сервер


    Дело в том, что сами приложения .NET Core используют свой веб-сервер Kestrel. Этот сервер не рекомендуется для работы на production. Почему? Есть несколько объяснений.
    Если есть несколько приложений, которые делят между собой IP и порт, то Kestrel не сможет распределять трафик. Кроме того, реверс прокси-сервер предоставляет собой дополнительный слой безопасности, упрощает балансировку нагрузки и настройку SSL, а также лучше интегрируется в существующую инфраструктуру. Для большинства разработчиков самой главной причиной необходимости реверс-прокси, является дополнительная безопасность.


    Для начала восстановим начальную конфигурацию Dockerfile. А после разберёмся с файлом docker-compose.yml и попробуем запустить наш сервис в одиночку. Формат файла yml читается так «ямл» и является аббревиатурой то ли от «Yet Another Markup Language», то ли от «YAML Ain't Markup Language». То ли еще один язык разметки, то ли не язык разметки совсем. Как-то все не определенно.


    Мой файл docker-compose созданный по умолчанию выглядит так:


    version: '3.4'
    
    services:
      dockerservicedemo:
        image: ${DOCKER_REGISTRY}dockerservicedemo
        build:
          context: .
          dockerfile: DockerServiceDemo/Dockerfile

    Файл docker-compose.override.yml добавляет в конфигурацию несколько настроек:
    version: '3.4'


    services:
      dockerservicedemo:
        environment:
          - ASPNETCORE_ENVIRONMENT=Development
        ports:
          - "80"

    Построить созданное решение мы можем с помощью docker-compose build, в вызвав команду docker-compose up мы запустим наш контейнер. Все работает? Тогда переходим к следующему шагу. Создаем файл nginx.info. Конфигурация будет примерно следующая:


    worker_processes 4;
    
    events { worker_connections 1024; }
    
    http {
        sendfile on;
    
        upstream app_servers {
            server dockerservicedemo:80;
        }
    
        server {
        listen 80;
        location / {
            proxy_pass         http://app_servers;
            proxy_http_version 1.1;
            proxy_set_header   Upgrade $http_upgrade;
            proxy_set_header   Connection keep-alive;
            proxy_set_header   Host $host;
            proxy_cache_bypass $http_upgrade;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;
          }
        }
    }

    Здесь мы указываем, что nginx будет прослушивать 80-ый порт (listen 80;). А полученные запросы будет переадресовывать на 80-ый порт хоста в контейнер dockerservicedemo. Кроме того, мы указываем nginx какие заголовки необходимо передавать дальше.


    Мы можем использовать http в nginx, а доступ к вебсайту осуществлять через https. Когда https запрос проходит через http прокси, то много информации из https не передается в http. Кроме того, при использовании прокси теряется внешний IP адрес. Для того, чтобы эта информация передавалась в заголовках, необходимо изменить код нашего ASP.NET проекта и добавить в начало метода Configure файла Startup.cs следующий код:


     app.UseForwardedHeaders(new ForwardedHeadersOptions {
                    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
                });

    Большинство прокси серверов используют заголовки X-Forwarded-For и X-Forwarded-Proto. Именно эти заголовки и указаны сейчас в конфигурации nginx.


    Теперь включим образ nginx и файл nginx.conf в конфигурацию doker-compose. Осторожно в YAML пробелы имеют значение:


    version: '3.4'
    
    services:
      dockerservicedemo:
        image: ${DOCKER_REGISTRY}dockerservicedemo
        build:
          context: .
          dockerfile: DockerServiceDemo/Dockerfile
        ports:
          - 5000:80
      proxy:
        image: nginx:latest
        volumes:
          - ./DockerServiceDemo/nginx.conf:/etc/nginx/nginx.conf
        ports:
          - 80:80

    Здесь мы добавляем к нашей конфигурации прокси в виде образа nginx. К этому образу «цепляем» внешний файл настроек. Его мы как-бы монтируем в файловую систему контейнера с помощью механизма под названием volume. Если добавить в конец :ro то объект будет смонтирован только для чтения.


    Прокси слушает внешний 80-ый порт машины на которой запущен контейнер и передает запрос на внутренний 80-ый порт контейнера.


    Выполнив команду doker-compose up мы запулим, то есть извлечем из репозитория образ nginx и стартанем наш контейнер вместе с контейнером прокси. Теперь по адресу http://localhost:80/ он будет доступен через nginx. На 5000-ом порту приложение «крутится» еще и под Kestrel.


    Проверить то, что запрос к веб приложению проходит через реверс-прокси мы можем так. Открыть в браузере Chrome developer tools и зайти на закладку Network. Здесь кликнуть на localhost и выбрать закладку Headers.



    Запускаем контейнер через прокси и HTTPS


    Версия ASP.NET Core 2.1 принесла с собой улучшения поддержки HTTPS.
    Скажем, следующий middleware позволяет совершать редирект с незащищенного соединения на защищенное:


    app.UseHttpsRedirection();

    А следующий позволяет использовать HTTP Strict Transport Security Protocol – HSTS.


    app.UseHsts();

    HSTS — это фича из протокола HTTP/2, спецификация которого была выпущена в 2015-ом году. Этот функционал поддерживается современными браузерами и информирует о том, что вебсайт использует только https. Таким образом происходит защита от downgrade атаки, во время которой атакующий может с помощью перехода на незащищенный протокол http каким-либо образом воспользоваться ситуацией. Например, понизить версию TLS или даже подменить сертификат.


    Как правило, данный вид атак используется совместно с man-in-the-middle атаками. Следует знать и помнить о том, что HSTS не спасает от ситуации, когда пользователь заходит на сайт по протоколу http и потом редиректится на https. Существует так называемый Chrome preload list, который содержит ссылки на сайты поддерживающие https. Другие браузеры (Firefox, Opera, Safari, Edge) также поддерживают списки https сайтов, созданные на базе списка Chrome. Но во всех этих списках содержатся далеко не все сайты.


    При первом запуске какого-либо Core приложения на Windows вы получите сообщение о том, что был создан и установлен сертификат разработчика. Кликнув кнопку и установив сертификат, вы таким образом сделаете его доверенным. С командной строки на macOS добавить доверие сертификату можно с помощью команды:
    dotnet dev-certs https –trust


    Если утилита dev-certs не установлена, то установить ее можно командой:


    dotnet tool install --global dotnet-dev-certs 

    Как добавить сертификат в trusted на Linux зависит от дистрибутива.
    В тестовых целях используем именно сертификат разработчика. Действия с сертификатом подписанным CA аналогичные. При желании можно использовать и бесплатные сертификаты LetsEncrypt


    Экспортировать сертификат разработчика в файл можно с помощью команды


    dotnet dev-certs https -ep путь_к_создаваемому_файлу.pfx

    Файл необходимо скопировать в директорию %APPDATA%/ASP.NET/Https/ под Windows или же в /root/.aspnet/https/ под macOS/Linux.


    Для того, чтобы контейнер подцепил путь к сертификату и его пароль необходимо создать User secrets со следующим содержимым:


    {
        "Kestrel":{
            "Certificates":{
                "Default":{
                    "Path":     "/root/.aspnet/https/имя_вашего_сертификата.pfx",
                    "Password": "пароль_от_вашего_сертификата"
                }
            }
        }
    }

    Этот файл хранит незашифрованные данные и потому используется только при разработке. Создается файл в Visual Studio через вызов контекстного меню на иконке проекта или с помощью утилиты user-secrets на Linux.


    На Windows файл будет сохранен в директории %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json, а на macOS и Linux он сохранится в ~/.microsoft/usersecrets/<user_secrets_id>/secrets.json


    Для сохранения настроек на продакшн некоторые дистрибутивы Linux могут использовать systemd Настройки сохраняются под атрибутом Service. Например, так:


    [Service]
    Environment="Kestrel _ Certificates _ Default _Path=/root/.aspnet/https/имя_вашего_сертификата.pfx"
    Environment="Kestrel _ Certificates _ Default _Password=пароль_от_вашего_сертификата"

    Далее приведу и разберу сразу рабочий вариант конфигурации докера для прокси и контейнера через https.


    Файл docker-compose:


    version: '3.4'
    
    services:
      dockerservicedemo21:
        image: ${DOCKER_REGISTRY}dockerservicedemo
        build:
          context: .
          dockerfile: DockerServiceDemo/Dockerfile
    
    Файл override:
    version: '3.4'
    
    services:
      dockerservicedemo:
        environment:
          - ASPNETCORE_ENVIRONMENT=Development
          - ASPNETCORE_URLS=https://+:44392;http://+:80
          - ASPNETCORE_HTTPS_PORT=44392
        ports:
          - "59404:80"
          - "44392:44392"
        volumes:
          - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
          - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
      proxy:
        image: nginx:latest
        volumes:
          - ./DockerServiceDemo/nginx.conf:/etc/nginx/nginx.conf
          - ./DockerServiceDemo/cert.crt:/etc/nginx/cert.crt
          - ./DockerServiceDemo/cert.rsa:/etc/nginx/cert.rsa
        ports:
          - "5001:44392"

    Теперь опишу непонятные моменты. ASPNETCORE_URLS позволяет нам не указывать в коде приложения с помощью app.UseUrl прослушиваемый приложением порт.


    ASPNETCORE_HTTPS_PORT делает редирект аналогичный тому, какой бы сделал следующий код:
    services.AddHttpsRedirection(options => options.HttpsPort = 44392)


    То есть трафик с http запросов будет перенаправлен на определенный порт https запросов.
    С помощью ports указывается, что запрос с внешнего 59404-ого порта будет перенаправлен на 80-ый контейнера, а с 44392-ого внешнего порта на 44392-ой. Теоретически, раз у нас будет настроен реверс прокси-сервер, то ports с этими перенаправлениями мы можем и убрать.
    С помощью volumes монтируется директория с pfx сертификатом и UserSecrets приложения с указанием пароля и ссылки на сертификат.


    В разделе proxy указывается что запросы с 5001-ого внешнего порта будут перенаправляться на 44392-ой порт nginx. Кроме того, монтируется файл с конфигурацией nginx, а также сертификат и ключ к сертификату.


    Для того, чтобы их одного сертификата формата pfx (который у нас уже есть) создать файлы crt и rsa можно воспользоваться OpenSSL. Сначала необходимо извлечь сертификат:


    openssl pkcs12 -in ./ваш_сертификат.pfx -clcerts -nokeys -out domain.crt

    А затем приватный ключ:


    openssl pkcs12 -in ./ваш_сертификат.pfx -nocerts -nodes -out domain.rsa

    Конфигурация nginx следующая:


    worker_processes 4;
    
    events { worker_connections 1024; }
    
    http {
        sendfile on;
    
        upstream app_servers {
            server dockerservicedemo:44392;
        }
    
        server {
        listen 44392 ssl;
        ssl_certificate /etc/nginx/cert.crt;
        ssl_certificate_key /etc/nginx/cert.rsa;
        location / {
            proxy_pass         https://app_servers;
            proxy_set_header   Upgrade $http_upgrade;
            proxy_set_header   Connection keep-alive;
            proxy_set_header   Host $host;
            proxy_cache_bypass $http_upgrade;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;
          }
        }
    }

    Прокси сервер прослушивает 44392-ой порт. На этот порт приходят запросы с 5001-ого порта хоста. Далее прокси перенаправляет запросы на 44392-ой порт контейнера dockerdemoservice.


    Разобравшись с данными примерами, вы получите хороший бэкграунд для работы с докером, микросервисами и nginx.


    Напоминаем, что это полная версия статьи из журнала Хакер. Ее автор — Алексей Соммер.

    • +22
    • 9,1k
    • 6
    Microsoft
    412,00
    Microsoft — мировой лидер в области ПО и ИТ-услуг
    Поделиться публикацией

    Комментарии 6

      0

      А зачем указывать image в compose файле, если дальше идёт build и происходит сборка контейнера?

        0

        Это фича docker-compose. Позволяет pull-ить образы, указанные в image, если вам не хочется собирать самому через команду docker-compose pull. Полезно для фронтендеров, которым нужно просто запустить последний рабочий билд бэкенда. Бэкендер делает docker-compose build.

        0
        У меня только один вопрос — pачем Net Core запихивать в докер? Чисто дань моде? Реальный профит от Docker вижу только в мертвых проектах, которые никто дальше поддерживать не планирует, но надо чтобы они работали и дальше. В остальном от Docker одни минусы.

        — Docker полезен исключительно для воссоздания кривых окружений кривых программ (непременно stateless)
        — В подавляющем большинстве случаев люди пытаются внедрением Docker компенсировать изначально кривую архитектуру своих приложений. Когда это не помогает начинаются разговоры о том, что Kubernetes поможет решить проблемы, но это приводит лишь к новым сложностям
        — Docker вводит лишний уровень абстракции, зачастую там где она не нужна
        — Содержимое Docker контейнера крайне плохо поддается аудиту
        — Docker крайне не прост в настройке и поддержке. Большинство людей которые все же используют докер редко уходят дальше «Just use the docker image»
        — Корректная настройка Docker требует найма дополнительного персонала с очень специфическими навыками. Уметь правильно настраивать сеть != уметь правильно настраивать сеть в Docker
        — Docker никогда не бывает один и тянет за собой огромную экосистему. Этим он похож на NodeJS, когда очень скоро оказывается, что ваше Hello World приложение зависит от 300 разных библиотек и плагинов.
          –1
          Вы просто не умеете его готовить. В разработке это мега удобно. Если весь сервис состоит из одного приложения и nginx, то профита наверное не много. Но вот когда сервис состоит из десятка приложений, то докер очень сильно помогает. Можно упаковать все приложения вместе с настройками в контейнеры, написать скрипт, который всё это поднимает и запускать на своем компе. Некоторые разработчики под каждую задачу поднимают новый сервис с нуля, благо это делается одной командой. Далее гитлаб CI — при пуше приложение собирается в контейнеры, поднимается весь сервис из собранных контейнеров и тестируется. После всех действий остаются артефакты, если нужно разобраться в каких-то проблемах, то можно всё это добро поднять прямо на своей тачке. У контейнеров свои имена, благодаря этому каждый контейнер кажется отдельным компьютером и можно поднимать просто кучу сервисов на своем компе, например моделируя горизонтальное масштабирование. Установленные обычном способом приложения часто распиханы по разным каталогам и оставляют свои следы везде где только можно. Докер это проблему решает элегантно — приложение гадит только в примонтированные папки. Благодаря этому можно весь свой локальный сервис забэкапить буквально копированием и поднять в случае чего на другой тачке за считанные минуты.
          +1
          У меня другой вопрос зачем в контейнере nginx?
          Почему его не расположить на системном уровне пусть себе обслуживает сайт с 10 контейнеров.
            0
            Базовые образы уже давно поменялись:
            для сборки: microsoft/dotnet:2.2-sdk
            для публикации: microsoft/dotnet:2.2-aspnetcore-runtime
            и минимальные образы: microsoft/dotnet:2.2-aspnetcore-runtime-alpine
            И уже в альфе 3.0 версия которая будет запускаться исключительно на netcoreapp 3.0.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое