Criação e configuração de um servidor Vault utilizando Terraform

Criação e configuração de um servidor Vault utilizando Terraform

O problema

Trabalhar com tecnologia nos dias de hoje normalmente quer dizer que você vai utilizar algum produto de software como serviço, que pode ser desde um aplicativo de mensagem (ex: Slack, RocketChat), um sistema que auxilia na organização do trabalho no dia a dia (ex: Trello, Jira) ou até mesmo um sistema que te permita subir e controlar toda sua infraestrutura em nuvem (ex: AWS, Azure).

O problema é que nem sempre é viável ter uma conta por pessoa na empresa, às vezes porque o produto em questão não dá suporte a multiplas contas, às vezes por uma questão de custo, já que muitos softwares cobram por usuário ou simplesmente por uma questão de praticidade, já que é mais fácil gerenciar uma conta só ao invés de vários acessos.

Por conta disso é normal pessoas trabalhando num mesmo projeto terem acesso a uma mesma conta, e o compartilhamento de senhas normalmente é feito por mensagens ou planilhas. Isso gera uma falha de segurança dentro do projeto porque aplicativos de mensagem e planilhas não foram feitos para enviar informações sensíveis como senhas, mas são utilizados dessa forma pela falta de uma alternativa melhor que seja tão prática quanto.

A solução

O Vault é um sistema criado pela Hashicorp com o intuito de tornar seguro esse compartilhamento de informações sensíveis. Ele permite não só o armazenamento de senhas, como também o controle de quem pode ver o que, geração de senhas e credenciais que expiram de forma automática, bem como a possibilidade de utilizar a apliação via interface Web, aplicação de linha de comando, ou requisições HTTP via API.

Só que agora nós caímos em outro problema: criar e configurar um servidor Vault com todas as políticas de acesso rapidamente se torna uma tarefa complicada, com muito passos, e até mesmo confusa, já que a Hashicorp permite que isso seja feito de diversas formas.

O objetivo desse artigo é mostrar como essa tarefa pode ser feita utilizando Terraform.

TLDR: Caso você não esteja interessado em ler o artigo e quer apenas saber como fica a solução final o código resultante está neste repositório.
Porém, se você apenas aplicar o código do repositório não vai funcionar. A ordem de execução de alguns passos é importante então use o repositório como referência e leia o artigo para entender como atingir o mesmo resultado.

Como funciona e por que Terraform?

O Terraform é uma ferramenta criada também pela Hashicorp, e é uma das principais quando se fala de Infraestrutura como Código, ou IaC ( Infrastructure as Code), em termos de orquestração e gerenciamento da infraestrutura.

IaC é uma das práticas do modelo DevOps onde o gerenciamento da infraestrutura é tratado como desenvolvimento de software. Isso permite a aplicação de boas práticas de desenvolvimento em cima não só do código, mas também da infraestrutura onde o código irá ser executado. Essas boas práticas envolvem por exemplo: controle de versão (git), teste e revisão de código, integração e entrega contínua, DRY (Don’t Repeat Yourself).

O Terraform usa a configuração escrita em código para planejar a infraestrutura e fornece um feedback, informando quais recursos serão adicionados, alterados ou excluídos. Se você estiver satisfeito com seu plano, poderá aplicar a infraestrutura, e o Terraform se encarregará de chamar as APIs certas para você.

Após aplicar a configuração escrita, o Terraform armazena o estado da infraestrutura em um arquivo no formato JSON que é usado de referência nos planos futuros.

Por padrão esse arquivo é armazenado localmente e chamado de terraform.tfstate, porém é recomendado que esse arquivo seja armazenado de forma segura e remota.

Esse artigo utilizará um Bucket da S3 para armazenar o estado do Terraform.

Pré-requisitos

Para seguir os passos descritos nesse artigo você precisa ter:

Você também precisa configurar a CLI da AWS com as suas credenciais. Você pode fazer isso através do seguinte comando:

aws configure

Servidor

Estrutura inicial

Comece o projeto criando uma pasta que irá conter a configuração do Terraform.

mkdir vault-terraform
cd vault-terraform

