Рубрики
Uncategorized

Создание конвейеров CICD для без серверных микросервисов с использованием AWS CDK

Как использовать AWS CDK для создания трубопроводов CodePipeline/Codebuild для служб, построенных в без серверов. Часть 5 серий подробно описывает решения, которые я принимаю на этом пути, при переносе монолитного производственного приложения в контейнер в Servers на AWS. Теги с сервером, AWS, CodePipeline, DevOps.

Это часть 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 маршрут). Этот трубопровод будет работать следующим образом:

  1. Действие источника GitHub Триггеры всякий раз, когда код выдвигается в главную ветвь репо хостинг My AC-REST-API оказание услуг.
  2. Запустите Lint + Unit/Integration Tests и пакет развертывания построения (с помощью пакета SLS
  3. Deploy Package (используя sls развернуть ) на этап разработчика и запустить приемочные тесты против него.
  4. Развернуть пакет на стадию постановки и запустить приемочные тесты против него.
  5. Развернуть пакет на этап PROD и запустить приемные тесты против него.

Вот как выглядит мой завершенный трубопровод:

Управление трубопроводами в качестве инфраструктуры как код

Как и в большинстве услуг AWS, легко найти учебные пособия о том, как создать трубопровод CodePipeline с использованием консоли AWS, но трудно найти подробные примеры инфраструктуры как код. В этом отношении ярмарки кодовой строительства лучше, так как документы, как правило, рекомендуют использовать buildspec.yml Файл в вашем репо, чтобы указать команды сборки.

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

Я рассмотрел эти варианты определения моих трубопроводов:

  1. Используйте необработанные файлы yaml.
  2. Используйте CloudFormation внутри раздела ресурсов Serverless.yml.
  3. Используйте новый 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. .yml ). Я хочу свести к минимуму количество логики в самом труде и сохранить ее единоличной ответственности за оркестровку. Вся логика будет жить в сценариях сборки.

Строительство и развертывание на стадии разработки

Как только толчок происходит к главной ветви в 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»