Levantando un NFS MultiAZ en AWS con Terraform.
Implementación de Amazon EFS con Terraform
Objetivo:
Escribir código en la herramienta Terraform, que nos permita levantar en primer lugar todo el networking de un esquema MultiAZ de dos niveles en AWS, luego setear un volumen EFS, finalmente levantar dos instancias EC2 para que consuman y compartan datos del volumen EFS
Pre-requisitos:
- Conocimiento en manejo de conceptos AWS VPC (Subnets, SG, IGW, RouteTables)
- Conocimiento básico en manejo del servicio EC2 de AWS
- Acceso programático a una cuenta AWS (set de keys)
- Conocimiento básico de la herramienta Terraform
- Conocimiento en manejo de SO GNU/Linux
- Conocimiento básico de cómo funciona el protocolo NFS (Network File System)
- Maquina con SO GNU/Linux y Terraform instalados
Que es EFS ?
Desde mi punto de vista… Amazon EFS es “el NFS de aws”, así de simple (como cuando montabamos un server NFS y tirabamos el mount con algun script o modificabamos el fstab de las máquinas clientes), es tener un NFS que puede ser compartido por varios EC2 asignando un punto de montaje en cada instancia, y de esta manera contar con almacenamiento compartido en aws sin tener por ej EBS volumes redundantes atachados a cada instancia. Tampoco vamos a subestimar a aws, EFS es el servicio que implementa una solución basada en NFS (mas precisamente utilizando el protocolo NFSv4), pero por supuesto, con TODAS las bondades de ser un servicio de aws, algunas de ellas son:
- Es un servicio totalmente administrado (no debemos preocuparnos por detalles en la implementación de la solución)
- EFS es Elástico (su volumen crece y/o decrece automáticamente a medida que escribis o borras archivos)
- Puede soportar concurrencia en el orden de las miles de requests (se lo puede cagar a palos tranquilo)
- Solo pagas por el almacenamiento usado (no hay que hacer cálculos de pre-provisioning de storage)
- Escalable hasta el orden de los Petabytes (otra vez… se lo puede cagar a palos tranquilo)
- Es multiAZ (podemos guardar y compartir data a través de múltiples zonas de disponibilidad dentro de una región)
- Es de fácil uso, ya sea con la consola, CLI, SDK o a través de herramientas como Terraform, levantar un EFS es extremadamente fácil.
- EFS es solamente para uso en SO GNU/Linux, para plataformas Windows existe otro servicio llamado Amazon FSx.
Arquitectura
Tenemos un típico esquema MultiAZ de dos niveles (AZa y AZb) con una instancia EC2 en cada una, las cuales van a consumir/escribir datos en EFS, cabe aclarar que EFS está fuera de las AZ que vamos a crear, ya que al ser un servicio totalmente administrado, aws se encarga de replicar cada objeto del filesystem (cada directorio, archivo y enlace) en varias AZ, lo que creamos nosotros en cada una de las AZ donde van a vivir las instancias EC2 son los destinos de montaje o MountTarget (una interfaz de red que nos va a permitir comunicarnos con el EFS), podemos dejar que aws asigne automáticamente la IP del punto de montaje o podemos también especificarla nosotros, cuando creamos un EFS por consola web, el wizard de aws nos crea automáticamente un destino de montaje en todas las AZ de la VPC elegida, nosotros lo a hacer a traves de Terraform.
Vamos a Terraform
Como todas las cosas en el mundo de la informática, hay dos maneras de utilizar Terraform, la mala y la buena, recuerdo que mis primeras prácticas y labs en Terraform tiraba todos los resources crudos dentro de un solo main.tf gigante, funciona pero guardar la infra en código de esa manera no es escalable en el tiempo y resulta muy difícil de mantener. Es por eso que Terraform nos ofrece recursos para que el trabajo sea más ordenado, utilizando módulos y plugins en Terraform vamos a construir la solución propuesta en este post.
Vamos a armar los módulos con sus respectivos inputs main y outputs, un módulo para levantar todo el networking, un módulo para construir el EFS con su respectivo MountTarget, y el file main.tf donde vamos a invocar estos módulos y por último construir 2 instancias EC2 para que consuman nuestro EFS.
Crear la siguiente estructura con el siguiente contenido:
Primero lo primero ====> Networking:
inputs.tf:
variable "az-subnets" {
description = "Una lista de subnets a ser creadas en sus respectivas AZ."
type = "list"
}
variable "cidr" {
description = "el bloque CIDR de la VPC"
type = "string"
}
main.tf:
#Creamos la VPC
resource "aws_vpc" "main" {
cidr_block = "${var.cidr}"
enable_dns_hostnames = true
}
# creamos el IGW
resource "aws_internet_gateway" "main" {
vpc_id = "${aws_vpc.main.id}"
}
#creamos la RT con la tipica route hacia afuera (0.0.0.0/0)
resource "aws_route" "internet_access" {
route_table_id = "${aws_vpc.main.main_route_table_id}"
destination_cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.main.id}"
}
#Creamos las N subnets según nuestra variable az.subnets
resource "aws_subnet" "main" {
count = "${length(var.az-subnets)}"
cidr_block = "${lookup(var.az-subnets[count.index], "cidr")}"
vpc_id = "${aws_vpc.main.id}"
map_public_ip_on_launch = true
availability_zone = "${lookup(var.az-subnets[count.index], "az")}"
tags = {
Name = "${lookup(var.az-subnets[count.index], "name")}"
}
}
outputs.tf:
output "az-subnet-id-mapping" {
description = "mapeamos el name de la subnet con su ID"
value = "${zipmap(aws_subnet.main.*.tags.Name, aws_subnet.main.*.id)}"
}
output "vpc-id" {
description = "ID de la VPC generada por el modulo networking"
value = "${aws_vpc.main.id}"
}
Modulo EFS
inputs.tf:
variable "name" {
type = "string"
description = "nombre del filesystem"
}
variable "subnets" {
type = "list"
description = "lista de subnets que van a montar el efs"
}
variable "subnets-count" {
type = "string"
description = "cantidad de subnets"
}
variable "vpc-id" {
type = "string"
description = "el VPC ID donde el EFS y las subnets van a convivir"
}
main.tf:
data "aws_vpc" "main" {
id = "${var.vpc-id}"
}
# creamos el recurso EFS
resource "aws_efs_file_system" "main" {
tags = {
Name = "${var.name}"
}
}
# creamos un MountTarget
resource "aws_efs_mount_target" "main" {
count = "${var.subnets-count}"
file_system_id = "${aws_efs_file_system.main.id}"
subnet_id = "${element(var.subnets, count.index)}"
security_groups = [
"${aws_security_group.efs.id}",
]
}
# Habilitamos el trafico en el puerto 2049 (puerto del protocolo NFS)
resource "aws_security_group" "efs" {
name = "efs-mnt"
description = "Allows NFS traffic from instances within the VPC."
vpc_id = "${var.vpc-id}"
ingress {
from_port = 2049
to_port = 2049
protocol = "tcp"
cidr_blocks = [
"${data.aws_vpc.main.cidr_block}",
]
}
egress {
from_port = 2049
to_port = 2049
protocol = "tcp"
cidr_blocks = [
"${data.aws_vpc.main.cidr_block}",
]
}
tags = {
Name = "allow_nfs-ec2"
}
}
outputs.tf:
output "mount-target-dns" {
description = "La direccion DNS del mount target EFS."
value = "${aws_efs_mount_target.main.0.dns_name}"
}
sg.tf:
resource "aws_security_group" "allow-ssh-and-egress" {
name = "allow-ssh-and-eggress"
description = "Habilitamos el trafico en el puerto ssh."
vpc_id = "${module.networking.vpc-id}"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "allow_ssh-all"
}
}
main.tf (el main del raiz):
provider "aws" {
region = "${var.region}"
access_key = "${var.access_key}"
secret_key = "${var.secret_key}"
}
variable "az-subnets" {
type = "list"
default = [
{
name = "us-east-1a"
az = "us-east-1a"
cidr = "10.0.1.0/24"
},
{
name = "us-east-1b"
az = "us-east-1b"
cidr = "10.0.2.0/24"
},
]
}
module "networking" {
source = "./networking"
cidr = "10.0.0.0/16"
az-subnets = "${var.az-subnets}"
}
module "efs" {
source = "./efs"
name = "efs-terraformado"
subnets-count = "${length(var.az-subnets)}"
subnets = "${values(module.networking.az-subnet-id-mapping)}"
vpc-id = "${module.networking.vpc-id}"
}
# Creamos dos instancias EC2 que consuman nuestro EFS
resource "aws_instance" "ec1-server" {
ami = "ami-0cfcbf9074150a0fd"
instance_type = "t2.micro"
key_name = "${aws_key_pair.main.key_name}"
availability_zone = "us-east-1a"
subnet_id = "${module.networking.az-subnet-id-mapping["us-east-1a"]}"
vpc_security_group_ids = [
"${aws_security_group.allow-ssh-and-egress.id}",
]
tags = {
Name = "ec1-server"
}
}
resource "aws_instance" "ec2-server" {
ami = "ami-0cfcbf9074150a0fd"
instance_type = "t2.micro"
key_name = "${aws_key_pair.main.key_name}"
availability_zone = "us-east-1b"
subnet_id = "${module.networking.az-subnet-id-mapping["us-east-1b"]}"
vpc_security_group_ids = [
"${aws_security_group.allow-ssh-and-egress.id}",
]
tags = {
Name = "ec2-server"
}
}
Consideraciones: recordá setear correctamente los datos de acceso programático a tu cuenta aws al igual que tus llaves ssh, una de las maneras de hacerlo es la siguiente:
Levantemos todo
Inicializamos Terraform:
$ terraform init
Terraform nos instalará el plugin de aws y organizará nuestros modulos en modules.json
Tiramos el Plan:
$ terraform plan
Con eso Terraform va a generar y a mostrarnos (sin crear nada todavía) un plan de ejecución, no deberíamos tener ningún error, asi que vamos al apply:
$ terraform apply
confirmamos y esperamos que Terraform haga lo suyo…
Luego de un poco mas de 3 minutos…
Ya tenemos levantada nuestra arquitectura, podemos observar el proceso y el resultado en la consola de aws, por ejemplo les dejo dos imágenes de los recursos EC2 y EFS:
Como podrán observar los recursos se crearon con las especificaciones de names, CIDRs, AZs, Region etc que codeamos en los manifiestos Terraform.
Tomen nota del DNS Name de acceso a EFS ya que va a ser necesario para montar el FileSystem en las dos instancias EC2 creadas y para N instancias extras que necesitemos por ejemplo en un escenario con Autoscaling Group (PD: para los del palo de Carlín Calvo, no van a poder pegarle a ningún endpoint, IP o ID de las imágenes porque obviamente esos recursos no existen más).
Ahora hay que aprovisionar las instancias con la configuración necesaria para conectarlas a nuestro EFS, esto se puede hacer de muchas maneras, por ejemplo utilizando bootstrap scripts en la configuración de las instancias, conectarse por ssh y realizar la configuración de manera manual, o mediante alguna herramienta de configuration manager, en mi caso lo hice con un playbook de Ansible (en un siguiente post vamos a explicar cómo integrar Terraform, Ansible y Packer para generar un Web/app server MultiAZ y Escalable utilizando Autoscaling groups), en fin, ya sea que lo hagan con un script bash, manualmente o con ansible, en cada una de las instancias deben ejecutar las siguientes instrucciones para montar el EFS:
Instalar paquetería nfs compatible con Ubuntu (la AMI base de nuestras instancias es un Ubuntu):
$ sudo apt-get update -y
$ sudo apt install nfs-common -y
NOTA: si usas una AMI Linux de tipo RHEL-CentOS-AmazonLinux etc el paquete es nfs-utils
Crear el mountPoint directory en la instancia y montar el EFS utilizando el DNS Name correspondiente:
$ MOUNT_POINT="/mnt/efs"
$ EFS_DNS="REEMPLAZAR-CON-TU-EFS-DNS-NAME"
$ sudo mkdir -p $MOUNT_POINT
$ sudo mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 $EFS_DNS:/ $MOUNT_POINT
Correr dichos comandos/script/playbooks en cada instancia que se desee conectar al FileSystem.
Ahora vamos a poder escribir, leer, borrar etc desde ambas instancias a un mismo volumen EFS:
Resumiendo, levantamos 13 recursos en aws con un solo comando, formando una arquitectura MultiAZ, en donde montamos un FyleSystem EFS y dos instancias EC2 que se conectaron con éxito a el FileSystem creado, y todo eso en solo 3 minutos, de la misma manera si quisiéramos bajar toda la arquitectura lo podemos hacer con un simple terraform destroy, ahora, en qué escenario de la vida real podríamos aplicar EFS?
Posibles casos de uso:
- Web Servers
- CMS
- Storage Persistente en Containers
- BigData
- Cualquier arquitectura que necesite un FileSystem compartido escalable y de alta disponibilidad
En un próximo post vamos a levantar un Wordpress combinando las herramientas que acabamos de ver y sumando algunas otras (ansible, packer, ALB, AutoscalingGroups).