Настройка CI/CD с AWS и Github Actions

—

от автора

Введение

В этой статье хотелось бы поведать о настройке CI/CD процессов на примере личного грантового проекта. Сам проект посвящен автоматической обработке T2 взвешенных снимков МРТ поясничного отдела позвоночника и представляет собой набор веб-приложений. На проекте используются разные веб-фреймворки, в частности, Flask, Django и Spring Boot. Приложения разворачиваются в инфраструктуре AWS, потому что это удобно, ибо Amazon остается гегемоном среди публичных облаков из-за огромного количества сервисов, а также, потому что это стильно, модно и молодежно. Чтобы избавиться от монотонной работы по разворачиванию веб-приложений, а также от постоянных проверок на их жизнеспособность, было решено настроить CI/CD процессы.

Про Github Actions

Для настройки CI/CD пайплайнов был выбран инструмент Github Actions, потому что все репозитории проекта размещены в Github, а еще, потому что не хочется держать свой Jenkins сервер. Учитывая то, что умельцы пишут свои собственные Actions, сейчас практически все потребности в разных операциях (например, подключение по SSH, работа с AWS CLI, сборка под разные ОС) удовлетворяются с лихвой. Определившись с инструментом разработки необходимо продумать требования к пайплайнам на примере Flask веб-приложения.

CI пайплайны, исходя из своего определения, должны выполнять сборку проекта, включая прогон тестов, а также размещать собранные артефакты в хранилище для последующего развертывания. Очевидно, что хранилищем артефактов в инфраструктуре AWS будет являться S3 bucket.

С CD пайплайном дела обстоят сложнее, ибо в данном случае необходимо продумать взаимодействие AWS сервисов между собой. Приложению нужно доменное имя (Route 53), которое будет смотреть на IP-адрес сервера (EC2) с самой программой. Также нужна база данных, в нашем случае реляционная (RDS). Для минимально жизнеспособного веб-приложения уже потребовалось 3 AWS сервиса. В существующем Flask проекте используется больше AWS сервисов. Ниже показана вольная схема AWS инфраструктуры для ранее описываемого проекта.

Инфраструктурная AWS схема веб-приложения
Инфраструктурная AWS схема веб-приложения

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

Конечно, можно настраивать эти сервисы каждый раз вручную, и при изменении программного кода мануально накатывать новую версию приложения на сервер, но лучше автоматизировать данный процесс, избавив себя от монотонной работы. У AWS и на этот случай есть сервис под названием CloudFormation, предоставляющий услуги вида «Infrastructure as a Code» (IaaC), позволяющий моделировать и управлять ресурсами AWS. Его и будем использовать.

Итак, можно приступать к непосредственной разработке пайплайнов.

Continuous Integration jobs

