Введение

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

Архитектура

Как мы можем видеть на диаграмме выше, мы собираемся написать 2 шаблона кода, первый будет шаблоном CloudFormation и будет отвечать за базовую инфраструктуру, которая в данном случае в основном является конвейером, второй будет шаблон AWS SAM, который будет шаблоном для развертывания нашей сервисной инфраструктуры.

Руки вверх!

Во-первых, давайте создадим пару файлов и каталогов, которые будут находиться в одном месте:

Начнем с написания кода нашей функции Lambda в файле main.js в папке /src:

const AWS = require('aws-sdk')
let dynamodb = new AWS.DynamoDB.DocumentClient({ region: 'us-east-1'})


exports.handler = (event, context, callback) => {
  let params = {
      TableName: process.env.ddbTable,
      Item: {
          pk: "[email protected]",
          sk: "user",
          first_name: "Marco",
          last_name: "Mercado"
      },
      ConditionExpression: "attribute_not_exists(pk)",
      ReturnConsumedCapacity: 'TOTAL'
  }

  dynamodb.put(params, (err, data) => {
      if (err) callback(err)
      else callback(null, data)
  })
}

Функция довольно проста, мы выполняем операцию помещения в таблицу DynamoDB с помощью AWS SDK для NodeJS. Единственное, что я хочу, чтобы вы заметили, это то, что в строке 7 мы вызываем переменную среды для таблицы DynamoDB, эта переменная будет определена в шаблоне SAM.

ПРИМЕЧАНИЕ. В этом примере файл package.json используется по умолчанию. Его можно создать, просто запустив npm init -y.

Теперь давайте создадим файл template.yml, в котором мы будем использовать AWS SAM для развертывания наших бессерверных ресурсов:

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  AWS SAM Lambda function example 
  
Parameters:
  Environment:
    Type: String
    Description: Environment name

Globals:
  Function:
    Timeout: 3

Resources:
  DdbTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Retain
    Properties:
      TableName: !Sub tutorial-dynamodb-cicd-${Environment}
      AttributeDefinitions:
        - AttributeName: pk
          AttributeType: S
        - AttributeName: sk
          AttributeType: S
      KeySchema:
        - AttributeName: pk
          KeyType: HASH
        - AttributeName: sk
          KeyType: RANGE
      BillingMode: PAY_PER_REQUEST
      ProvisionedThroughput:
        ReadCapacityUnits: 0
        WriteCapacityUnits: 0

  MainFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub sam-function-example-${Environment}
      CodeUri: src/
      Handler: main.handler
      Runtime: nodejs12.x
      Environment:
        Variables:
          ddbTable: !Ref DdbTable
      Policies:
      - Statement:
        - Sid: DynamoDBPolicy
          Effect: Allow
          Action:
            - dynamodb:PutItem
          Resource:
            - !GetAtt DdbTable.Arn

Outputs:
  MainFunction:
    Description: "Main Lambda Function ARN"
    Value: !GetAtt MainFunction.Arn

Шаблон будет развертывать таблицу DynamoDB со средой в качестве суффикса для разделения ресурсов по средам и функцию Lambda с ролью IAM, которая позволяет записывать в таблицу DynamoDB.

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

Служба, в которой будет работать шаблон AWS SAM, называется CodeBuild. Зная, что нам нужно написать пошаговые инструкции по сборке и развертыванию нашего проекта с помощью AWS SAM, эти инструкции будут записаны в файле buildspec.yml в разделе папку .awscodepipeline, как следующий код:

version: 0.2

phases:
  build:
    commands:
      - sam build -t template.yml

  post_build:
    commands:
      - |
        sam deploy --no-confirm-changeset --no-fail-on-empty-changeset \
        --s3-bucket $CODEPIPELINE_BUCKET \
        --s3-prefix $STACK_NAME \
        --region $AWS_REGION \
        --stack-name $STACK_NAME \
        --capabilities CAPABILITY_IAM \
        --parameter-overrides \
        Environment=$ENVIRONMENT

Теперь у нас есть компоненты для настройки нашего конвейера автоматизации, последний шаг — написать наш шаблон CloudFormation, расположенный в файле main.yml в каталоге .cloudformation, это самая длинная часть примера, поэтому я собираюсь ее разбить. по шагам, если вы хотите посмотреть весь файл, перейдите по ссылке на репозиторий GitHub в конце этого поста.

Сначала, как и для каждого шаблона CloudFormation, мы определяем версию и пишем краткое описание шаблона:

AWSTemplateFormatVersion: '2010-09-09'

Description: 'CloudFormation template to create an automation pipeline for AWS SAM projects'

Далее определяем наши параметры:

Parameters:
  FunctionName:
    Type: String
    Description: Name of the function to deploy with the pipeline
  Environment:
    Type: String
    Description: Environment name
  GitHubRepo:
    Type: String
    Description: GitHub repository name
  GitHubUser:
    Type: String
    Description: GitHub user name
  GitHubOAuthToken:
    Type: String
    Description: GitHub auth token
 
 Resources:

Теперь мы собираемся написать, какие ресурсы будет создавать этот шаблон. Прежде всего, мы собираемся определить роли IAM, которые будут принимать CodePipeline и CodeBuild, чтобы иметь необходимые разрешения:

