Introduction au modèle d'application sans serveur AWS

1. Vue d'ensemble

Dans notre article précédent, nous avons déjà implémenté une application sans serveur à pile complète sur AWS, en utilisant API Gateway pour les points de terminaison REST, AWS Lambda pour la logique métier, ainsi qu'un DynamoDB en tant que base de données.

Cependant, le déploiement consiste en de nombreuses étapes manuelles, qui peuvent devenir peu pratiques avec une complexité croissante et avec le nombre d'environnements.

Dans ce didacticiel, nous allons maintenant expliquer comment utiliser le modèle d'application sans serveur AWS (SAM), qui permet une description basée sur un modèle et un déploiement automatisé d'applications sans serveur sur AWS .

En détail, nous examinerons les sujets suivants:

  • Principes de base du modèle d'application sans serveur (SAM), ainsi que du CloudFormation sous-jacent
  • Définition d'une application sans serveur, à l'aide de la syntaxe du modèle SAM
  • Déploiement automatisé de l'application, à l'aide de la CLI CloudFormation

2. Bases

Comme indiqué précédemment, AWS nous permet de mettre en œuvre des applications entièrement sans serveur, en utilisant API Gateway, les fonctions Lambda et DynamoDB. Sans aucun doute, cela offre déjà de nombreux avantages en termes de performances, de coût et d'évolutivité.

Cependant, l'inconvénient est que nous avons besoin de nombreuses étapes manuelles dans la console AWS pour le moment, telles que la création de chaque fonction, le téléchargement de code, la création de la table DynamoDB, la création de rôles IAM, la création d'une structure d'API et d'API, etc.

Pour les applications complexes et avec plusieurs environnements tels que le test, la préparation et la production, cet effort se multiplie rapidement.

C'est là que CloudFormation pour les applications sur AWS en général, et le modèle d'application sans serveur (SAM) spécifiquement pour les applications sans serveur, entre en jeu.

2.1. AWS CloudFormation

CloudFormation est un service AWS pour le provisionnement automatique des ressources d'infrastructure AWS. Un utilisateur définit toutes les ressources requises dans un plan directeur (appelé modèle), et AWS se charge de l'approvisionnement et de la configuration.

Les termes et concepts suivants sont essentiels pour comprendre CloudFormation et SAM:

Un modèle est une description d'une application , comment elle doit être structurée au moment de l'exécution. Nous pouvons définir un ensemble de ressources requises, ainsi que la manière dont ces ressources doivent être configurées. CloudFormation fournit un langage commun pour définir des modèles, prenant en charge JSON et YAML en tant que format.

Les ressources sont les éléments de base de CloudFormation. Une ressource peut être n'importe quoi, comme un RestApi, une étape d'un RestApi, un travail par lots, une table DynamoDB, une instance EC2, une interface réseau, un rôle IAM et bien d'autres. La documentation officielle répertorie actuellement environ 300 types de ressources pour CloudFormation.

Une pile est l'instanciation d'un modèle. CloudFormation prend en charge l'approvisionnement et la configuration de la pile.

2.2. Modèle d'application sans serveur (SAM)

Comme souvent, l'utilisation d'outils puissants peut devenir très complexe et peu pratique, ce qui est également le cas pour CloudFormation.

C'est pourquoi Amazon a introduit le modèle d'application sans serveur (SAM). SAM a commencé avec la revendication de fournir une syntaxe propre et simple pour définir des applications sans serveur. Actuellement, il ne dispose que de trois types de ressources, à savoir les fonctions Lambda, les tables DynamoDB et les API .

SAM est basé sur la syntaxe du modèle CloudFormation, nous pouvons donc définir notre modèle à l'aide de la syntaxe SAM simple, et CloudFormation traitera davantage ce modèle.

Plus de détails sont disponibles dans le référentiel officiel GitHub ainsi que dans la documentation AWS.

3. Prérequis

Pour le didacticiel suivant, nous aurons besoin d'un compte AWS. Un compte gratuit devrait être suffisant.

En plus de cela, nous devons installer l'AWS CLI.

Enfin, nous avons besoin d'un compartiment S3 dans notre région, qui peut être créé via l'AWS CLI avec la commande suivante:

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

Bien que le didacticiel utilise baeldung-sam-bucket dans ce qui suit, sachez que les noms de compartiment doivent être uniques, vous devez donc choisir votre nom.

En tant qu'application de démonstration, nous utiliserons le code de Utilisation d'AWS Lambda avec API Gateway.

4. Création du modèle

Dans cette section, nous allons créer notre modèle SAM.

Nous allons d'abord examiner la structure globale, avant de définir les ressources individuelles.

4.1. Structure du modèle

Tout d'abord, examinons la structure globale de notre modèle:

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

Comme nous pouvons le voir, le modèle se compose d'un en-tête et d'un corps:

L'en-tête spécifie la version du modèle CloudFormation ( AWSTemplateFormatVersion ) ainsi que la version de notre modèle SAM ( Transform ). Nous pouvons également spécifier une description .

Le corps se compose d'un ensemble de ressources: chaque ressource a un nom, un type de ressource et un ensemble de propriétés .

La spécification SAM prend actuellement en charge trois types: AWS :: Serverless :: Api , AWS :: Serverless :: Function ainsi que AWS :: Serverless :: SimpleTable .

Comme nous voulons déployer notre exemple d'application, nous devons définir un SimpleTable , deux Fonctions , ainsi qu'une Api dans notre corps de modèle.

4.2. Définition de la table DynamoDB

Définissons maintenant notre table 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:

  • Principes de base du modèle d'application sans serveur (SAM), ainsi que du CloudFormation sous-jacent
  • Définition d'une application sans serveur, à l'aide de la syntaxe du modèle SAM
  • Déploiement automatisé de l'application, à l'aide de la CLI CloudFormation

Comme d'habitude, tout le code de cet article est disponible à l'adresse over sur GitHub.