Crie o arquivo onde vamos começar a definir nossa configuração, chamado de main.tf.

touch main.tf

Abra o arquivo no seu editor de texto favorito e cole a seguinte configuração:

terraform {
  backend "s3" {
    bucket = ""
    key    = "vault/terraform.tfstate"
    region = "us-east-1"
  }

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.27"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

O bloco terraform contém as configurações básicas do próprio Terraform. O bloco required_providers indica quais módulos devem ser baixados do Terraform Registry, aqui no caso estamos usando o módulo da AWS.

O bloco backend é a configuração que indica onde e como vamos salvar o arquivo de estado do Terraform. Note que nesse caso estamos declarando que o estado deve ser salvo em um Bucket da S3, porém o nome do Bucket está vazio na configuração.

Utilize a CLI da AWS para criar um Bucket privado, trocando <nome-do-bucket> por um nome válido:

aws s3api create-bucket --bucket <nome-do-bucket> --acl private

Ative o versionamento e bloqueie todo o acesso público ao Bucket:

ws s3api put-bucket-versioning --bucket <nome-do-bucket> --versioning-configuration Status=Enabled
aws s3api put-public-access-block \
    --bucket <nome-do-bucket> \
    --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Coloque o nome do bucket no bloco de configuração backend:

backend "s3" {
	bucket = "<nome-do-bucket>"
	key    = "vault/terraform.tfstate"
	region = "us-east-1"
}

Quando você cria uma nova configuração do Terraform você precisa inicializar o diretório, isso irá fazer o download dos módulos necessários, neste caso o módulo da AWS. Faça isso com o seguinte comando:

terraform init

Armazenamento

Agora vamos criar um módulo para armazenar a configuração do servidor. Um módulo consiste em um conjunto de recursos que são utilizados juntos.

Comece criando uma pasta e um arquivo main.tf para armazenar a configuração do módulo:

mkdir -p modules/server
touch modules/server/main.tf

Vamos começar criando um Bucket que será usado pelo Vault como forma de armazenamento:

resource "aws_s3_bucket" "vault" {
  bucket = var.vault_bucket_name
  acl    = "private"

  tags = {
    Name        = "Vault Bucket"
    Description = "Vaults backend S3 Bucket"
    Project     = "Vault"
    Terraform   = "true"
  }

  versioning {
    enabled    = true
    mfa_delete = false
  }
}

Note que o nome do Bucket não está explicitamente definido, ao invés disso está esperando o valor de uma variável. Porém a variável não está definida.

Crie um arquivo dentro do módulo para definir as variáveis.

touch modules/server/variables.tf

Declare a variável do nome do bucket:

variable "vault_bucket_name" {
  type        = string
  description = "The name of the bucket that will be used as backend for Vault"
}

Chave para criptografia

O Vault possui uma funcionalidade de criptografia adicional, mas recomendada. Crie uma chave do AWS KMS que será usada pelo Vault.

resource "aws_kms_key" "vault_key" {
  description = "Vault KMS key for Auto Unseal"

  tags = {
    Name        = "Vault KMS Key"
    Description = "Vault KMS key for Auto Unseal"
    Project     = "Vault"
    Terraform   = "true"
  }
}

Orquestração de containers com ECS

Task Definition

Para executar o Vault vamos utilizar o serviço ECS, o orquestrador de containers da AWS. Para isso vamos criar uma Task Definition, que é o artefato da ECS que irá conter as configurações sobre como executar o container do Vault.

resource "aws_ecs_task_definition" "vault" {
  family                   = "vault"
  network_mode             = "bridge"
  task_role_arn            = ""
  execution_role_arn       = ""
  requires_compatibilities = ["EC2"]

  tags = {
    Name        = "Vault Task Definition"
    Description = "Vaults tasks execution configuration"
    Project     = "Vault"
    Terraform   = "true"
  }

  container_definitions = jsonencode([{
    name              = "vault"
    image             = "vault:latest"
    essential         = true
    cpu               = 256
    memory            = 512
    memoryReservation = 256
    mountPoints       = []
    environment       = []
    command = [
      "vault",
      "server",
      "-config=/vault/config/local.json",
    ]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group         = "/ecs/vault"
        awslogs-region        = "us-east-1"
        awslogs-stream-prefix = "ecs"
      }
    }
    volumesFrom = []
    portMappings = [
      {
        hostPort      = 8200
        containerPort = 8200
        protocol      = "tcp"
      },
    ]
    secrets = [
      {
        name      = "AWS_REGION"
        valueFrom = ""
      },
      {
        name      = "AWS_S3_BUCKET"
        valueFrom = ""
      },
      {
        name      = "SKIP_SETCAP"
        valueFrom = ""
      },
      {
        name      = "VAULT_ADDR"
        valueFrom = ""
      },
      {
        name      = "VAULT_AWSKMS_SEAL_KEY_ID"
        valueFrom = ""
      },
      {
        name      = "VAULT_LOCAL_CONFIG"
        valueFrom = ""
      },
      {
        name      = "VAULT_SEAL_TYPE"
        valueFrom = ""
      },
    ]
  }])
}