Все давно знают, что Github Actions пайплайны складируются в директории репозитория: .github/workflows/*.yml

Первым делом настроим прогон тестов на пулл реквестах, чтобы в main ветку не попал нерабочий код. Ниже представлен код пайплайна для вышеописанных действий.

name: Pipeline name  # Имя пайплайна env:  # Переменные среды   VARIABLE: "var"  on:  # Триггеры для запуска пайплайна   workflow_dispatch:  # Ручной запуск через UI Гитхаба   pull_request:  # Для пул реквестов  jobs: # Список джоб   validation: # Имя джобы     runs-on: ubuntu-latest     steps:   # Действия для нашей job       - name: Clone repo  # Клонируем наш репозиторий (можно клонировать и другие (даже приватные), только нужен другой action)         uses: actions/checkout@v2        - name: Set up Python 3.7  # Устанавливаем питончик нужной версии         uses: actions/setup-python@v1         with:           python-version: 3.7        - name: Configure AWS Credentials  # Конфигурируем креды для работы с AWS         uses: aws-actions/configure-aws-credentials@v1         with:           aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}  # Ниже будет картинка, где показана настройка secrets           aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}           aws-region: ${{ env.AWS_REGION_NAME }}        - name: Get response of RDS DB structure  # Получаем адрес БД через AWS CLI         run: aws rds describe-db-instances --db-instance-identifier ${{ secrets.MYSQL_SCHEMA_NAME }} >> rds_response.json        - name: Get database URL         id: url         uses: sergeysova/jq-action@v2         with:           cmd: "jq -r '.DBInstances[].Endpoint.Address' rds_response.json"  # Фильтруем JSON        - name: Install dependencies  # Устанавлиаем Python зависимости         run: |           python -m pip install --upgrade pip           pip install -r requirements.txt                - name: Lint with pycodestyle  # Проверяем соответствует ли наш код PEP8         run: |           source venv/bin/activate           pycodestyle --exclude=venv --max-line-length=150 .        - name: Run unit tests  # Запускаем тестики         run: |           sudo apt upgrade           pytest         env:  # Необходимые системные переменные, чтобы проект не крашнулся           # Configure AWS settings environments for tests           AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}           AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }}           # Configure production MySQL settings           MYSQL_USER: ${{ secrets.MYSQL_USER }}           MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }}           MYSQL_URL: ${{ steps.url.outputs.value }}           MYSQL_DB: ${{ secrets.MYSQL_DB }}           # Configure SQLAlchemy           SQL_ALCHEMY_SECRET_KEY: ${{ secrets.SQL_ALCHEMY_SECRET_KEY }} 

Ниже показана настройка secrets для репозитория.

Github secrets репозитория
Github secrets репозитория

Ниже приведу пример конфигурации джобы (это уже другой .yml файлик) для сборки проекта с комментариями, которая скажет все за меня.

name: Pipeline name  # Все еще имя пайлайна env:  # Все еще переменные среды, которые все также используются в виде: ${{ env.VAR_NAME }}   ENV_VARIABLE: "value"  on:   workflow_dispatch:   push:     branches: [ main ]  # При merge в main ветку  jobs:     ci:     runs-on: ubuntu-latest  # Я офигел, когда узнал что можно и на MacOS бесплатно запускать      steps:       - name: Git clone our repo         uses: actions/checkout@v1        - name: Install zip  # Устаналиваем zip архиватор, т.к. проект на Python         run: sudo apt-get install zip gzip tar        - name: Archive project  # Архивируем наш проект из рабочей директории *         run: sudo zip -r project.zip *        - name: Configure my AWS Credentials         uses: aws-actions/configure-aws-credentials@v1         with:           aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}           aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}           aws-region: ${{ env.AWS_REGION_NAME }}        - name: Copy app to S3 bucket  # Копируем архив с проектом в AWS S3 bucket          run: aws s3 cp "project.zip" s3://${{ env.BUCKET_NAME }}/project.zip        - name: Print Happy Message for CI finish         run: echo "CI Pipeline part Finished successfully"

Таким образом, имеем CI джобы, которые прогоняют тестики для пул реквестов, а при коммите в main ветку, архивируют проект и отправляют его в AWS S3 bucket. Из облачного хранилища уже можно брать актуальную версию проекта через AWS CLI и накатывать на EC2 инстансы.

Continious delivery job & CF template

Весь интерес и сложность CD джобы для проекта кроется не в пайплайне для Github Actions, а в CloudFormation шаблоне. Так исторически сложилось, что официальная документация для CloudFormation ужасна. Так считаю не только я, но и другие программисты. Даже имеющийся редактор шаблонов для CloudFormation не спасает положение. Поэтому приходится искать готовые сниппеты умельцев по всему Интернету и отсекать от сниппетов все ненужное. Например, для лендинг-страниц есть готовый CloudFormation шаблон с CDN (AWS CloudFront). Но для полноценных веб-приложений дела обстоят немного интереснее. Поскольку в существующем Flask проекте используется нестандартный Django-style менеджер запуска (Flask-Script), то использование AWS ELB для автоматического развертывания отпадает, потому что в конфигурации этого сервиса сам черт ногу сломит. Используется стандартный EC2.

Итак, начну с демонстрации CD джобы, которая заканчивается разворачиванием CloudFormation шаблона в AWS инфраструктуре. Эта джоба является продолжением предыдущего пайплайна.

... cd_part:  # Имя джобы   runs-on: ubuntu-latest   needs: [ci_part]  # Запустится после успешного выполнения предыдущей джобы    steps:     - name: Git clone our repo  # Нам понадобится наш репозиторий, т.к. в нем хранится CloudFormation шаблон       uses: actions/checkout@v1      - name: Configure my AWS Credentials  # Все еще конфигурируем AWS креды, т.к. они сбрасываются после завершения предыдущей джобы       uses: aws-actions/configure-aws-credentials@v1       with:         aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}         aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}         aws-region: ${{ env.AWS_REGION_NAME }}      - name: Deploy main stack  # AWS CLI команда для запуска CloudFormation шаблона, который хранится у нас в репозитории       run: |         aws cloudformation deploy \         --stack-name main-stack \  # Имя стека (у AWS есть ограничения на спец. символы для имен CloudFormation стэков)         --template-file cloudformation/main_stack.json \  # Путь до файла с CloudFormation шаблоном         --capabilities CAPABILITY_NAMED_IAM \  # Магический параметр, про который мне лень писать         --parameter-overrides YourParameterName=${{ secrets.PARAMETER }} \  # Наши параметры, которые можно прокидывать в CloudFormation шаблон         --no-fail-on-empty-changeset  # Околомагический параметр, который нужен, чтобы при следующем старте джобы CloudFormation шаблон не падал с ошибкой, если не было изменений в шаблоне      - name: Print Happy Message for CD finish       run: echo "CD Pipeline part Finished successfully"

Это весьма простой пайплайн по сравнению с предыдущими. Просто нужно знать как использовать AWS CLI команду для работы с CloudFormation. Можно читать документацию по AWS CLI либо с официального сайта, либо непосредственно из консольки. Теперь самое интересное —  CloudFormation шаблон:

{   "Description": "AWS CloudFormation stack",   "Parameters": {     "ParameterName": {       "Type": "String"// в 99% случаев используем String     }   },   "Resources": { // Создаем наши AWS ресурсы     "S3Bucket": { // Файловое хранилище       "Type": "AWS::S3::Bucket",       "Properties": {         "BucketName": "S3BucketName",         "PublicAccessBlockConfiguration": { // Эти настройки нужны для конфигурирования открытости/закрытости S3 bucket (можно ли обращаться к файлам из хранилища по URL)           "BlockPublicAcls": false,           "BlockPublicPolicy": false,           "IgnorePublicAcls": false,           "RestrictPublicBuckets": false         }       }     },     "LogGroup": { // Это облачное хранилище логов, запись в них настаивается в коде приложения       "Type": "AWS::Logs::LogGroup",       "Properties": {         "LogGroupName": "LogGroupName"       }     },     "SecurityIAMRole": {  // Это настройки безопасности для EC2       "Type": "AWS::IAM::Role",       "Properties": {         "AssumeRolePolicyDocument": {           "Version": "2012-10-17",           "Statement": [             {               "Effect": "Allow",               "Principal": {                 "Service": [                   "ec2.amazonaws.com"                 ]               },               "Action": [                 "sts:AssumeRole"               ]             }           ]         },         "Policies": [           {             "PolicyName": "S3Policy",             "PolicyDocument": {               "Version": "2012-10-17",               "Statement": [                 {                   "Effect": "Allow",                   "Action": "s3:*",                   "Resource": "arn:aws:s3:::YOUR_S3_BUCKET_FOLDER/*"                 }               ]             }           }         ],         "RoleName": "IAMRoleName"       }     },     "InstanceProfile": {  // Такая же околобезопасная штука для EC2       "Type": "AWS::IAM::InstanceProfile",       "Properties": {         "Roles": [           {             "Ref": "SecurityIAMRole"           }         ]       }     },     "SecurityGroup": {       "Type": "AWS::EC2::SecurityGroup",       "Properties": {         "GroupName": "SecurityGroupName",         "GroupDescription": "SecurityGroupDescription",         "SecurityGroupIngress": [           { // Порты, которые мы можем выставлять наружу для EC2 инстанса             "IpProtocol": "tcp",             "CidrIp": "0.0.0.0/0",             "FromPort": 22,             "ToPort": 22           },           {             "IpProtocol": "tcp",             "CidrIpv6": "::/0",             "FromPort": 22,             "ToPort": 22           },           {             "IpProtocol": "tcp",             "CidrIp": "0.0.0.0/0",             "FromPort": 80,             "ToPort": 80           },           {             "IpProtocol": "tcp",             "CidrIpv6": "::/0",             "FromPort": 80,             "ToPort": 80           },           {             "IpProtocol": "tcp",             "CidrIp": "0.0.0.0/0",             "FromPort": 443,             "ToPort": 443           },           {             "IpProtocol": "tcp",             "CidrIpv6": "::/0",             "FromPort": 443,             "ToPort": 443           }         ]       }     },     "CertificateManagerCertificate": { // HTTPS сертификат       "Type": "AWS::CertificateManager::Certificate",       "Properties": {         "DomainName": {           "Ref": "ParameterUrlName" // Ваш url адрес         },         "ValidationMethod": "DNS",         "SubjectAlternativeNames": [           {             "Ref": "ParameterUrlName"           },           {             "Fn::Sub": [               "www.${Domain}", // Чтобы работало обращение через www.               {                 "Domain": {                   "Ref": "ParameterUrlName"                 }               }             ]           }         ],         "DomainValidationOptions": [           {             "DomainName": {               "Ref": "ParameterUrlName"             },             "HostedZoneId": {               "Ref": "HostZoneId" // Параметр на HostedZone id в Route53             }           },           {             "DomainName": {               "Fn::Sub": [                 "www.${Domain}",                 {                   "Domain": {                     "Ref": "ParameterUrlName"                   }                 }               ]             },             "HostedZoneId": {               "Ref": "HostZoneId"             }           }         ]       }     },     "Ec2Instance": { // Сам сервак с приложенькой       "Type": "AWS::EC2::Instance",       "Properties": {         "IamInstanceProfile": {           "Ref": "InstanceProfile"         },         "AvailabilityZone": "us-east-1c", // Можете поставить свое         "ImageId": "ami-047a51fa27710816e", // Amazon Linux ОС         "InstanceType": "t2.micro", //          "KeyName": "key-pair", // ssh ключик         "SecurityGroups": [            {             "Ref": "SecurityGroup"           }         ],         "Tags": [           {             "Key": "Name",             "Value": "Имя вашего EC2 инстанса"           }         ],         "UserData": { // Команды запуска и настройки ec2 под наши нужды           "Fn::Base64": {             "Fn::Join": [               "",               [                  "Content-Type: multipart/mixed; boundary=\"//\"\n", // Фигня без которой ничего не работает                 "MIME-Version: 1.0\n\n",                 "--//\n",                 "Content-Type: text/cloud-config; charset=\"us-ascii\"\n",                 "MIME-Version: 1.0\n",                 "Content-Transfer-Encoding: 7bit\n",                 "Content-Disposition: attachment; filename=\"cloud-config.txt\"\n\n",                 "#cloud-config\n",                 "cloud_final_modules:\n",                 "- [scripts-user, always]\n\n",                 "--//\n",                 "Content-Type: text/x-shellscript; charset=\"us-ascii\"\nMIME-Version: 1.0\n",                 "Content-Transfer-Encoding: 7bit\n",                 "Content-Disposition: attachment; filename=\"userdata.txt\"\n\n",                  "#!/bin/bash\n",                 "sudo yum -y install gcc openssl-devel bzip2-devel libffi-devel libssl-dev\n", // Устанавливаем python 3.7.2 и pip3                 "sudo yum -y install python37 python3-devel\n",                 "sudo yum -y install mysql-devel\n",                 "sudo yum -y install vim\n",                 "sudo yum -y install libXext libSM libXrender\n",                 "sudo curl -O https://bootstrap.pypa.io/get-pip.py\n",                 "sudo python3 get-pip.py\n",                 "pip3 install awsebcli --upgrade --user\n",                 "pip3 install jmespath==0.7.1 python-dateutil\n",                 "pip3 install awsebcli --upgrade --user\n",                 "sudo echo 'export ENV_VARIABLE=", // Сетаем переменную среды                 {                   "Ref": "ParameterName"                 },                 "'>>/home/ec2-user/.bash_profile\n",                 // Удалил отсюда часть команд, т.к. они особой роли не играют                 "aws s3 cp s3://bucket/file.zip /home/ec2-user/file.zip\n", // Копируем исходный код проекта                 "nohup gunicorn -w 1 --reload -b 127.0.0.1:5000 --chdir /home/ec2-user 'app:create_app()' > log.txt 2>&1 &\n", // запускаем gunicorn                 "sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm\n", // Устанавливаем Let's encrypt                 "sudo yum install -y epel-release\n",                 "sudo yum install nginx -y\n",                 "sudo amazon-linux-extras install -y epel\n",                 "sudo yum-config-manager --enable epel*\n",                 "sudo yum install -y certbot\n",                 "sudo yum install -y python-certbot-nginx\n",                 "pip3 install certbot-nginx\n",                 "sudo sed -i '2 a server_name ", // Вставить url адрес для Let's Encrypt в nginx конфигурацию на 2 строчку                 {                   "Ref": "ParameterUrlName"                 },                 " www.",                 {                   "Ref": "ParameterUrlName"                 },                 ";' /home/ec2-user/deploy/app.conf\n",                 "sudo cp /home/ec2-user/deploy/app.conf /etc/nginx/conf.d/app.conf\n",                 "sudo certbot --nginx --non-interactive --agree-tos -d ",                 {                   "Ref": "ParameterUrlName"                 },                 " -d www.",                 {                   "Ref": "ParameterUrlName"                 },                 " -m ",                 {                   "Ref": "YourEmailParameter"                 },                 "\n",                 "sudo certbot renew --dry-run\n",                  "sudo systemctl stop nginx\n",                 "sudo pkill -f nginx & wait $!\n", // Костыль без которого у меня не работало                 "sudo systemctl start nginx\n",                 "sudo systemctl enable nginx\n"               ]             ]           }         }       },       "DependsOn": [ // EC2 не должен создаваться раньше чем логи и s3 хранилище         "LogGroup",         "S3Bucket"       ]     },     "ElasticIP": { // постоянный ip адрес (можно и без него)       "Type": "AWS::EC2::EIP",       "Properties": {         "Domain": "vpc",         "InstanceId": {           "Ref": "Ec2Instance"         },         "Tags": [           {             "Key": "Name",             "Value": "YourElasticIPName"           }         ]       }     },     "DNSRecord": { // Запись DNS        "Type": "AWS::Route53::RecordSetGroup",       "Properties": {         "HostedZoneId": {           "Ref": "HostZoneId"         },         "RecordSets": [           {             "Name": {               "Ref": "ParameterUrlName"             },             "Type": "A",             "TTL": 900,             "ResourceRecords": [               {                 "Ref": "ElasticIP"               }             ]           },           {             "Name": {               "Fn::Sub": [                 "www.${Domain}",                 {                   "Domain": {                     "Ref": "ParameterUrlName"                   }                 }               ]             },             "Type": "A",             "TTL": 900,             "ResourceRecords": [               {                 "Ref": "ElasticIP"               }             ]           }         ]       }     }   } }}

Наш ClodFormation шаблон отправится прямиком в AWS и там скомпилится. Отработав приличное количество времени, CloudFormation стэк создастся и мы сразу будем иметь развернутое приложение со всеми хранилищами, логами и другими ресурсами.

Пример развернутого CloudFormation stack с MySQL RDS БД
Пример развернутого CloudFormation stack с MySQL RDS БД

Заключение

В данной статье была рассмотрена настройка CI/CD пайплайнов на основе Github Actions в инфраструктуре AWS. Автоматизация развертывания AWS ресурсов проводилась с использованием сервиса CloudFormation.  У данного подхода есть минус в виде программирования на JSON (YAML) в CloudFormation шаблонах, но также имеется и плюс в виде гибкости конфигурации.

Кому будет интересно покопаться еще, то я создал Github Gist, где написал CloudFormation шаблон для реляционной базы данных (AWS RDS MySQL) и простейшую nginx конфигурацию, которую использует ранее представленный CloudFormation template.


ссылка на оригинал статьи https://habr.com/ru/post/657549/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *