Это часть 5 в серии Мигрирование монолитного приложения SaaS в Serverless — журнал решений .
Чтобы убедиться, что каждый новый маршрут I мигрирует от API Legacy Express.js в лямбду, тщательно протестирован перед выпуском до производства, я начал устанавливать процесс CICD на место.
Мои основные цели для процесса CICD:
- Каждый серверный сервис будет иметь свой собственный конвейер, так как каждый будет развернуться независимо.
- Слияние для мастера должно вызвать основной трубопровод, который будет выполнять тесты и развертывания через DEV для постановки до производства.
- CICD -трубопроводы будут размещены в учетной записи разработчиков, но должны быть в состоянии развертывать другие учетные записи AWS.
- Вся конфигурация CICD будет выполнена с помощью инфраструктуры как код, поэтому я могу легко настроить трубопроводы для новых служб по мере их создания. Консоль доступа будет строго только чтение.
Я решил использовать AWS Codepipeline и Кодовая строительство Чтобы создать трубопроводы в качестве без сервера и иметь встроенную поддержку IAM, которую я могу использовать для надежного развертывания для серверов Prod. Codepipeline действует как оркестратор, в то время как кодовая строительство действует как бегун задачи. Каждый этап в трубопроводе Codepipeline состоит из одного или нескольких действий, которые вызывают проект CodeBuild, который содержит фактические команды для создания пакета выпуска, запуска тестов, выполнения развертывания и т. Д.
Обзор трубопровода
Для первой версии моего CICD -процесса я буду развернуть одну услугу AC-REST-API
(который в настоящее время содержит только один тест Функция Lambda + API GW маршрут). Этот трубопровод будет работать следующим образом:
- Действие источника GitHub Триггеры всякий раз, когда код выдвигается в главную ветвь репо хостинг My
AC-REST-API
оказание услуг. - Запустите Lint + Unit/Integration Tests и пакет развертывания построения (с помощью пакета
SLS
- Deploy Package (используя
sls развернуть
) на этап разработчика и запустить приемочные тесты против него. - Развернуть пакет на стадию постановки и запустить приемочные тесты против него.
- Развернуть пакет на этап PROD и запустить приемные тесты против него.
Вот как выглядит мой завершенный трубопровод:
Управление трубопроводами в качестве инфраструктуры как код
Как и в большинстве услуг AWS, легко найти учебные пособия о том, как создать трубопровод CodePipeline с использованием консоли AWS, но трудно найти подробные примеры инфраструктуры как код. В этом отношении ярмарки кодовой строительства лучше, так как документы, как правило, рекомендуют использовать buildspec.yml
Файл в вашем репо, чтобы указать команды сборки.
Поскольку я буду создавать несколько трубопроводов для Каждый микросервис, который я идентифицировал Когда я прохожу процесс миграции, я хочу что -то, что я могу легко использовать.
Я рассмотрел эти варианты определения моих трубопроводов:
- Используйте необработанные файлы yaml.
- Используйте CloudFormation внутри раздела ресурсов Serverless.yml.
- Используйте новый AWS Cloud Development Kit (CDK) , что позволяет вам определять конструкции инфраструктуры высокой уровня с использованием популярных языков (JavaScript/TypeScript, Java, C#), которые генерируют стеки облачныхформ.
Я относительно опытный с облачной информацией, но я нахожу его опытом разработки очень расстраивающим, и в прошлом сгорел дни, получая сложные стеки облачных сформирований и запущены. Мои основные меры с этим:
- Медленная петля обратной связи между авторизацией и просмотром развертывания завершено (не удастся или преуспевает)
- Отсутствие шаблонов/модулей, которое затрудняет повторное использование, не прибегая к копированию и вставке груза шаблона (я знаю о таких инструментах, как тропосфера, которые помогают в этом отношении Но я разработчик JavaScript и не хочу изучать новый язык (Python))
- Мне всегда нужно открыть документы на вкладке браузера, чтобы увидеть доступные свойства на ресурсе
Учитывая это, я решил продолжить использование CDK, который (в теории) должен помочь смягчить все 3 из этих жалоб.
Создание моего приложения CDK
CDK предоставляет CLI, который вы используете для развертывания «приложения». Приложение в этом контексте является фактически деревом ресурсов, которое вы составляете с использованием объектно-ориентированной модели программирования, где каждый узел в дереве называется «конструкцией». CDK предоставляет готовые конструкции для всех основных ресурсов AWS, а также позволяет (и поощряет ) вы писать свои собственные. Приложение CDK содержит по крайней мере одну конструкцию стека облачныхформ, которые он использует для развертывания под капотом. Таким образом, вы по -прежнему получаете автоматическое преимущество отката, которое дает вам Cloudformation.
Для получения более подробной информации о том, что такое CDK и как вы можете установить его и загрузить собственное приложение, я бы порекомендовал начать здесь .
Я также решил попробовать TypeScript (вместо простого JavaScript), так как CDK был написан в TypeScript, и сильная набор, по -видимому, подходит для ресурсов инфраструктуры со сложными наборами атрибутов.
Я начал с создания пользовательской конструкции верхнего уровня под названием Servicecicdpipelines
который выступает в качестве контейнера для всех трубопроводов для каждой услуги, которую я буду создавать, и любые дополнительные ресурсы. Я включил весь исходный код в эта суть , но основные его элементы следующие:
CICD-Pipelines-Stack
: Одиночный стек облаков для развертывания всех ресурсов, связанных с CICD.трубопроводы []
: СписокServicePipeline
Объекты — пользовательская конструкция, которая инкапсулирует логику для создания одного конвейера для одного без сервера.Alertstopic
: SNS Тема, которая будет получать уведомления об ошибках трубопровода.
Пользователь ServicePipeline
Конструкция — это то, где лежит большая часть логики. Это требует ServiceedEfinition
объект как параметр, который выглядит так:
export interface ServiceDefinition { serviceName: string; githubRepo: string; githubOwner: string; githubTokenSsmPath: string; /** Permissions that CodeBuild role needs to assume to deploy serverless stack */ deployPermissions: PolicyStatement[]; }
Этот объект — это то, где определяются все уникальные атрибуты развертываемого сервиса. На данный момент у меня здесь не так много вариантов Но я ожидаю добавить к этому с течением времени.
ServicePipeline
Конструкция также, где определяется каждая стадия трубопровода:
export class ServicePipeline extends Construct { readonly pipeline: Pipeline; readonly alert: PipelineFailedAlert; constructor(scope: Construct, id: string, props: ServicePipelineProps) { super(scope, id); const pipelineName = `${props.service.serviceName}_${props.sourceTrigger}`; this.pipeline = new Pipeline(scope, pipelineName, { pipelineName, }); // Read Github Oauth token from SSM Parameter Store // https://docs.aws.amazon.com/codepipeline/latest/userguide/GitHub-rotate-personal-token-CLI.html const oauth = new SecretParameter(scope, 'GithubPersonalAccessToken', { ssmParameter: props.service.githubTokenSsmPath, }); const sourceAction = new GitHubSourceAction({ actionName: props.sourceTrigger === SourceTrigger.PullRequest ? 'GitHub_SubmitPR' : 'GitHub_PushToMaster', owner: props.service.githubOwner, repo: props.service.githubRepo, branch: 'master', oauthToken: oauth.value, outputArtifactName: 'SourceOutput', }); this.pipeline.addStage({ name: 'Source', actions: [sourceAction], }); // Create stages for DEV => STAGING => PROD. // Each stage defines its own steps in its own build file const buildProject = new ServiceCodebuildProject(this.pipeline, 'buildProject', { projectName: `${pipelineName}_dev`, buildSpec: 'buildspec.dev.yml', deployerRoleArn: CrossAccountDeploymentRole.getRoleArnForService( props.service.serviceName, 'dev', deploymentTargetAccounts.dev.accountId, ), }); const buildAction = buildProject.project.toCodePipelineBuildAction({ actionName: 'Build_Deploy_DEV', inputArtifact: sourceAction.outputArtifact, outputArtifactName: 'sourceOutput', additionalOutputArtifactNames: [ 'devPackage', 'stagingPackage', 'prodPackage', ], }); this.pipeline.addStage({ name: 'Build_Deploy_DEV', actions: [buildAction], }); const stagingProject = new ServiceCodebuildProject(this.pipeline, 'deploy-staging', { projectName: `${pipelineName}_staging`, buildSpec: 'buildspec.staging.yml', deployerRoleArn: CrossAccountDeploymentRole.getRoleArnForService( props.service.serviceName, 'staging', deploymentTargetAccounts.staging.accountId, ), }); const stagingAction = stagingProject.project.toCodePipelineBuildAction({ actionName: 'Deploy_STAGING', inputArtifact: sourceAction.outputArtifact, additionalInputArtifacts: [ buildAction.additionalOutputArtifact('stagingPackage'), ], }); this.pipeline.addStage({ name: 'Deploy_STAGING', actions: [stagingAction], }); // Prod stage requires cross-account access as codebuild isn't running in same account const prodProject = new ServiceCodebuildProject(this.pipeline, 'deploy-prod', { projectName: `${pipelineName}_prod`, buildSpec: 'buildspec.prod.yml', deployerRoleArn: CrossAccountDeploymentRole.getRoleArnForService( props.service.serviceName, 'prod', deploymentTargetAccounts.prod.accountId, ), }); const prodAction = prodProject.project.toCodePipelineBuildAction({ actionName: 'Deploy_PROD', inputArtifact: sourceAction.outputArtifact, additionalInputArtifacts: [ buildAction.additionalOutputArtifact('prodPackage'), ], }); this.pipeline.addStage({ name: 'Deploy_PROD', actions: [prodAction], }); // Wire up pipeline error notifications if (props.alertsTopic) { this.alert = new PipelineFailedAlert(this, 'pipeline-failed-alert', { pipeline: this.pipeline, alertsTopic: props.alertsTopic, }); } } } export interface ServicePipelineProps { /** Information about service to be built & deployed (source repo, etc) */ service: ServiceDefinition; /** Trigger on PR or Master merge? */ sourceTrigger: SourceTrigger; /** Account details for where this service will be deployed to */ deploymentTargetAccounts: DeploymentTargetAccounts; /** Optional SNS topic to send pipeline failure notifications to */ alertsTopic?: Topic; } /** Wrapper around the CodeBuild Project to set standard props and create IAM role */ export class ServiceCodebuildProject extends Construct { readonly buildRole: Role; readonly project: Project; constructor(scope: Construct, id: string, props: ServiceCodebuildActionProps) { super(scope, id); this.buildRole = new ServiceDeployerRole(this, 'project-role', { deployerRoleArn: props.deployerRoleArn, }).buildRole; this.project = new Project(this, 'build-project', { projectName: props.projectName, timeout: 10, // minutes environment: { buildImage: LinuxBuildImage.UBUNTU_14_04_NODEJS_8_11_0, }, source: new CodePipelineSource(), buildSpec: props.buildSpec || 'buildspec.yml', role: this.buildRole, }); } }
Быстрая заметка об организации кода трубопровода
Я создал все эти индивидуальные конструкции CDK в общих Autochart-Infrastructure
Репо, я использую для определения общих, перекрестных инфраструктурных ресурсов, которые используются несколькими услугами. Файлы BuildSpec, которые я освещаю ниже, живут в том же хранилище, что и их сервис (а именно так). Я не совсем доволен определением трубопровода и сценариями сборки в отдельных репо, и я, вероятно, рассмотрю способ перенести мои многократные конструкции CDK в общую библиотеку и иметь источник для приложений CDK, определенных в специфике для обслуживания Репо при ссылке на эту библиотеку.
Написание сценариев кодовой строки
В приведенном выше коде определения трубопровода вы, возможно, заметили, что на каждом этапе развертывания есть свой собственный файл BuildSpec (используя соглашение о именовании buildspec.
). Я хочу свести к минимуму количество логики в самом труде и сохранить ее единоличной ответственности за оркестровку. Вся логика будет жить в сценариях сборки.
Строительство и развертывание на стадии разработки
Как только толчок происходит к главной ветви в GitHub, следующим шагом трубопровода является запуск тестов, упаковку и развертывание на этапе разработки. Это показано ниже:
# buildspec.dev.yml version: 0.2 env: variables: TARGET_REGION: us-east-1 SLS_DEBUG: '' DEPLOYER_ROLE_ARN: 'arn:aws:iam:::role/ac-rest-api-dev-deployer-role' phases: pre_build: commands: - chmod +x build.sh - ./build.sh install build: commands: # Do some local testing - ./build.sh test-unit - ./build.sh test-integration # Create separate packages for each target environment - ./build.sh clean - ./build.sh package dev $TARGET_REGION - ./build.sh package staging $TARGET_REGION - ./build.sh package prod $TARGET_REGION # Deploy to DEV and run acceptance tests there - ./build.sh deploy dev $TARGET_REGION dist/dev - ./build.sh test-acceptance dev artifacts: files: - '**/*' secondary-artifacts: devPackage: base-directory: ./dist/dev files: - '**/*' stagingPackage: base-directory: ./dist/staging files: - '**/*' prodPackage: base-directory: ./dist/prod files: - '**/*'
Здесь есть несколько вещей:
- A
Deployer_Role_ARN
Требуется переменная среды, которая определяет ранее существовавшую роль IAM, которая имеет разрешения на выполнение этапов развертывания в целевой учетной записи. Мы рассмотрим, как настроить это позже. - Каждая команда сборки ссылается на
build.sh
файл. Причина этого косвенности состоит в том, чтобы облегчить тестирование сценариев сборки во время разработки, так как у меня нет возможности призвать файл BuildSpec локально (Это основано на подходе, который рекомендует Yan Cui в модуле CI/CD его готового к производству курса без сервера ) Я запускаю модульные и интеграционные тесты, прежде чем делать упаковку. Он выполняет функциональный код Lambda локально (в контейнере кодовой строительства), а не через AWS Lambda, хотя интеграционные тесты могут попасть в существующие нижестоящие ресурсы AWS, которые вызывает функции. - Я создаю здесь 3 артефакта развертывания, по одному для каждого этапа, и включаю их в качестве вывода артефактов, которые могут использоваться на будущих этапах. Я не был полностью доволен этим, поскольку это нарушает лучшую практику DevOps, чтобы иметь единый неизвестный поток артефакта на каждом этапе. Моя причина для этого заключалась в том, что
- SLS Package
Команда, которую использует файл build.sh, требует определенной стадии для него. Однако с тех пор я узнал, что
Есть обходной путь для этого , так что я, вероятно, скоро это изменим. Я запускаю приемные тесты (которые достигли недавно развернутых конечных точек API Gateway) в качестве последнего шага.
build.sh
Файл ниже:
#!/bin/bash set -e set -o pipefail instruction() { echo "usage: ./build.sh package" echo "" echo "/build.sh deploy " echo "" echo "/build.sh test- " } assume_role() { if [ -n "$DEPLOYER_ROLE_ARN" ]; then echo "Assuming role $DEPLOYER_ROLE_ARN ..." CREDS=$(aws sts assume-role --role-arn $DEPLOYER_ROLE_ARN \ --role-session-name my-sls-session --out json) echo $CREDS > temp_creds.json export AWS_ACCESS_KEY_ID=$(node -p "require('./temp_creds.json').Credentials.AccessKeyId") export AWS_SECRET_ACCESS_KEY=$(node -p "require('./temp_creds.json').Credentials.SecretAccessKey") export AWS_SESSION_TOKEN=$(node -p "require('./temp_creds.json').Credentials.SessionToken") aws sts get-caller-identity fi } unassume_role() { unset AWS_ACCESS_KEY_ID unset AWS_SECRET_ACCESS_KEY unset AWS_SESSION_TOKEN } if [ $# -eq 0 ]; then instruction exit 1 elif [ "$1" = "install" ] && [ $# -eq 1 ]; then npm install elif [ "$1" = "test-unit" ] && [ $# -eq 1 ]; then npm run lint npm run test elif [ "$1" = "test-integration" ] && [ $# -eq 1 ]; then echo "Running INTEGRATION tests..." npm run test-integration echo "INTEGRATION tests complete." elif [ "$1" = "clean" ] && [ $# -eq 1 ]; then rm -rf ./dist elif [ "$1" = "package" ] && [ $# -eq 3 ]; then STAGE=$2 REGION=$3 'node_modules/.bin/sls' package -s $STAGE -p "./dist/${STAGE}" -r $REGION elif [ "$1" = "deploy" ] && [ $# -eq 4 ]; then STAGE=$2 REGION=$3 ARTIFACT_FOLDER=$4 echo "Deploying from ARTIFACT_FOLDER=${ARTIFACT_FOLDER}" assume_role # 'node_modules/.bin/sls' create_domain -s $STAGE -r $REGION echo "Deploying service to stage $STAGE..." 'node_modules/.bin/sls' deploy --force -s $STAGE -r $REGION -p $ARTIFACT_FOLDER unassume_role elif [ "$1" = "test-acceptance" ] && [ $# -eq 2 ]; then STAGE=$2 echo "Running ACCEPTANCE tests for stage $STAGE..." STAGE=$STAGE npm run test-acceptance echo "ACCEPTANCE tests complete." elif [ "$1" = "clearcreds" ] && [ $# -eq 1 ]; then unassume_role rm -f ./temp_creds.json else instruction exit 1 fi
Главное, что нужно отметить в вышеуказанном сценарии, это ammer_role
функция, которая вызывается перед командой развертывания. Для того, чтобы кодовая строительство развернулась в другой учетной записи AWS, SLS развернут
Команда без серверной структуры должна работать как роль, определенная в целевой учетной записи. Чтобы сделать это, роль Codebuild IAM (которая работает в учетной записи DEV) должна взять на себя эту роль. Я сейчас делаю это, вызывая AWS STS предполагают роле
командование AWS CLI, чтобы получить временные полномочия для этой роли развертывания. Однако это кажется грязным, поэтому, если кто -то знает более чистый способ сделать это, я бы хотел услышать это.
Развертывание для постановки и производства
Сценарий для развертывания и запуска тестов против постановки аналогичен сценарию DEV, но проще:
# buildspec.staging.yml version: 0.2 env: variables: TARGET_REGION: us-east-1 SLS_DEBUG: '*' DEPLOYER_ROLE_ARN: 'arn:aws:iam:::role/ac-rest-api-staging-deployer-role' # Deploy to STAGING stage and run acceptance tests. phases: pre_build: commands: - chmod +x build.sh - ./build.sh install build: commands: - ./build.sh deploy staging $TARGET_REGION $CODEBUILD_SRC_DIR_stagingPackage - ./build.sh test-acceptance staging
Ключевые вещи, которые следует отметить, это:
- Я использую
$ Codebuild_src_dir_stagingpackage
Переменная среда для доступа к каталогу, где выводный артефакт с именемStagingPackage
От последнего шага трубопровода находится.
buildspec.prod.yml
Файл в значительной степени такой же, как и постановка, за исключением того, что он ссылается на производство роли IAM и справочник артефактов.
Разрешения на развертывание и доступ к перекрестному доступу
Это была, вероятно, самая сложная часть всего процесса. Как я упоминал выше, сценарий развертывания предполагает ранее существовавшее Deployer-Role
IAM Роль, которая существует в счете AWS о развертываемой сцене. Чтобы настроить эти роли, я снова использовал CDK для определения пользовательской конструкции CrossAccountDeploymentRole
следующим образом:
export interface CrossAccountDeploymentRoleProps { serviceName: string; /** account ID where CodePipeline/CodeBuild is hosted */ deployingAccountId: string; /** stage for which this role is being created */ targetStageName: string; /** Permissions that deployer needs to assume to deploy stack */ deployPermissions: PolicyStatement[]; } /** * Creates an IAM role to allow for cross-account deployment of a service's resources. */ export class CrossAccountDeploymentRole extends Construct { public static getRoleNameForService(serviceName: string, stage: string): string { return `${serviceName}-${stage}-deployer-role`; } public static getRoleArnForService(serviceName: string, stage: string, accountId: string): string { return `arn:aws:iam::${accountId}:role/${CrossAccountDeploymentRole.getRoleNameForService(serviceName, stage)}`; } readonly deployerRole: Role; readonly deployerPolicy: Policy; readonly roleName: string; public constructor(parent: Construct, id: string, props: CrossAccountDeploymentRoleProps) { super(parent, id); this.roleName = CrossAccountDeploymentRole.getRoleNameForService(props.serviceName, props.targetStageName); // Cross-account assume role // https://awslabs.github.io/aws-cdk/refs/_aws-cdk_aws-iam.html#configuring-an-externalid this.deployerRole = new Role(this, 'deployerRole', { roleName: this.roleName, assumedBy: new AccountPrincipal(props.deployingAccountId), }); const passrole = new PolicyStatement(PolicyStatementEffect.Allow) .addActions( 'iam:PassRole', ).addAllResources(); this.deployerPolicy = new Policy(this, 'deployerPolicy', { policyName: `${this.roleName}-policy`, statements: [passrole, ...props.deployPermissions], }); this.deployerPolicy.attachToRole(this.deployerRole); } }
реквизит
Объект, который передается конструктору, содержит конкретные требования Сервиса. Главное здесь — это Deploypermissions
который представляет собой набор операторов политики IAM, чтобы позволить пользователю запускать без серверов Framework сестренка развернута
Команда для развертывания всех необходимых ресурсов (стеки CloudFormation, функции Lambda и т. Д.).
Важно отметить, что этот набор разрешений отличается от время выполнения Разрешения вашей службы без сервера должны выполнять. Работайте, что Время развертывания IAM разрешения Потребности в обслуживании-это хорошо известная проблема в пространстве без сервера, и я еще не нашел хорошего решения. Первоначально я начал с того, что предоставил свою роль доступа к администратору, пока не получил сквозной трубопровод, а затем работал в обратном направлении, чтобы добавить больше гранулированных разрешений, но это связано с множеством проб и ошибок.
Для получения дополнительной информации о том, как IAMS работает с без серверной структуры, я бы порекомендовал прочитать ABCS IAM: управление разрешениями с без сервера Анкет
Несколько советов по строительству и тестированию собственных трубопроводов
- Всегда сначала запускайте файл build.sh локально, чтобы убедиться, что он работает на вашей машине
- Используйте кнопку «Выпуск изменения» в консоли CodePipeline, чтобы запустить новое выполнение трубопровода, не прибегая к натоплению фиктивных коммитов к GitHub.
Будущие улучшения
В будущем, вот несколько дополнений, которые я хотел бы сделать в моем процессе CICD:
- Добавить Ручной шаг одобрения Перед развертыванием в Prod.
- Добавьте автоматическое действие отката, если сбой приема на приемочную тестирование против развертывания Prod.
- Творения/обновления запросов на вывод должны вызвать более короткий тестовый трубопровод. Это поможет выявить проблемы интеграции раньше.
- Slack Integration для уведомлений по сборке.
- Обновите мой шаг пакета так, чтобы он создавал только один неизменный пакет, который будет развернут во всех средах.
- Получите дополнительную мета и получите конвейер для CICD -кода 🤯. Изменения в моем трубопроводе будут проходить тесты против них, прежде чем развернуть новый трубопровод.
Следующие шаги
Следующим в моем плане миграции является создание механизма монтировки API, который могут использовать новые маршруты. Это будет первый реальный производственный код, проверенный моим новым CICD -конвейером.
✉ Если бы вам понравилась эта статья и вы хотели бы узнать больше о Serverless, вам может понравиться Мой еженедельный информационный бюллетень на создание без серверных приложений в AWS Анкет
Вы также можете наслаждаться:
Первоначально опубликовано в Winterwindsoftware.com Анкет
Оригинал: «https://dev.to/paulswail/building-cicd-pipelines-for-serverless-microservices-using-the-aws-cdk-1o7d»