AWS AppSync: Desenvolvimento centrado em código com AWS CloudFormation
Tags: ptbr, aws, graphql, webdev
Published: 2019-08-06 02:59:26

O AWS AppSync é um serverless GraphQL **backend-as-a-service**. A maioria dos artigos online mostra seu uso através do console da AWS e configurando via interface web, o que não é uma abordagem centrada em código. Eu gostaria de mostrar uma abordagem orientada a código, empregando uma técnica chamada **Infraestrutura como Código** usando o **AWS CloudFormation**. O CloudFormation permite que você especifique todos os recursos da AWS na forma de código e automatize suas implantações por meio da linha de comando. Desta forma, todos os recursos de infraestrutura necessários para o seu aplicativo se tornam parte do repositório git - controlados por versão. Você pode reproduzir a **stack** inteira (conjunto de recursos relacionados) na nuvem com um único comando, ao invés de repetir as etapas manualmente por meio da GUI. Depois que a stack inicial é criada, você pode implantar facilmente alterações em sua infraestrutura, modificando o arquivo de modelo e, em seguida, executando comandos por meio da linha de comando. Essa abordagem oferece controle total direto por meio de código e é muito flexível para se integrar ao restante do ecossistema da AWS.

Mostrarei passo a passo como criar e implantar uma API AppSync GraphQL de nível de produção usando o DynamoDB, tudo a partir do conforto de seu editor e terminal.

# 1. Definindo o esquema GraphQL

Iremos criar um esquema de exemplo com uma consulta para obter contatos e uma mutação para adicionar um novo contato e salvar o arquivo como **schema.graphql** no diretório principal do projeto.

```bash
mkidr exemplo-appsync
cd exemplo-appsync
touch schema.graphql
vim schema.graphql
```

Adicionamos:

```graphql
type Contact {
  username: String!
  fullName: String!
  email: String!
  phone: String
}

input ContactInput {
  fullName: String!
  email: String!
  phone: String
}

type Query {
  getContacts: [Contact!]!
}

type Mutation {
  addContact(username: String! contact: ContactInput!): Contact
}

type Schema {
  query: Query
  mutation: Mutation
}
```

# 2. Adicionar modelos de mapeamento para os Resolvers

