Contents

Terraform - Implementando Serverless NodeJS Webapp, Secret Mgmt e RDS database no AWS usando Terraform

Views

Terraform é uma ótima ferramenta de infraestrutura como código (IaC) de código aberto. O Terraform é usado para definir e provisionar a infraestrutura completa na nuvem, mantendo e criando versões com segurança e eficiência.

Há algum tempo, participei de um desafio DevOps Terraform/AWS que deve implantar um site estático em menos de 4 horas.

O Desafio

“Com poucas linhas, crie uma API simples em uma linguagem de sua escolha que retorne uma mensagem secreta. A mensagem é muito sensível, então você precisa descobrir a melhor forma de armazená-la. Prevemos que também precisaremos armazenar alguns dados em um banco de dados relacional no futuro, portanto, nosso aplicativo também precisará de acesso ao RDS, embora a configuração da conexão em seu aplicativo seja um crédito extra.

Depois que sua API for montada, escreva um script Terraform para provisionar e carregar a API a ser hospedada no Elastic Beanstalk junto com uma instância RDS. Estamos procurando o uso das melhores práticas de segurança na configuração para ambos.

The approach

Implementamos um aplicativo NodeJS Express capaz de recuperar um segredo do AWS Secret Manager e se conectar a um banco de dados MySQL.

Todos os recursos mencionados devem ser implantados na AWS usando o Terraform.

Alguns deles são:

  • AWS Elastic Beanstalk Environment, Application and Versioning
  • AWS IAM rules to allow multi-service communication
  • AWS S3 Bucket to keep the application code versioning
  • AWS Secret Manager to keep a application secret
  • AWS VPC/Subnets/Gateway - To allow Public and Private network access of services
  • AWS RDS to deploy a private MySQL database

Durante a implantação dos scripts Terraform, o código do aplicação é copiado para o S3 e, em seguida, a nova versão é implantada no ambiente do EBS.

Solução

Todos os scripts do Terraform podem ser encontrados no repositório a seguir.https://github.com/mvitor/aws-api-rds-secret Estou destacando o código mais relevante abaixo.

AWS Elastic Beanstalk Environment, Application and Versioning

AWS Beanstalk Application

Cria um recurso de aplicativo Elastic Beanstalk. O AWS EBS é um serviço que nos permite implantar e gerenciar aplicativos da web na nuvem AWS sem gerenciar a infraestrutura do servidor web.

1
2
3
4
resource "aws_elastic_beanstalk_application" "app" {
  name        = "${var.service_name}"
  description = "${var.service_description}"
}

Terraform resource link: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elastic_beanstalk_application

AWS Beanstalk Environment

