Introduzione al modello di applicazione serverless di AWS

1. Panoramica

Nel nostro articolo precedente, abbiamo già implementato un'applicazione serverless a stack completo su AWS, utilizzando API Gateway per gli endpoint REST, AWS Lambda per la logica aziendale e DynamoDB come database.

Tuttavia, la distribuzione consiste in molti passaggi manuali, che potrebbero non essere utili con la crescente complessità e con il numero di ambienti.

In questo tutorial ora, discuteremo come utilizzare AWS Serverless Application Model (SAM), che consente una descrizione basata su modello e la distribuzione automatizzata di applicazioni serverless su AWS .

In dettaglio, daremo uno sguardo ai seguenti argomenti:

  • Nozioni di base sul Serverless Application Model (SAM) e sul CloudFormation sottostante
  • Definizione di un'applicazione serverless, utilizzando la sintassi del modello SAM
  • Distribuzione automatizzata dell'applicazione, utilizzando la CLI di CloudFormation

2. Nozioni di base

Come discusso in precedenza, AWS ci consente di implementare applicazioni completamente serverless, utilizzando API Gateway, funzioni Lambda e DynamoDB. Indubbiamente, questo offre già molti vantaggi in termini di prestazioni, costi e scalabilità.

Tuttavia, lo svantaggio è che al momento abbiamo bisogno di molti passaggi manuali nella Console AWS, come la creazione di ciascuna funzione, il caricamento del codice, la creazione della tabella DynamoDB, la creazione di ruoli IAM, la creazione di API e struttura API, ecc.

Per applicazioni complesse e con più ambienti come test, staging e produzione, questo impegno si moltiplica rapidamente.

È qui che entrano in gioco CloudFormation per le applicazioni su AWS in generale e Serverless Application Model (SAM) in particolare per le applicazioni serverless.

2.1. AWS CloudFormation

CloudFormation è un servizio AWS per il provisioning automatico delle risorse dell'infrastruttura AWS. Un utente definisce tutte le risorse richieste in un progetto (chiamato modello) e AWS si occupa del provisioning e della configurazione.

I seguenti termini e concetti sono essenziali per comprendere CloudFormation e SAM:

Un modello è una descrizione di un'applicazione , come dovrebbe essere strutturata in fase di esecuzione. Possiamo definire un insieme di risorse richieste e come queste devono essere configurate. CloudFormation fornisce un linguaggio comune per la definizione dei modelli, supportando JSON e YAML come formato.

Le risorse sono gli elementi costitutivi di CloudFormation. Una risorsa può essere qualsiasi cosa, come un RestApi, una fase di un RestApi, un lavoro batch, una tabella DynamoDB, un'istanza EC2, un'interfaccia di rete, un ruolo IAM e molti altri. La documentazione ufficiale attualmente elenca circa 300 tipi di risorse per CloudFormation.

Uno stack è l'istanza di un modello. CloudFormation si occupa del provisioning e della configurazione dello stack.

2.2. Modello di applicazione serverless (SAM)

Come spesso accade, l'uso di strumenti potenti può diventare molto complesso e poco pratico, come è anche il caso di CloudFormation.

Questo è il motivo per cui Amazon ha introdotto il Serverless Application Model (SAM). SAM ha iniziato con l'affermazione di fornire una sintassi chiara e semplice per la definizione delle applicazioni serverless. Attualmente, ha solo tre tipi di risorse, che sono funzioni Lambda, tabelle DynamoDB e API .

SAM si basa sulla sintassi del modello CloudFormation, quindi possiamo definire il nostro modello utilizzando la semplice sintassi SAM e CloudFormation elaborerà ulteriormente quel modello.

Maggiori dettagli sono disponibili nel repository ufficiale di GitHub e nella documentazione di AWS.

3. Prerequisiti

Per il seguente tutorial, avremo bisogno di un account AWS. Un account di livello gratuito dovrebbe essere sufficiente.

Oltre a ciò, abbiamo bisogno di installare l'AWS CLI.

Infine, abbiamo bisogno di un bucket S3 nella nostra regione, che può essere creato tramite l'AWS CLI con il seguente comando:

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

Sebbene il tutorial utilizzi baeldung-sam-bucket di seguito, tieni presente che i nomi dei bucket devono essere univoci, quindi devi scegliere il tuo nome.

Come applicazione demo, useremo il codice da Using AWS Lambda with API Gateway.

4. Creazione del modello

In questa sezione, creeremo il nostro modello SAM.

Daremo prima uno sguardo alla struttura complessiva, prima di definire le singole risorse.

4.1. Struttura del modello

Innanzitutto, diamo un'occhiata alla struttura generale del nostro modello:

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

Come possiamo vedere, il modello è costituito da un'intestazione e un corpo:

L'intestazione specifica la versione del modello CloudFormation ( AWSTemplateFormatVersion ) e la versione del nostro modello SAM ( Transform ). Possiamo anche specificare una descrizione .

The body consists of a set of resources: each resource has a name, a resource Type, and a set of Properties.

The SAM specification currently supports three types: AWS::Serverless::Api, AWS::Serverless::Function as well as AWS::Serverless::SimpleTable.

As we want to deploy our example application, we have to define one SimpleTable, two Functions, as well as one Api in our template-body.

4.2. DynamoDB Table Definition

Let's define our DynamoDB table now:

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:

  • Nozioni di base sul Serverless Application Model (SAM), nonché su CloudFormation sottostante
  • Definizione di un'applicazione serverless, utilizzando la sintassi del modello SAM
  • Distribuzione automatizzata dell'applicazione, utilizzando la CLI di CloudFormation

Come al solito, tutto il codice per questo articolo è disponibile su GitHub.