Note que a configuração ainda não está completa, é preciso ainda preencher os valores das variáveis de ambiente.

Variáveis de ambiente

Vamos utilizar o Parameter Store da AWS para armazenar as variáveis:

resource "aws_ssm_parameter" "vault_aws_region" {
  name        = "/vault/AWS_REGION"
  description = "Vault task definition environment variable for AWS_REGION"
  type        = "String"
  value       = "us-east-1"

  tags = {
    Name        = "SSM Parameter /vault/AWS_REGION"
    Description = "Vault task definition environment variable for AWS_REGION"
    Project     = "Vault"
    Terraform   = "true"
  }
}

resource "aws_ssm_parameter" "vault_aws_s3_bucket" {
  name        = "/vault/AWS_S3_BUCKET"
  description = "Vault task definition environment variable for AWS_S3_BUCKET"
  type        = "String"
  value       = aws_s3_bucket.vault.bucket

  tags = {
    Name        = "SSM Parameter /vault/AWS_S3_BUCKET"
    Description = "Vault task definition environment variable for AWS_S3_BUCKET"
    Project     = "Vault"
    Terraform   = "true"
  }
}

resource "aws_ssm_parameter" "vault_skip_setcap" {
  name        = "/vault/SKIP_SETCAP"
  description = "Vault task definition environment variable for SKIP_SETCAP"
  type        = "String"
  value       = "true"

  tags = {
    Name        = "SSM Parameter /vault/SKIP_SETCAP"
    Description = "Vault task definition environment variable for SKIP_SETCAP"
    Project     = "Vault"
    Terraform   = "true"
  }
}

resource "aws_ssm_parameter" "vault_vault_addr" {
  name        = "/vault/VAULT_ADDR"
  description = "Vault task definition environment variable for VAULT_ADDR"
  type        = "String"
  value       = "http://127.0.0.1:8200"

  tags = {
    Name        = "SSM Parameter /vault/VAULT_ADDR"
    Description = "Vault task definition environment variable for VAULT_ADDR"
    Project     = "Vault"
    Terraform   = "true"
  }
}

resource "aws_ssm_parameter" "vault_vault_awskms_seal_key_id" {
  name        = "/vault/VAULT_AWSKMS_SEAL_KEY_ID"
  description = "Vault task definition environment variable for VAULT_AWSKMS_SEAL_KEY_ID"
  type        = "String"
  value       = aws_kms_key.vault_key.id

  tags = {
    Name        = "SSM Parameter /vault/VAULT_AWSKMS_SEAL_KEY_ID"
    description = "Vault task definition environment variable for VAULT_AWSKMS_SEAL_KEY_ID"
    Project     = "Vault"
    Terraform   = "true"
  }
}

resource "aws_ssm_parameter" "vault_vault_local_config" {
  name        = "/vault/VAULT_LOCAL_CONFIG"
  description = "Vault task definition environment variable for VAULT_LOCAL_CONFIG"
  type        = "String"
  value       = "{\"ui\":true,\"backend\":{\"s3\":{}},\"seal\":{\"awskms\":{}},\"listener\":{\"tcp\":{\"address\":\"0.0.0.0:8200\",\"tls_disable\":1}}}"

  tags = {
    Name        = "SSM Parameter /vault/VAULT_LOCAL_CONFIG"
    Description = "Vault task definition environment variable for VAULT_LOCAL_CONFIG"
    Project     = "Vault"
    Terraform   = "true"
  }
}