Cria um recurso de Environment Elastic Beanstalk. Elastic Beanstalk Environment é uma coleção de recursos da AWS executando uma ou mais versões de aplicativos.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
resource "aws_elastic_beanstalk_environment" "app-prod" {
  name = "app-prod"
  application = "${aws_elastic_beanstalk_application.app.name}"
  solution_stack_name = "64bit Amazon Linux 2 v5.4.5 running Node.js 14"
  setting {
    namespace = "aws:ec2:vpc"
    name      = "VPCId"
    value     = "${aws_vpc.main.id}"
  }
  setting {
    namespace = "aws:ec2:vpc"
    name = "Subnets"
    value = "${aws_subnet.subnet-private-1.id},${aws_subnet.subnet-private-2.id}"
  }
  setting {
    namespace = "aws:ec2:vpc"
    name = "AssociatePublicIpAddress"
    value = "false"
  }
  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name = "IamInstanceProfile"
    value = "app-ec2-rolepf"
  }
  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name = "SecurityGroups"
    value = "${aws_security_group.app-prod.id}"
  }
  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name = "EC2KeyName"
    value = "${aws_key_pair.app.id}"
  }
  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name = "InstanceType"
    value = "t2.micro"
  }
  setting {
    namespace = "aws:elasticbeanstalk:environment"
    name = "ServiceRole"
    value = "aws-elasticbeanstalk-service-role"
  }
  setting {
    namespace = "aws:ec2:vpc"
    name = "ELBScheme"
    value = "public"
  }
  setting {
    namespace = "aws:ec2:vpc"
    name = "ELBSubnets"
    value = "${aws_subnet.subnet-public-1.id},${aws_subnet.subnet-public-2.id}"
  }
  setting {
    namespace = "aws:elb:loadbalancer"
    name = "CrossZone"
    value = "true"
  }
  setting {
    namespace = "aws:elasticbeanstalk:command"
    name = "BatchSize"
    value = "30"
  }
  setting {
    namespace = "aws:elasticbeanstalk:command"
    name = "BatchSizeType"
    value = "Percentage"
  }
  setting {
    namespace = "aws:autoscaling:asg"
    name = "Availability Zones"
    value = "Any 2"
  }
  setting {
    namespace = "aws:autoscaling:asg"
    name = "MinSize"
    value = "1"
  }
  setting {
    namespace = "aws:autoscaling:updatepolicy:rollingupdate"
    name = "RollingUpdateType"
    value = "Health"
  }
  # LB healthcheck
  setting {
    namespace = "${var.environmentType == "UseLoadBalancer" ? "aws:elb:healthcheck" : "aws:elasticbeanstalk:environment"}"
    name      = "${var.environmentType == "UseLoadBalancer" ? "HealthyThreshold" : "EnvironmentType"}"
    value     = "${var.environmentType == "UseLoadBalancer" ? var.healthcheck_healthy_threshold : var.environmentType}"
  }
  setting {
    namespace = "${var.environmentType == "UseLoadBalancer" ? "aws:elb:healthcheck" : "aws:elasticbeanstalk:environment"}"
    name      = "${var.environmentType == "UseLoadBalancer" ? "UnhealthyThreshold" : "EnvironmentType"}"
    value     = "${var.environmentType == "UseLoadBalancer" ? var.healthcheck_unhealthy_threshold : var.environmentType}"
  }
  setting {
    namespace = "${var.environmentType == "UseLoadBalancer" ? "aws:elb:healthcheck" : "aws:elasticbeanstalk:environment"}"
    name      = "${var.environmentType == "UseLoadBalancer" ? "Interval" : "EnvironmentType"}"
    value     = "${var.environmentType == "UseLoadBalancer" ? var.healthcheck_interval : var.environmentType}"
  }
  setting {
    namespace = "${var.environmentType == "UseLoadBalancer" ? "aws:elb:healthcheck" : "aws:elasticbeanstalk:environment"}"
    name      = "${var.environmentType == "UseLoadBalancer" ? "Timeout" : "EnvironmentType"}"
    value     = "${var.environmentType == "UseLoadBalancer" ? var.healthcheck_timeout : var.environmentType}"
  }
  # Autoscaling group triggers.
  setting {
    namespace = "${var.environmentType == "UseLoadBalancer" ? "aws:autoscaling:trigger" : "aws:elasticbeanstalk:environment"}"
    name      = "${var.environmentType == "UseLoadBalancer" ? "BreachDuration" : "EnvironmentType"}"
    value     = "${var.environmentType == "UseLoadBalancer" ? var.as_breach_duration : var.environmentType}"
  }
  setting {
    namespace = "${var.environmentType == "UseLoadBalancer" ? "aws:autoscaling:trigger" : "aws:elasticbeanstalk:environment"}"
    name      = "${var.environmentType == "UseLoadBalancer" ? "LowerBreachScaleIncrement" : "EnvironmentType"}"
    value     = "${var.environmentType == "UseLoadBalancer" ? var.as_lower_breach_scale_increment : var.environmentType}"
  }
  setting {
    namespace = "${var.environmentType == "UseLoadBalancer" ? "aws:autoscaling:trigger" : "aws:elasticbeanstalk:environment"}"
    name      = "${var.environmentType == "UseLoadBalancer" ? "LowerThreshold" : "EnvironmentType"}"
    value     = "${var.environmentType == "UseLoadBalancer" ? var.as_lower_threshold : var.environmentType}"
  }
  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name = "RDS_USERNAME"
    value = "${aws_db_instance.rds-mysqldb.username}"
  }
  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name = "RDS_PASSWORD"
    value = "${aws_db_instance.rds-mysqldb.password}"
  }
  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name = "RDS_DATABASE"
    value = "${aws_db_instance.rds-mysqldb.name}"
  }
  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name = "RDS_HOSTNAME"
    value = "${aws_db_instance.rds-mysqldb.address}"
  }
  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name = "AWS_REGION"
    value = "${var.AWS_REGION}"
  }
}
Terraform resource link: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elastic_beanstalk_environment

AWS S3 Bucket to keep the application code versioning

