Задача: продумать автоматизацию развёртывания AWS Elastic Kubernetes Service кластера.
Используем:
- Ansible: для автоматизации создания CloudFormation стеков и запуска
eksctlс нужными параметрами - CloudFormation с NestedStacks: для создания инфрастуктуры — VPC, подсетей, SecurityGroups, IAM-роли, etc
eksctl: для создания самого кластера, используя ресурсы, созданные CloudFormation
Идея заключается в следующем:
- Ansible использует модуль cloudformation , создаёт инфрастуктуру
- используя
Outputsсозданного стека CloudFormation — Ansible из шаблона генерирует файл настроек дляeksctl - Ansible вызывает
eksctl, передавая ему конфиг кластера, и создаёт или обновляет кластер
Выбор eksctl обуславливался во-первых крайне сжатыми сроками, во-вторых — под капотом он использует CloudFormation, который используется в проекте давно, так что наши стеки будут более-менее однородными.
Запускаться всё будет из Jenkins-джоб, используя Docker-образ с AWS CLI, Ansible и eksctl.
Вообще, не стоит данный пример рассматривать именно как «Best Practice» для автоматизации развёртывания кластеров Elastic Kubernetes Service — скорее, это такой себе Proof of Concept, и больше пример того, как шаг за шагом смутная идея в голове превращается в реальный код и работающие сервисы. А как и какие инструменты использовать — Terraform или CloudFormation, или kops или eksctl — вопрос вторичный.
Вообще, для Ansible имеется два модуля, которые могут облегчить работу с Kubernetes — k8s и kubectl, но их статусы preview и community не обрадовали — поэтому пока обойдёмся без них.
Пост получился очень большим, потому разбит на две части:
- в первой, этой — займёмся написанием шаблона CloudFormation
- во втором — напишем Ansible плейбук и роли, которые будут запускать CloudFormation и
eksctl
Надеюсь, что в процессе написания особых неточностей не допустил, тем не менее — могут быть, т.к. писалось не один день с неоднократными исправлениями-переделываниями. Но всё описано пошагово, так что общую идею ухватить можно.
Все получившиеся в результате написания файлы доступны в репозитории eksctl-cf-ansible — тут ссылка на бранч с результатами, получившимися именно в конце написания этого и следующего постов. Возможно потом будут ещё изменения.
Вторая часть — AWS: Elastic Kubernetes Service — автоматизация создания кластера, часть 2 — Ansible, eksctl.
Содержание
CloudFormation stacks
Начнём с CloudFormation стека.
Нам потребуются ресурсы:
- 1 VPC
- две публичные подсети под Application Load Balancers, Bastion-хосты, Internet Gateways
- две приватные подсети под Kubernetes Worker Nodes EC2, NAT Gateways
EKS AMI для Worker Nodes eksctl найдёт сам, но список есть тут>>>.
Используем CloudFormation Netsted Stacks (см. AWS: CloudFormation — вложенные стеки и Import/Export параметров):
- корневой стек,
eks-root.json— описывает ресурсы, определяет шаблоны- шаблон
eks-region-networking.json:- одна VPC
- Internet Gateway
- Internet Gateway Association
- шаблон
eks-azs-networking.json— все ресурсы дублируются в двух разных AvailabilityZones:- по одной публичной подсети
- по одной приватной подсети
- RouteTable для публичных подсетей
- к ней Route в сеть 0.0.0.0/0 через Internet Gateway
- и SubnetRouteTableAssociation для подключения RouteTable к публичной подсети в этой AvailabilityZone
- приватная RouteTable
- к ней Route в сеть 0.0.0.0/0 через NAT Gateway
- SubnetRouteTableAssociation для подключения RouteTable к приватной подсети в этой AvailabilityZone
- NAT Gateway
- Elastic IP для NAT Gateway
- шаблон
Начинаем с рутового шаблона.
Root stack
Создадим корневой шаблон, который будет вызывать все вложенные.
В корне репозитория создаём каталоги:
[simterm]
$ mkdir -p roles/cloudformation/{tasks,files,templates}
[/simterm]
В каталоге roles/cloudformation/files/ создаём файл eks-root.json — это будет шаблон нашего корневого стека:
[simterm]
$ cd roles/cloudformation/files/ $ touch eks-root.json
[/simterm]
Параметры
Тут сразу стоит подумать о том — как будут использоваться блоки адресов в проекте. Например — стоит избегать использования сетей, которые могут пересекаться, во избежание будущих проблем с VPC Peering.
Второй нюанс, о котором стоит подумать сразу — это модель сети в будущем кластере вообще, и используемый плагин.
Elastic Kubernetes Service по-умолчанию предлагает плагин CNI (Container Network Interface), который позволяет использование сетевого интерфейса Worker Node ЕС2 (ENI — Elastic Network Interface). Используя этот CNI — Kubernetes будет выделять подам IP из пула сети VPC, см. amazon-vpc-cni-k8s и Pod Networking (CNI).
Такое решение имеет плюсы и недостатки, см. отличный обзор от Weave Net — AWS and Kubernetes Networking Options and Trade-offs, f про другие плагины в документации самого Kubernetes — Cluster Networking.
См. также VPC and Subnet Sizing.
Пока добавляем сеть 10.0.0.0/16 для VPC — потом её «нарежем» на 4 подсети:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "AWS CloudFormation stack for Kubernetes cluster",
"Parameters": {
"VPCCIDRBlock": {
"Description": "VPC CidrBlock",
"Type": "String",
"Default": "10.0.0.0/16"
}
},
Подсети поделим так:
- одна публичная в AvailabilityZone 1-А, /20, 4094 адресов
- одна приватная в AvailabilityZone 1-А, /20, 4094 адресов
- одна публичная в AvailabilityZone 1-В, /20, 4094 адресов
- одна приватная в AvailabilityZone 1-В, /20, 4094 адресов
Можно использовать ipcalc:
[simterm]
$ ipcalc 10.0.0.0/16 --s 4094 4094 4094 4094 | grep Network | cut -d" " -f 1,4 | tail -4 Network: 10.0.0.0/20 Network: 10.0.16.0/20 Network: 10.0.32.0/20 Network: 10.0.48.0/20
[/simterm]
4094 адреса должно хватить на все EС2 и поды в каждой подсети.
По ходу дела нашёл очень классный калькулятор подсетей — http://www.subnetmask.info.
И добавляем параметр EKSClusterName — сюда из Ansible будем передавать имя создаваемого кластера для создания тегов:
...
"EKSClusterName": {
"Description": "EKS cluster name",
"Type": "String"
}
...
Network Region stack
Создаём второй шаблон, назовём его eks-region-networking.json.
VPC
В нём будем создавать VPC, и из рутового шаблона в сетевой передадим параметр VPC сети и имя кластера, а обратно — через Outputs получим ID созданной VPC:
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "AWS CloudFormation Region Networking stack for Kubernetes cluster",
"Parameters" : {
"VPCCIDRBlock": {
"Description": "VPC CidrBlock",
"Type": "String"
},
"EKSClusterName": {
"Description": "EKS cluster name",
"Type": "String"
}
},
"Resources" : {
"VPC" : {
"Type" : "AWS::EC2::VPC",
"Properties" : {
"CidrBlock" : { "Ref": "VPCCIDRBlock" },
"EnableDnsHostnames": true,
"EnableDnsSupport": true,
"Tags" : [
{
"Key" : "Name",
"Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "vpc"] ] } },
{
"Key" : { "Fn::Join" : [ "", [ "kubernetes.io/cluster/", {"Ref" : "EKSClusterName"}] ] },
"Value" : "owned"
}
]
}
}
},
"Outputs" : {
"VPCID" : {
"Description" : "EKS VPC ID",
"Value" : { "Ref" : "VPC" }
}
}
}
Возвращаемся к рутовому шаблону — добавляем создание первого вложенного стека.
VPC ID получаем из Outputs network region стека, и выводим в Outputs корневого стека, что бы потом в Ansible использовать его для значения переменной, которая потом будет использоваться в шаблоне файла настроек для eksctl.
Теперь полность root шаблон выглядит так:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "AWS CloudFormation stack for Kubernetes cluster",
"Parameters": {
"VPCCIDRBlock": {
"Description": "VPC CidrBlock",
"Type": "String",
"Default": "10.0.0.0/16"
}
},
"Resources": {
"RegionNetworkStack": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "eks-region-networking.json",
"Parameters": {
"VPCCIDRBlock": { "Ref": "VPCCIDRBlock" },
"EKSClusterName": { "Ref": "EKSClusterName"}
}
}
}
},
"Outputs": {
"VPCID" : {
"Description" : "EKS VPC ID",
"Value" : { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] }
}
}
}
Создаём корзину в этом регионе:
[simterm]
$ aws --profile arseniy --region eu-west-2 s3api create-bucket --bucket eks-cloudformation-eu-west-2 --region eu-west-2 --create-bucket-configuration LocationConstraint=eu-west-2
[/simterm]
В Production сетапе для такой корзины будет крайне полезно включить S3 Versioning, что бы хранить копии предыдущих шаблонов на случай проблем (хотя всё-равно все исходные шаблоны хранятся в Github-репозиториях проекта).
Включаем:
[simterm]
$ aws --region eu-west-2 --profile arseniy s3api put-bucket-versioning --bucket eks-cloudformation-eu-west-2 --versioning-configuration Status=Enabled
[/simterm]
Упаковываем eks-root.json и eks-region-networking.json в AWS S3, результат сохраняем в /tmp в файле packed-eks-stacks.json:
[simterm]
$ cd roles/cloudformation/files/ $ aws --profile arseniy --region eu-west-2 cloudformation package --template-file eks-root.json --output-template /tmp/packed-eks-stacks.json --s3-bucket eks-cloudformation-eu-west-2 --use-json
[/simterm]
Деплоим:
[simterm]
$ aws --profile arseniy --region eu-west-2 cloudformation deploy --template-file /tmp/packed-eks-stacks.json --stack-name eks-dev Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - eks-dev
[/simterm]
Проверяем:
Дочерний стек создан, VPC в нём тоже.
Internet Gateway
Добавляем Internet Gateway и VPCGatewayAttachment, приводим Resources регион-стека к виду:
...
"Resources" : {
"VPC" : {
"Type" : "AWS::EC2::VPC",
"Properties" : {
"CidrBlock" : { "Ref": "VPCCIDRBlock" },
"EnableDnsHostnames": true,
"EnableDnsSupport": true,
"Tags" : [
{
"Key" : "Name",
"Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "vpc"] ] } },
{
"Key" : { "Fn::Join" : [ "", [ "kubernetes.io/cluster/", {"Ref" : "EKSClusterName"}] ] },
"Value" : "owned"
}
]
}
},
"InternetGateway" : {
"Type" : "AWS::EC2::InternetGateway",
"Properties" : {
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "igw"] ] } }
]
}
},
"AttachGateway" : {
"Type" : "AWS::EC2::VPCGatewayAttachment",
"Properties" : {
"VpcId" : { "Ref" : "VPC" },
"InternetGatewayId" : { "Ref" : "InternetGateway" }
}
}
},
...
В Outputs добавляем передачу InternetGateway ID в рутовый стек, откуда он передаст его в Network AvailabilityZones stack для создания RouteTables для публичных подсетей:
...
"Outputs" : {
"VPCID" : {
"Description" : "EKS VPC ID",
"Value" : { "Ref" : "VPC" }
},
"IGWID" : {
"Description" : "InternetGateway ID",
"Value" : { "Ref" : "InternetGateway" }
}
}
}
Переходим к Network AvailabilityZones стеку.
Network AvailabilityZones stack
Далее начнём создавать ресурсы, которые будут дублироваться в двух AvailabilityZone.
К ним относятся:
- по одной публичной подсети
- по одной приватной подсети
- RouteTable для публичных подсетей
- к ней Route в сеть 0.0.0.0/0 через Internet Gateway
- и SubnetRouteTableAssociation для подключения RouteTable к публичной подсети в этой AvailabilityZone
- приватная RouteTable
- к ней Route в сеть 0.0.0.0/0 через NAT Gateway
- SubnetRouteTableAssociation для подключения RouteTable к приватной подсети в этой AvailabilityZone
- NAT Gateway
- Elastic IP для NAT Gateway
Основной вопрос — как выбрать AvailabilityZone для этих дочерних стеков, ведь при создании ресурсов в них, например AWS::EC2::Subnet, нам надо указать AvailabilityZone?
Решение — используем Fn::GetAZs, которую будем вызывать из рутового стека — и получим все AvailabilityZone региона, в котором он создан, а затем, используя их (1a, 1b, 1c) — будем передавать в наши NetworkAvailabilityZones-стеки.
В большинстве регионов есть три AvailabilityZone — будем использовать первые две.
Начнём с создания подсетей — по одной публичной и приватной, в двух AvailabilityZone региона.
В этот стек нам надо будет передавать несколько новых параметров:
- VPC ID из «регионального» сетевого стека
- CIDR публичной подсети
- CIDR приватной подсети
- AvailabilityZone, в которой будут создаваться ресурсы
- Internet Gateway ID из «регионального» сетевого стека, потребуется позже для RouteTables
Создаём файл шаблона, назовём его eks-azs-networking.json.
Параметры
Добавляем в него параметры:
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "AWS CloudFormation AvailabilityZones Networking stack for Kubernetes cluster",
"Parameters" : {
"VPCID": {
"Description": "VPC for resources",
"Type": "String"
},
"EKSClusterName": {
"Description": "EKS cluster name",
"Type": "String"
},
"PublicSubnetCIDR": {
"Description": "PublicSubnetCIDR",
"Type": "String"
},
"PrivateSubnetCIDR": {
"Description": "PrivateSubnetCIDR",
"Type": "String"
},
"AZ": {
"Description": "AvailabilityZone for resources",
"Type": "String"
},
"IGWID": {
"Description": "InternetGateway for PublicRoutes",
"Type": "String"
}
},
Подсети
Добавляем блок Resources и создание двух ресурсов — публичная и приватная подсети:
...
"Resources" : {
"PublicSubnet" : {
"Type" : "AWS::EC2::Subnet",
"Properties" : {
"VpcId" : { "Ref" : "VPCID" },
"CidrBlock" : {"Ref" : "PublicSubnetCIDR"},
"AvailabilityZone" : { "Ref": "AZ" },
"Tags" : [
{
"Key" : "Name",
"Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "public-net", { "Ref": "AZ" } ] ] }
},
{
"Key" : { "Fn::Join" : [ "", [ "kubernetes.io/cluster/", {"Ref" : "EKSClusterName"}] ] },
"Value" : "shared"
},
{
"Key" : "kubernetes.io/role/elb",
"Value" : "1"
}
]
}
},
"PrivateSubnet" : {
"Type" : "AWS::EC2::Subnet",
"Properties" : {
"VpcId" : { "Ref" : "VPCID" },
"CidrBlock" : {"Ref" : "PrivateSubnetCIDR"},
"AvailabilityZone" : { "Ref": "AZ" },
"Tags" : [
{
"Key" : "Name",
"Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "private-net", { "Ref": "AZ" } ] ] }
},
{
"Key" : { "Fn::Join" : [ "", [ "kubernetes.io/cluster/", {"Ref" : "EKSClusterName"}] ] },
"Value" : "shared"
},
{
"Key" : "kubernetes.io/role/internal-elb",
"Value" : "1"
}
]
}
}
},
Обратите внимание на теги "kubernetes.io/role/elb" у публичной и "kubernetes.io/role/internal-elb" у приватных подсетей — они потребуются позже для запуска Kubernetes AWS ALB Ingress controller.
В Outputs добавляем передачу созданных subnets-ID обратно в рутовый стек, что бы Ansible потом добавил их в конфиг для eksctl, заодно выводим AvailabilityZone, в которой создан стек:
...
"Outputs" : {
"StackAZ" : {
"Description" : "Stack location",
"Value" : { "Ref" : "AZ" }
},
"PublicSubnetID" : {
"Description" : "PublicSubnet ID",
"Value" : { "Ref" : "PublicSubnet" }
},
"PrivateSubnetID" : {
"Description" : "PrivateSubnet ID",
"Value" : { "Ref" : "PrivateSubnet" }
}
}
}
Возвращаемся к рутовому шаблону — добавляем создание ещё двух стеков, по одному на каждую AvailabilityZone — приводим его блок Resources к виду:
...
"Resources": {
"RegionNetworkStack": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "eks-region-networking.json",
"Parameters": {
"VPCCIDRBlock": { "Ref": "VPCCIDRBlock" }
}
}
},
"AZNetworkStackA": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "eks-azs-networking.json",
"Parameters": {
"VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] },
"AZ": { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] },
"IGWID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.IGWID"] },
"EKSClusterName": { "Ref": "EKSClusterName"},
"PublicSubnetCIDR": "10.0.0.0/20",
"PrivateSubnetCIDR": "10.0.32.0/20"
}
}
},
"AZNetworkStackB": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "eks-azs-networking.json",
"Parameters": {
"VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] },
"AZ": { "Fn::Select": [ "1", { "Fn::GetAZs": "" } ] },
"IGWID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.IGWID"] },
"EKSClusterName": { "Ref": "EKSClusterName"},
"PublicSubnetCIDR": "10.0.16.0/20",
"PrivateSubnetCIDR": "10.0.48.0/20"
}
}
}
},
...
Internet Gateway ID получаем из Outputs «регионального» стека, и передаём через Parameters ресурсов AZNetworkStackА и AZNetworkStackB.
Блоки адресов для подсетей пока хардкодим — позже используем Mappings.
Тут:
Fn::GetAZs"VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] }— в создаваемые стеки передаём VPC ID созданной из region networking стека"AZ": { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] }— выбираем первый элемент («0«) из списка AvailabilityZones региона, во втором стеке — второй элемент («1«)PublicSubnetCIDRиPrivateSubnetCIDRпока хардкодим
Кроме того, в Outputs рутового стека добавляем subnets-ID из наших AvailabilityZones-стеков, что бы потом использовать их в Ansible для файла параметров eksctl:
...
"Outputs": {
"VPCID" : {
"Description" : "EKS VPC ID",
"Value" : { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] }
},
"AStackAZ" : {
"Description" : "Stack location",
"Value" : { "Fn::GetAtt": ["AZNetworkStackA", "Outputs.StackAZ"] }
},
"APublicSubnetID" : {
"Description" : "PublicSubnet ID",
"Value" : { "Fn::GetAtt": ["AZNetworkStackA", "Outputs.PublicSubnetID"] }
},
"APrivateSubnetID" : {
"Description" : "PrivateSubnet ID",
"Value" : { "Fn::GetAtt": ["AZNetworkStackA", "Outputs.PrivateSubnetID"] }
},
"BStackAZ" : {
"Description" : "Stack location",
"Value" : { "Fn::GetAtt": ["AZNetworkStackB", "Outputs.StackAZ"] }
},
"BPublicSubnetID" : {
"Description" : "PublicSubnet ID",
"Value" : { "Fn::GetAtt": ["AZNetworkStackB", "Outputs.PublicSubnetID"] }
},
"BPrivateSubnetID" : {
"Description" : "PrivateSubnet ID",
"Value" : { "Fn::GetAtt": ["AZNetworkStackB", "Outputs.PrivateSubnetID"] }
}
}
}
Упаковываем, генерируем новый шаблон:
[simterm]
$ !610 aws --profile arseniy --region eu-west-2 cloudformation package --template-file eks-root.json --output-template /tmp/packed-eks-stacks.json --s3-bucket eks-cloudformation-eu-west-2 --use-json
[/simterm]
Деплоим:
[simterm]
$ aws --profile arseniy --region eu-west-2 cloudformation deploy --template-file /tmp/packed-eks-stacks.json --stack-name eks-dev
[/simterm]
Проверяем:
Заканчиваем — добавляем оставшиеся ресурсы.
Нам осталось добавить:
- RouteTable для публичной подсети
- к ней Route в сеть 0.0.0.0/0 через Internet Gateway
- и SubnetRouteTableAssociation для подключения RouteTable к публичной подсети в этой AvailabilityZone
- RouteTable для приватной посети
- к ней Route в сеть 0.0.0.0/0 через NAT Gateway
- SubnetRouteTableAssociation для подключения RouteTable к приватной подсети в этой AvailabilityZone
- NAT Gateway
- Elastic IP для NAT Gateway
NAT Gateway
В Resources добавляем создание NAT Gateway и Elastic IP для него:
...
"NatGwIPAddress" : {
"Type" : "AWS::EC2::EIP",
"Properties" : {
"Domain" : "vpc"
}
},
"NATGW" : {
"DependsOn" : "NatGwIPAddress",
"Type" : "AWS::EC2::NatGateway",
"Properties" : {
"AllocationId" : { "Fn::GetAtt" : ["NatGwIPAddress", "AllocationId"]},
"SubnetId" : { "Ref" : "PublicSubnet"},
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "nat-gw", { "Ref": "AZ" } ] ] } }
]
}
}
...
Public RouteTable
Добавляем в каждой AvailabilityZone по RouteTable для публичных подсетей.
Для паблик роута нужен Internet Gateway ID, который передаём из Регионального стека в рутовый, а потом в AvailabilityZones-стек.
Добавляем RouteTable, один Route в 0.0.0.0/0 через Internet Gateway, и создаём SubnetRouteTableAssociation:
...
"PublicRouteTable": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": { "Ref": "VPCID" },
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "public-rtb"] ] } }
]
}
},
"PublicRoute": {
"Type": "AWS::EC2::Route",
"Properties": {
"RouteTableId": {
"Ref": "PublicRouteTable"
},
"DestinationCidrBlock": "0.0.0.0/0",
"GatewayId": {
"Ref": "IGWID"
}
}
},
"PublicSubnetRouteTableAssociation": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"DependsOn": "PublicRouteTable",
"Properties": {
"SubnetId": {
"Ref": "PublicSubnet"
},
"RouteTableId": {
"Ref": "PublicRouteTable"
}
}
}
...
Private RouteTable
Аналогично — в AvailabilityZones стек добавляем RouteTable и связанные с ней ресурсы, только Route задаём уже не через Internet Gateway — а через NAT Gateway:
...
"PrivateRouteTable": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": { "Ref": "VPCID" },
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "priv-route", { "Ref": "AZ" } ] ] } }
]
}
},
"PrivateRoute": {
"Type": "AWS::EC2::Route",
"Properties": {
"RouteTableId": {
"Ref": "PrivateRouteTable"
},
"DestinationCidrBlock": "0.0.0.0/0",
"NatGatewayId": {
"Ref": "NATGW"
}
}
},
"PrivateSubnetRouteTableAssociation": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"SubnetId": {
"Ref": "PrivateSubnet"
},
"RouteTableId": {
"Ref": "PrivateRouteTable"
}
}
}
...
Упаковываем, деплоим, проверяем:
Все сети и маршруты поднялись — уже всё должно работать.
Теперь можно вручную запустить ЕС2 инстансы в публичной и приватной подсетях, и проверить:
- SSH к ЕС2 в публичной подсети, что бы проверить работу публичной подсети
- SSH с ЕС2 в публичной к инстансу в приватной, что бы проверить роутинг приватной подсети в целом
pingиз приватной в мир, что бы проверить работу NAT
Mappings, адреса для подсетей
Последнее, что хотелось бы изменить в AvailabilityZones стеке — это реализовать нормальную передачу блоков для подсетей.
В параметр VPCCIDRBlock мы передаём полную сеть, типа 10.0.0.0/16:
...
"VPCCIDRBlock": {
"Description": "VPC CidrBlock",
"Type": "String",
"Default": "10.0.0.0/16"
}
...
Из которой нам надо нарезать 4 посети по /20 — для двух публичных, и двух приватных подсетей.
Сейчас блоки адресов для них мы передаём явно при создании самих ресурсов подсетей:
...
"Parameters": {
"VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] },
"AZ": { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] },
"IGWID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.IGWID"] },
"EKSClusterName": { "Ref": "EKSClusterName"},
"PublicSubnetCIDR": "10.0.0.0/20",
"PrivateSubnetCIDR": "10.0.32.0/20"
}
...
Что, разумеется, нехорошо, ибо лишает гибкости — хочется иметь возможность передавать из Jenkins-параметров желаемый блок адресов для VPC, что бы потом не возникало проблем при создании VPC peerings.
Посмотрим, что у нас используется при построении 4-х блоков адресов /20 для подсети в VPC с блоком 10.0.0.0/16:
- 10.0 — начало сети
- блок третьего октета — 0, 16, 32, 48
- маска подсети — /20
При этом сети VPC будут в виде 10.0.0.0/16, 10.1.0.0/16, 10.2.0.0/16 и т.д. — под каждое рабочее окружение своя сеть.
Как бы их скомбинировать?
Можно использовать Fn::Split — по точке вырезать первые два октета из сети VPC — получим 10.0. или 10.1 и т.д.
А если VPC передадут в виде 192.168.0.0/16?… Значит — первые два октета будем получать отдельными выборками.
А для последних двух октетов и маски подсети — можно добавить CloudFormation Mappings, и потом объединить всё через Fn::Join.
Попробуем так — добавляем маппинг в рутовый стек:
...
"Mappings": {
"AZSubNets": {
"public": {
"zoneA": "0.0/20",
"zoneB": "16.0/20"
},
"private": {
"zoneA": "32.0/20",
"zoneB": "48.0/20"
}
}
},
...
Теперь самое интересное: в ресурсах AZNetworkStackА и AZNetworkStackB рутового стека в их Parameters стека вместо:
... "PublicSubnetCIDR": "10.0.0.0/20", ...
Нам надо создать что-то вроде:
<VPC-CIDR-ПЕРВЫЙ-ОКТЕТ> + <VPC-CIDR-ВТОРОЙ-ОКТЕТ> + <ЗОНА-ИЗ-МАППИНГА>
Т.е. что-то вроде такого:
{ «Fn::Join» : [ «.», [ { «VPC-CIDR-ПЕРВЫЕ-ДВА-ОКТЕТА» ] }, «ЗОНА-ИЗ-МАППИНГА»] ] } }
Что бы получить VPC-CIDR-ПЕРВЫЙ-ОКТЕТ — используем Fn::Select и Fn::Split:
{ «Fn::Select» : [ «0», { «Fn::Split»: [«.», { «Ref»: «VPCCIDRBlock»}]}] }
Аналогично — для второго, но в Fn::Select используем индекс 1:
{ «Fn::Select» : [ «1», { «Fn::Split»: [«.», { «Ref»: «VPCCIDRBlock»}]}] }
А для выбора из маппинга используем Fn::FindInMap, в которой используем тип подсети public или private, и делаем выборку по AvailabilityZone:
{ «Fn::FindInMap» : [ «AZSubNets», «public», «zone-a»» ] }
Собираем всё вместе, для AZNetworkStackА получается следующий блок:
...
"PublicSubnetCIDR": {
"Fn::Join" : [".", [
{ "Fn::Select": [ "0", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::Select": [ "1", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::FindInMap" : [ "AZSubNets", "public", "zoneA" ] }
]]
},
"PrivateSubnetCIDR": {
"Fn::Join" : [".", [
{ "Fn::Select": [ "0", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::Select": [ "1", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::FindInMap" : [ "AZSubNets", "private", "zoneA" ] }
]]
}
..
В { "Fn::FindInMap" : [ "AZSubNets", "private", "zoneA" ] } для AZNetworkStackB соответсвенно используем zoneB.
Собираем всё вместе — теперь ресурсы вложенных стеков должны выглядеть так:
...
"AZNetworkStackA": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "eks-azs-networking.json",
"Parameters": {
"VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] },
"AZ": { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] },
"IGWID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.IGWID"] },
"EKSClusterName": { "Ref": "EKSClusterName"},
"PublicSubnetCIDR": {
"Fn::Join" : [".", [
{ "Fn::Select": [ "0", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::Select": [ "1", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::FindInMap" : [ "AZSubNets", "public", "zoneA" ] }
]]
},
"PrivateSubnetCIDR": {
"Fn::Join" : [".", [
{ "Fn::Select": [ "0", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::Select": [ "1", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::FindInMap" : [ "AZSubNets", "private", "zoneA" ] }
]]
}
}
}
},
"AZNetworkStackB": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "eks-azs-networking.json",
"Parameters": {
"VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] },
"AZ": { "Fn::Select": [ "1", { "Fn::GetAZs": "" } ] },
"IGWID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.IGWID"] },
"EKSClusterName": { "Ref": "EKSClusterName"},
"PublicSubnetCIDR": {
"Fn::Join" : [".", [
{ "Fn::Select": [ "0", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::Select": [ "1", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::FindInMap" : [ "AZSubNets", "public", "zoneB" ] }
]]
},
"PrivateSubnetCIDR": {
"Fn::Join" : [".", [
{ "Fn::Select": [ "0", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::Select": [ "1", { "Fn::Split": [".", { "Ref": "VPCCIDRBlock"} ] } ] },
{ "Fn::FindInMap" : [ "AZSubNets", "private", "zoneB" ] }
]]
}
}
}
}
...
Деплоим, проверяем:
Собственно — ничего и не изменилось, т.к. блоки подсетей у нас остались прежними.
eksctl — создание стека
Запустим тестовый кластер, что бы проверить, что всё работает — а потом займёмся Ansible и остальной автоматизацией.
Параметры берём из Outputs корневого стека:
Можно уже сразу создать каталоги для будущей роли eksctl, аналогично тому, как мы создавали их для CloudFormation в самом начале этого поста:
[simterm]
$ cd ../../../
$ mkdir -p roles/eksctl/{templates,tasks}
[/simterm]
Пробуем — создаём делаем конфиг eks-cluster-config.yml:
[simterm]
$ touch roles/eksctl/templates/eks-cluster-config.yml $ cd roles/eksctl/templates/
[/simterm]
В файл вносим основные параметры для создания кластера:
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: eks-dev
region: eu-west-2
version: "1.15"
nodeGroups:
- name: worker-nodes
instanceType: t3.medium
desiredCapacity: 2
privateNetworking: true
vpc:
id: "vpc-00f7f307d5c7ae70d"
subnets:
public:
eu-west-2a:
id: "subnet-06e8424b48709425a"
eu-west-2b:
id: "subnet-07a23a9e23cbb382a"
private:
eu-west-2a:
id: "subnet-0c8a44bdc9aa6726f"
eu-west-2b:
id: "subnet-026c14589f4a41900"
nat:
gateway: Disable
cloudWatch:
clusterLogging:
enableTypes: ["*"]
Создаём кластер:
[simterm]
$ eksctl --profile arseniy create cluster -f eks-cluster-config.yml
[/simterm]
Обратите внимание, что eksctl при создании стека для кластера добавляет к его имени eksctl + <НАШЕ-ИМЯ-КЛАСТЕРА> + cluster — будем учитывать в следующей задаче, когда начнём писать роли для Ansible.
Создание кластера занимает около 15 минут, плюс после него CloudFormation создаст второй стек, с Worker Nodes — так что пока можно попить чай.
Ждём, пока запустятся Worker Nodes:
[simterm]
... [ℹ] nodegroup "worker-nodes" has 2 node(s) [ℹ] node "ip-10-0-40-30.eu-west-2.compute.internal" is ready [ℹ] node "ip-10-0-63-187.eu-west-2.compute.internal" is ready [ℹ] kubectl command should work with "/home/setevoy/.kube/config", try 'kubectl get nodes' [✔] EKS cluster "eks-dev" in "eu-west-2" region is ready
[/simterm]
Проверяем:
Стек готов.
Наш локальный kubectl уже должен быть настроен на новый кластер самим eksctl — проверяем текущий конекст:
[simterm]
$ kubectl config current-context [email protected]
[/simterm]
Проверяем доступ к кластеру и его ноды:
[simterm]
$ kubectl get nodes NAME STATUS ROLES AGE VERSION ip-10-0-40-30.eu-west-2.compute.internal Ready <none> 84s v1.15.10-eks-bac369 ip-10-0-63-187.eu-west-2.compute.internal Ready <none> 81s v1.15.10-eks-bac369
[/simterm]
Собственно — с CloudFormation мы закончили.
Вторая часть — AWS: Elastic Kubernetes Service — автоматизация создания кластера, часть 2 — Ansible, eksctl.
Ссылки по теме
Kubernetes
- Introduction to Kubernetes Pod Networking
- Kubernetes on AWS: Tutorial and Best Practices for Deployment
- How to Manage Kubernetes With Kubectl
- Building large clusters
- Kubernetes production best practices
Ansible
AWS
EKS
- kubernetes cluster on AWS EKS
- EKS vs GKE vs AKS — Evaluating Kubernetes in the Cloud
- Modular and Scalable Amazon EKS Architecture
- Build a kubernetes cluster with eksctl
- Amazon EKS Security Group Considerations
CloudFormation
- Managing AWS Infrastructure as Code using Ansible, CloudFormation, and CodeBuild
- Nested CloudFormation Stack: a guide for developers and system administrators
- Walkthrough with Nested CloudFormation Stacks
- How do I pass CommaDelimitedList parameters to nested stacks in AWS CloudFormation?
- How do I use multiple values for individual parameters in an AWS CloudFormation template?
- Two years with CloudFormation: lessons learned
- Shrinking Bloated CloudFormation Templates With Nested Stack
- CloudFormation Best-Practices
- 7 Awesome CloudFormation Hacks
- AWS CloudFormation Best Practices – Certification
- Defining Resource Properties Conditionally Using AWS::NoValue on CloudFormation





