O blog da AWS
CloudFormation: Publicando um micro serviço usando arquitetura serverless
Introdução
O objetivo deste documento é apresentar um exemplo funcional e comentado de uma pilha de CloudFormation capaz de publicar um micro serviço totalmente funcional usando AWS API Gateway e AWS Lambda.
O Template de CloudFormation executará as seguintes tarefas:
- Criação de uma VPC
- Criação de uma Role para execução de lambda
- Criação da função lambda usando a Role criada no passo anterior
- Criação de um API Gateway
- Criação de métodos e recursos para o serviço
- Integração do API Gateway com Lambda
- Criação de um “stage”
- Deploy do “stage”
- Informar no “Output” o endpoint do serviço criado.
Antes de iniciarmos, um breve descritivo dos produtos utilizados neste documento:
AWS Lambda
O AWS Lambda permite que você execute códigos sem provisionar ou gerenciar servidores. Você paga apenas pelo tempo de computação que você utilizar. Não haverá cobranças quando o seu código não estiver em execução. Com o Lambda, você pode executar o código para praticamente qualquer tipo de aplicativo ou serviço de back-end, tudo sem precisar de administração. Basta carregar o código e o Lambda toma conta de tudo o que for necessário para executar e escalar o seu código com alta disponibilidade. Você pode configurar o seu código para que ele seja acionado automaticamente por meio de outros serviços da AWS ou chamá-lo diretamente usando qualquer aplicativo móvel ou da web.
API Gateway
O Amazon API Gateway é um serviço totalmente gerenciado que permite que desenvolvedores criem, publiquem, mantenham, monitorem e protejam APIs em qualquer escala. Com apenas alguns cliques no Console de Gerenciamento da AWS, você pode criar uma API que atua como uma “porta de entrada” para que aplicativos acessem dados, lógica de negócios ou funcionalidades a partir de serviços de back-end, como cargas de trabalho executadas no Amazon Elastic Compute Cloud (Amazon EC2), código executado no AWS Lambda ou qualquer aplicação web. O Amazon API Gateway processa todas as tarefas relacionadas à aceitação e ao processamento de até centenas de milhares de chamadas simultâneas de APIs, incluindo gerenciamento de tráfego, autorização e controle de acesso, monitoramento e gerenciamento de versões de APIs. O Amazon API Gateway não tem taxas mínimas ou custos antecipados. Você paga apenas pelas chamadas de API recebidas e pela quantidade transferida de dados de saída.
CloudFormation
O AWS CloudFormation oferece aos desenvolvedores e administradores de sistemas uma maneira fácil de criar e gerenciar um grupo de recursos relacionados à AWS, e fornecê-los e atualizá-los de uma forma organizada e previsível.
É possível usar os modelos de exemplo do AWS CloudFormation ou criar seus próprios modelos para descrever os recursos da AWS e quaisquer dependências associadas ou parâmetros de tempo de execução exigidos para executar seu aplicativo. Você não precisa saber a ordem de provisionamento dos serviços da AWS ou os detalhes para as dependências funcionarem. O CloudFormation cuida disso para você. Após a implantação dos recursos da AWS, você pode modificá-los e atualizá-los de uma forma controlada e previsível, efetivamente aplicando o controle de versão à sua infraestrutura na AWS da mesma forma que faz com o seu software. Você também pode visualizar seus modelos como diagramas e editá-los usando uma interface com a funcionalidade arrastar e soltar usando o AWS CloudFormation Designer.
Você pode implementar e atualizar um modelo e o conjunto de recursos associados (chamado de pilha) usando o AWS Management Console, a interface de linha de comando da AWS ou as APIs. O CloudFormation está disponível gratuitamente e você paga somente pelos recursos de AWS necessários para executar seus aplicativos.
O micro serviço
Para este documento vamos usar um micro serviço muito simples, uma calculadora com 4 operações, acessada por uma interface REST. Esta calculadora está implementada em Python usando o AWS Lambda para o seu código e o AWS API Gateway para criar a estrutura REST.
Após publicado, o serviço estará disponível em uma URL e receberá 3 parâmetros, no seguinte formato:
http://<api.url>/<op>/<n1>/<n2> aonde:
<api.url> = endpoint criado pelo API Gateway
<op> = Operação a ser executada, podendo ser:
A : Adição
D : Divisão
M : Multiplicação
S : Subtração
<n1> = Primeiro parâmetro numérico para a Calculadora
<n2> = Segundo parâmetro numérico para a Calculadora
Obviamente qualquer linguagem que tenha recursos o suficiente para consumir uma API remota faz operações básicas, porém a ideia deste documento é demonstrar um template de Cloudformation que implemente uma API Rest completemante funcional, este exemplo poderá ser alterado e expandido livremente para publicar as suas próprias APIs.
O Template Cloudformation
Antes de detalhar o template, vamos ver as partes que o formam.
Um Template Cloudformation pode ser um documento JSON ou YAML, o exemplo deste documento é feito usando o formato JSON.
O Template deste documento tem algumas seções:
- Versão do Formato
- Metadata
- Parâmetros
- Recursos
- Saídas
Vou mostrar o respectivo conteúdo abaixo de cada uma.
Versão do Formato
Descreve qual versão o Template segue, por enquanto o único valor válido é “2010-09-09”
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Publish a serverless Micro Service",
E dá uma pequena descrição do Template
Metadata
Esta seção serve apenas para organizar os parâmetros da próxima seção, definir a ordem em que serão exibidos e os textos das entradas.
"Metadata": {
"AWS::CloudFormation::Interface": {
"ParameterGroups": [
{
"Label": {
"default": "Network Configuration"
},
"Parameters": [
"SubnetId",
"SecurityGroupId"
]
}
],
"ParameterLabels": {
"SubnetId": {
"default": "Select 2 subnets belonging to the same VPC"
},
"SecurityGroupId": {
"default": "Select the security group to run the Lambda function"
}
}
}
}
Parâmetros
O template precisa de 3 parâmetros para executar, 2 subnets de uma mesma VPC e um security group.
A função lambda será criada nestas subnets e terá o tráfego controlado pelo security group.
Este exemplo, a priori, não precisaria usar Lambda em VPC, porém como boa parte dos casos de produção precisam, optei por fazer assim pra já mostrar como configurar:
"Parameters": {
"SubnetId": {
"Description": "Subnet",
"Type": "List<AWS::EC2::Subnet::Id>"
},
"SecurityGroupId": {
"Description": "Security Group",
"Type": "AWS::EC2::SecurityGroup::Id"
}
}
Recursos
Esta é a parte principal do Template, tudo que é criado na sua conta AWS é definido aqui. Inicia com esta definição.
"Resources": {
O primeiro objeto a ser criado é a função Lambda, é o objeto abaixo do tipo:
AWS::Lambda::Function
"LbdFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "index.handler",
"Role": {
"Fn::Join": [
"",
[
"arn:aws:iam::",
{
"Ref": "AWS::AccountId"
},
":role/lambda_basic_vpc_execution"
]
]
},
"Code": {
"ZipFile": {
"Fn::Join": [
"\n",
[
"def handler(event, context):",
" op=event['op']",
" n1=event['n1']",
" n2=event['n2']",
" if op == 'A':",
" result = n1 + n2",
" elif op == 'S':",
" result = n1 - n2",
" elif op == 'M':",
" result = n1 * n2",
" elif op == 'D':",
" result = n1 / n2",
" return { \"result\" : result }"
]
]
}
},
"Runtime": "python2.7",
"Description": "Lambda function created by cloudformation",
"FunctionName": {
"Fn::Join": [
"_",
[
{
"Ref": "AWS::StackName"
},
"Calc"
]
]
},
"Timeout": "25",
"VpcConfig": {
"SecurityGroupIds": [
{
"Ref": "SecurityGroupId"
}
],
"SubnetIds": [
{
"Fn::Select": [
"0",
{
"Ref": "SubnetId"
}
]
},
{
"Fn::Select": [
"1",
{
"Ref": "SubnetId"
}
]
}
]
}
}
},
Tudo que é necessário para criar a função está descrito acima, mas vamos prestar atenção em alguns detalhes:
Role: É a role que define as permissões desta função Lambda, se você já criou alguma função Lambda em VPC, você provavelmente já tem alguma com o nome lambda_basic_vpn_execution, caso não tenha, crie uma nova Role com este nome e adicione a seguinte Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DetachNetworkInterface",
"ec2:DeleteNetworkInterface"
],
"Resource": "*"
}
]
}
FunctionName: É o nome da função Lambda, estou usando aqui o nome da pilha, ao ser criada no Cloudformation e adicionando um “_Calc”, assim o Template pode ser executado várias vezes sem erros.
Code: É o código fonte da função, você tem 2 alternativas aqui, ou colocar o fonte diretamente no texto, como neste exemplo, o que só faz sentido em funções menores, ou apontar um endereço em um bucket S3 aonde está o seu arquivo.
Neste caso, troque o
“Code”: {
"ZipFile" : String
}
Por
“Code”:{
"S3Bucket" : String,
"S3Key" : String,
"S3ObjectVersion" : String,
}
Mais detalhes em: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html
O próximo recurso é a permissão para que o API Gateway possa invocar a função lambda recém criada, isso é interessante para que você tenha controle sobre quais APIs podem chamar quais funções. Aqui eu libero a função para o produto API Gateway, então qualquer API terá permissão para invocar esta função Lambda, veja que eu estou usando a função “Ref” para ler o AccountID de quem estiver executando este Template, de forma que a permissão seja dada corretamente.
AWS::Lambda::Permission
"CalcPermission": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"LbdFunction",
"Arn"
]
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:aws:execute-api:us-east-1:",
{
"Ref": "AWS::AccountId"
},
":*"
]
]
}
}
},
Agora vamos criar a API Rest em si, ela é criada com o trecho de código abaixo, praticamente só pede uma descrição e nome, assim como na função lambda, eu estou prefixando o nome da API com o nome da stack do cloudformation, assim o script pode ser executado várias vezes sem erro.
AWS::ApiGateway::RestApi
"CalcAPI": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Description": "Calc created using cloudformation",
"Name": {
"Fn::Join": [
"_",
[
{
"Ref": "AWS::StackName"
},
"CalcAPI"
]
]
}
}
},
Agora vamos criar os ‘recursos’ da API, que são os objetos que montam a URL da API, no nosso caso: /calc/op/n1/n2 que estão definidos abaixo.
Você pode notar que os objetos desse tipo tem uma propriedade que é o “ParentId”, ou seja o “objeto pai” do qual você está criando, assim você consegue definir a estrutura na ordem que desejar.
Em todos eles, exceto o primeiro, eu uso sempre uma referência ao objeto criado anteriormente, ou seja o “ParentId” de “n2” é “n1”, o “ParentId” de “n1” é “op” e assim vai.
O primeiro tem um tratamento especial, o “ParentId” dele deve ser a raíz da API e você consegue uma referência a este objeto usando a função GetAtt, veja no código abaixo que eu estou usando esta função para ler a propriedade “RootResourceId” do objeto “CalcAPI” (que foi criado no bloco anterior).
AWS::ApiGateway::Resource
"calc": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ParentId": {
"Fn::GetAtt": [
"CalcAPI",
"RootResourceId"
]
},
"PathPart": "calc"
}
},
"op": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ParentId": {
"Ref": "calc"
},
"PathPart": "{op}"
}
},
"n1": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ParentId": {
"Ref": "op"
},
"PathPart": "{n1}"
}
},
"n2": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ParentId": {
"Ref": "n1"
},
"PathPart": "{n2}"
}
},
Agora vamos criar um dos objetos mais importantes, o “método”, é ele quem liga a sua API ao código que será executado, neste caso uma função lambda.
Essa ligação é feita pela propriedade “Uri”
AWS::ApiGateway::Method
"CalcMethod": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ResourceId": {
"Ref": "n2"
},
"HttpMethod": "GET",
"AuthorizationType": "NONE",
"Integration": {
"IntegrationHttpMethod": "POST",
"IntegrationResponses": [
{
"SelectionPattern": "-",
"StatusCode": "200"
}
],
"RequestTemplates": {
"application/json": {
"Fn::Join": [
"\n",
[
"{",
"\"op\" : \"$input.params('op')\",",
"\"n1\" : $input.params('n1'),",
"\"n2\" : $input.params('n2')",
"}"
]
]
}
},
"PassthroughBehavior": "WHEN_NO_TEMPLATES",
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region"
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"LbdFunction",
"Arn"
]
},
"/invocations"
]
]
}
},
"MethodResponses": [
{
"StatusCode": "200"
}
]
}
},
Os 2 passos a seguir criam um “Stage” chamado Test e um Prod para esta API e fazem o deploy, assim a API já está pronta pra ser usada.
AWS::ApiGateway::Deployment
"TestDeployment": {
"Type": "AWS::ApiGateway::Deployment",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"Description": "Test deployment",
"StageName": "Test"
},
"DependsOn": "CalcMethod"
}
AWS::ApiGateway::Stage
"ProdStage": {
"Type": "AWS::ApiGateway::Stage",
"Properties": {
"StageName": "Prod",
"Description": "Prod Stage",
"RestApiId": {
"Ref": "CalcAPI"
},
"DeploymentId": {
"Ref": "TestDeployment"
},
"Variables": {
"Stack": "Prod"
}
},
"DependsOn": "TestDeployment"
}
Saídas
As saídas podem ser usadas para interligar templates distintos, serem listados por linha de comando ou visualizados na console, geralmente são utilizadas para encontrar mais facilmente objetos criados pelo template.
Neste caso estou usando as saídas para documentar a URL criada para o deploy da API e exibir um exemplo de acesso à API.
"Outputs": {
"API": {
"Description": "API URL",
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "CalcAPI"
},
".execute-api.",
{
"Ref": "AWS::Region"
},
".amazonaws.com/",
{
"Ref": "ProdStage"
},
"/calc"
]
]
}
},
"EXAMPLE": {
"Description": "Example: How to call the calc and ADD 2 and 3",
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "CalcAPI"
},
".execute-api.",
{
"Ref": "AWS::Region"
},
".amazonaws.com/",
{
"Ref": "ProdStage"
},
"/calc/A/2/3"
]
]
}
}
Estes dados podem ser vistos na aba ‘Outputs’ da sua stack no CloudFormation.
Clicando-se na URL do exemplo a API deve ser chamada e passas os parâmetros “A”, “2” e “3” para a função lambda que deve adicionar 2 e 3 e retornar:
{"result": 5}
Se isto ocorrer, o processo de criação ocorreu sem falhas.
Leitura recomendada
Para se aprofundar no assunto, leia a documentação dos produtos envolvidos:
Amazon API Gateway: https://aws.amazon.com/documentation/apigateway/
AWS CloudFormation: https://aws.amazon.com/pt/documentation/cloudformation/
AWS Lambda: https://aws.amazon.com/pt/documentation/lambda/
Template Completo
Segue abaixo o Template completo, você pode copiá-lo em um arquivo novo e salvá-lo como texto puro, sem formatação nenhuma:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Create Lambda Function",
"Metadata": {
"AWS::CloudFormation::Interface": {
"ParameterGroups": [
{
"Label": {
"default": "Network Configuration"
},
"Parameters": [
"SubnetId",
"SecurityGroupId"
]
}
],
"ParameterLabels": {
"SubnetId": {
"default": "Select 2 subnets belonging to the same VPC"
},
"SecurityGroupId": {
"default": "Select the security group to run the Lambda function"
}
}
}
},
"Parameters": {
"SubnetId": {
"Description": "Subnet",
"Type": "List<AWS::EC2::Subnet::Id>"
},
"SecurityGroupId": {
"Description": "Security Group",
"Type": "AWS::EC2::SecurityGroup::Id"
}
},
"Resources": {
"LbdFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "index.handler",
"Role": {
"Fn::Join": [
"",
[
"arn:aws:iam::",
{
"Ref": "AWS::AccountId"
},
":role/lambda_basic_vpc_execution"
]
]
},
"Code": {
"ZipFile": {
"Fn::Join": [
"\n",
[
"def handler(event, context):",
" op=event['op']",
" n1=event['n1']",
" n2=event['n2']",
" if op == 'A':",
" result = n1 + n2",
" elif op == 'S':",
" result = n1 - n2",
" elif op == 'M':",
" result = n1 * n2",
" elif op == 'D':",
" result = n1 / n2",
" return { \"result\" : result }"
]
]
}
},
"Runtime": "python2.7",
"Description": "Lambda function created by cloudformation",
"FunctionName": {
"Fn::Join": [
"_",
[
{
"Ref": "AWS::StackName"
},
"Calc"
]
]
},
"Timeout": "25",
"VpcConfig": {
"SecurityGroupIds": [
{
"Ref": "SecurityGroupId"
}
],
"SubnetIds": [
{
"Fn::Select": [
"0",
{
"Ref": "SubnetId"
}
]
},
{
"Fn::Select": [
"1",
{
"Ref": "SubnetId"
}
]
}
]
}
}
},
"CalcPermission": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"LbdFunction",
"Arn"
]
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:aws:execute-api:us-east-1:",
{
"Ref": "AWS::AccountId"
},
":*"
]
]
}
}
},
"CalcAPI": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Description": "Calc created using cloudformation",
"Name": {
"Fn::Join": [
"_",
[
{
"Ref": "AWS::StackName"
},
"CalcAPI"
]
]
}
}
},
"calc": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ParentId": {
"Fn::GetAtt": [
"CalcAPI",
"RootResourceId"
]
},
"PathPart": "calc"
}
},
"op": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ParentId": {
"Ref": "calc"
},
"PathPart": "{op}"
}
},
"n1": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ParentId": {
"Ref": "op"
},
"PathPart": "{n1}"
}
},
"n2": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ParentId": {
"Ref": "n1"
},
"PathPart": "{n2}"
}
},
"CalcMethod": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"ResourceId": {
"Ref": "n2"
},
"HttpMethod": "GET",
"AuthorizationType": "NONE",
"Integration": {
"IntegrationHttpMethod": "POST",
"IntegrationResponses": [
{
"SelectionPattern": "-",
"StatusCode": "200"
}
],
"RequestTemplates": {
"application/json": {
"Fn::Join": [
"\n",
[
"{",
"\"op\" : \"$input.params('op')\",",
"\"n1\" : $input.params('n1'),",
"\"n2\" : $input.params('n2')",
"}"
]
]
}
},
"PassthroughBehavior": "WHEN_NO_TEMPLATES",
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region"
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"LbdFunction",
"Arn"
]
},
"/invocations"
]
]
}
},
"MethodResponses": [
{
"StatusCode": "200"
}
]
}
},
"TestDeployment": {
"Type": "AWS::ApiGateway::Deployment",
"Properties": {
"RestApiId": {
"Ref": "CalcAPI"
},
"Description": "Test deployment",
"StageName": "Test"
},
"DependsOn": "CalcMethod"
},
"ProdStage": {
"Type": "AWS::ApiGateway::Stage",
"Properties": {
"StageName": "Prod",
"Description": "Prod Stage",
"RestApiId": {
"Ref": "CalcAPI"
},
"DeploymentId": {
"Ref": "TestDeployment"
},
"Variables": {
"Stack": "Prod"
}
},
"DependsOn": "TestDeployment"
}
},
"Outputs": {
"API": {
"Description": "API URL",
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "CalcAPI"
},
".execute-api.",
{
"Ref": "AWS::Region"
},
".amazonaws.com/",
{
"Ref": "ProdStage"
},
"/calc"
]
]
}
},
"EXAMPLE": {
"Description": "Example: How to call the calc and ADD 2 and 3",
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "CalcAPI"
},
".execute-api.",
{
"Ref": "AWS::Region"
},
".amazonaws.com/",
{
"Ref": "ProdStage"
},
"/calc/A/2/3"
]
]
}
}
}
}