Задача — написать шаблон для развёртывания стека, в котором будут два AWS RDS PostgreSQL инстанса (Oscar и Grover).
К инстансам необходимо обеспечить доступ из другого AWS региона, т.к. EC2 интанс с Tableau сервером находится (пока) в us-west-1
(N. California), а новые RDS сервера — в Ирландии (eu-west-1
). Поэтому — подсети в VPC будут публичными, ограничение доступа будет реализовано на уровне DB Security Group (подробнее — в документации), а для шифрования трафика между Tableau и RDS будет применяться TLS/SSL.
Содержание
Подготовка
IAM пользователь
Создаём пользователя, подключаем ему роли.
Одна есть в AWS IAM по умолчанию — arn:aws:iam::aws:policy/AmazonRDSFullAccess
.
Вторую — создаём руками. Можно просто скопировать из существующей роли AWSCloudFormationReadOnlyAccess
, и отредактировать:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "cloudformation:*" ], "Resource": "*" } ] }
Третья — тоже есть в IAM, AmazonVPCFullAccess
.
Именованный профиль AWS CLI
Настраиваем AWS CLI:
[simterm]
$ aws configure --profile ads-rds-migration AWS Access Key ID [None]: AKI***56Q AWS Secret Access Key [None]: +RT***PhW Default region name [None]: eu-west-1 Default output format [None]: json
[/simterm]
Проверяем:
[simterm]
$ aws cloudformation --profile ads-rds-migration describe-stacks { "Stacks": [ { "StackId": "arn:aws:cloudformation:eu-west-1:159524815787:stack/***", ...
[/simterm]
Шаблон CloudFormation
Параметры
Добавляем необходимый минимум параметров:
... "Parameters" : { "VPCCIDRBlock": { "Description": "VPC CIDR block", "Type": "String", "Default": "10.0.0.0/16" }, "PublicSubnet1aCIDR": { "Description": "Public Subnet CIDR, zone A", "Type": "String", "Default": "10.0.1.0/28" }, "PublicSubnet1bCIDR": { "Description": "Public Subnet CIDR, zone B", "Type": "String", "Default": "10.0.16.0/28" }, "ENV": { "Description": "Environment type. tbl-pg-rds-dev or tbl-pg-rds-production.", "Type": "String", "Default": "tbl-pg-rds-dev" }, "KievAllowLocation": { "Description": "The IP address range that can be used to access RDS instances", "Type": "String", "MinLength": "9", "MaxLength": "18", "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x.", "Default": "194.***.***.45/32" } }, ...
Mappings
Добавляем список Avalability Zones, т.к. RDS требует минимум две подсети в разных AZ:
... "Mappings" : { "SubnetsAZs" : { "public-subnets" : { "PublicSubnet1a" : "eu-west-1a", "PublicSubnet1b" : "eu-west-1b" } } }, ...
VPC, подсети
Начинаем с сети — VPC и подсети.
Добавляем три ресурса, в ресурсе VPC включаем EnableDnsSupport
и EnableDnsHostnames
для RDS интансов:
... "Resources" : { "VPC" : { "Type" : "AWS::EC2::VPC", "Properties" : { "CidrBlock" : {"Ref" : "VPCCIDRBlock"}, "EnableDnsSupport" : "true", "EnableDnsHostnames" : "true", "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "vpc"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } }, "PublicSubnet1a" : { "Type" : "AWS::EC2::Subnet", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "CidrBlock" : {"Ref" : "PublicSubnet1aCIDR"}, "AvailabilityZone" : { "Fn::FindInMap" : [ "SubnetsAZs", "public-subnets", "PublicSubnet1a" ] }, "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "public-net-a"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } }, "PublicSubnet1b" : { "Type" : "AWS::EC2::Subnet", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "CidrBlock" : {"Ref" : "PublicSubnet1bCIDR"}, "AvailabilityZone" : { "Fn::FindInMap" : [ "SubnetsAZs", "public-subnets", "PublicSubnet1b" ] }, "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "public-net-b"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } } } ...
Проверяем шаблон:
[simterm]
$ aws cloudformation --profile ads-rds-migration validate-template --template-body file://ads-pg-rds-stack.json
[/simterm]
Всё ОК — продолжаем.
Internet Gateway
Добавляем ресурс IGW:
... "InternetGateway" : { "Type" : "AWS::EC2::InternetGateway", "Properties" : { "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "igw"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } }, ...
Добавляем подключение IGW к VPC:
... "AttachGateway" : { "Type" : "AWS::EC2::VPCGatewayAttachment", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "InternetGatewayId" : { "Ref" : "InternetGateway" } } }, ...
RouteTable
Добавляем таблицу маршрутизации:
... "PublicRouteTable": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { "Ref": "VPC" }, "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "public-route"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } }, ...
Маршрут для подсети PublicSubnet в 0.0.0.0 через созданный Internet Gateway:
... "PublicRoute": { "Type": "AWS::EC2::Route", "Properties": { "RouteTableId": { "Ref": "PublicRouteTable" }, "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "InternetGateway" } } }, ...
И подключаем таблицу маршрутизации к подсетям:
... "PublicSubnetRouteTableAssociation1A": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "SubnetId": { "Ref": "PublicSubnet1a" }, "RouteTableId": { "Ref": "PublicRouteTable" } } }, "PublicSubnetRouteTableAssociation1B": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "SubnetId": { "Ref": "PublicSubnet1b" }, "RouteTableId": { "Ref": "PublicRouteTable" } } } ...
Создаём стек:
[simterm]
$ aws cloudformation --profile ads-rds-migration validate-template --template-body file://ads-pg-rds-stack.json $ aws cloudformation --profile ads-rds-migration create-stack --stack-name ads-pg-rds-dev --template-body file://ads-pg-rds-stack.json
[/simterm]
RDS
Parameters
Для серверов баз данных нам потребуются параметры:
- имя пользователя
- пароль
- имена серверов — два штука
- тип интансов
- размер дисков
Добавляем в Parameters
:
... "OscarDBInstanceName": { "Default": "oscar-rds-pg", "Description" : "Oscar DB instance name", "Type": "String", "MinLength": "1", "MaxLength": "15", "AllowedPattern" : "[a-zA-Z-][a-zA-Z0-9-]*" }, "GroverDBInstanceName": { "Default": "grover-rds-pg", "Description" : "Grover DB instance name", "Type": "String", "MinLength": "1", "MaxLength": "15", "AllowedPattern" : "[a-zA-Z-][a-zA-Z0-9-]*" }, "DBUsername": { "Default": "pgadmin", "NoEcho": "true", "Description" : "The database admin account username", "Type": "String", "MinLength": "1", "MaxLength": "16", "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*", "ConstraintDescription" : "Must begin with a letter and contain only alphanumeric characters." }, "DBPassword": { "Default": "pass", "NoEcho": "true", "Description" : "The database admin account password", "Type": "String", "MinLength": "8" }, "DBClass" : { "Default" : "db.t2.micro", "Description" : "Database instance class", "Type" : "String", "AllowedValues" : [ "db.t2.micro", "db.m1.small", "db.m1.large", "db.m1.xlarge", "db.m2.xlarge" ], "ConstraintDescription" : "Must select a valid database instance type." }, "DBAllocatedStorage" : { "Default": "5", "Description" : "The size of the database (Gb)", "Type": "Number", "MinValue": "5", "MaxValue": "6144", "ConstraintDescription" : "Must be between 5 - 1024 GB. Use at least 1024 for Production." } ...
Resources
Далее — добавляем ресурсы.
Потребуются:
AWS::EC2::SecurityGroupAWS::RDS::DBParameterGroup
AWS::RDS::DBSubnetGroup- и два
AWS::RDS::DBInstance
Для AWS::RDS::DBParameterGroup
надо будет указать параметр Family
.
Находим актуальную версию для PostgreSQL RDS:
[simterm]
$ aws rds --profile ads-rds-migration describe-db-engine-versions --engine postgres --default-only --output text DBENGINEVERSIONS PostgreSQL PostgreSQL 9.6.3-R1 postgres9.6 postgres 9.6.3 VALIDUPGRADETARGET False PostgreSQL 9.6.5-R1 postgres 9.6.5 False
[/simterm]
Добавляем ресурсы:
... "DBSecurityGroup": { "Type": "AWS::EC2::SecurityGroup", "Properties" : { "GroupDescription" : "PostgreSQL server Access", "VpcId" : { "Ref": "VPC" }, "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : 5432, "ToPort" : 5432, "CidrIp" : { "Ref": "KievAllowLocation" } }, { "IpProtocol" : "tcp", "FromPort" : 5432, "ToPort" : 5432, "CidrIp" : { "Ref": "KievVentiAllowLocation" } } ], "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "db-sg"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } }, "DBParamGroup": { "Type": "AWS::RDS::DBParameterGroup", "Properties": { "Description": "Database Parameter Group + pg_stat_statements", "Family": "postgres9.6", "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "db-params"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } }, "DBSubnetGroup" : { "Type" : "AWS::RDS::DBSubnetGroup", "Properties" : { "DBSubnetGroupDescription" : "DB Private Subnet", "SubnetIds" : [ { "Ref": "PublicSubnet1a" }, { "Ref": "PublicSubnet1b" } ], "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "db-subnet-group"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } }, "OscarRDSDB" : { "Type" : "AWS::RDS::DBInstance", "Properties" : { "DBInstanceIdentifier" : { "Ref" : "OscarDBInstanceName" }, "AllocatedStorage" : { "Ref" : "DBAllocatedStorage" }, "DBInstanceClass" : { "Ref" : "DBClass" }, "Engine" : "postgres", "MasterUsername" : { "Ref" : "DBUsername" } , "MasterUserPassword" : { "Ref" : "DBPassword" }, "DBSubnetGroupName" : { "Ref" : "DBSubnetGroup" }, "DBParameterGroupName" : {"Ref" : "DBParamGroup" }, "VPCSecurityGroups" : [ { "Fn::GetAtt" : [ "DBSecurityGroup", "GroupId" ] } ], "MultiAZ": "true", "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "oscar-rds-pg"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } }, "GroverRDSDB" : { "Type" : "AWS::RDS::DBInstance", "Properties" : { "DBInstanceIdentifier" : { "Ref" : "GroverDBInstanceName" }, "AllocatedStorage" : { "Ref" : "DBAllocatedStorage" }, "DBInstanceClass" : { "Ref" : "DBClass" }, "Engine" : "postgres", "MasterUsername" : { "Ref" : "DBUsername" } , "MasterUserPassword" : { "Ref" : "DBPassword" }, "DBSubnetGroupName" : { "Ref" : "DBSubnetGroup" }, "DBParameterGroupName" : {"Ref" : "DBParamGroup" }, "VPCSecurityGroups" : [ { "Fn::GetAtt" : [ "DBSecurityGroup", "GroupId" ] } ], "MultiAZ": "true", "Tags" : [ {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "grover-rds-pg"] ] } }, {"Key" : "Env", "Value" : {"Ref" : "ENV"} } ] } } ...
Сохраняем, выполняем validate-template
и stack-update
(или stack-create
):
[simterm]
$ aws cloudformation --profile ads-rds-migration validate-template --template-body file://ads-pg-rds-stack.json $ aws cloudformation --profile ads-rds-migration update-stack --stack-name ads-pg-rds-dev --template-body file://ads-pg-rds-stack.json
[/simterm]
Ждём обновления — проверяем:
[simterm]
$ aws rds --profile ads-rds-migration describe-db-instances | grep 'oscar-\|grover-' "DBInstanceIdentifier": "grover-rds-pg", "Address": "grover-rds-pg.***.eu-west-1.rds.amazonaws.com", "DBInstanceArn": "arn:aws:rds:eu-west-1:159524815787:db:grover-rds-pg" "DBInstanceIdentifier": "oscar-rds-pg", "Address": "oscar-rds-pg.***.eu-west-1.rds.amazonaws.com", "DBInstanceArn": "arn:aws:rds:eu-west-1:159524815787:db:oscar-rds-pg"
[/simterm]
Outputs
Последнее, что надо добавить — Outputs
:
... "Outputs" : { "OscarEndpoint": { "Description" : "Oscar PostgreSQL servers endpoint", "Value" : { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "OscarRDSDB", "Endpoint.Address" ] }, ":", { "Fn::GetAtt": [ "OscarRDSDB", "Endpoint.Port" ] } ]]} }, "GroverEndpoint": { "Description" : "Grover PostgreSQL servers endpoint", "Value" : { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "GroverRDSDB", "Endpoint.Address" ] }, ":", { "Fn::GetAtt": [ "GroverRDSDB", "Endpoint.Port" ] } ]]} } } ...
Обновляем стек, теперь с тегом Env
(для Resource Group):
$ aws cloudformation --profile ads-rds-migration update-stack --stack-name ads-pg-rds-dev --tags Key=Env,Value=tbl-pg-rds-dev --template-body file://ads-pg-rds-stack.json
Проверяем:
Группа ресурсов:
Готово.