resource "aws_ssm_parameter" "vault_vault_seal_type" {
  name        = "/vault/VAULT_SEAL_TYPE"
  description = "Vault task definition environment variable for VAULT_SEAL_TYPE"
  type        = "String"
  value       = "awskms"

  tags = {
    Name        = "SSM Parameter /vault/VAULT_SEAL_TYPE"
    Description = "Vault task definition environment variable for VAULT_SEAL_TYPE"
    Project     = "Vault"
    Terraform   = "true"
  }
}

A configuração do Vault presente na variável VAULT_LOCAL_CONFIG é a seguinte:

{
	"ui": true,
	"backend": {
		"s3": {}
	},
	"seal": {
		"awskms": {}
	},
	"listener": {
		"tcp": {
			"address": "0.0.0.0:8200",
			"tls_disable": 1
		}
	}
}

A configuração de backend e seal é feita através das variáveis AWS_S3_BUCKET e VAULT_AWSKMS_SEAL_KEY_ID.

Além disso, seus valores não são passados de forma arbitrária, mas sim de forma dinâmica através dos recursos criados anteriormente, referenciados com aws_s3_bucket.vault.bucket e aws_kms_key.vault_key.id.

Volte na configuração da Task Definition e atualize o valor das variáveis através dos recursos do tipo aws_ssm_parameter criados.

resource "aws_ecs_task_definition" "vault" {
	family                   = "vault"
	network_mode             = "bridge"
  
  ...
  
	container_definitions = jsonencode([{
		name              = "vault"
		image             = "vault:latest"
		
		...
  
		secrets = [
			{
				name      = "AWS_REGION"
				valueFrom = aws_ssm_parameter.vault_aws_region.arn
			},
			{
				name      = "AWS_S3_BUCKET"
				valueFrom = aws_ssm_parameter.vault_aws_s3_bucket.arn
			},
			{
				name      = "SKIP_SETCAP"
				valueFrom = aws_ssm_parameter.vault_skip_setcap.arn
			},
			{
				name      = "VAULT_ADDR"
				valueFrom = aws_ssm_parameter.vault_vault_addr.arn
			},
			{
				name      = "VAULT_AWSKMS_SEAL_KEY_ID"
				valueFrom = aws_ssm_parameter.vault_vault_awskms_seal_key_id.arn
			},
			{
				name      = "VAULT_LOCAL_CONFIG"
				valueFrom = aws_ssm_parameter.vault_vault_local_config.arn
			},
			{
				name      = "VAULT_SEAL_TYPE"
				valueFrom = aws_ssm_parameter.vault_vault_seal_type.arn
			},
		]
	}])
}

Permissionamento

Além das variáveis de ambiente, o container do Vault também precisa ter acesso a outros recursos da AWS.

Além de acesso ao Bucket da S3 e à chave do KMS, o container também precisa de acesso à SSM para recuperar o valor das variáveis de ambiente, bem como ao CloudWatch para escrever os logs da aplicação.

A configuração da Task Definition possui dois atributos: task_role_arn e execution_role_arn, mas para fins de simplicidade vamos colocar todos os acessos em uma só role.

Primeiro, crie uma policy que dê acesso à chave KMS:

resource "aws_iam_policy" "kms_management_access" {
  name = "KMSManagementAccess"
  path = "/"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:DescribeKey",
      ]
      Effect   = "Allow"
      Resource = "*"
    }]
  })

  tags = {
    Name        = "KMS Management Access Policy"
    Description = "Policy that allows KMS Keys encryption, decription and describing"
    Project     = "Vault"
    Terraform   = "true"
  }
}

Agora crie uma role para a Task Definition que contenha os acessos mencionados:

