Блог Amazon Web Services

Как тестировать AWS Step Functions и создавать CI/CD pipelines для них

Оригинал статьи: ссылка (Matt Noyce, Cloud Application Architect)

Сервис AWS Step Functions позволяет пользователям легко создавать высокодоступные, интуитивно понятные serverless рабочие процессы (workflows). Step Functions легко интегрируется с остальными сервисами AWS, включая, AWS Lambda, AWS Batch, AWS Fargate, Amazon SageMaker, и многими другими. Он предлагает возможность добавить встроенные повторы вызовов и обработку ошибок, сложные условные ветвления, и все это – с помощью простого в использовании JSON-ориентированного языка под названием Amazon States Language.

AWS CodePipeline – это полностью управляемая система непрерывной доставки (Continuous Delivery), которая позволяет легко и быстро настраивать автоматизацию процесса создания новых версий программного обеспечения. CodePipeline позволяет пользователям надежно и воспроизводимо строить, тестировать и развертывать свои наиболее важные приложения и инфраструктуру.

AWS CodeCommit – это полностью управляемый и безопасный сервис репозиториев исходного кода. Он устраняет необходимость в поддержании и масштабировании инфраструктуры для работы критически важных систем репозиториев кода.

Этот пост в блоге демонстрирует, как создать CI/CD pipeline для комплексного тестирования конечного автомата AWS Step Function с помощью CodeCommit, AWS CodeBuild, CodePipeline и Python.

Этапы CI/CD pipeline

Pipeline состоит из следующих этапов (как показано на диаграмме)

CI/CD for Step Functions

  1. Скачать исходный код из репозитория.
  2. Проверить все конфигурационные файлы.
  3. Запустить модульные тесты всех функций AWS Lambda из репозитория.
  4. Развернуть pipeline тестирования.
  5. Запустить end-to-end тесты в pipeline тестирования.
  6. Очистить тестовый конечный автомат и тестовую инфраструктуру.
  7. Отправить запрос на утверждение релиза.
  8. Развернуть новую версию в Production.

Необходимые условия

Чтобы начать делать этот CI/CD pipeline, необходимо выполнить несколько предварительных условий:

  1. Создать или использовать существующий аккаунт AWS (инструкции по созданию аккаунта можно найти здесь).
  2. Определить конечный автомат самостоятельно, или использовать пример определения состояний AWS Step Function (см. ниже).
  3. Написать соответствующие модульные тесты для своих Lambda функций.
  4. Определить end-to-end тесты, которые будут выполняться на конечном автомате AWS Step Function.

Проект CodePipeline

На следующем изображении показано, как выглядит проект CodePipeline. Виден набор этапов для безопасного и надежного развертывания конечного автомата AWS Step Function в Production.

CodePipeline project

Создание репозитория CodeCommit

Зайдите в AWS-консоль для создания нового репозитория CodeCommit для вашего конечного автомата.

Create repository

В данном примере репозиторий называется CalculationStateMachine – он содержит определение конечного автомата, тесты на Python и конфигурацию CodeBuild.

Repository structure

Структура репозитория

В репозитории CodeCommit у нас есть следующая структура папок:

  1. config – здесь будут жить все файлы Buildspec для наших заданий AWS CodeBuild.
  2. lambdas – здесь будут храниться все наши функции AWS Lambda.
  3. tests – это папка верхнего уровня для модульных и end-to-end тестов. Она содержит две подпапки (unit и e2e).
  4. cloudformation – сюда мы добавляем любые дополнительные шаблоны CloudFormation.

Определение конечного автомата

Внутри репозитория CodeCommit создайте файл State Machine Definition под названием sm_def.json. Это файл содержит определение конечного автомата, написанное на языке Amazon States Language.

В этом примере создается конечный автомат, который вызывает коллекцию Lambda функций для выполнения вычислений по заданным входным значениям. Обратите внимание, что он также выполняет проверку определенного значения и, используя состояние Choice, либо продолжает работу, либо заканчивает ее.

sm_def.json file:

{
  "Comment": "CalulationStateMachine",
  "StartAt": "CleanInput",
  "States": {
    "CleanInput": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "CleanInput",
        "Payload": {
          "input.$": "$"
        }
      },
      "Next": "Multiply"
    },
    "Multiply": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "Multiply",
        "Payload": {
          "input.$": "$.Payload"
        }
      },
      "Next": "Choice"
    },
    "Choice": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.Payload.result",
          "NumericGreaterThanEquals": 20,
          "Next": "Subtract"
        }
      ],
      "Default": "Notify"
    },
    "Subtract": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "Subtract",
        "Payload": {
          "input.$": "$.Payload"
        }
      },
      "Next": "Add"
    },
    "Notify": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "TopicArn": "arn:aws:sns:us-east-1:657860672583:CalculateNotify",
        "Message.$": "$$",
        "Subject": "Failed Test"
      },
      "End": true
    },
    "Add": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "Add",
        "Payload": {
          "input.$": "$.Payload"
        }
      },
      "Next": "Divide"
    },
    "Divide": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "Divide",
        "Payload": {
          "input.$": "$.Payload"
        }
      },
      "End": true
    }
  }
}

