Въведение в AWS Serverless Application Model

1. Общ преглед

В предишната ни статия вече внедрихме пълно стеково безсървърно приложение на AWS, използвайки API шлюз за REST крайни точки, AWS Lambda за бизнес логика, както и DynamoDB като база данни.

Разгръщането обаче се състои от много ръчни стъпки, които могат да станат неприятни с нарастващата сложност и с броя на средите.

В този урок сега ще обсъдим как да използваме AWS Serverless Application Model (SAM), който дава възможност за базирано на шаблони описание и автоматично внедряване на безсървърни приложения на AWS .

По-подробно ще разгледаме следните теми:

  • Основи на безсървърния модел на приложение (SAM), както и на основната CloudFormation
  • Определение на безсървърно приложение, използващо синтаксиса на SAM шаблона
  • Автоматично внедряване на приложението с помощта на CLI на CloudFormation

2. Основни положения

Както беше обсъдено по-рано, AWS ни позволява да внедрим изцяло безсървърни приложения, като използваме API Gateway, Lambda функции и DynamoDB. Несъмнено това предлага вече много предимства за производителност, цена и мащабируемост.

Недостатъкът обаче е, че в момента се нуждаем от много ръчни стъпки в AWS Console, като създаване на всяка функция, качване на код, създаване на таблица DynamoDB, създаване на IAM роли, създаване на API и API структура и т.н.

За сложни приложения и с множество среди като тестване, постановка и производство, това усилие се умножава бързо.

Тук влиза в игра CloudFormation за приложения на AWS като цяло и Безсървърния модел на приложения (SAM), специално за безсървърни приложения.

2.1. AWS CloudFormation

CloudFormation е услуга на AWS за автоматично предоставяне на инфраструктурни ресурси на AWS. Потребителят дефинира всички необходими ресурси в план (наречен шаблон), а AWS се грижи за осигуряването и конфигурирането.

Следните термини и понятия са от съществено значение за разбирането на CloudFormation и SAM:

Шаблонът е описание на приложение , как трябва да бъде структурирано по време на изпълнение. Можем да определим набор от необходими ресурси, както и как тези ресурси трябва да бъдат конфигурирани. CloudFormation предоставя общ език за дефиниране на шаблони, поддържащ JSON и YAML като формат.

Ресурсите са градивните елементи в CloudFormation. Ресурсът може да бъде всичко, като RestApi, етап на RestApi, групово задание, таблица DynamoDB, екземпляр EC2, мрежов интерфейс, роля на IAM и много други. Понастоящем в официалната документация са изброени около 300 вида ресурси за CloudFormation.

Стекът е екземпляр на шаблон. CloudFormation се грижи за осигуряването и конфигурирането на стека.

2.2. Модел на приложение без сървър (SAM)

Както често, използването на мощни инструменти може да стане много сложно и нестандартно, какъвто е случаят и с CloudFormation.

Ето защо Amazon представи Безсървърния модел на приложение (SAM). SAM започна с претенцията да предостави чист и ясен синтаксис за дефиниране на безсървърни приложения. В момента той има само три типа ресурси, които са Lambda функции, таблици DynamoDB и API .

SAM се основава на синтаксиса на шаблона CloudFormation, така че можем да дефинираме нашия шаблон, използвайки простия синтаксис на SAM, и CloudFormation ще обработи допълнително този шаблон.

Повече подробности можете да намерите в официалното хранилище на GitHub, както и в документацията на AWS.

3. Предпоставки

За следващия урок ще ни трябва акаунт в AWS. Свободната сметка на ниво трябва да е достатъчна.

Освен това трябва да имаме инсталиран AWS CLI.

И накрая, имаме нужда от S3 Bucket в нашия регион, който може да бъде създаден чрез AWS CLI със следната команда:

$>aws s3 mb s3://baeldung-sam-bucket

Докато урокът използва baeldung-sam-bucket по-долу, имайте предвид, че имената на сегменти трябва да са уникални, така че трябва да изберете името си.

