AWS ECS: Como executar Docker, Clusters e Tarefas
Tags: ptbr, aws, docker, webdev
Published: 2019-10-27 05:41:50

Existem muitos serviços de computação diferentes no Amazon Web Services. Existe a rota Serverless com o AWS Lambda, na qual você pode provisionar sua carga de trabalho e executá-la somente quando necessário. O Elastic Compute Cloud (EC2) permite executar qualquer carga de trabalho dentro de máquinas virtuais pelas quais você paga por hora.

Hoje, porém, muitas pessoas estão criando cargas de trabalho em contêineres usando o Docker. Então, que opções você tem para executar seus contêineres na AWS? Nesta postagem, criaremos uma imagem de exemplo em Docker. Em seguida, criaremos a infraestrutura da AWS para hospedar essa imagem e executá-la por meio do AWS Elastic Container Service (ECS). A partir daí, exploraremos como você pode implantar novas versões da sua imagem diretamente do seu terminal.

Vamos começar criando um contêiner do de exemplo do Docker para podermos realizar o deploy.

# Imagem de exemplo do Docker

Para nosso propósito, vamos criar um aplicativo Node.js utilizando Express que contenha uma única rota. Vamos começar iniciando o projeto. Executamos **yarn init** e para o ponto de entrada, usaremos **server.js**:

```bash
$ mkdir sample-express-app
$ cd $_
$ yarn init -y
```

Depois que o projeto for configurado, vamos instalar o **express** no projeto:

```bash
$ yarn add express
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 📃  Building fresh packages...
success Saved lockfile.
✨  Done in 0.79s.
```

Legal! Agora podemos prosseguir e configurar a nossa rota adicionando o seguinte ao arquivo **‌server.js**:

```js
const express = require('express');

const PORT = 8080;
const HOST = '0.0.0.0';

const api = express();
api.get('/', (req, res) => {
  res.send('Sample Endpoint\n');
});

api.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
```

Agora que temos nossa API **express** de exemplo, vamos em frente e configurar nosso Dockerfile. Aqui está o que o nosso Dockerfile vai acabar parecendo:

```dockerfile
FROM node:10

WORKDIR /src/api

COPY package.json ./
COPY yarn.lock ./

RUN yarn install

COPY . .

EXPOSE 8080
CMD ["node", "server.js"]
```

Para verificar a integridade, vamos construir esta imagem e lançar um contêiner com ela:

```bash
$ docker build -t sample-express-app .
$ docker run -p 8080:8080 -d sample-express-app
5f3eaa088b35d895411c8d60f684aeba5d68d85f3bc07172c672542fe6b95537
$ curl localhost:8080
Sample Endpoint
```

Ótimo! Vemos que, quando executamos o contêiner na porta **8080**, podemos chamar nossa rota via **curl** e receber de volta a resposta **Sample Endpoint**.

Agora que temos uma imagem do Docker para criar e implementar, vamos configurar um registro de contêiners na AWS para o qual podemos enviar nossas imagens.

# Publicando imagens do Docker no Elastic Container Repository (ECR)