Этот файл позволит получить следующий автомат AWS Step Function после завершения pipeline:

Step Function state machine

Файлы спецификаций CodeBuild

В CI/CD pipeline используется коллекция BuildSpec файлов CodeBuild, связанных между собой через CodePipeline. В следующих разделах показано, как выглядят эти BuildSpec файлы и как их можно использовать для объединения в цепочку и построения полноценного CI/CD pipeline.

Проверка определений Amazon States Language

Для того, чтобы проверить определение конечного автомата на языке Amazon States Language, включите этап проверки в конфигурацию вашего CodePipeline. Пример такого этапа приведен ниже. Он использует Ruby Gem statelint:

lint_buildspec.yaml file:

version: 0.2
env:
  git-credential-helper: yes
phases:
  install:
    runtime-versions:
      ruby: 2.6
    commands:
      - yum -y install rubygems
      - gem install statelint

  build:
    commands:
      - statelint sm_def.json

Если ваша конфигурация верна, вы не увидите никаких выходных сообщений. Если конфигурация содержит ошибки, вы получаете сообщение об этом, и pipeline завершается.

Модульное тестирование Lambda функций

Для того, чтобы проверить код Lambda функции, необходимо оценить, пройдет ли он набор тестов. Вы можете протестировать каждую Lambda функцию, развернутую и используемую внутри конечного автомата, отдельно. Нужно подавать различные входные значения в Lambda функцию и проверять, что выходные значения такие, как вы ожидаете. В этом случае мы используем Python библиотеку pytest для запуска тестов и проверки результатов.

unit_test_buildspec.yaml file:

version: 0.2
env:
  git-credential-helper: yes
phases:
  install:
    runtime-versions:
      python: 3.8
    commands:
      - pip3 install -r tests/requirements.txt

  build:
    commands:
      - pytest -s -vvv tests/unit/ --junitxml=reports/unit.xml

reports:
  StateMachineUnitTestReports:
    files:
      - "**/*"
    base-directory: "reports"

Обратите внимание, что в репозитории CodeCommit есть каталог под названием tests/unit, который включает в себя коллекцию модульных тестов, которые запускаются и проверяют код ваших Lambda функций. Другой очень важной частью этого BuildSpec файла является раздел reports, который генерирует отчеты и метрики о результатах и трендах отдельных тестов, а также об общем статусе тестирования.

Отчеты о тестировании в CodeBuild

После запуска модульных тестов можно просмотреть отчеты о результатах запуска. Обратите внимание на раздел отчётов файла BuildSpec, а также на параметр junitxml=reports/unit.xml, добавленный к команде pytest. Эта команда генерирует набор отчетов, которые можно визуализировать в CodeBuild.

Перейдите к конкретному проекту CodeBuild, который вы хотите просмотреть, и нажмите на интересующий вас запуск. Там есть вкладка Reports, как видно на следующей иллюстрации:

Test reports

Выберите интересующий вас отчет, чтобы увидеть разбивку по выполненным тестам, как показано ниже:

Report group summary

С помощью функции Report Groups вы также можете просмотреть сводный список всех когда-либо запускавшихся тестов. Этот сводный отчет включает в себя различные метрики, такие как среднее количество запущенных тестов, их средняя продолжительность и общая скорость тестирования, как показано на следующем рисунке:

Report groups - trends

Этап создания шаблона AWS CloudFormation

Следующий файл BuildSpec используется для создания шаблона AWS CloudFormation, который содержит State Machine Definition:

template_sm_buildspec.yaml file:

version: 0.2
env:
  git-credential-helper: yes
phases:
  install:
    runtime-versions:
      python: 3.8

  build:
    commands:
      - python template_statemachine_cf.py

Использован скрипт Python, который читает файл sm_def.json в вашем репозитории, и генерирует шаблон AWS CloudFormation, добавляя в него определение конечного автомата:

template_statemachine_cf.py file:

import sys
import json

def read_sm_def (
    sm_def_file: str
) -> dict:
    """
    Reads state machine definition from a file and returns it as a dictionary.

    Parameters:
        sm_def_file (str) = the name of the state machine definition file.

    Returns:
        sm_def_dict (dict) = the state machine definition as a dictionary.
    """

    try:
        with open(f"{sm_def_file}", "r") as f:
            return f.read()
    except IOError as e:
        print("Path does not exist!")
        print(e)
        sys.exit(1)