Като демо приложение ще използваме кода от Използване на AWS Lambda с API шлюз.

4. Създаване на шаблон

В този раздел ще създадем нашия шаблон SAM.

Първо ще разгледаме цялостната структура, преди да дефинираме отделните ресурси.

4.1. Структура на шаблона

Първо, нека да разгледаме цялостната структура на нашия шаблон:

AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: Baeldung Serverless Application Model example Resources: PersonTable: Type: AWS::Serverless::SimpleTable Properties: # Define table properties here StorePersonFunction: Type: AWS::Serverless::Function Properties: # Define function properties here GetPersonByHTTPParamFunction: Type: AWS::Serverless::Function Properties: # Define function properties here MyApi: Type: AWS::Serverless::Api Properties: # Define API properties here

Както виждаме, шаблонът се състои от заглавка и тяло:

Заглавката посочва версията на шаблона CloudFormation ( AWSTemplateFormatVersion ), както и версията на нашия шаблон SAM ( Transform ). Можем също да посочим Описание .

Тялото се състои от набор от ресурси: всеки ресурс има име, ресурс тип , както и набор от имоти .

Понастоящем спецификацията SAM поддържа три типа: AWS :: Без сървър :: Api , AWS :: Без сървър :: Функция, както и AWS :: Без сървър :: SimpleTable .

Тъй като искаме да разгърнем нашето примерно приложение, трябва да дефинираме една SimpleTable , две функции , както и един Api в нашето тяло на шаблона.

4.2. Определение на таблицата DynamoDB

Нека дефинираме нашата таблица DynamoDB сега:

AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: Baeldung Serverless Application Model example Resources: PersonTable: Type: AWS::Serverless::SimpleTable Properties: PrimaryKey: Name: id Type: Number TableName: Person

We only need to define two properties for our SimpleTable: the table name, as well as a primary key, which is called id and has the type Number in our case.

A full list of supported SimpleTable properties can be found in the official specification.

Note: As we only want to access the table using the primary key, the AWS::Serverless::SimpleTable is sufficient for us. For more complex requirements, the native CloudFormation type AWS::DynamoDB::Table can be used instead.

4.3. Definition of the Lambda Functions

Next, let's define our two functions:

AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: Baeldung Serverless Application Model example Resources: StorePersonFunction: Type: AWS::Serverless::Function Properties: Handler: com.baeldung.lambda.apigateway.APIDemoHandler::handleRequest Runtime: java8 Timeout: 15 MemorySize: 512 CodeUri: ../target/aws-lambda-0.1.0-SNAPSHOT.jar Policies: DynamoDBCrudPolicy Environment: Variables: TABLE_NAME: !Ref PersonTable Events: StoreApi: Type: Api Properties: Path: /persons Method: PUT RestApiId: Ref: MyApi GetPersonByHTTPParamFunction: Type: AWS::Serverless::Function Properties: Handler: com.baeldung.lambda.apigateway.APIDemoHandler::handleGetByParam Runtime: java8 Timeout: 15 MemorySize: 512 CodeUri: ../target/aws-lambda-0.1.0-SNAPSHOT.jar Policies: DynamoDBReadPolicy Environment: Variables: TABLE_NAME: !Ref PersonTable Events: GetByPathApi: Type: Api Properties: Path: /persons/{id} Method: GET RestApiId: Ref: MyApi GetByQueryApi: Type: Api Properties: Path: /persons Method: GET RestApiId: Ref: MyApi

As we can see, each function has the same properties:

Handler defines the logic of the function. As we are using Java, it is the class name including the package, in connection with the method name.

Runtime defines how the function was implemented, which is Java 8 in our case.

Timeout defines how long the execution of the code may take at most before AWS terminates the execution.

MemorySizedefines the size of the assigned memory in MB. It's important to know, that AWS assigns CPU resources proportionally to MemorySize. So in the case of a CPU-intensive function, it might be required to increase MemorySize, even if the function doesn't need that much memory.