Primeiro, crie um arquivos zip de implantação com o utilitário terraform ‘archive_file’ e, em seguida, criamos um novo bucket com esse arquivo zip.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
data "archive_file" "api_dist_zip" {
  type        = "zip"
  source_dir = "../${path.root}/${var.api_dist}"
  output_path = "../${path.root}/${var.api_dist}.zip"
}
resource "aws_s3_bucket" "dist_bucket" {
  bucket = "${var.namespace}-elb-dist"
  acl    = "private"
}
resource "aws_s3_bucket_object" "dist_item" {
  key    = "${var.env}/dist-${uuid()}"
  bucket = "${aws_s3_bucket.dist_bucket.id}"
  source = "${var.dist_zip}"
}
Terraform resource link: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_object

AWS Beanstalk Versioning

Cria um recurso de versão do aplicativo Elastic Beanstalk. Elastic Beanstalk Version é o serviço que permite carregar e manter nosso código no ambiente EBS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
resource "aws_elastic_beanstalk_application_version" "app-prod" {
  name        = "${var.namespace}-${var.env}-${uuid()}"
  application = "My application"
  description = "application version created by terraform"
  bucket      = "${aws_s3_bucket.dist_bucket.id}"
  key         = "${aws_s3_bucket_object.dist_item.id}"
  provisioner "local-exec" {
    command = "aws --region ${var.AWS_REGION} elasticbeanstalk update-environment --environment-name app-prod --version-label ${aws_elastic_beanstalk_application_version.app-prod.name}"
  }
}
Terraform resource link: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elastic_beanstalk_application

AWS Secret Manager to keep the application secret

That secret will be dynamically queried during Webapp Index loading.

1
2
3
4
5
6
7
resource "aws_secretsmanager_secret" "mysecretmanager" {
  name = "mysecret003"
}
resource "aws_secretsmanager_secret_version" "mysecretmanager" {
  secret_id     = aws_secretsmanager_secret.mysecretmanager.id
  secret_string = "mysecretvalue003"
}
Terraform resource link: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret

AWS RDS to deploy a private MySQL database

Precisamos fornecer um banco de dados MySQL privado e conectar com o driver MySQL no aplicativo NodeJS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
resource "aws_db_instance" "rds-mysqldb" {
  allocated_storage    = 10
  engine               = "mysql"
  engine_version       = "5.7"
  instance_class       = "db.t2.micro"
  identifier           = "mysqldb"
  name                 = "mydb"
  username             = "root"
  password             = "change1intPws"
  db_subnet_group_name = "${aws_db_subnet_group.rds-mysqldb.name}"
  parameter_group_name = "default.mysql5.7"
  multi_az             = "false"
  vpc_security_group_ids = ["${aws_security_group.rds-mysqldbsg.id}"]
  storage_type         = "gp2"
  backup_retention_period = 30
  publicly_accessible =  false
  deletion_protection = false
  skip_final_snapshot = true
  tags = {
      Name = "rds-appprod"
      Type = "course_exam"
  }
}
Terraform resource link: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance

AWS RDS - Security Groups and Subnets

Precisamos de de alguns security groups para permitir a comunicação por meio dos recursos das Subnets privada.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
resource "aws_security_group" "rds-mysqldbsg" {
  vpc_id = "${aws_vpc.main.id}"
  name = "rds-mysqldb-sg"
  description = "Allow inbound mysql traffic"
  tags = {
    Name = "rds-mysqldb"
    Type = "course_exam"

  }
}
resource "aws_security_group_rule" "allow-mysql" {
    type = "ingress"
    from_port = 3306
    to_port = 3306
    protocol = "tcp"
    security_group_id = "${aws_security_group.rds-mysqldbsg.id}"
    source_security_group_id = "${aws_security_group.app-prod.id}"
}
resource "aws_security_group_rule" "allow-outgoing" {
    type = "egress"
    from_port = 0
    to_port = 0
    protocol = "-1"
    security_group_id = "${aws_security_group.rds-mysqldbsg.id}"
    cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_db_subnet_group" "rds-mysqldb" {
    name = "rds-mysqldb"
    description = "RDS subnet group"
    subnet_ids = ["${aws_subnet.subnet-private-1.id}", "${aws_subnet.subnet-private-2.id}"]
}
Terraform resource link: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group

Regras de IAM, VPC, NAT e configuração de sub-rede podem ser encontradas no repositório GitHub.