Добрый день, в данной статье я покажу как развернуть 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!», содержащее следующие пакеты:

`symfony/webpack-encore-bundle symfony/form symfony/orm-pack symfony/profiler-pack symfony/security-bundle symfony/twig-bundle symfony/validator symfony/phpunit-bridge` — минимальный джентельменский набор. На данный момент структура папок у вас должна быть такая:

image

Теперь необходимо настроить облачную инфраструктуру. Не буду заострять внимание на регистрации и активации пробного периода 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 на этапах сборок, остальное вам уже должно быть известно из содержания данной статьи. Вот и все, теперь структура нашего проекта выглядит следующем образом:

image

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

Исходный код на GitHub: tutorial-aws-symfony-ci

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


  1. VolCh
    04.08.2019 13:11
    +1

    Хорошая идеологически попытка автоматизации всего, но довольно много применено сомнительных, скажем так, решений, которые сводят классические плюсы от использование докера и CI/CD к нулю. Навскидку:


    • разные образы не то что для dev и prod, а даже для stage и prod.
    • использование .env файла в образе, а не настройка приложения для, как минимум, stage и prod через env переменные при запуске контейнера (можно указывать тот же env файл как параметр запуска, а не как часть образа)
    • сами образы совмещают и php, и статику для веб, что исключает отдельный билд и деплой бэка и фронта, а также раздельное масштабирование
    • сами образы содержат билд/дев зависимости, не нужные в prod/stage окружениях, билд-контейнеры или мультистейдж билды не используются
    • установка зависимостей composer и yarn/npm будет проходить на каждый чих, кэширования vendor/node_modules нет
    • используется редактирование конфиг-файлов php и apache в процессе билда образа, а не просто копирование готовых конфигов

    По флоу сразу бросается в глаза, что на stage деплоится каждый пуш в репу, кроме мастера, что несколько отличается, во-первых, от обычного понимания stage как полной копии того, что будет деплоиться на прод, а, во-вторых, разработчик запушивший свою ветку может очень долго пытаться понять, что не так, потому что кто-то запушил свою через секунду и перезатёр его. Обычно два основных варианта используется для деплоя прод и стейдж:


    • деплой стейджа и его проверка просто шаг во флоу деплоя прода. Деплоится один и тот же образ, построенный по последнему коммиту (или по конкретному тагу, если используется релиз по тагам) в мастер. Проверка может быть и ручной или полуавтоматической (кнопочку QA/релиз-менеджер нажимает), так и полностью автоматической.
    • деплой прода по каждому коммитув мастер или релизному тегу, а стейджа из специальной ветки (develop/stage/release/..) или RC-тега.

    Что-то замечания скоро будут больше по объёму чем статья...


    1. MrWishmaster Автор
      04.08.2019 14:32

      Вы не комментируете статью, а пишите про другое флоу. Для разработчика, не для команды, это будет хорошим стартом, к примеру, если программист (один, не команда) постоянно поддерживает проект. Стейджинг, описанный мною, это окружение для тестирования кода, не более и не менее, дев кластер я не вводил, для старта я считаю его избыточным. Разумеется при наличии дев'а, где разработчик обкатывает свою часть задачи, затем собирается релиз из многих для стейджа, конфиги бы были иными, но статья несколько о другом, потому я специально в начале указал кому она будет интересна — понятно, что она не для devops инженеров, которые, разумеется, будут указывать на минусы конкретной реализации


      1. VolCh
        04.08.2019 14:51

        Я пишу как разработчик и никогда не был девопс-инженером, указываю прежде всего на недостатки DX. Как разработчик вижу, что разрабатывать будет неудобно:


        • практически полный ребилд фронта и бэка на каждое изменение исходников, например, маленькое изменение CSS-файлов "сделай цвет кнопки повеселее" потребует выполнения composer install и yarn install
        • постоянное слежение за синхронизацией стейдж и прод докерфайлов и их енв-файлов или большое количество ситуаций "ну на стейдже же работает"
        • при каких-то проблемах в конфигах php и apache нужно будет каждій раз лезть внутрь образа или даже работающего контейнера, чтоб посмотреть а какие текущие значения конфигов. Не говоря, что для многих синтаксис sed вовсе не что-то легкочитаемое и редактируемое.
        • ничего про собственно процесс локальной разработки (кластера на каждую ветку у нас же нет:) )

        И основные замечания прежде всего не к флоу (допустим для единственного разработчика, который ещё и сам себе девопс, они действительно не очень актуальны, хотя в посте вроде нет ничего про единственного), а к содержимому докерфайлов причём почти все замечания поправить — один из них удалить полностью, удалить одну строку из оставшегося, перегруппировать существующие и добавить несколько "заголовков" для этих групп.


        1. MrWishmaster Автор
          04.08.2019 19:51
          -1

          Владимир, отвечу по пунктам:

          практически полный ребилд фронта и бэка на каждое изменение исходников
          все верно, если тут вы говорите про то, что следовало бы применить сначала копирование package.json и установить зависимости, а затем уже всего остального, чтобы воспользоваться преимуществом промежуточных контейнеров, то какой в этом профит, когда вы запускаете сборку из CI? а если в ответ вы напишите подобное «неправильно запускать сборку из CI» — значит другой флоу. я исправлю этот момент в статье и в репозитории, но скорее для того, что так правильно и лучше всегда привыкать писать так (установка зависимостей отдельно, а код отдельно), конкретно в данном случае, повторюсь, выгоды вы не увидите. поправьте, если ошибаюсь

          постоянное слежение за синхронизацией стейдж и прод докерфайлов и их енв-файлов
          тут я с вами согласен, хотя и постоянного слежения не предполагается, так как изменять содержимое Dockerfile'ов нужно крайне редко, если процесс деплоя, за который они отвечают, отлажен, тоже касается и .env файлов. но и тем не менее да, лучше иметь один Dockerfile и один .env, а строку подключения к БД задавать в переменных окружения системы, по крайней мере это убирает шанс на ошибку. мною приведен такого рода пример главным образом в образовательных целях, потому как он более нагляден и приближен к той версии приложения, которая есть у разработчика до внедрения данных практик. однако, если сообщество сочтет нужным — я изменю данный пункт, пока же оставлю как домашнее задание для тех, кому такой подход выгрузки кода придется по душе.

          ничего про собственно процесс локальной разработки
          верно, но и статья о деплое, а не о том, как правильно готовить окружение. лично я не использую docker для dev, у меня mamp, все по-старинке, и мне так удобнее, есть коллеги, которые поднимают БД и другие сервисы в docker-compose, пользуются встроенным веб-сервером. суть в том, что как бы у вас не была настроена система локальной разработки, после прочтения данной статьи деплой у вас будет настроен.

          при каких-то проблемах в конфигах php и apache нужно будет каждій раз лезть внутрь образа или даже работающего контейнера
          есть задача — развернуть приложение на AWS. можно раскатывать операционную систему и весь необходимый софт на EC2, настраивать компоненты отдельно, тогда бы этот пункт у вас ушел. я использую Elasticbeanstalk, указав причину. можно ввести php.ini в файлы проекта и копировать его в Dockerfile'е, тогда опять же, настройки будут «под руками» и вопрос будет закрыт, я же написал в конце статьи, что нужно идти дальше, а данный материал не серебряная пуля, хотя и приведенные настройки абсолютно рабочие и вопрос покрывают.

          удалить одну строку из оставшегося, перегруппировать существующие и добавить несколько «заголовков» для этих групп
          Владимир, если вы хотите поделиться опытом и улучшить данную статью, ведь через время пользователи не будут влезать в дебри комментариев, а использовать лишь приведенный в материале код, то welcome, напишите конкретно, что именно по вашему мнению будет лучше, я изменю материал, если в этом действительно есть смысл. я не занимаюсь бахвальством, и уверен, что материал статьи действительно стоящий и будет полезен коллегам.


          1. lair
            04.08.2019 23:46

            есть задача — развернуть приложение на AWS. можно раскатывать операционную систему и весь необходимый софт на EC2, настраивать компоненты отдельно, тогда бы этот пункт у вас ушел. я использую Elasticbeanstalk, указав причину.

            Я, надо заметить, не понимаю, зачем вам EBS, если все приложение у вас все равно в докере (который вы собираете на стороннем билд-сервере). Чего бы уж не взять тогда чистый ECS?


            А если к этому добавить развертывание через CloudFormation, то вы вообще избавитесь от ручных действий вида "перейдите в сервис RDS и создайте 2 инстанса необходимой вам мощности и объема" (а создание репозитория в ECR вы и вовсе пропустили).


            1. MrWishmaster Автор
              05.08.2019 08:17

              Ровно потому, что сказано в aws.amazon.com/ru/ecs/faqs:

              Elastic Beanstalk является идеальным решением, если вы хотите воспользоваться преимуществами контейнеров, но при этом вам требуется только простота развертывания приложений от разработки и до рабочей стадии через загрузку образа контейнера. Вы можете работать с Amazon ECS напрямую, если хотите иметь более полный контроль над архитектурой настраиваемых приложений.
              действительно, для разработчиков, которые вчера выкладывали приложение посредством git pull в консоли сервера (а может и заливкой файлов по FTP) информации и новых механизмов хватает, чтобы еще и тут же погружать его в систему оркестрации… И, при всем уважении, EBS — это в терминологии AWS Elastic Block Store, не Elasticbeanstalk