CodeUridefines the location of the function code. It currently references the target folder in our local workspace. When we upload our function later using CloudFormation, we'll get an updated file with a reference to an S3 object.

Policiescan hold a set of AWS-managed IAM policies or SAM-specific policy templates. We use the SAM-specific policies DynamoDBCrudPolicy for the StorePersonFunction and DynamoDBReadPolicy for GetPersonByPathParamFunction and GetPersonByQueryParamFunction.

Environmentdefines environment properties at runtime. We use an environment variable for holding the name of our DynamoDB table.

Eventscan hold a set of AWS events, which shall be able to trigger the function. In our case, we define an Event of type Api. The unique combination of path, an HTTP Method, and a RestApiId links the function to a method of our API, which we'll define in the next section.

A full list of supported Function properties can be found in the official specification.

4.4. API Definition as Swagger File

After defining DynamoDB table and functions, we can now define the API.

The first possibility is to define our API inline using the Swagger format:

AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: Baeldung Serverless Application Model example Resources: MyApi: Type: AWS::Serverless::Api Properties: StageName: test EndpointConfiguration: REGIONAL DefinitionBody: swagger: "2.0" info: title: "TestAPI" paths: /persons: get: parameters: - name: "id" in: "query" required: true type: "string" x-amazon-apigateway-request-validator: "Validate query string parameters and\ \ headers" x-amazon-apigateway-integration: uri: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetPersonByHTTPParamFunction.Arn}/invocations responses: {} httpMethod: "POST" type: "aws_proxy" put: x-amazon-apigateway-integration: uri: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${StorePersonFunction.Arn}/invocations responses: {} httpMethod: "POST" type: "aws_proxy" /persons/{id}: get: parameters: - name: "id" in: "path" required: true type: "string" responses: {} x-amazon-apigateway-integration: uri: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetPersonByHTTPParamFunction.Arn}/invocations responses: {} httpMethod: "POST" type: "aws_proxy" x-amazon-apigateway-request-validators: Validate query string parameters and headers: validateRequestParameters: true validateRequestBody: false

Our Api has three properties: StageNamedefines the stage of the API, EndpointConfigurationdefines whether the API is regional or edge-optimized, and DefinitionBody contains the actual structure of the API.

In the DefinitionBody, we define three parameters: the swagger version as “2.0”, the info:title: as “TestAPI”, as well as a set of paths.

As we can see, the paths represent the API structure, which we had to define manually before. The paths in Swagger are equivalent to the resources in the AWS Console. Just like that, each path can have one or more HTTP verbs, which are equivalent to the methods in the AWS Console.

Each method can have one or more parameters as well as a request validator.

The most exciting part is the attribute x-amazon-apigateway-integration, which is an AWS-specific extension to Swagger:

uri specifies which Lambda function shall be invoked.

responses specify rules how to transform the responses returned by the function. As we are using Lambda Proxy Integration, we don't need any specific rule.

type defines that we want to use Lambda Proxy Integration, and thereby we have to set httpMethod to “POST”, as this is what Lambda functions expect.

A full list of supported Api properties can be found in the official specification.

4.5. Implicit API Definition

A second option is to define the API implicitly within the Function resources:

AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: Baeldung Serverless Application Model Example with Implicit API Definition Globals: Api: EndpointConfiguration: REGIONAL Name: "TestAPI" Resources: StorePersonFunction: Type: AWS::Serverless::Function Properties: Handler: com.baeldung.lambda.apigateway.APIDemoHandler::handleRequest Runtime: java8 Timeout: 15 MemorySize: 512 CodeUri: ../target/aws-lambda-0.1.0-SNAPSHOT.jar Policies: - DynamoDBCrudPolicy: TableName: !Ref PersonTable Environment: Variables: TABLE_NAME: !Ref PersonTable Events: StoreApi: Type: Api Properties: Path: /persons Method: PUT GetPersonByHTTPParamFunction: Type: AWS::Serverless::Function Properties: Handler: com.baeldung.lambda.apigateway.APIDemoHandler::handleGetByParam Runtime: java8 Timeout: 15 MemorySize: 512 CodeUri: ../target/aws-lambda-0.1.0-SNAPSHOT.jar Policies: - DynamoDBReadPolicy: TableName: !Ref PersonTable Environment: Variables: TABLE_NAME: !Ref PersonTable Events: GetByPathApi: Type: Api Properties: Path: /persons/{id} Method: GET GetByQueryApi: Type: Api Properties: Path: /persons Method: GET

