Одной из неприятных неожиданностей в использовании 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]
Готово.