Para continuar com o nosso aprendizado da AWS, vamos configurar um repositório de ECR na AWS. Usaremos este repositório para hospedar nossa imagem do Docker. Antes de podermos fazer isso, você precisará ter a [CLI da AWS instalada e configurada](https://aws.amazon.com/cli/). Também usaremos o [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) para representar nossa infraestrutura como código, então também configure sua CLI.

A CLI e o CDK da AWS estão instalados e configurados? Legal, vamos inicializar nosso projeto CDK para começar a representar nossa **infraestrutura** em Typescript. Crie um novo diretório chamado **infrastructure** na raiz do seu repositório. Em seguida, inicialize um novo projeto via CDK:

```bash
$ cdk init --language typescript
Applying project template app for typescript
Executing npm install...
# Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `npm run test`    perform the jest unit tests
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template
```

Agora temos um projeto CDK dentro de nossa pasta **infrastructure**. O arquivo-chave que iremos adicionar a nossa infraestrutura é **lib/infrastructure-stack.ts**.

Para começar, vamos adicionar nosso recurso de repositório ECR. Primeiro, precisamos adicionar o módulo ECR ao nosso projeto CDK:

```bash
$ yarn add @aws-cdk/aws-ecr
```

Agora podemos provisionar nosso repositório de ECR, atualizando nosso **infrastructure-stack.ts** para ficar assim:

```tsx
import cdk = require('@aws-cdk/core');
import ecr = require('@aws-cdk/aws-ecr');

export class InfrastructureStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Repositório ECR
    const repository = new ecr.Repository(this, 'sample-express-app', {
      repositoryName: 'sample-express-app'
    });  
  }
}
```

Para implantar nossa infraestrutura CDK, precisamos executar o comando **deploy** em nossa linha de comando:

```bash
$ cdk deploy
InfrastructureStack: deploying...
InfrastructureStack: creating CloudFormation changeset...
 0/3 | 15:51:50 | CREATE_IN_PROGRESS   | AWS::CloudFormation::Stack | InfrastructureStack User Initiated
 0/3 | 15:51:53 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata   | CDKMetadata
 0/3 | 15:51:53 | CREATE_IN_PROGRESS   | AWS::ECR::Repository | sample-express-app (sampleexpressapp99ADE4E3)
 0/3 | 15:51:54 | CREATE_IN_PROGRESS   | AWS::ECR::Repository | sample-express-app (sampleexpressapp99ADE4E3) Resource creation Initiated
 1/3 | 15:51:54 | CREATE_COMPLETE      | AWS::ECR::Repository | sample-express-app (sampleexpressapp99ADE4E3)
 1/3 | 15:51:55 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata   | CDKMetadata Resource creation Initiated
 2/3 | 15:51:55 | CREATE_COMPLETE      | AWS::CDK::Metadata   | CDKMetadata
 3/3 | 15:51:57 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | InfrastructureStack
```

Agora temos um repositório de ECR em nossa conta da AWS para hospedar nossa nova imagem do Docker. Com o nosso repositório criado, precisamos fazer o login antes de empurrar nossa nova imagem. Para fazer isso, executamos o comando abaixo dentro dos *backticks*, para que o comando **docker login** seja chamado uma vez que o **get-login** retornar:

```bash
$ `aws ecr get-login --no-include-email`
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
$ aws ecr describe-repositories
{
    "repositories": [
        {
            "registryId": "<aws-id>",
            "repositoryName": "sample-express-app",
            "repositoryArn": "arn:aws:ecr:us-west-2:<aws-id>:repository/infra-sampl-1ewaboppskux6",
            "createdAt": 1571007114.0,
            "repositoryUri": "<aws-id>.dkr.ecr.us-west-2.amazonaws.com/sample-express-app"
        }
    ]
}
```

Agora que estamos todos logados, podemos marcar e enviar nossa imagem para o nosso novo repositório do ECR. Pegue o **repositoryUri** do seu repositório no comando **describe-repositories** acima. Vamos usar isso na tag e no comando push abaixo:

```bash
$ docker tag sample-express-app <aws-id>.dkr.ecr.us-west-2.amazonaws.com/sample-express-app
$ docker push <aws-id>.dkr.ecr.us-west-2.amazonaws.com/sample-express-app
0574222c01c4: Pushed 
28c9fa7f105e: Pushed 
8942b63b65a1: Pushed 
b91230b492da: Pushed 
6ad739b471d2: Pushed 
954f92adc866: Pushed 
adca1e83b51a: Pushed 
73982c948de0: Pushed 
84d0c4b192e8: Pushed 
a637c551a0da: Pushed 
2c8d31157b81: Pushed 
7b76d801397d: Pushed 
f32868cde90b: Pushed 
0db06dff9d9a: Pushed
```

Ótimo, nossa imagem do Docker está em nosso repositório ECR. Agora podemos prosseguir para implantá-lo e executá-lo via Elastic Container Service (ECS).

# Configurando nossa infraestrutura de ECS

Antes de podermos executar um contêiner Docker em nossa conta da AWS usando nossa nova imagem, precisamos criar a infraestrutura na qual ele será executado. Nesse artigo, focaremos na execução de nosso contêiner no Elastic Container Service (ECS) fornecido pela AWS.

O ECS é um serviço de orquestração de contêiner fornecido pela AWS. Isso elimina a necessidade de gerenciar a infraestrutura, o planejamento e o dimensionamento de cargas de trabalho em contêiner.

O ECS consiste em três termos principais que devem ser lembrados ao pensar no serviço.

- **Cluster:** este é o grupo lógico de instâncias subjacentes do EC2 em que nossos contêineres estão sendo executados. Não temos acesso a essas instâncias e a AWS as gerencia em nosso nome.
- **Serviço (_Service_):** um processo de longa duração, como um servidor web ou banco de dados, é executado como um serviço em nosso cluster. Podemos definir quantos contêineres devem estar em execução para este serviço.
- **Definição da Tarefa (_Task Definition_)**: Esta é a definição do nosso contêiner que pode ser executado no cluster individualmente ou por meio de um serviço. Quando uma definição de tarefa está sendo executada em nosso cluster, geralmente a chamamos de tarefa, portanto, um contêiner em execução === uma tarefa no ECS.

A ordem dessas definições também são relevantes. Podemos pensar no cluster como o nível mais baixo do serviço, as instâncias do EC2 em que nossos contêineres são executados. Enquanto uma tarefa é uma instância do nosso contêiner em execução em uma ou mais dessas instâncias.

Com essa terminologia em nossa memória, vamos realmente provisionar nossa infraestrutura. Vamos atualizar o nosso **infrastructure-stack.ts** para ter os recursos necessários para o cluster do ECS. Primeiro, precisamos adicionar alguns outros módulos.

```bash
$ yarn add @aws-cdk/aws-ecs
$ yarn add @aws-cdk/aws-ec2
$ yarn add @aws-cdk/aws-ecs-patterns
```

Agora podemos adicionar os recursos necessários ao nosso cluster que executará nossa nova imagem do Docker. Nosso **infrastructure-stack.ts** deve ficar:

```tsx
import cdk = require('@aws-cdk/core');
import ecr = require('@aws-cdk/aws-ecr');
import ecs = require('@aws-cdk/aws-ecs');
import ec2 = require('@aws-cdk/aws-ec2');
import ecsPatterns = require('@aws-cdk/aws-ecs-patterns');

export class InfrastructureStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Repositório ECR
    const repository = new ecr.Repository(this, 'sample-express-app', {
      repositoryName: 'sample-express-app'
    });

    // Clusters e Recursos do ECS
    const cluster = new ecs.Cluster(this, 'app-cluster', {
      clusterName: 'app-cluster'
    });

    cluster.addCapacity('app-scaling-group', {
      instanceType: new ec2.InstanceType("t2.micro"),
      desiredCapacity: 1
    });

    const loadBalancedService = new ecsPatterns.ApplicationLoadBalancedEc2Service(this, 'app-service', {
      cluster,
      memoryLimitMiB: 512,
      cpu: 5,
      desiredCount: 1,
      serviceName: 'sample-express-app',
      taskImageOptions: {
        image: ecs.ContainerImage.fromEcrRepository(repository),
        containerPort: 8080
      },
      publicLoadBalancer: true
    });
  }
}
```

A primeira coisa que notamos é que primeiro definimos nosso cluster do ECS **app-cluster**,. Em seguida, precisamos adicionar uma instância ao nosso cluster **app-scaling-group**. Esse é um grupo de escalonamento automático de tipos de instância **t2.micro** nos quais nossos contêineres podem executar. Em seguida, usamos um padrão de balanceamento de carga de serviço fornecido pelo módulo **ecsPatterns**.

Observe que apontamos nosso serviço ECS para a imagem hospedada em nosso repositório ECR. Nós também definimos o **containerPort** que **8080** é a porta que nosso aplicativo **express** está sendo executado no interior do recipiente.

Esse padrão cria um balanceador de carga voltado para o público que poderemos chamar por **curl** ou em nosso navegador da web. Esse balanceador de carga encaminhará chamadas para nosso contêiner em execução na porta **8080** dentro de nosso serviço ECS.

Implementamos essas alterações por meio de outro comando **deploy**. Desta vez, vamos especificar --**require-approval never** para não sermos avisados ​​sobre as alterações do IAM.

```bash
$ cdk deploy --require-approval never
InfrastructureStack: deploying...
InfrastructureStack: creating CloudFormation changeset...
  0/46 | 16:41:46 | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack            | InfrastructureStack User Initiated
  0/46 | 16:42:07 | CREATE_IN_PROGRESS   | AWS::EC2::EIP                         | app-cluster/Vpc/PublicSubnet2/EIP (appclusterVpcPublicSubnet2EIPD0A381A3)
  0/46 | 16:42:07 | CREATE_IN_PROGRESS   | AWS::ECS::Cluster                     | app-cluster (appclusterD09F8E40)
  0/46 | 16:42:07 | CREATE_IN_PROGRESS   | AWS::EC2::InternetGateway             | app-cluster/Vpc/IGW (appclusterVpcIGW17A11835)
  0/46 | 16:42:07 | CREATE_IN_PROGRESS   | AWS::EC2::EIP                         | app-cluster/Vpc/PublicSubnet1/EIP (appclusterVpcPublicSubnet1EIP791F54CD)
...
....
.....
44/46 | 16:46:04 | CREATE_COMPLETE      | AWS::Lambda::Permission               | app-cluster/DefaultAutoScalingGroup/DrainECSHook/Function/AllowInvoke:InfrastructureStackappclusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic2C88B6D3 (appclusterDefaultAutoScalingGroupDrainECSHookFunctionAllowInvokeInfrastructureStackappclusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic2C88B6D3036C2EFB)
 45/46 | 16:46:33 | CREATE_COMPLETE      | AWS::ECS::Service                     | app-service/Service (appserviceServiceA5AB3AA1)
 45/46 | 16:46:37 | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack            | InfrastructureStack
 46/46 | 16:46:38 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack            | InfrastructureStack

Outputs:
InfrastructureStack.appserviceLoadBalancerDNS0A615BF5 = Infra-appse-187228PB273DW-1700265048.us-west-2.elb.amazonaws.com
InfrastructureStack.appserviceServiceURL90EC0456 = http://Infra-appse-187228PB273DW-1700265048.us-west-2.elb.amazonaws.com
```

Usando o módulo AWS CDK para o Elastic Container Service, criamos todos os recursos necessários para o nosso novo cluster. Como você pode ver na saída, uma nova VPC com sub-redes associadas foi criada em nosso nome. Esse é um bom benefício de uma ferramenta como o CDK, que criou padrões sensíveis para o nosso novo cluster sem precisar especificá-los.

Agora devemos ver que criamos um novo cluster ECS com nosso serviço executando nossa definição de tarefa atual. O CDK nos ajudou, produzindo a URL do nosso balanceador de carga de serviço. Vamos pegar essa URL e verificar se nosso contêiner está executando e aceitando tráfego:

```bash
$ curl Infra-appse-187228PB273DW-1700265048.us-west-2.elb.amazonaws.com
Sample Endpoint
```

![](https://i.giphy.com/media/YucHVxk7WYIc8/giphy.gif)

# Para onde ir daqui?

Agora que nosso contêiner inicial está sendo executado em nosso cluster ECS, podemos dar um passo atrás e explorar para onde podemos ir daqui.

Atualmente, nossa definição de tarefa do ECS está configurada para apontar para a tag **latest** da imagem do Docker que publicamos. Isso significa que podemos atualizar nossa imagem e as alterações podem ser implantadas em nosso cluster. Vamos atualizar a resposta que retornamos da nossa API:

```tsx
api.get('/', (req, res) => {
  res.send('New Response\n');
});
```

Agora vamos criar e enviar uma nova versão da nossa imagem do Docker:

```bash
$ docker build -t sample-express-app .
$ `aws ecr get-login --no-include-email`
$ docker tag sample-express-app <aws-id>.dkr.ecr.us-west-2.amazonaws.com/sample-express-app:latest
$ docker push <aws-id>.dkr.ecr.us-west-2.amazonaws.com/sample-express-app:latest
9a7704a19307: Pushed 
03a86aeeb52b: Layer already exists 
c14651828ff6: Layer already exists 
4ecb552d7aff: Layer already exists 
6ad739b471d2: Layer already exists 
954f92adc866: Layer already exists 
adca1e83b51a: Layer already exists 
73982c948de0: Layer already exists 
84d0c4b192e8: Layer already exists 
a637c551a0da: Layer already exists 
2c8d31157b81: Layer already exists 
7b76d801397d: Layer already exists 
f32868cde90b: Layer already exists 
0db06dff9d9a: Layer already exists 
latest: digest: sha256:ed82982bfa5fe6333c4b67afaae0f36e3208a588736c9586ff81dbdd7e1bc0f5 size: 3256
```

Agora temos uma nova versão da nossa imagem enviada para o nosso repositório. Mas ela não foi implantada em nosso serviço em execução em nosso cluster. Para captar a alteração em nosso cluster, precisamos reiniciar o serviço para que ele puxe a tag **latest** da nossa imagem.

Felizmente, podemos fazer isso com uma chamada de CLI da AWS em nossa linha de comando:

```bash
$ aws ecs update-service --force-new-deployment --cluster app-cluster --service sample-express-app
```

Depois que nossa nova imagem é lançada no serviço (que pode levar alguns minutos), podemos usar **curl** novamente e ver nossa nova resposta:

```bash
$ curl Infra-appse-187228PB273DW-1700265048.us-west-2.elb.amazonaws.com
New Response
```

# Conclusão

Agora, realizamos todo o exercício de criar uma imagem do Docker, provisionar um cluster do ECS e executar nossa imagem dentro do cluster. Nós até demonstramos como podemos atualizar nossa imagem e implantar novas versões em nosso serviço em execução.

Você pode seguir muitas direções. O próximo nível para o DevOps seria configurar um pipeline de CI / CD que permita implantar continuamente suas novas imagens no cluster do ECS. Tenho uma postagem no blog que se concentra na criação de imagens do [Docker usando o AWS CodePipeline e o CodeBuild](https://blog.kylegalbraith.com/2018/08/30/how-to-build-your-docker-images-in-aws-with-ease/) para você começar.

A partir daí, pode valer a pena reduzir os tempos de implantação ao iniciar uma nova imagem em nosso cluster ECS. A razão pela qual isso não é instantâneo é que o balanceador de carga associado ao nosso serviço tem que drenar as conexões ativas. Isso nos permite implementar de forma incremental novas versões para o nosso serviço sem interromper o serviço existente imediatamente. Isso é ideal para uma implantação imperceptível, mas significa que nosso tempo de implantação demoram um pouco mais.

# Quer conferir meus outros projetos?

Eu sou um grande fã da comunidade DEV. Se você tiver alguma dúvida ou quiser conversar sobre idéias diferentes relacionadas à refatoração, [entre em contato no Twitter](https://twitter.com/kylegalbraith) ou envie um comentário abaixo.

Fora dos blogs, criei um curso [Learn AWS Using It](https://kylegalbraith.com/learn-aws/). No curso, nos concentramos em aprender o Amazon Web Services, para hospedar, proteger e fornecer sites estáticos. É um problema simples, com muitas soluções, mas é perfeito para aumentar sua compreensão da AWS. Recentemente, adicionei dois novos capítulos de bônus ao curso, focados na infraestrutura como código e implantação contínua.

Também organizo meu próprio boletim semanal. O boletim [Learn By Doing](https://kylegalbraith.com/learn-by-doing/) é repleto de artigos impressionantes sobre nuvem, codificação e DevOps a cada semana. Inscreva-se para obtê-lo em sua caixa de entrada.

---

# Créditos

- [How to Run Docker Containers via AWS Elastic Container Service](https://dev.to/kylegalbraith/how-to-run-docker-containers-via-aws-elastic-container-service-p47), escrito originalmente por [Kyle Galbraith](https://twitter.com/kylegalbraith)