ПРИМЕЧАНИЕ. На этом этапе каждый блок кода будет иметь отступ в разделе Ресурсы.

Для кодбилда:

CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: "SAM"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:DescribeTable
                  - dynamodb:CreateTable
                  - dynamodb:DeleteTable
                Resource: '*'
              - Effect: Allow
                Action:
                  - cloudformation:CreateChangeSet
                  - cloudformation:DescribeChangeSet
                  - cloudformation:ExecuteChangeSet
                  - cloudformation:DescribeStackEvents
                  - cloudformation:DescribeStacks
                  - cloudformation:GetTemplateSummary
                  - cloudformation:UpdateStack
                Resource: '*'
              - Effect: Allow
                Action:
                  - lambda:AddPermission
                  - lambda:CreateFunction
                  - lambda:DeleteFunction
                  - lambda:GetFunction
                  - lambda:GetFunctionConfiguration
                  - lambda:ListTags
                  - lambda:RemovePermission
                  - lambda:TagResource
                  - lambda:UntagResource
                  - lambda:UpdateFunctionCode
                  - lambda:UpdateFunctionConfiguration
                Resource: arn:aws:lambda:*:*:function:*
              - Effect: Allow
                Action: 
                  - iam:PassRole
                Resource: "*"
                Condition:
                  StringEquals:
                    iam:PassedToService: lambda.amazonaws.com
              - Effect: Allow
                Action: 
                  - iam:CreateRole
                  - iam:DetachRolePolicy
                  - iam:PutRolePolicy
                  - iam:AttachRolePolicy
                  - iam:DeleteRole
                  - iam:GetRole
                Resource: "arn:aws:iam::*:role/*"
        - PolicyName: "logs"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - ssm:GetParameters
                Resource: '*'
        - PolicyName: "S3"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
                - Effect: Allow
                  Action:
                      - s3:GetObject
                      - s3:PutObject
                      - s3:GetObjectVersion
                  Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*

Для CodePipeline:

CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                  - !Sub arn:aws:s3:::${ArtifactBucket}
                Effect: Allow
                Action:
                  - s3:*
              - Resource: "*"
                Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
                  - iam:PassRole
              - Resource: "*"
                Effect: Allow
                Action:
                  - codecommit:CancelUplodaArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive

Далее мы собираемся создать корзину S3 для хранения наших артефактов:

  ArtifactBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete

Теперь нам нужен проект CodeBuild, помните, что CodeBuild — это вычислительный экземпляр, управляемый , который будет выполнять список шагов для сборки вашего проекта, поэтому нам нужно настроить этот экземпляр:

CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: .awscodepipeline/buildspec.yml
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:2.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: CODEPIPELINE_BUCKET
            Type: PLAINTEXT
            Value: !Ref ArtifactBucket
          - Name: STACK_NAME
            Type: PLAINTEXT
            Value: !Sub ${FunctionName}-${Environment}
          - Name: ENVIRONMENT
            Type: PLAINTEXT
            Value: !Ref Environment
      Name: !Ref AWS::StackName
      ServiceRole: !Ref CodeBuildServiceRole

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

  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub ${FunctionName}-${Environment}
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: Function
              ActionTypeId:
                Category: Source
                Owner: ThirdParty
                Version: '1'
                Provider: GitHub
              Configuration:
                Owner: !Ref GitHubUser
                Repo: !Ref GitHubRepo 
                Branch: !Ref Environment
                OAuthToken: !Ref GitHubOAuthToken
              RunOrder: 1
              OutputArtifacts:
                - Name: SourceArtifact
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: '1'
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: BuildArtifact
              RunOrder: 2

Прежде чем развертывать проект, убедитесь, что ваш код обновлен в репозитории GitHub в ветке, которая соответствует имени среды, которую вы собираетесь выбрать (т. е. staging).

Теперь мы можем развернуть наш проект с помощью следующей команды с помощью интерфейса командной строки AWS:

ПРИМЕЧАНИЕ. Важно, чтобы на этом этапе у вас был ранее установлен интерфейс командной строки AWS и токен доступа GitHub OAuth, если вы не знаете, как сгенерировать свой токен или как установить интерфейс командной строки AWS. оставлю вам пару полезных ссылок.

AWS CLI

Создать токен доступа GitHub OAuth

aws cloudformation create-stack \
--stack-name lambda-cicd-post \
--template-body file://.cloudformation/main.yml \
--parameters \
ParameterKey=FunctionName,ParameterValue=<LAMBDA_FUNCTION_NAME> \
ParameterKey=Environment,ParameterValue=<ENVIRONMENT> \
ParameterKey=GitHubUser,ParameterValue=<GITHUB_USER> \
ParameterKey=GitHubRepo,ParameterValue=<REPO_NAME> \
ParameterKey=GitHubOAuthToken,ParameterValue=<GITHUB_OAUTH_TOKEN> \
--region us-east-1 \
--capabilities CAPABILITY_IAM

Должно произойти следующее:

Стек должен успешно завершиться в CloudFormation:

Следует создать конвейер CodePipeline, который немедленно запускает свой первый запуск:

Этот первый запуск должен создать следующий стек:

И теперь вы сможете проверить свою функцию:

И это все для этого урока, помните, что весь код будет здесь, увидимся в следующий раз!