Одной из неприятных неожиданностей в использовании CloudFormation оказался факт того, что он не умеет создавать VPC peering между VPC в разных регионах.
Т.е., сама возможность создания cross-region peering была анонсирована>>> AWS в ноябре 2017, но по состоянию на июнь 2018 – AWS CloudFormation просто не имеет опции region.
В результате он пытается выполнить пиринг между сетями в одном регионе, и, конечно же, фейлится.
На форумах AWS есть соответствующая тема>>>, но от разработчиков пока комментариев нет.
Что бы решить эту проблему – было решено стек для Jenkins создавать с помощью Terraform (он у нас единственный размещается в Европе, поближе к нам, остальные стеки в США).
Ниже – пример создания EC2, VPC и VPC peering между сетями в двух регионах.
В стеке (регион eu-west-1) будет работать Jenkins, который будет подключен к Prometheus мониторингу в регионе us-east-2 через VPC peering.
Содержание
Структура проекта
Структура файлов и каталогов получается следующая:
[simterm]
$ tree terraform/ terraform/ ├── ec2 │ ├── ec2_icmp_and_default_sg.tf │ ├── ec2.tf │ ├── jenkins_security_group.tf │ └── variables.tf ├── main.tf ├── terraform_exec.sh ├── variables.tf └── vpc ├── variables.tf └── vpc.tf
[/simterm]
Тут каталоги ec2
и vpc
– модули, а скрипт terraform_exec.sh
используется для запуска Terraform plan
/apply
/destroy
.
Скрипт
Скрипт – просто черновой набросок того, как Terraform будет запускаться в самом Jenkins, сейчас используется для удобства.
В проекте используется AWS S3 в качестве бекенда, инициализируется в функции terraform_config()
.
Затем при создании стека выполняется terraform_plan()
, запрашивается подтверждение, и затем – terraform_apply()
. Аналогично – при выполнении destroy
.
Сам скрипт:
#/usr/bin/env bash HELP="\n\t-a: apply \n\t-D: delete \n\t-e: environement to be used, default to \"dev\"\n\t" # set default action to apply apply=1 destroy= while getopts "aDe:h" opt; do case $opt in a) apply=1 ;; D) apply= destroy=1 ;; e) ENV=$OPTARG ;; h) echo -e $HELP exit 0 ;; ?) echo -e $HELP && exit 1 ;; esac done # global vars [[ -z $ENV ]] && ENV="dev" AWS_PROFILE="jenkins-ci-provisioning" AWS_REGION="eu-west-1" CLUSTER_NAME="jenkins-ci-$ENV" # monitoring peering data MON_PROD_VPC_ID="vpc-51e8b639" MON_PROD_REGION="us-east-2" MON_PROD_VPC_CIDR="10.0.1.0/24" # terraform backend vars TF_BE_S3_BUCKET="terraform-$CLUSTER_NAME" TF_BE_S3_STATE_KEY="$TF_BE_S3_BUCKET.tfstate" echo -e "\nENV=$ENV AWS CLI profile: $AWS_PROFILE AWS region: $AWS_REGION Application cluster name: $CLUSTER_NAME Terraform backend S3 bucket name: $TF_BE_S3_BUCKET Terraform backend key filename: $TF_BE_S3_STATE_KEY " read -p "Are you sure to proceed? [y/n] " -r if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi # load modules terraform get # setup backend terraform_config () { terraform init \ -backend-config="bucket=$TF_BE_S3_BUCKET" \ -backend-config="key=$TF_BE_S3_STATE_KEY" \ -backend-config="region=$AWS_REGION" \ -backend-config="profile=$AWS_PROFILE" } terraform_plan () { terraform plan \ -var env=$ENV \ -var aws-region=$AWS_REGION \ -var aws-profile=$AWS_PROFILE \ -var cluster-name=$CLUSTER_NAME \ -var monitoring-prod-vpc-id=$MON_PROD_VPC_ID \ -var monitoring-prod-region=$MON_PROD_REGION \ -var monitoring-prod-vpc-cidr=$MON_PROD_VPC_CIDR } terraform_apply () { terraform apply \ -var env=$ENV \ -var aws-region=$AWS_REGION \ -var aws-profile=$AWS_PROFILE \ -var cluster-name=$CLUSTER_NAME \ -var monitoring-prod-vpc-id=$MON_PROD_VPC_ID \ -var monitoring-prod-region=$MON_PROD_REGION \ -var monitoring-prod-vpc-cidr=$MON_PROD_VPC_CIDR } terraform_destroy () { terraform plan -destroy \ -var env=$ENV \ -var aws-region=$AWS_REGION \ -var aws-profile=$AWS_PROFILE \ -var cluster-name=$CLUSTER_NAME \ -var monitoring-prod-vpc-id=$MON_PROD_VPC_ID \ -var monitoring-prod-region=$MON_PROD_REGION \ -var monitoring-prod-vpc-cidr=$MON_PROD_VPC_CIDR echo read -p "Plan complete. Are you sure to proceed? [y/n] " -r if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi terraform destroy \ -var env=$ENV \ -var aws-region=$AWS_REGION \ -var aws-profile=$AWS_PROFILE \ -var cluster-name=$CLUSTER_NAME \ -var monitoring-prod-vpc-id=$MON_PROD_VPC_ID \ -var monitoring-prod-region=$MON_PROD_REGION \ -var monitoring-prod-vpc-cidr=$MON_PROD_VPC_CIDR } apply () { terraform_config || exit 1 terraform_plan || exit 1 read -p "Plan complete. Are you sure to proceed? [y/n] " -r if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi echo terraform_apply || exit 1 } if [[ $apply == 1 ]]; then echo -e "\nRunning Apply action..." apply elif [[ $destroy == 1 ]]; then echo -e "\nRunning Destroy action..." terraform_destroy else echo -e "\nERROR: action does not set, exitting." exit 1 fi
Terraform
main.tf
В main.tf
настраивается бекенд, создаётся провайдер, получается список availability зон.
Затем вызываются два модуля – ec2
и vpc
, которым передаются необходимые переменные, часть которых передаётся из глобальных переменных скрипта, часть формируется в самом main.tf
(например, eu-west-1a получается из data.aws_availability_zones.available.names[0]
).
Файл выглядит так:
terraform { backend "s3" { } } provider "aws" { region = "${var.aws-region}" profile = "${var.aws-profile}" } data "aws_availability_zones" "available" { state = "available" } module "vpc" { source = "vpc" env = "${var.env}" aws-availability-zone = "${data.aws_availability_zones.available.names[0]}" monitoring-prod-vpc-id = "${var.monitoring-prod-vpc-id}" monitoring-prod-region = "${var.monitoring-prod-region}" monitoring-prod-vpc-cidr = "${var.monitoring-prod-vpc-cidr}" aws-profile = "${var.aws-profile}" } module "ec2" { source = "ec2" env = "${var.env}" aws-availability-zone = "${data.aws_availability_zones.available.names[0]}" jenkins-public-subnet-id = "${module.vpc.jenkins-public-subnet-id}" jenkins-vpc-id = "${module.vpc.jenkins-vpc-id}" }
Модуль ec2
Тут выполняется создание EC2 инстанса, к нему подключается существующий EBS с данными Jenkins и Elastic IP.
Содержимое каталога модуля:
[simterm]
$ ls -l ec2/ total 16 -rw-r--r-- 1 setevoy setevoy 440 Jun 27 16:22 ec2_icmp_and_default_sg.tf -rw-r--r-- 1 setevoy setevoy 1176 Jun 27 15:20 ec2.tf -rw-r--r-- 1 setevoy setevoy 1032 Jun 28 10:25 jenkins_security_group.tf -rw-r--r-- 1 setevoy setevoy 1611 Jun 27 14:27 variables.tf
[/simterm]
Основной файл модуля – ec2.tf
:
resource "aws_volume_attachment" "jenkins-data-ebs-attach" { device_name = "/dev/xvdb" volume_id = "${lookup(var.ec2-data-ebs-id, var.env)}" instance_id = "${aws_instance.jenkins-ec2.id}" } resource "aws_instance" "jenkins-ec2" { ami = "${var.aws-ec2-ami-id}" instance_type = "${lookup(var.aws-ec2-type, var.env)}" key_name = "${lookup(var.aws-key-name, var.env)}" associate_public_ip_address = "true" availability_zone = "${var.aws-availability-zone}" vpc_security_group_ids = ["${aws_security_group.jenkins-web-ssh-sg.id}", "${aws_security_group.jenkins-default-sg.id}"] subnet_id = "${var.jenkins-public-subnet-id}" tags { "Name" = "jenkins-ec2-${var.env}" } } resource "aws_eip" "jenkins-eip" { instance = "${aws_instance.jenkins-ec2.id}" vpc = true tags { "Name" = "jenkins-ec2-${var.env}-eip" } }
VPC subnet получается из outputs модуля vpc
.
Dev/Production в Terraform
Для того, что переопределить различные параметры для Dev и Production окружений в Terrafrom можно применить три подхода.
variables mapping
Первый вариант, который и используется в данном случае – это mapping в переменных.
Например, параметр instance_type
для модуля ec2
и его ресурса aws_instance
создаётся следующим образом:
... instance_type = "${lookup(var.aws-ec2-type, var.env)}" ...
Далее, в файле variables.tf
, создаётся сам mapping, в котором задаются два типа инстансов:
... variable "aws-ec2-type" { description = "EC2 instance type for Dev and prod" type = "map" default = { "dev" = "t2.nano" "production" = "t2.medium" } } ...
В зависимости от значения переменной env
, которая задаётся в скрипте – выбирается одно из значений – t2.nano
для Dev, или t2.medium
для Production.
End/Prod каталоги
Другой вариант – более гибкий и позволяющий правильнее использовать саму концепцию модулей в Terraform – это создать каталоги, например develop и production, каждый со своим файлом main.tf
и variables.tf
.
Далее из main.tf
каждого каталога вызывался бы модуль ec2
, а из variables.tf
каждого каталога – задавались бы значения переменных.
Вариант хороший, но в данном случае стек достаточно простой, с одним EC2, потому использовался mapping.
Terraform workspaces
И третий вариант – workspaces в Terraform. Но всё-таки его предназначение другое (тестирование проекта без внесения изменений в state-данные текущей инфрастуктуры).
Тем не менее – использовать тоже можно, например из документации:
... resource "aws_instance" "example" { count = "${terraform.workspace == "default" ? 5 : 1}" # ... other arguments } ...
Модуль vpc
И последним – пример модуля vpc
:
[simterm]
$ ls -l vpc/ total 8 -rw-r--r-- 1 setevoy setevoy 1073 Jun 28 10:37 variables.tf -rw-r--r-- 1 setevoy setevoy 2382 Jun 28 10:39 vpc.tf
[/simterm]
Основной файл – vpc.tf
:
provider "aws" { alias = "peer" region = "${var.monitoring-prod-region}" profile = "${var.aws-profile}" } resource "aws_vpc" "jenkins-vpc" { cidr_block = "${lookup(var.jenkins-vpc-cidr, var.env)}" assign_generated_ipv6_cidr_block = true enable_dns_hostnames = true tags { "Name" = "jenkins-${var.env}-vpc" } } resource "aws_subnet" "jenkins-public-subnet" { vpc_id = "${aws_vpc.jenkins-vpc.id}" cidr_block = "${lookup(var.jenkins-pub-subnet-cidr, var.env)}" availability_zone = "${var.aws-availability-zone}" tags { "Name" = "jenkins-${var.env}-pub-net" } } resource "aws_internet_gateway" "jenkins-igw" { vpc_id = "${aws_vpc.jenkins-vpc.id}" } resource "aws_route_table" "jenkins-route-tbl" { vpc_id = "${aws_vpc.jenkins-vpc.id}" route { cidr_block = "0.0.0.0/0" gateway_id = "${aws_internet_gateway.jenkins-igw.id}" } route { cidr_block = "${var.monitoring-prod-vpc-cidr}" gateway_id = "${aws_vpc_peering_connection.monitoring-prod-vpc-peer.id}" } tags { Name = "jenkins-${var.env}-route-table" } } resource "aws_route_table_association" "public-assoc" { subnet_id = "${aws_subnet.jenkins-public-subnet.id}" route_table_id = "${aws_route_table.jenkins-route-tbl.id}" } resource "aws_vpc_peering_connection" "monitoring-prod-vpc-peer" { peer_vpc_id = "${var.monitoring-prod-vpc-id}" vpc_id = "${aws_vpc.jenkins-vpc.id}" peer_region ="${var.monitoring-prod-region}" tags { Name = "VPC Peering Jenkins and Monitoring Prod" } } resource "aws_vpc_peering_connection_accepter" "monitoring-peer-accepter" { provider = "aws.peer" vpc_peering_connection_id = "${aws_vpc_peering_connection.monitoring-prod-vpc-peer.id}" auto_accept = true } output "jenkins-public-subnet-id" { value = "${aws_subnet.jenkins-public-subnet.id}" } output "jenkins-vpc-id" { value = "${aws_vpc.jenkins-vpc.id}" }
cross-region VPC peering
VPC peering создаётся с помощью ресурса aws_vpc_peering_connection
:
... resource "aws_vpc_peering_connection" "monitoring-prod-vpc-peer" { peer_vpc_id = "${var.monitoring-prod-vpc-id}" vpc_id = "${aws_vpc.jenkins-vpc.id}" peer_region ="${var.monitoring-prod-region}" tags { Name = "VPC Peering Jenkins and Monitoring Prod" } } ...
Которому в параметре peer_region
передаётся регион стека мониторинга (us-east-2), который определяется в переменной $MON_PROD_REGION
скрипта terraform_exec.sh
.
Для активации пиринга – используется ресурс aws_vpc_peering_connection_accepter
, который использует дополнительный aws-провайдер, у которого задаётся регион и alias
:
... provider "aws" { alias = "peer" region = "${var.monitoring-prod-region}" profile = "${var.aws-profile}" } ...
Переменная monitoring-prod-region
так же задаётся в скрипте terraform_exec.sh
в переменной $MON_PROD_REGION
.
Как и в модуле ec2
– тут используется mapping для разделения dev/prod параметров, например VPC CIDR-ы:
... variable "jenkins-vpc-cidr" { type = "map" default = { "dev" = "10.0.4.0/24" "production" = "10.0.5.0/24" } } ...
Создание стека
“А теперь со всем этим говном на борту мы попробуем взлететь” (с)
Создание стека (привычка от CloudFormation называть стеком):
[simterm]
$ ./terraform_exec.sh -a ENV=dev AWS CLI profile: jenkins-ci-provisioning AWS region: eu-west-1 Application cluster name: jenkins-ci-dev Terraform backend S3 bucket name: terraform-jenkins-ci-dev Terraform backend key filename: terraform-jenkins-ci-dev.tfstate Are you sure to proceed? [y/n] y - module.vpc - module.ec2 Running Apply action... Initializing modules... - module.vpc - module.ec2 Initializing the backend... Initializing provider plugins... ... Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: + module.ec2.aws_eip.jenkins-eip id: <computed> allocation_id: <computed> association_id: <computed> domain: <computed> instance: "${aws_instance.jenkins-ec2.id}" network_interface: <computed> private_ip: <computed> public_ip: <computed> tags.%: "1" tags.Name: "jenkins-ec2-dev-eip" vpc: "true" + module.ec2.aws_instance.jenkins-ec2 id: <computed> ami: "ami-34414d4d" associate_public_ip_address: "true" availability_zone: "eu-west-1a" ... Plan: 12 to add, 0 to change, 0 to destroy. ------------------------------------------------------------------------ Note: You didn't specify an "-out" parameter to save this plan, so Terraform can't guarantee that exactly these actions will be performed if "terraform apply" is subsequently run. Plan complete. Are you sure to proceed? [y/n] y data.aws_availability_zones.available: Refreshing state... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: + module.ec2.aws_eip.jenkins-eip id: <computed> ... module.ec2.aws_eip.jenkins-eip: Creation complete after 2s (ID: eipalloc-ec98d0d1) module.ec2.aws_volume_attachment.jenkins-data-ebs-attach: Still creating... (10s elapsed) module.ec2.aws_volume_attachment.jenkins-data-ebs-attach: Still creating... (20s elapsed) module.ec2.aws_volume_attachment.jenkins-data-ebs-attach: Creation complete after 23s (ID: vai-1099139600) Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
[/simterm]
Пиринг:
Проверяем пинг с хоста Jenkins к хосту с мониторингом:
[simterm]
admin@ip-10-0-4-10:~$ ping 10.0.1.6 -c 1 PING 10.0.1.6 (10.0.1.6) 56(84) bytes of data. 64 bytes from 10.0.1.6: icmp_seq=1 ttl=64 time=85.2 ms --- 10.0.1.6 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 85.214/85.214/85.214/0.000 ms
[/simterm]
И обратно:
[simterm]
admin@monitonrig-production:~$ ping 10.0.4.10 -c 1 PING 10.0.4.10 (10.0.4.10) 56(84) bytes of data. 64 bytes from 10.0.4.10: icmp_seq=1 ttl=64 time=85.4 ms --- 10.0.4.10 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 85.440/85.440/85.440/0.000 ms
[/simterm]
Готово.