resource "aws_iam_role" "vault_task_role" {
  name        = "VaultTaskRole"
  description = "Allows Vault task to call necessaries AWS services in order to start"
  path        = "/"

  managed_policy_arns = [
    aws_iam_policy.kms_management_access.arn,
    "arn:aws:iam::aws:policy/AmazonS3FullAccess",
    "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess",
    "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess",
  ]

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid    = ""
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ecs-tasks.amazonaws.com"
      }
    }]
  })

  tags = {
    Name        = "Vault Task Role"
    Description = "Allows Vault task to call necessaries AWS services in order to start"
    Project     = "Vault"
    Terraform   = "true"
  }
}

Volte à configuração da Task Definition e passe o ARN da role para os atributos necessários:

resource "aws_ecs_task_definition" "vault" {
	family                   = "vault"
	network_mode             = "bridge"
	task_role_arn            = aws_iam_role.vault_task_role.arn
	execution_role_arn       = aws_iam_role.vault_task_role.arn
	# ...
}

Cluster

Crie o Cluster da ECS. Um Cluster é um agrupamento lógico de serviços dentro da ECS, sendo um serviço o próximo recurso que iremos configurar. É também no Cluster que iremos atrelar uma instância da EC2.

resource "aws_ecs_cluster" "vault" {
  name = "Vault"

  tags = {
    Name        = "Vault Cluster"
    Description = "Vault ECS Cluster"
    Project     = "Vault"
    Terraform   = "true"
  }
}

Serviço

O último recurso a ser criado na ECS é o serviço, que é um conjunto de configurações sobre como executar a Task Definition dentro do Cluster.

resource "aws_ecs_service" "vault" {
  name            = "Vault"
  cluster         = aws_ecs_cluster.vault.id
  task_definition = aws_ecs_task_definition.vault.arn
  desired_count   = 1

  ordered_placement_strategy {
    field = "instanceId"
    type  = "spread"
  }

  ordered_placement_strategy {
    field = "attribute:ecs.availability-zone"
    type  = "spread"
  }

  tags = {
    Name        = "Vault Service"
    Description = "Vault ECS Service"
    Project     = "Vault"
    Terraform   = "true"
  }
}

Instância EC2 via solicitação Spot

Agora precisamos criar as instâncias da EC2 onde se executará os serviços do Cluster. Para isso vamos utilizar instâncias Spot, um modelo de leilão de instâncias ociosas, com a intenção de reduzir o custo da infraestrutura.

Modelo de execução

Vamos começar criando um modelo de execução que especifica as configurações da instância:

resource "aws_launch_template" "vault" {
  name          = "Vault"
  image_id      = ""
  instance_type = ""
  key_name      = ""
  user_data     = ""

  placement {
    tenancy = "default"
  }

  iam_instance_profile {
    arn = ""
  }

  block_device_mappings {
    device_name = "/dev/sda1"
    ebs {
      delete_on_termination = true
      volume_size           = 30
      volume_type           = "gp2"
    }
  }

  network_interfaces {
    description                 = "Vault spot instance network interface"
    device_index                = 0
    delete_on_termination       = true
    associate_public_ip_address = true
    security_groups             = []
  }

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name        = "Vault"
      Description = "Vault spot fleet request's instance"
      Project     = "Vault"
      Terraform   = "true"
    }
  }

  tags = {
    Name        = "Vault"
    Description = "Vault instance launch template"
    Project     = "Vault"
    Terraform   = "true"
  }
}

Note que ainda há algumas configurações que precisamos completar.

AMI

Primeiro, vamos buscar na AWS a versão mais recente da AMI feita especificamente para executar Clusters da ECS:

data "aws_ami" "ecs_ami" {
  most_recent      = true
  owners           = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn-ami-*-amazon-ecs-optimized"]
  }
}

Tipos de instância

Crie uma variável no arquivo modules/server/variables.tf para receber os tipos de instância como um parâmetro do módulo. A requisição Spot vai tentar localizar uma instância ociosa dentre esses tipos, quanto maior o número de instâncias mais fácil achar uma disponível, mas tipicamente com 3 tipos já se consegue uma alta disponibilidade.

