Docker для разработки C#

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

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

Возможности Docker достаточно обширны, однако в данной статье хотелось бы остановиться на трех:

  • Возможность быстро и безопасно развернуть инфраструктурные приложения (Postgres, Rabbit, Redis и другие),

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

  • Возможность публикации нескольких приложений, взаимодействующих между собой в виртуальной частной сети, таких как: микросервисы,

Инфраструктура в Docker.

Docker позволяет безопасно и быстро разрабатывать и отлаживать приложения на локальной машине и экспериментировать с различными компонентами или даже наборами компонент инфраструктуры. 

Используя Docker нет необходимости ставить и настраивать локально базу данных, кэш или обменник. Пусть эти компоненты находятся в образах Docker. Тем более, что образы таких компонент скорее всего уже существуют на Docker Hub. 

Так, например, для развертывания Postgres необходимо выполнить:

docker run --name habr-pg-13.3 
-p 5432:5432 
-e POSTGRES_USER=habrpguser 
-e POSTGRES_PASSWORD=pgpwd4habr 
-e POSTGRES_DB=habrdb 
-d postgres:13.3

При выполнении команды скачается образ postgres:13.3 и запуститься на порту 5432 с пользователем habrpguser, паролем pgpwd4habr и названием базы habrdb.

И всё! Postgres запущен! Теперь его можно использовать для разработки приложений. Пожалуй, единственное нетривиальное место в запуске - это порты. Тут важно понимать, что контейнеры живут в своей экосистеме, и именно поэтому каждый контейнер имеет два типа портов: внутренние - для своей экосистемы и внешние - для доступа извне.

И всё! Postgres запущен! Теперь его можно использовать для разработки приложений. Пожалуй, единственное нетривиальное место в запуске — это порты. Тут важно понимать, что контейнеры живут в своей экосистеме, и именно поэтому каждый контейнер имеет два типа портов: внутренние — для своей экосистемы и внешние — для доступа извне.

После запуска в Docker Desktop появится контейнер с наименованием habr-pg-13.3:

После запуска в Docker Desktop появится контейнер с наименованием habr-pg-13.3:

Приложение в контейнере.

Если с инфраструктурой всё понятно и достаточно только найти нужный образ, скачать его и запустить в контейнере, то со своим собственным приложением всё не так просто. Чтобы собрать Docker-образ необходимо сформировать Dockerfile. 

Если вы используете Visual Studio, редактор сам предоставляет такую возможность если добавить «Docker Support»:

4ad49fec73704c68e24d330d0e5efc15.png

Если выбрать, например, платформу Linux, получим такой Dockerfile:

FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Dir/App.csproj", "Dir/"]
RUN dotnet restore "Dir/App.csproj"
COPY . .
WORKDIR "/src/Dir"
RUN dotnet build "App.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "App.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "App.dll"]

Здесь скачиваются образа mcr.microsoft.com/dotnet/runtime:6.0 и mcr.microsoft.com/dotnet/sdk:6.0, затем в контейнер помещается директория с проектом восстанавливаются все зависимости, используемые проектом, далее проект билдится в режиме релиза, выполняется publish, производятся копирования и объявляется точкой входа команда dotnet app.dll приложения. Помимо этого в VS студии появится профиль запуска «Docker». 

После запуска в Docker Desktop будет создан контейнер с нашим приложением. Если выбрать его в перечне, то мы сможем посмотреть логи, помимо логов можно увидеть вкладки Inspect, где отображаются параметры запуска и Stats, где отображается статистика ресурсов, используемых контейнером. 

Помимо вкладок есть управляющие кнопки контейнера, Stop, Restart, Delete, Open In Browser, Cli. Особый интерес здесь представляет Cli эта команда откроет терминал в контейнере. Другой способ открытия терминала: написать в командной строке

docker exec -it <имя контейнера> /bin/bash

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

3c814618eb51f26c3ec5ca8184045151.png

Микросервисы. Docker Compose.

Запустить приложение в контейнере оказалось достаточно несложно, особенно используя VS, но как развернуть несколько связанных приложений? Конечно, можно каждый раз отдельно запускать группу приложений, по отдельности и инфраструктурные компоненты. Да, такой вариант, в целом, обеспечит нам группу сервисов, но управлять их конфигурацией будет достаточно проблематично. Как же быть? На этот случай существует Docker Compose. Docker Compose используется для одновременного управления несколькими контейнерами, входящими в состав приложения. 

Для использования Docker Compose необходимо сформировать файл docker-compose.yml. В этом файле конфигурируются все контейнеры, которые мы собираем вместе:

version: '3.8'

networks:
    app-net:
        external: false