As we can see, our template is slightly different now: There is no AWS::Serverless::Api resource anymore.

However, CloudFormation takes the Events attributes of type Api as an implicit definition and creates an API anyway. As soon as we test our application, we'll see that it behaves the same as when defining the API explicitly using Swagger.

Besides, there is a Globals section, where we can define the name of our API, as well as that our endpoint shall be regional.

Only one limitation occurs: when defining the API implicitly, we are not able to set a stage name. This is why AWS will create a stage called Prod in any case.

5. Deployment and Test

After creating the template, we can now proceed with deployment and testing.

For this, we'll upload our function code to S3 before triggering the actual deployment.

In the end, we can test our application using any HTTP client.

5.1. Code Upload to S3

In a first step, we have to upload the function code to S3.

We can do that by calling CloudFormation via the AWS CLI:

$> aws cloudformation package --template-file ./sam-templates/template.yml --s3-bucket baeldung-sam-bucket --output-template-file ./sam-templates/packaged-template.yml

With this command, we trigger CloudFormation to take the function code specified in CodeUri: and to upload it to S3. CloudFormation will create a packaged-template.yml file, which has the same content, except that CodeUri: now points to the S3 object.

Let's take a look at the CLI output:

Uploading to 4b445c195c24d05d8a9eee4cd07f34d0 92702076 / 92702076.0 (100.00%) Successfully packaged artifacts and wrote output template to file packaged-template.yml. Execute the following command to deploy the packaged template aws cloudformation deploy --template-file c:\zz_workspace\tutorials\aws-lambda\sam-templates\packaged-template.yml --stack-name 

5.2. Deployment

Now, we can trigger the actual deployment:

$> aws cloudformation deploy --template-file ./sam-templates/packaged-template.yml --stack-name baeldung-sam-stack  --capabilities CAPABILITY_IAM

As our stack also needs IAM roles (like the functions' roles for accessing our DynamoDB table), we must explicitly acknowledge that by specifying the –capabilities parameter.

And the CLI output should look like:

Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - baeldung-sam-stack

5.3. Deployment Review

After the deployment, we can review the result:

$> aws cloudformation describe-stack-resources --stack-name baeldung-sam-stack

CloudFormation will list all resources, which are part of our stack.

5.4. Test

Finally, we can test our application using any HTTP client.

Let's see some sample cURL commands we can use for these tests.

StorePersonFunction:

$> curl -X PUT '//0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons' \   -H 'content-type: application/json' \   -d '{"id": 1, "name": "John Doe"}'

GetPersonByPathParamFunction:

$> curl -X GET '//0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons/1' \   -H 'content-type: application/json'

GetPersonByQueryParamFunction:

$> curl -X GET '//0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons?id=1' \   -H 'content-type: application/json'

5.5. Clean Up

In the end, we can clean up by removing the stack and all included resources:

aws cloudformation delete-stack --stack-name baeldung-sam-stack

6. Conclusion

In this article, we had a look at the AWS Serverless Application Model (SAM), which enables a template-based description and automated deployment of serverless applications on AWS.

In detail, we discussed the following topics:

  • Основи на безсървърния модел на приложение (SAM), както и на основната CloudFormation
  • Определение на безсървърно приложение, използващо синтаксиса на SAM шаблона
  • Автоматично внедряване на приложението с помощта на CLI на CloudFormation

Както обикновено, целият код за тази статия е достъпен в GitHub.