variable "instance_type" {
  type        = list(string)
  description = "Instance type for server's EC2 instance"
  default     = ["t3.micro", "t3a.micro", "t2.micro"]
}

Chave SSH / Keypair

Agora vamos criar um par de chaves que serão utilizadas na comunicação SSH com a instância:

resource "tls_private_key" "vault" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "aws_key_pair" "vault" {
  key_name   = "vault"
  public_key = tls_private_key.vault.public_key_openssh

  tags = {
    Name        = "Vault public key"
    Description = "Vault Instance OpenSSH public key"
    Project     = "Vault"
    Terraform   = "true"
  }
}

User data

Para associar a instância ao Cluster é preciso adicionar um script ao atributo user_data. Scripts passados para esse parâmetro são executados quando a instância inicia.

data "template_file" "vault_instance_user_data" {
  template = <<EOF
    #!/bin/bash
    echo "ECS_CLUSTER=${aws_ecs_cluster.vault.name}" >> /etc/ecs/ecs.config
  EOF
}

Permissionamento da instância

A instância da EC2 precisa também de uma role que contenha a policy AmazonEC2ContainerServiceforEC2Role, que contém as permissões que uma instância necessita para ser utilizada por um Cluster da ECS.

resource "aws_iam_role" "ecs_instance_role" {
  name        = "EcsInstanceRole"
  description = "Allows ECS instances to register to containers"
  path        = "/"

  managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"]

  assume_role_policy = jsonencode({
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
        Sid = ""
      },
    ]
    Version = "2008-10-17"
  })


  tags = {
    Name        = "ECS Instance Role"
    Description = "Allows EC2 instances to be used as an ECS Cluster"
    Terraform   = "true"
  }
}

É preciso criar juntamente da role um Instance Profile para associar a role à configuração da instância.

resource "aws_iam_instance_profile" "ecs_instance_profile" {
  name = "EcsInstanceProfile"
  role = aws_iam_role.ecs_instance_role.name

  tags = {
    Name        = "ECS Instance Profile"
    Description = "Allows EC2 instances to be used as an ECS Cluster"
    Terraform   = "true"
  }
}

Security Group

E para finalizar as configurações da instância, vamos criar o Security Group que vai atuar como firewall virtual do servidor:

resource "aws_security_group" "vault_instance" {
  name        = "Vault Instance"
  description = "Vault Instance Security Group rules"

  ingress = [
    {
      description      = "SSH"
      from_port        = 22
      to_port          = 22
      protocol         = "tcp"
      cidr_blocks      = ["0.0.0.0/0"]
      self             = false
      security_groups  = []
      prefix_list_ids  = []
      ipv6_cidr_blocks = []
    },
    {
      description      = "SSH"
      from_port        = 8200
      to_port          = 8200
      protocol         = "tcp"
      cidr_blocks      = ["0.0.0.0/0"]
      self             = false
      security_groups  = []
      prefix_list_ids  = []
      ipv6_cidr_blocks = []
    },
  ]

  egress = [{
    description      = "Default"
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    self             = false
    security_groups  = []
    prefix_list_ids  = []
    ipv6_cidr_blocks = []
  }]

  tags = {
    Name        = "Vault Instance SG"
    Description = "Vault Instance Security Group rules"
    Project     = "Vault"
    Terraform   = "true"
  }
}

A configuração acima vai permitir trafego na porta 8200, onde estará rodando a aplicação do Vault, e na porta 22, que será utilizada para a conexão SSH.

Finalização da configuração do modelo

Volte na configuração do modelo de execução e preencha os valores vazios, fazendo referência aos respectivos recursos que acabamos de criar:

