Задача: продумать автоматизацию развёртывания 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