def template_state_machine(
    sm_def: dict
) -> dict:
    """
    Templates out the CloudFormation for creating a state machine.

    Parameters:
        sm_def (dict) = a dictionary definition of the aws states language state machine.

    Returns:
        templated_cf (dict) = a dictionary definition of the state machine.
    """
    
    templated_cf = {
        "AWSTemplateFormatVersion": "2010-09-09",
        "Description": "Creates the Step Function State Machine and associated IAM roles and policies",
        "Parameters": {
            "StateMachineName": {
                "Description": "The name of the State Machine",
                "Type": "String"
            }
        },
        "Resources": {
            "StateMachineLambdaRole": {
                "Type": "AWS::IAM::Role",
                "Properties": {
                    "AssumeRolePolicyDocument": {
                        "Version": "2012-10-17",
                        "Statement": [
                            {
                                "Effect": "Allow",
                                "Principal": {
                                    "Service": "states.amazonaws.com"
                                },
                                "Action": "sts:AssumeRole"
                            }
                        ]
                    },
                    "Policies": [
                        {
                            "PolicyName": {
                                "Fn::Sub": "States-Lambda-Execution-${AWS::StackName}-Policy"
                            },
                            "PolicyDocument": {
                                "Version": "2012-10-17",
                                "Statement": [
                                    {
                                        "Effect": "Allow",
                                        "Action": [
                                            "logs:CreateLogStream",
                                            "logs:CreateLogGroup",
                                            "logs:PutLogEvents",
                                            "sns:*"             
                                        ],
                                        "Resource": "*"
                                    },
                                    {
                                        "Effect": "Allow",
                                        "Action": [
                                            "lambda:InvokeFunction"
                                        ],
                                        "Resource": "*"
                                    }
                                ]
                            }
                        }
                    ]
                }
            },
            "StateMachine": {
                "Type": "AWS::StepFunctions::StateMachine",
                "Properties": {
                    "DefinitionString": sm_def,
                    "RoleArn": {
                        "Fn::GetAtt": [
                            "StateMachineLambdaRole",
                            "Arn"
                        ]
                    },
                    "StateMachineName": {
                        "Ref": "StateMachineName"
                    }
                }
            }
        }
    }

    return templated_cf


sm_def_dict = read_sm_def(
    sm_def_file='sm_def.json'
)

print(sm_def_dict)

cfm_sm_def = template_state_machine(
    sm_def=sm_def_dict
)

with open("sm_cfm.json", "w") as f:
    f.write(json.dumps(cfm_sm_def))

Развертывание тестовой среды

Для того, чтобы проверить работоспособность всего конечного автомата, нужно его запустить в тестовой среде. Эта среда является точной копией того, что вы будете использовать в Production. Среда, полностью отделенная от текущей Production среды, разворачивается из шаблона AWS CloudFormation после прохождения соответствующих end-to-end тестов и подтверждений. Для этого можно использовать функцию AWS CloudFormation target из CodePipeline. Обратите внимание на конфигурацию на следующем скриншоте, который показывает, как настроить этот шаг в консоли AWS:

"Deploy test pipeline" action in CodePipeline

End-to-end тестирование

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

e2e_tests_buildspec.yaml file:

version: 0.2
env:
  git-credential-helper: yes
phases:
  install:
    runtime-versions:
      python: 3.8
    commands:
      - pip3 install -r tests/requirements.txt

  build:
    commands:
      - pytest -s -vvv tests/e2e/ --junitxml=reports/e2e.xml

reports:
  StateMachineReports:
    files:
      - "**/*"
    base-directory: "reports"

Подтверждение вручную (уведомление через сервис Amazon SNS)

Для того чтобы продолжить работу нашего CI/CD pipeline, необходимо пройти этап официального подтверждения, прежде чем применять изменения в Production. Используя этап Manual Approval в AWS CodePipeline, можно остановить pipeline перед запуском в Prod и отправить сообщение через Amazon SNS. На одну тему (topic) SNS могут быть подписаны различные клиенты, но в нашем случае нужно подписать на нее электронный адрес сотрудника, который может подтвердить релиз. На этот адрес будет отправлено уведомление с запросом подтверждения. Как только сотрудник одобрит запуск в Production, pipeline начнет разворачивать новую Production версию конечного автомата Step Function.

Этап подтверждения вручную может быть сконфигурирован в консоли AWS с использованием настроек, аналогичных описанным ниже:

Mauanl Approval step

Развертывание в Production

После того, как этапы проверки, модульного и end-to-end тестирования, ручного подтверждения прошли, можно перейти к запуску конечного автомата Step Function в Production. Этот этап похож на этап развертывания тестовой среды, за исключением того, что название стека AWS CloudFormation отличается. В этом случае мы также используем AWS CloudFormation target для CodeDeploy:

Deploy to Production

После успешного завершения этого этапа весь pipeline будет завершен.

Удаление тестовой среды

После подтверждения того, что тестовая машина состояния и Lambda функции работают, включите запуск CloudFormation, который уберет существующую тестовую среду (так как она больше не нужна). Это можно настроить как новый этап CodePipeline, аналогичный приведенной ниже конфигурации:

"Destroy Test Pipeline" step

Заключение

Таким образом мы создали и проверили свое определение конечного автомата на языке Amazon States Language, протестировали код Lambda функций, развернули тестовую среду с конечным автоматом, запустили end-to-end тесты, получили ручное подтверждение для продолжения развертывания, и развернули в Production. Использование такого подхода дает вам и вашей команде уверенность в том, что любые изменения конечного автомата и кода Lambda функций будут правильно работать в Production.