resource "aws_launch_template" "vault" {
  name          = "Vault"
  image_id      = data.aws_ami.ecs_ami
  instance_type = var.instance_types[0]
  key_name      = aws_key_pair.vault.key_name
  user_data     = base64encode(data.template_file.vault_instance_user_data.rendered)

  placement {
    tenancy = "default"
  }

  iam_instance_profile {
    arn = aws_iam_instance_profile.ecs_instance_profile.arn
  }

  block_device_mappings {
    device_name = "/dev/sda1"
    ebs {
      delete_on_termination = true
      volume_size           = 30
      volume_type           = "gp2"
    }
  }

  network_interfaces {
    description                 = "Vault spot instance network interface"
    device_index                = 0
    delete_on_termination       = true
    associate_public_ip_address = true
    security_groups             = [aws_security_group.vault_instance.id]
  }

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name        = "Vault"
      Description = "Vault spot fleet request's instance"
      Project     = "Vault"
      Terraform   = "true"
    }
  }

  tags = {
    Name        = "Vault"
    Description = "Vault instance launch template"
    Project     = "Vault"
    Terraform   = "true"
  }
}

Solicitação Spot

Agora vamos criar a configuração para a solicitação Spot passando o modelo de execução:

resource "aws_spot_fleet_request" "vault" {
  instance_pools_to_use_count         = 1
  target_capacity                     = 1
  terminate_instances_with_expiration = false
  allocation_strategy                 = "lowestPrice"
  excess_capacity_termination_policy  = "Default"
  valid_until                         = "2999-11-04T20:44:20Z"
  iam_fleet_role                      = ""

  tags = {
    "Name"        = "Vault"
    "Description" = "Vault spot fleet request"
    "Project"     = "Vault"
    "Terraform"   = "true"
  }

  launch_template_config {
    launch_template_specification {
      id      = aws_launch_template.vault.id
      version = aws_launch_template.vault.latest_version
    }

    dynamic "overrides" {
      for_each = var.instance_types
      content {
        instance_type = overrides.value
      }
    }
  }
}

O bloco dynamic "overrides" itera sobre a variável de instance_types e declara um bloco do tipo overrides para cada item da lista.

Permissionamento da solicitação

A solicitação Spot precisa de permissões para solicitar, executar, encerrar e marcar recursos.

Quando a solicitação é feita pelo console é criado automaticamente uma role chamada aws-ec2-spot-fleet-tagging-role com as permissões necessárias, crie essa role utilizando a CLI:

aws iam create-service-linked-role --aws-service-name spotfleet.amazonaws.com

Agora é necessário adicionar o ARN da role na configuração, que é um dado que segue o seguinte formato: arn:aws:iam::<id-da-conta>:role/aws-ec2-spot-fleet-tagging-role.

Vamos buscar o id da conta na AWS:

data "aws_caller_identity" "current" {}

locals {
  account_id = data.aws_caller_identity.current.account_id
}

O bloco locals define uma variável local que extrai o id da conta da fonte de dados aws_caller_identity.

Atribua o ARN da role ao atributo iam_fleet_role:

resource "aws_spot_fleet_request" "vault" {
	# ...
	iam_fleet_role                      = "arn:aws:iam::${local.account_id}:role/aws-ec2-spot-fleet-tagging-role"
}

Importando o módulo do servidor

Volte ao arquivo main.tf e importe o módulo do servidor, lembrando de passar uma variável para o nome do Bucket:

module "server" {
  source = "./modules/server"

  vault_bucket_name = "<nome-do-bucket>"
}

Planejando e aplicando a configuração

Rode o comando de plan para ter noção do que será criado pelo Terraform: terraform plan.

Use o comando apply para criar a infraestrutura: terraform apply.

Pronto, sua infraestrutura foi criada e o seu Vault já está acessível 🚀 mas nosso trabalho ainda não acabou.

E agora?

Com o Vault já disponível é preciso criar os usuários e configurar as todas as políticas, cada uma com seu nível de acesso, que vão ser utilizadas para dar as devidas permissões aos usuários.

Porém, essa é uma tarefa trabalhosa e que não cabe nesse artigo, já que estamos falando sobre a infraestrutura necessária para operar um servidor Vault na AWS e não sobre a aplicação em si.

A essa altura do campeonato você deve estar achando que eu vou deixar o trabalho pela metade, mas não precisa de preocupar!

Pensando em resolver de fato o problema inicial eu escrevi uma Parte 2 onde nós vamos configurar toda a parte de acessos do Vault, e obviamente utilizando Terraform.

Obrigado por ler até aqui, deixa seu feedback e eu espero você na parte 2 😉