Добрый день, в данной статье я покажу как развернуть Symfony 4 приложение на AWS. В официальной документации есть пример подобного процесса, однако мой вариант не столь тривиален, как загрузка zip архива с приложением. На дворе 2019, в моде docker, микросервисная архитектура и CI/CD практики наконец-то начинают входит в инструментарий не только DevOps-инженеров, но и простых смертных разработчиков. Чтобы статья была более интересна, я добавил фронт на React.JS, для охвата потребностей большей массы людей, если ваше приложение не использует Encore — не беда, я укажу как изменить Docker-файл для вас, поддержка React.JS тут влияет только на него. Кому будет интересен данный туториал? В первую очередь он направлен на PHP-разработчиков, желающих изменить свою практику деплоя — отойти от привычных канонов и воспользоваться docker для паковки своего приложения и выкладки образа. Но можно пойти чуть глубже, и дальнейшее повествование будет направлено на автоматический деплой приложения из Git'а посредством CI/CD платформы (будет использован CircleCI, но если интересует конфиг Gitlab'а, пишите в комментариях, я приложу). По сути, тут абсолютно не важно React/PHP ли у вас приложение или, скажем, на .NET Core, данная часть будет интересна разработчикам для получения навыков автоматизации деплоя в целом. Исходный код доступен в github-репозитории, ссылка в конце статьи. Ну что же, поехали!
Я предполагаю, что у вас имеется собственное Symfony приложение, я же в демонстрационных целях набросал «hello, world!», содержащее следующие пакеты:
Теперь необходимо настроить облачную инфраструктуру. Не буду заострять внимание на регистрации и активации пробного периода AWS, на данном этапе нам нужно создать 2 инстанса БД — я буду использовать 2 типа окружения: STG (staging) для тестирования внедрения новых «фич» и PROD (production) как непосредственно «боевой» сервер. О преимуществах, managed service БД написано немало статей, к тому же мы преследуем в данном руководстве главным образом удобство для разработчика, потому пользуемся именно RDS, а не поднимаем свой отдельный сервер БД. В качестве СУБД для данного примера я использовал PostgreSQL, вы вольны выбирать любую подходящую вам, перейдите в сервис RDS и создайте 2 инстанса необходимой вам мощности и объема. Так как «из коробки» в Symfony для нас доступен файл
Прекрасно, начало положено! Как известно, Symfony-зависимости устанавливаются через composer, для его установки воспользуемся файлом composer.sh, который положим в корень проекта:
Это инструкция по программной установке с сайта composer.
Теперь, для каждого из окружений создадим свой Dockerfile в корне проекта:
и
Файлы вполне можно использовать «как есть», никаких макросов для изменения не используется. Давайте пройдемся по содержимому Dockerfile'а, чтобы развеять налет «магии». В качестве «основы» мы используем официальный образ PHP 7.2.19 с интегрированным веб-сервером Apache (вы вольны использовать любой по своему усмотрению, настроить связку с Nginx и так далее, в данном примере я использую означенную выше как самую, на мой взгляд, удобную). Строка expose в данный момент нам не важна, сама по себе она ничего не делает, однако в дальнейшем ее будет использовать ElasticBeanstalk, которому она необходима для корректного развертывания. Следующие конструкции используют оптимизированные для production настройки PHP, рекомендованные производителем, активируют mod_rewrite для Apache и увеличивают максимальный объем памяти для PHP скрипта со 128 до 256 мб, что необходимо для корректной работы composer. Далее мы устанавливаем необходимые приложения, PHP-зависимости и расширения, и сразу их настраиваем. Рабочей директорией нашего приложения назначаем папку /var/www – туда будет скопирован исходный код нашего приложения. Так как apache по-умолчанию использует /var/www как энтрипоинт для своего хоста, а индексный файл symfony находится в /var/www/public, следующей конструкцией меняем apache document root. Затем мы последовательно устанавливаем composer, nodejs и yarn (если в своем приложении вы не используете encore/react.js, то последние из указанных два пункта вам не нужны). Наконец мы копируем наш исходный код и запускаем установку зависимостей через composer для symfony и yarn для react.js. Смысл отдельного Dockerfile’а для STG кроется в предпоследней инструкции для docker’а – копировании .env.stg в .env, таким образом в образе для STG .env-файл будет содержать актуальные для данного окружения параметры. Можете локально (разумеется при установленном docker) собрать образ, запустить и убедиться, что приложение работает и ничего больше для этой работы ему не требуется:
для STG и
для PROD.
Мы можем использовать EC2, настраивать ELB/ASG и тд или же воспользоваться ElasticBeanstalk, что для нас в плане удобства просто подарок. Зайдите в раздел ElasticBeanstalk и создайте новое приложение, указав его название и описание. Затем создадите 2 окружения, о которых говорилось ранее: STG и PROD, оба окружения создайте как окружение веб-сервера (Web server environment), в качестве платформы укажите “Docker”, а в качестве кода приложения оставьте Sample application. Деплой на ElasticBeanstalk осуществляется посредством «заливки» файлов проекта или инструкций, обычно в zip-архиве. В нашем случае флоу будет таким: собираем docker-образ нашего приложения, загружаем его в хранилище и загружаем вместо архива с исходниками или docker-образа инструкцию, которая укажет ElasticBeanstalk взять образ с удаленного сервера и развернуть. И все это – автоматически.
Начнем с создания репозитория для хранения docker-образов. Тут есть 2 варианта:
1 – у вас проект приватный, код его закрыт и репозиторий, соответственно, тоже должен быть закрыт. В данном случае вы или держите где-то свой собственный регистр образов или пользуетесь приватным облаком. Для данных целей в AWS есть ECR, вы можете создать репозиторий там, но никто вас к этому не принуждает.
2 – у вас проект с открытым кодом и вы можете использовать dockerhub.
В нашем примере код хоть и открыт, но я покажу как использовать именно закрытые репозитории, после понимания данного процесса подключение образа из dockerhub не составит труда. Первое, что нам понадобится это создание самого репозитория, после этого вы получите его уникальный URI. Дальнейшее повествование пойдет для сторонних (не AWS ECR репозиториях и их интеграции), для ECR напишу после этого.
После создания репозитория, нам необходимо авторизоваться на в данном сервисе и тут есть небольшая хитрость… Зайдите в настройки своего локально установленного docker’а и проверьте, чтобы у вас была убрана возможность сохранения паролей во внешнем хранилище (для пользователей macOS: “Securely store docker logins in macOS Keychain”), иначе необходимый нам конфигурационный файл будет пуст. И так, авторизуемся в выбранном сервисе хранения регистров ваших образов:
после удачной аутентификации в конфигурационном файле ~/.docker/config.json появится конструкция следующего вида:
Если не появилась, еще раз проверьте настройку docker, описанную выше.
Теперь все готово, чтобы приготовить файл-инструкцию для ElasticBeanstalk – Dockerrun.aws.json, его код будет таким:
В общем виде инструкция выглядит так: авторизовавшись с помощью ключа, расположенного по KEY_PATH в S3-хранилище BUCKET_ID, загрузить образ по IMAGE_URL перезаписав сохраненный, запустить его пробросив 80 порт на такой же порт контейнера. Теперь про использованные константы:
BUCKET_ID – это автоматически созданный для вас «рюкзак» в сервисе S3, имеющий вид elasticbeanstalk-REGION-HASH, именно там система располагает служебные файлы для вашего ElasticBeanstalk, в том числе и файлы приложения, которые вы загружаете по кнопке «Upload and deploy».
KEY_PATH – путь до авторизационного файла к хранилищу образов, я использую формат APP_NAME/cr.json, то есть в папку внутри BUCKET_ID под именем моего приложения (создаю, если пока нет) кладу файл cr.json, содержащий код, полученный после авторизации в регистре образов локально:
IMAGE_URL – это уникальный URI вашего регистра образов + тег самого образа, тут все должно быть понятно.
Все, теперь можем загрузить данный файл как версию нашего приложения в ElasticBeanstalk, и он сам подтянет указанный образ и развернет его.
Осталось автоматизировать данный процесс. А чтобы было совсем интересно, я реализую последовательность шагов для следующего флоу: для всех коммитов НЕ в мастер-ветку образ будет собираться и разворачиваться в STG окружении, а если мы пушим в мастер, или что лучше, его вообще закрыть и наливать только merge request’ами, то код будет деплоиться на PROD. Таким образом мы получим в PROD актуальный мастер, в котором все должно быть хорошо, а ветки для разработки и тестирования нового кода в STG. Для данной реализации нам понадобится инструкция для заливки не latest-образов, скопируйте Dockerrun.aws.json в Dockerrun.aws.stg.json, а сам Dockerrun.aws.json переименуйте в Dockerrun.aws.prod.json (просто для удобства).
Единственное, что отличает Dockerrun.aws.stg.json от Dockerrun.aws.prod.json – это IMAGE_URL:
Как я уже говорил в начале статьи, в качестве CI/CD я буду использовать CircleCI, который по личным ощущениям быстрее GitlabCI, если использовать бесплатную SaaS версию. Бесплатный Travis бы подошел, но так как с приватными git-репозиториями он работать не умеет, я не стал специально проводить демонстрацию на нем, дабы не было разочарования, когда такая возможность понадобится. Настройку проекта в CircleCI я оставлю читателям для самостоятельного изучения, сам же приведу необходимые для деплоя инструкции — в корне нашего проекта создадим папку .circleci, в ней config.yml следующего содержания:
Сам флоу я расписал чуть раньше, тут он переведен в yaml-инструкции для CircleCI, пройдемся по реализации конкретных шагов. Важно отметить наличие определенных для CI переменных окружения, которые будут использованы им в процессе работы:
CI_REGISTRY, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD необходимы для доступа в хранилище docker-образов — тоже самое, что мы клали в cr.json, только без base64
CI_REGISTRY / CI_REGISTRY_ID составляют уникальный URL образа, без тега
AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY — названия говорят за себя сами, это креды AWS пользователя, от имени которого CircleCI будет производить деплой. Зайдите в AWS IAM и создайте пользователя, добавьте его в группу администраторов и предоставьте только программный доступ. Помните, что AWS_SECRET_ACCESS_KEY доступен для просмотра/копирования лишь однажды, после того как нажмете ссылку «show», больше вы ее не увидите.
Вернемся к шагам конфигурации CircleCI. В чем «магия»? Checkout загружает исходный код из git-ветки в текущую рабочую директорию, этот процесс повторяется в каждом джобике. В процессе build мы последовательно авторизуемся в хранилище, собираем код на основе Dockerfile.stg под тегом XXX:dev и отправляем его в хранилище. build-master делает тоже самое, только для сборки использует «обычный» Dockerfile под тегом XXX:latest.
deploy-stg устанавливает AWS EB CLI и создает авторизационный профиль в файле ~/.aws/config, который необходим для корректной работы CLI, далее инициализирует переменные для CLI – потребуется указать выбранный вами регион, платформу – всегда Docker и имя вашего приложения. Следом мы копируем содержимое Dockerrun.aws.stg.json в новый файл Dockerrun.aws.json и, используя конкретный environment и регион, даем команду на деплой нашего приложения, применив созданный авторизационный профиль. По умолчанию в результате работы данной команды весь код отслеживаемой ветки попадет в zip-архив, который будет загружен в ElasticBeanstalk и там распакован, но эта операция сравнительно затратная, потому мы и создали новый файл Dockerrun.aws.json, которого достаточно для развертывания созданного нами удаленного образа, и загрузить нам, по сути, нужно только его. Создадим для этого в корне проекта файл .ebignore:
Данный файл использует синтаксис .gitignore да и является он .gitignore, но не для Git CLI, а для AWS EB CLI. В данном файле я говорю CLI пропустить все файлы, кроме Dockerrun.aws.json. Все, теперь при выполнении джобика deploy-stg в ElasticBeanstalk отправится только созданный нами файл. deploy-prod делает все тоже самое, только копирует в Dockerrun.aws.json содержимое файла Dockerrun.aws.prod.json, и последнее, это указание в «формате» CircleCI последовательности джобов (deploy-stg после build и deploy-prod после build-master), и на какие ветки смотрят данные джобики (ignore: — master и only: — master).
Немного иначе дело состоит с AWS ECR, как я и обещал, вернемся к нему. Вам не нужно логиниться удаленно на ECR и создавать файл cr.json, так как ElasticBeanstalk “знает собрата в лицо”. Соответственно и Dockerrun.aws.json будет выглядеть иначе – там попросту будет отсутствовать блок аутентификации:
Но как же тогда произойдет аутентификация? Дело в том, что сервис, который обращается к ECR имеет некий набор прав, которые в свою очередь базируются на определенных политиках безопасности. В нашем случае, когда деплой запускается посредством AWS CLI со стороннего сервера (из CI), применяется роль “aws-elasticbeanstalk-ec2-role”, найдите ее в AWS IAM в разделе ролей и прикрепите к ней дополнительную политику “AmazonEC2ContainerRegistryReadOnly”. Теперь загрузка из приватного репозитория его «соседу» удастся без ошибок.
Но это именно загрузка из той же VPC, посредством CLI же команда docker login то же не «без примудростей»: креденшалы для docker login вы должны получить (именно получить) посредством AWS CLI, для этого существует команда
Данная команда вернет вам строку вида docker login …, проще говоря, в консоли необходимо выполнить
Команда сначала получит строку для аутентификации, а затем запустит соответствующий процесс. В виду данных правил для AWS ECR файл-инструкций для CircleCI будет выглядеть следующим образом:
Для поддержки docker-in-docker мы добавили setup_remote_docker на этапах сборок, остальное вам уже должно быть известно из содержания данной статьи. Вот и все, теперь структура нашего проекта выглядит следующем образом:
Попробуйте изменить код, запушить его в задачную ветку, а потом сделать мердж (пулл) реквест на мастер и принять его. Никаких больше «движений» для выкладки обновлений. Можно (и кому-то нужно) пойти далее, написать свои джобики для раскатки миграций, сделать промежуточный обязательный шаг прохождения автотестов и так далее, надеюсь данная статья положит начало для подобных экспериментов и последующих внедрений качественной и правильной доставки контента.
Исходный код на GitHub: tutorial-aws-symfony-ci
Я предполагаю, что у вас имеется собственное Symfony приложение, я же в демонстрационных целях набросал «hello, world!», содержащее следующие пакеты:
`symfony/webpack-encore-bundle symfony/form symfony/orm-pack symfony/profiler-pack symfony/security-bundle symfony/twig-bundle symfony/validator symfony/phpunit-bridge`
— минимальный джентельменский набор. На данный момент структура папок у вас должна быть такая:Теперь необходимо настроить облачную инфраструктуру. Не буду заострять внимание на регистрации и активации пробного периода AWS, на данном этапе нам нужно создать 2 инстанса БД — я буду использовать 2 типа окружения: STG (staging) для тестирования внедрения новых «фич» и PROD (production) как непосредственно «боевой» сервер. О преимуществах, managed service БД написано немало статей, к тому же мы преследуем в данном руководстве главным образом удобство для разработчика, потому пользуемся именно RDS, а не поднимаем свой отдельный сервер БД. В качестве СУБД для данного примера я использовал PostgreSQL, вы вольны выбирать любую подходящую вам, перейдите в сервис RDS и создайте 2 инстанса необходимой вам мощности и объема. Так как «из коробки» в Symfony для нас доступен файл
.env
, будем использовать его, например, для PROD, а для STG создадим его копию .env.stg
и изменим APP_ENV=dev
на APP_ENV=stg
в .env.stg
и APP_ENV=dev
на APP_ENV=prod
в .env
, а также впишем параметры подключения к Базе Данных для каждого из созданных экземпляров. Прекрасно, начало положено! Как известно, Symfony-зависимости устанавливаются через composer, для его установки воспользуемся файлом composer.sh, который положим в корень проекта:
composer.sh
#!/bin/sh
EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
then
>&2 echo 'ERROR: Invalid installer signature'
rm composer-setup.php
exit 1
fi
php composer-setup.php --quiet
RESULT=$?
rm composer-setup.php
exit $RESULT
Это инструкция по программной установке с сайта composer.
Теперь, для каждого из окружений создадим свой Dockerfile в корне проекта:
Dockerfile.stg (staging)
FROM php:7.2.19-apache
EXPOSE 80
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && a2enmod rewrite
RUN sed -ri -e 's!memory_limit = 128M!memory_limit = 256M!g' "$PHP_INI_DIR/php.ini"
RUN apt-get update && apt-get install -y wget curl libfreetype6-dev libjpeg62-turbo-dev libpng-dev libzip-dev zip libpq-dev && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && docker-php-ext-configure zip --with-libzip && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql && docker-php-ext-install -j$(nproc) gd && docker-php-ext-install zip && docker-php-ext-install pdo pdo_pgsql pgsql
WORKDIR /var/www
ENV APACHE_DOCUMENT_ROOT /var/www/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
COPY ./composer.sh ./
RUN chmod +x ./composer.sh && ./composer.sh && mv composer.phar /usr/local/bin/composer
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && apt-get update -qq && apt-get install -y yarn
COPY ./ ./
COPY ./.env.stg ./.env
RUN composer install && yarn && yarn run build
и
Dockerfile (production)
FROM php:7.2.19-apache
EXPOSE 80
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && a2enmod rewrite
RUN sed -ri -e 's!memory_limit = 128M!memory_limit = 256M!g' "$PHP_INI_DIR/php.ini"
RUN apt-get update && apt-get install -y wget curl libfreetype6-dev libjpeg62-turbo-dev libpng-dev libzip-dev zip libpq-dev && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && docker-php-ext-configure zip --with-libzip && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql && docker-php-ext-install -j$(nproc) gd && docker-php-ext-install zip && docker-php-ext-install pdo pdo_pgsql pgsql
WORKDIR /var/www
ENV APACHE_DOCUMENT_ROOT /var/www/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
COPY ./composer.sh ./
RUN chmod +x ./composer.sh && ./composer.sh && mv composer.phar /usr/local/bin/composer
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && apt-get update -qq && apt-get install -y yarn
COPY ./ ./
RUN composer install && yarn && yarn run build
Файлы вполне можно использовать «как есть», никаких макросов для изменения не используется. Давайте пройдемся по содержимому Dockerfile'а, чтобы развеять налет «магии». В качестве «основы» мы используем официальный образ PHP 7.2.19 с интегрированным веб-сервером Apache (вы вольны использовать любой по своему усмотрению, настроить связку с Nginx и так далее, в данном примере я использую означенную выше как самую, на мой взгляд, удобную). Строка expose в данный момент нам не важна, сама по себе она ничего не делает, однако в дальнейшем ее будет использовать ElasticBeanstalk, которому она необходима для корректного развертывания. Следующие конструкции используют оптимизированные для production настройки PHP, рекомендованные производителем, активируют mod_rewrite для Apache и увеличивают максимальный объем памяти для PHP скрипта со 128 до 256 мб, что необходимо для корректной работы composer. Далее мы устанавливаем необходимые приложения, PHP-зависимости и расширения, и сразу их настраиваем. Рабочей директорией нашего приложения назначаем папку /var/www – туда будет скопирован исходный код нашего приложения. Так как apache по-умолчанию использует /var/www как энтрипоинт для своего хоста, а индексный файл symfony находится в /var/www/public, следующей конструкцией меняем apache document root. Затем мы последовательно устанавливаем composer, nodejs и yarn (если в своем приложении вы не используете encore/react.js, то последние из указанных два пункта вам не нужны). Наконец мы копируем наш исходный код и запускаем установку зависимостей через composer для symfony и yarn для react.js. Смысл отдельного Dockerfile’а для STG кроется в предпоследней инструкции для docker’а – копировании .env.stg в .env, таким образом в образе для STG .env-файл будет содержать актуальные для данного окружения параметры. Можете локально (разумеется при установленном docker) собрать образ, запустить и убедиться, что приложение работает и ничего больше для этой работы ему не требуется:
docker build -t tmp:stg -f Dockerfile.stg .
docker run -p 80:80 tmp:stg
для STG и
docker build -t tmp:prod .
docker run -p 80:80 tmp:prod
для PROD.
Мы можем использовать EC2, настраивать ELB/ASG и тд или же воспользоваться ElasticBeanstalk, что для нас в плане удобства просто подарок. Зайдите в раздел ElasticBeanstalk и создайте новое приложение, указав его название и описание. Затем создадите 2 окружения, о которых говорилось ранее: STG и PROD, оба окружения создайте как окружение веб-сервера (Web server environment), в качестве платформы укажите “Docker”, а в качестве кода приложения оставьте Sample application. Деплой на ElasticBeanstalk осуществляется посредством «заливки» файлов проекта или инструкций, обычно в zip-архиве. В нашем случае флоу будет таким: собираем docker-образ нашего приложения, загружаем его в хранилище и загружаем вместо архива с исходниками или docker-образа инструкцию, которая укажет ElasticBeanstalk взять образ с удаленного сервера и развернуть. И все это – автоматически.
Начнем с создания репозитория для хранения docker-образов. Тут есть 2 варианта:
1 – у вас проект приватный, код его закрыт и репозиторий, соответственно, тоже должен быть закрыт. В данном случае вы или держите где-то свой собственный регистр образов или пользуетесь приватным облаком. Для данных целей в AWS есть ECR, вы можете создать репозиторий там, но никто вас к этому не принуждает.
2 – у вас проект с открытым кодом и вы можете использовать dockerhub.
В нашем примере код хоть и открыт, но я покажу как использовать именно закрытые репозитории, после понимания данного процесса подключение образа из dockerhub не составит труда. Первое, что нам понадобится это создание самого репозитория, после этого вы получите его уникальный URI. Дальнейшее повествование пойдет для сторонних (не AWS ECR репозиториях и их интеграции), для ECR напишу после этого.
После создания репозитория, нам необходимо авторизоваться на в данном сервисе и тут есть небольшая хитрость… Зайдите в настройки своего локально установленного docker’а и проверьте, чтобы у вас была убрана возможность сохранения паролей во внешнем хранилище (для пользователей macOS: “Securely store docker logins in macOS Keychain”), иначе необходимый нам конфигурационный файл будет пуст. И так, авторизуемся в выбранном сервисе хранения регистров ваших образов:
docker login -u LOGIN -p PASSWORD REGISTRY
после удачной аутентификации в конфигурационном файле ~/.docker/config.json появится конструкция следующего вида:
"REGISTRY" : {
"auth" : "BASE64_ENCODED_TOKEN"
}
Если не появилась, еще раз проверьте настройку docker, описанную выше.
Теперь все готово, чтобы приготовить файл-инструкцию для ElasticBeanstalk – Dockerrun.aws.json, его код будет таким:
Dockerrun.aws.json
{
"AWSEBDockerrunVersion": "1",
"Authentication": {
"Bucket": "BUCKET_ID",
"Key": "KEY_PATH"
},
"Image": {
"Name": "IMAGE_URL",
"Update": "true"
},
"Ports": [
{
"ContainerPort": "80"
}
]
}
В общем виде инструкция выглядит так: авторизовавшись с помощью ключа, расположенного по KEY_PATH в S3-хранилище BUCKET_ID, загрузить образ по IMAGE_URL перезаписав сохраненный, запустить его пробросив 80 порт на такой же порт контейнера. Теперь про использованные константы:
BUCKET_ID – это автоматически созданный для вас «рюкзак» в сервисе S3, имеющий вид elasticbeanstalk-REGION-HASH, именно там система располагает служебные файлы для вашего ElasticBeanstalk, в том числе и файлы приложения, которые вы загружаете по кнопке «Upload and deploy».
KEY_PATH – путь до авторизационного файла к хранилищу образов, я использую формат APP_NAME/cr.json, то есть в папку внутри BUCKET_ID под именем моего приложения (создаю, если пока нет) кладу файл cr.json, содержащий код, полученный после авторизации в регистре образов локально:
BUCKET_ID/APP_NAME/cr.json
{
"REGISTRY" : {
"auth" : "BASE64_ENCODED_TOKEN"
}
}
IMAGE_URL – это уникальный URI вашего регистра образов + тег самого образа, тут все должно быть понятно.
Все, теперь можем загрузить данный файл как версию нашего приложения в ElasticBeanstalk, и он сам подтянет указанный образ и развернет его.
Осталось автоматизировать данный процесс. А чтобы было совсем интересно, я реализую последовательность шагов для следующего флоу: для всех коммитов НЕ в мастер-ветку образ будет собираться и разворачиваться в STG окружении, а если мы пушим в мастер, или что лучше, его вообще закрыть и наливать только merge request’ами, то код будет деплоиться на PROD. Таким образом мы получим в PROD актуальный мастер, в котором все должно быть хорошо, а ветки для разработки и тестирования нового кода в STG. Для данной реализации нам понадобится инструкция для заливки не latest-образов, скопируйте Dockerrun.aws.json в Dockerrun.aws.stg.json, а сам Dockerrun.aws.json переименуйте в Dockerrun.aws.prod.json (просто для удобства).
Единственное, что отличает Dockerrun.aws.stg.json от Dockerrun.aws.prod.json – это IMAGE_URL:
Dockerrun.aws.stg.json
{
"AWSEBDockerrunVersion": "1",
"Authentication": {
"Bucket": "BUCKET_ID",
"Key": "KEY_PATH"
},
"Image": {
"Name": "IMAGE_URL:dev",
"Update": "true"
},
"Ports": [
{
"ContainerPort": "80"
}
]
}
Как я уже говорил в начале статьи, в качестве CI/CD я буду использовать CircleCI, который по личным ощущениям быстрее GitlabCI, если использовать бесплатную SaaS версию. Бесплатный Travis бы подошел, но так как с приватными git-репозиториями он работать не умеет, я не стал специально проводить демонстрацию на нем, дабы не было разочарования, когда такая возможность понадобится. Настройку проекта в CircleCI я оставлю читателям для самостоятельного изучения, сам же приведу необходимые для деплоя инструкции — в корне нашего проекта создадим папку .circleci, в ней config.yml следующего содержания:
.circleci/config.yml
version: 2
jobs:
build:
machine: true
steps:
- checkout
- run: echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
- run: docker build -t $CI_REGISTRY/$CI_REGISTRY_ID:dev -f Dockerfile.stg .
- run: docker push $CI_REGISTRY/$CI_REGISTRY_ID:dev
build-master:
machine: true
steps:
- checkout
- run: echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
- run: docker build -t $CI_REGISTRY/$CI_REGISTRY_ID:latest .
- run: docker push $CI_REGISTRY/$CI_REGISTRY_ID:latest
deploy-stg:
docker:
- image: circleci/python:latest
steps:
- checkout
- run: sudo pip install awsebcli --upgrade
- run: |
mkdir ~/.aws
touch ~/.aws/config
chmod 600 ~/.aws/config
echo "[profile eb-cli]" > ~/.aws/config
echo "aws_access_key_id=$AWS_ACCESS_KEY_ID" >> ~/.aws/config
echo "aws_secret_access_key=$AWS_SECRET_ACCESS_KEY" >> ~/.aws/config
- run: eb init --region EB_REGION --platform Docker EB_APP
- run: cp Dockerrun.aws.stg.json Dockerrun.aws.json
- run: eb use EB_ENV_STG --region EB_REGION
- run: eb deploy -v --staged --profile eb-cli
deploy-prod:
docker:
- image: circleci/python:latest
steps:
- checkout
- run: sudo pip install awsebcli --upgrade
- run: |
mkdir ~/.aws
touch ~/.aws/config
chmod 600 ~/.aws/config
echo "[profile eb-cli]" > ~/.aws/config
echo "aws_access_key_id=$AWS_ACCESS_KEY_ID" >> ~/.aws/config
echo "aws_secret_access_key=$AWS_SECRET_ACCESS_KEY" >> ~/.aws/config
- run: eb init --region EB_REGION --platform Docker EB_APP
- run: cp Dockerrun.aws.prod.json Dockerrun.aws.json
- run: eb use EB_ENV_STG --region EB_REGION
- run: eb deploy -v --staged --profile eb-cli
workflows:
version: 2
build:
jobs:
- build:
filters:
branches:
ignore:
- master
- deploy-stg:
requires:
- build
filters:
branches:
ignore:
- master
build-deploy:
jobs:
- build-master:
filters:
branches:
only:
- master
- deploy-prod:
requires:
- build-master
filters:
branches:
only:
- master
Сам флоу я расписал чуть раньше, тут он переведен в yaml-инструкции для CircleCI, пройдемся по реализации конкретных шагов. Важно отметить наличие определенных для CI переменных окружения, которые будут использованы им в процессе работы:
CI_REGISTRY, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD необходимы для доступа в хранилище docker-образов — тоже самое, что мы клали в cr.json, только без base64
CI_REGISTRY / CI_REGISTRY_ID составляют уникальный URL образа, без тега
AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY — названия говорят за себя сами, это креды AWS пользователя, от имени которого CircleCI будет производить деплой. Зайдите в AWS IAM и создайте пользователя, добавьте его в группу администраторов и предоставьте только программный доступ. Помните, что AWS_SECRET_ACCESS_KEY доступен для просмотра/копирования лишь однажды, после того как нажмете ссылку «show», больше вы ее не увидите.
Вернемся к шагам конфигурации CircleCI. В чем «магия»? Checkout загружает исходный код из git-ветки в текущую рабочую директорию, этот процесс повторяется в каждом джобике. В процессе build мы последовательно авторизуемся в хранилище, собираем код на основе Dockerfile.stg под тегом XXX:dev и отправляем его в хранилище. build-master делает тоже самое, только для сборки использует «обычный» Dockerfile под тегом XXX:latest.
deploy-stg устанавливает AWS EB CLI и создает авторизационный профиль в файле ~/.aws/config, который необходим для корректной работы CLI, далее инициализирует переменные для CLI – потребуется указать выбранный вами регион, платформу – всегда Docker и имя вашего приложения. Следом мы копируем содержимое Dockerrun.aws.stg.json в новый файл Dockerrun.aws.json и, используя конкретный environment и регион, даем команду на деплой нашего приложения, применив созданный авторизационный профиль. По умолчанию в результате работы данной команды весь код отслеживаемой ветки попадет в zip-архив, который будет загружен в ElasticBeanstalk и там распакован, но эта операция сравнительно затратная, потому мы и создали новый файл Dockerrun.aws.json, которого достаточно для развертывания созданного нами удаленного образа, и загрузить нам, по сути, нужно только его. Создадим для этого в корне проекта файл .ebignore:
.ebignore
*
!Dockerrun.aws.json
Данный файл использует синтаксис .gitignore да и является он .gitignore, но не для Git CLI, а для AWS EB CLI. В данном файле я говорю CLI пропустить все файлы, кроме Dockerrun.aws.json. Все, теперь при выполнении джобика deploy-stg в ElasticBeanstalk отправится только созданный нами файл. deploy-prod делает все тоже самое, только копирует в Dockerrun.aws.json содержимое файла Dockerrun.aws.prod.json, и последнее, это указание в «формате» CircleCI последовательности джобов (deploy-stg после build и deploy-prod после build-master), и на какие ветки смотрят данные джобики (ignore: — master и only: — master).
Немного иначе дело состоит с AWS ECR, как я и обещал, вернемся к нему. Вам не нужно логиниться удаленно на ECR и создавать файл cr.json, так как ElasticBeanstalk “знает собрата в лицо”. Соответственно и Dockerrun.aws.json будет выглядеть иначе – там попросту будет отсутствовать блок аутентификации:
Dockerrun.aws.json (AWS ECR)
{
"AWSEBDockerrunVersion": "1",
"Image": {
"Name": "IMAGE_URL",
"Update": "true"
},
"Ports": [
{
"ContainerPort": "80"
}
]
}
Но как же тогда произойдет аутентификация? Дело в том, что сервис, который обращается к ECR имеет некий набор прав, которые в свою очередь базируются на определенных политиках безопасности. В нашем случае, когда деплой запускается посредством AWS CLI со стороннего сервера (из CI), применяется роль “aws-elasticbeanstalk-ec2-role”, найдите ее в AWS IAM в разделе ролей и прикрепите к ней дополнительную политику “AmazonEC2ContainerRegistryReadOnly”. Теперь загрузка из приватного репозитория его «соседу» удастся без ошибок.
Но это именно загрузка из той же VPC, посредством CLI же команда docker login то же не «без примудростей»: креденшалы для docker login вы должны получить (именно получить) посредством AWS CLI, для этого существует команда
aws ecr get-login --region REGION --no-include-email
Данная команда вернет вам строку вида docker login …, проще говоря, в консоли необходимо выполнить
eval $(aws ecr get-login --region EB_REGION --no-include-email)
Команда сначала получит строку для аутентификации, а затем запустит соответствующий процесс. В виду данных правил для AWS ECR файл-инструкций для CircleCI будет выглядеть следующим образом:
.circleci/config.yml (для AWS ECR)
version: 2
jobs:
build:
docker:
- image: circleci/python:latest
steps:
- checkout
- run: sudo pip install awscli --upgrade
- run: |
mkdir ~/.aws
touch ~/.aws/config
chmod 600 ~/.aws/config
echo "[profile eb-cli]" > ~/.aws/config
echo "aws_access_key_id=$AWS_ACCESS_KEY_ID" >> ~/.aws/config
echo "aws_secret_access_key=$AWS_SECRET_ACCESS_KEY" >> ~/.aws/config
- setup_remote_docker
- run: eval $(aws ecr get-login --region EB_REGION --no-include-email)
- run: docker build -t $CI_REGISTRY/$CI_REGISTRY_ID:dev -f Dockerfile.stg .
- run: docker push $CI_REGISTRY/$CI_REGISTRY_ID:dev
build-master:
docker:
- image: circleci/python:latest
steps:
- checkout
- run: sudo pip install awscli --upgrade
- run: |
mkdir ~/.aws
touch ~/.aws/config
chmod 600 ~/.aws/config
echo "[profile eb-cli]" > ~/.aws/config
echo "aws_access_key_id=$AWS_ACCESS_KEY_ID" >> ~/.aws/config
echo "aws_secret_access_key=$AWS_SECRET_ACCESS_KEY" >> ~/.aws/config
- setup_remote_docker
- run: eval $(aws ecr get-login --region EB_REGION --no-include-email)
- run: docker build -t $CI_REGISTRY/$CI_REGISTRY_ID:latest .
- run: docker push $CI_REGISTRY/$CI_REGISTRY_ID:latest
deploy-stg:
docker:
- image: circleci/python:latest
steps:
- checkout
- run: sudo pip install awsebcli --upgrade
- run: |
mkdir ~/.aws
touch ~/.aws/config
chmod 600 ~/.aws/config
echo "[profile eb-cli]" > ~/.aws/config
echo "aws_access_key_id=$AWS_ACCESS_KEY_ID" >> ~/.aws/config
echo "aws_secret_access_key=$AWS_SECRET_ACCESS_KEY" >> ~/.aws/config
- run: eb init --region EB_REGION --platform Docker EB_APP
- run: cp Dockerrun.aws.stg.json Dockerrun.aws.json
- run: eb use EB_ENV_STG --region EB_REGION
- run: eb deploy -v --staged --profile eb-cli
deploy-prod:
docker:
- image: circleci/python:latest
steps:
- checkout
- run: sudo pip install awsebcli --upgrade
- run: |
mkdir ~/.aws
touch ~/.aws/config
chmod 600 ~/.aws/config
echo "[profile eb-cli]" > ~/.aws/config
echo "aws_access_key_id=$AWS_ACCESS_KEY_ID" >> ~/.aws/config
echo "aws_secret_access_key=$AWS_SECRET_ACCESS_KEY" >> ~/.aws/config
- run: eb init --region EB_REGION --platform Docker EB_APP
- run: cp Dockerrun.aws.prod.json Dockerrun.aws.json
- run: eb use EB_ENV_STG --region EB_REGION
- run: eb deploy -v --staged --profile eb-cli
workflows:
version: 2
build:
jobs:
- build:
filters:
branches:
ignore:
- master
- deploy-stg:
requires:
- build
filters:
branches:
ignore:
- master
build-deploy:
jobs:
- build-master:
filters:
branches:
only:
- master
- deploy-prod:
requires:
- build-master
filters:
branches:
only:
- master
Для поддержки docker-in-docker мы добавили setup_remote_docker на этапах сборок, остальное вам уже должно быть известно из содержания данной статьи. Вот и все, теперь структура нашего проекта выглядит следующем образом:
Попробуйте изменить код, запушить его в задачную ветку, а потом сделать мердж (пулл) реквест на мастер и принять его. Никаких больше «движений» для выкладки обновлений. Можно (и кому-то нужно) пойти далее, написать свои джобики для раскатки миграций, сделать промежуточный обязательный шаг прохождения автотестов и так далее, надеюсь данная статья положит начало для подобных экспериментов и последующих внедрений качественной и правильной доставки контента.
Исходный код на GitHub: tutorial-aws-symfony-ci
VolCh
Хорошая идеологически попытка автоматизации всего, но довольно много применено сомнительных, скажем так, решений, которые сводят классические плюсы от использование докера и CI/CD к нулю. Навскидку:
По флоу сразу бросается в глаза, что на stage деплоится каждый пуш в репу, кроме мастера, что несколько отличается, во-первых, от обычного понимания stage как полной копии того, что будет деплоиться на прод, а, во-вторых, разработчик запушивший свою ветку может очень долго пытаться понять, что не так, потому что кто-то запушил свою через секунду и перезатёр его. Обычно два основных варианта используется для деплоя прод и стейдж:
Что-то замечания скоро будут больше по объёму чем статья...
MrWishmaster Автор
Вы не комментируете статью, а пишите про другое флоу. Для разработчика, не для команды, это будет хорошим стартом, к примеру, если программист (один, не команда) постоянно поддерживает проект. Стейджинг, описанный мною, это окружение для тестирования кода, не более и не менее, дев кластер я не вводил, для старта я считаю его избыточным. Разумеется при наличии дев'а, где разработчик обкатывает свою часть задачи, затем собирается релиз из многих для стейджа, конфиги бы были иными, но статья несколько о другом, потому я специально в начале указал кому она будет интересна — понятно, что она не для devops инженеров, которые, разумеется, будут указывать на минусы конкретной реализации
VolCh
Я пишу как разработчик и никогда не был девопс-инженером, указываю прежде всего на недостатки DX. Как разработчик вижу, что разрабатывать будет неудобно:
И основные замечания прежде всего не к флоу (допустим для единственного разработчика, который ещё и сам себе девопс, они действительно не очень актуальны, хотя в посте вроде нет ничего про единственного), а к содержимому докерфайлов причём почти все замечания поправить — один из них удалить полностью, удалить одну строку из оставшегося, перегруппировать существующие и добавить несколько "заголовков" для этих групп.
MrWishmaster Автор
Владимир, отвечу по пунктам:
все верно, если тут вы говорите про то, что следовало бы применить сначала копирование package.json и установить зависимости, а затем уже всего остального, чтобы воспользоваться преимуществом промежуточных контейнеров, то какой в этом профит, когда вы запускаете сборку из CI? а если в ответ вы напишите подобное «неправильно запускать сборку из CI» — значит другой флоу. я исправлю этот момент в статье и в репозитории, но скорее для того, что так правильно и лучше всегда привыкать писать так (установка зависимостей отдельно, а код отдельно), конкретно в данном случае, повторюсь, выгоды вы не увидите. поправьте, если ошибаюсьтут я с вами согласен, хотя и постоянного слежения не предполагается, так как изменять содержимое Dockerfile'ов нужно крайне редко, если процесс деплоя, за который они отвечают, отлажен, тоже касается и .env файлов. но и тем не менее да, лучше иметь один Dockerfile и один .env, а строку подключения к БД задавать в переменных окружения системы, по крайней мере это убирает шанс на ошибку. мною приведен такого рода пример главным образом в образовательных целях, потому как он более нагляден и приближен к той версии приложения, которая есть у разработчика до внедрения данных практик. однако, если сообщество сочтет нужным — я изменю данный пункт, пока же оставлю как домашнее задание для тех, кому такой подход выгрузки кода придется по душе.
верно, но и статья о деплое, а не о том, как правильно готовить окружение. лично я не использую docker для dev, у меня mamp, все по-старинке, и мне так удобнее, есть коллеги, которые поднимают БД и другие сервисы в docker-compose, пользуются встроенным веб-сервером. суть в том, что как бы у вас не была настроена система локальной разработки, после прочтения данной статьи деплой у вас будет настроен.
есть задача — развернуть приложение на AWS. можно раскатывать операционную систему и весь необходимый софт на EC2, настраивать компоненты отдельно, тогда бы этот пункт у вас ушел. я использую Elasticbeanstalk, указав причину. можно ввести php.ini в файлы проекта и копировать его в Dockerfile'е, тогда опять же, настройки будут «под руками» и вопрос будет закрыт, я же написал в конце статьи, что нужно идти дальше, а данный материал не серебряная пуля, хотя и приведенные настройки абсолютно рабочие и вопрос покрывают.
Владимир, если вы хотите поделиться опытом и улучшить данную статью, ведь через время пользователи не будут влезать в дебри комментариев, а использовать лишь приведенный в материале код, то welcome, напишите конкретно, что именно по вашему мнению будет лучше, я изменю материал, если в этом действительно есть смысл. я не занимаюсь бахвальством, и уверен, что материал статьи действительно стоящий и будет полезен коллегам.
lair
Я, надо заметить, не понимаю, зачем вам EBS, если все приложение у вас все равно в докере (который вы собираете на стороннем билд-сервере). Чего бы уж не взять тогда чистый ECS?
А если к этому добавить развертывание через CloudFormation, то вы вообще избавитесь от ручных действий вида "перейдите в сервис RDS и создайте 2 инстанса необходимой вам мощности и объема" (а создание репозитория в ECR вы и вовсе пропустили).
MrWishmaster Автор
Ровно потому, что сказано в aws.amazon.com/ru/ecs/faqs:
действительно, для разработчиков, которые вчера выкладывали приложение посредством git pull в консоли сервера (а может и заливкой файлов по FTP) информации и новых механизмов хватает, чтобы еще и тут же погружать его в систему оркестрации… И, при всем уважении, EBS — это в терминологии AWS Elastic Block Store, не Elasticbeanstalk