services:
    app1:
        image: app1:latest
        container_name: app1
        hostname: app1
        depends_on:
            - mongo_image
            - rabbitmq_image
        build:
            context: "../"
            dockerfile: "App1/Dockerfile"
        ports:
            - "63696:63696"
            - "63695:63695"
        environment:
            - ConnectionStrings__mongo=mongodb://mongo_image:27017/app?appName=app&waitQueueMultiple=7&maxPoolSize=100
            - ConnectionStrings__Queues=amqp://myuser:mypass@rabbitmq_image:5672/app
            - ASPNETCORE_ENVIRONMENT=Production
            - ASPNETCORE_URLS=https://*:63696;http://*:63695
            - ConnectionStrings__redis=redis:6379
        networks:
            - app-net
    app2:
        image: app2:latest
        container_name: app2
        hostname: app2
        environment:
            - ASPNETCORE_URLS=http://*:65000
            - ConnectionStrings__redis=redis:6379
            - Cors=["https://localhost:63696","http://localhost:63695"]
            - ConnectionStrings__main=Server=postgres_image;User Id=postgres;Password=123;Database=app;Port=5432
        depends_on:
            - postgres_image
            - cache
        build:
            context: "../"
            dockerfile: "App2/Dockerfile"
        ports:
            - "65000:65000"
        networks:
            - app-net
    rabbitmq_image:
        image: rabbitmq:3.8.8-management
        ports:
            - 5673:5672
            - 15673:15672
        restart: always
        networks:
            - app-net
        environment:
            RABBITMQ_DEFAULT_VHOST: app
            RABBITMQ_DEFAULT_USER: myuser
            RABBITMQ_DEFAULT_PASS: mypass
    mongo_image:
        image: mongo:4.2.8
        ports:
            - "27019:27017"
        restart: always
        environment:
            MONGO_INITDB_DATABASE: app
        networks:
            - app-net
    postgres_image:
        image: postgres:11
        ports:
            - "5431:5432"
        restart: always
        environment:
            POSTGRES_USER: "postgres"
            POSTGRES_PASSWORD: "123"
            POSTGRES_DB: "app"
        networks:
            - app-net
    cache:
        image: redis:latest
        hostname: redis
        restart: always
        ports:
            - '6379:6379'
        command: redis-server --save 20 1 --loglevel warning
        networks:
            - app-net
    app3:
        image: app3:latest
        container_name: app3
        hostname: app3
        depends_on:
            - rabbitmq_image
            - clickhouse_image
            - cache
            - app1
        build:
            context: "../"
            dockerfile: "App3/Dockerfile"
        environment:
            - ConnectionStrings__Queues=amqp://myuser:mypass@rabbitmq_image:5672/app
            - ConnectionStrings__ClickHouse=Host=clickhouse_image;Port=9000;Database=default
            - app__url=https://app:63696
            - DOTNET_ENVIRONMENT=Production
        networks:
            - app-net

Здесь описаны три сервиса: app1, app2, app3 и инфраструктурные компоненты: rabbitmq_image, mongo_image, postgres_image, cache  через переменные секции environment обеспечивается передача параметров для связки сервисов между собой, а также инфраструктуры. Параметры секции environment будут отображаться во вкладке Inspect каждого контейнера. Все компоненты находятся в сети app-net. Чтобы развернуть все эти компоненты необходимо из директории с файлом вызвать команды:

  • docker-compose build

  • docker-compose up

Такой файл можно сделать руками, однако в VS есть возможность данный процесс автоматизировать за счет добавления оркестратора контейнеров:

69aa714373561ceea0f1ebfeaead63ba.png

После добавления поддержки оркестратора контейнеров добавится поддержка docker-compose, в котором, по показанному ранее примеру можно сконфигурировать стенд из необходимых компонент. 

Несложно заметить, что появились файлы .dockerignore, docker-compose.yml и docker-compose.override.yml, где последние два определяю конечный docker-compose. Стоит заметить, что .yml файлов может быть значительно больше, например: чтобы тематически отделять сервисы друг от друга. 

Несложно заметить, что появились файлы .dockerignore, docker-compose.yml и docker-compose.override.yml, где последние два определяю конечный docker-compose. Стоит заметить, что .yml файлов может быть значительно больше, например: чтобы тематически отделять сервисы друг от друга. 

Для корректной работы приложений важно правильно указать строки доступа к компонентам инфраструктуры и к сервисам. Выделение контейнеров в отдельную сеть обеспечивает их изолированность от внешних приложений и дает возможность взаимодействия между собой. Обеспечив правильные параметры запуска в секции environment и параметры инфраструктуры получаем готовый стенд из нескольких приложений, который сможет развернуть любой, у кого есть Docker и доступ в интернет.

© Habrahabr.ru