[O modelo de mapeamento de resolução](https://faisalhasnain.com/articles/aws-appsync-code-centric-development-using-cloudformation/) informa ao AppSync como converter uma solicitação de GraphQL recebida em instruções para sua fonte de dados do back-end e como traduzir a resposta dessa origem de dados de volta para uma resposta do GraphQL. Criaremos modelos de mapeamento de solicitação e resposta separados em arquivos diferentes para cada **resolver**.

```bash
$ mkdir mapping-templates
$ cd mapping-templates
$ touch addContact.request.vm
$ touch addContact.response.vm
$ touch getContacts.request.vm
$ touch getContacts.response.vm
```

Em `addContact.request.vm`, adicione:

```vtl
## mapeamento de resolução da requisição da mutação `addContact`, mapeando para a operação `PutItem` do DynamoDB.
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "username": $util.dynamodb.toDynamoDBJson($ctx.args.username)
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.contact)
}
```

Em `addContact.response.vm`, adicione:

```vtl
## mapeamento de resolução da resposta da mutação `addContact`, retornando o novo objeto de contato adicionado.
$util.toJson($ctx.result)
```

Em `getContacts.request.vm`, adicione:

```vtl
## mapeamento de resolução da requisição da query `getContacts`, mapeando para a operação `Scan` do DynamoDB.
{
  "version": "2017-02-28",
  "operation": "Scan"
}
```

Em `getContacts.response.vm`, adicione:

```vtl
## mapeamento de resolução da resposta da query `getContacts`, retornando todos os contatos.
$util.toJson($ctx.result.items)
```

# 3. Especifique recursos no modelo do CloudFormation

Aqui criaremos o modelo [CloudFormation](https://aws.amazon.com/cloudformation/) no formato YAML, que é mais legível do que sua contraparte JSON. Ele especifica detalhes sobre nossa AppSync API, API Key, o esquema GraphQL, a tabela do DynamoDB, nossas Fontes de Dados para a API AppSync, os Resolvers do GraphQL e o acesso à Tabela do DynamoDB usando uma IAM Role.

```bash
$ touch template.yml
$ vim template.yml
```

E em seguida:

```yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Create AppSync GraphQL API using DynamoDB

Parameters:

  ApiName:
    Type: String
    Description: Name of the API - used to generate unique names for resources
    MinLength: 3
    MaxLength: 25
    AllowedPattern: '^[a-zA-Z][a-zA-Z0-9\-]*$'

Resources:

  GraphQLApi:
    Type: AWS::AppSync::GraphQLApi
    Properties:
      Name: !Sub ${ApiName}-graphql-api
      AuthenticationType: API_KEY

  ApiKey:
    Type: AWS::AppSync::ApiKey
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId

  GraphQLSchema:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId
      # caminho para o esquema graphql
      DefinitionS3Location: schema.graphql

  ContactsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub ${ApiName}-contacts
      KeySchema:
      - AttributeName: username
        KeyType: HASH
      AttributeDefinitions:
      - AttributeName: username
        AttributeType: S
      BillingMode: PAY_PER_REQUEST

  ApiRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - appsync.amazonaws.com
          Action:
          - sts:AssumeRole
      Policies:
      - PolicyName: !Sub ${ApiName}-dynamo-access
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:DeleteItem
            - dynamodb:UpdateItem
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:BatchGetItem
            - dynamodb:BatchWriteItem
            Resource: !GetAtt ContactsTable.Arn

  ContactsTableDataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      Name: ContactsTableDataSource
      Type: AMAZON_DYNAMODB
      ServiceRoleArn: !GetAtt ApiRole.Arn
      DynamoDBConfig:
        AwsRegion: !Ref AWS::Region
        TableName: !Ref ContactsTable
      ApiId: !GetAtt GraphQLApi.ApiId

  QueryGetContactsResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      TypeName: Query
      FieldName: getContacts
      DataSourceName: !GetAtt ContactsTableDataSource.Name
      # caminho para o arquivo de mapeamento de resolução da requisição
      RequestMappingTemplateS3Location: mapping-templates/getContacts.request.vm
      # caminho para o arquivo de mapeamento de resolução da resposta
      ResponseMappingTemplateS3Location: mapping-templates/getContacts.response.vm
      ApiId: !GetAtt GraphQLApi.ApiId

  MutationAddContactResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      TypeName: Mutation
      FieldName: addContact
      DataSourceName: !GetAtt ContactsTableDataSource.Name
      # caminho para o arquivo de mapeamento de resolução da requisição
      RequestMappingTemplateS3Location: mapping-templates/addContact.request.vm
      # caminho para o arquivo de mapeamento de resolução da resposta
      ResponseMappingTemplateS3Location: mapping-templates/addContact.response.vm
      ApiId: !GetAtt GraphQLApi.ApiId
  
Outputs:

  GraphQLApiEndpoint:
    Description: GraphQL API URL
    Value: !GetAtt GraphQLApi.GraphQLUrl

  ApiKey:
    Description: API Key
    Value: !GetAtt ApiKey.ApiKey
```

# 4. Deploy usando o AWS CLI

Primeiro, [instale o AWS CLI](https://aws.amazon.com/cli/) e configure-o especificando as credenciais de segurança e as informações de região da sua conta. **Forneça a opção de perfil em todos os comandos se você quiser usar uma configuração de perfil específica, caso contrário, usará o perfil padrão.**

Crie um bucket do S3 para fazer o upload de arquivos de código:

```bash
aws s3 mb s3://contacts-dev-codes  --profile faisal
```

Execute o comando **package** para empacotar os artefatos locais referenciados no arquivo CloudFormation que criamos, **template.yml**, para o bucket S3 especificado. Isso gerará um novo arquivo, que se irá usar referências aos arquivos no bucket S3 ao invés dos arquivos locais.

```bash
aws cloudformation package \
    --s3-bucket contacts-dev-codes \
    --template-file template.yml \
    --output-template-file packaged-template.yml \
    --profile faisal
```

Execute o comando **deploy** utilizando o arquivo recém-gerado para criar todos os recursos de infraestrutura. Além disso, não esqueça de fornecer o nome da stack e os parâmetros necessários do template:

```bash
aws cloudformation deploy \
    --stack-name contacts-dev \
    --template-file packaged-template.yml \
    --capabilities CAPABILITY_IAM \
    --parameter-overrides ApiName=contacts-dev \
    --profile faisal
```

Agora você tem uma API AppSync GraphQL totalmente funcional com DynamoDB na AWS. Você pode usar os comandos **package** e **deploy** repetidamente sempre que fizer alterações no template, ele criará automaticamente o conjunto de mudanças e o executará. Se você estiver usando o Node.js, poderá adicionar esses comandos como scripts do NPM ao package.json de um projeto, por conveniência.

Eu [criei o repositório GitHub](https://github.com/faisalhasnain/aws-appsync-boilerplate) que contém todo o código padrão com a configuração completa necessária para o kickstart do projeto AppSync.

# Crédito

- [AWS AppSync code-centric development using CloudFormation](https://faisalhasnain.com/articles/aws-appsync-code-centric-development-using-cloudformation/), escrito originalmente por [Faisal Hasnain](https://twitter.com/faisalhasnainz)