AWS: создание стека в AWS – bash-скрипт и CloudFormation шаблон

Автор: | 10/05/2018
 

Задача – развернуть мониторинг Prometehus + Grafana в AWS (в противоположность Azure на предыдущем проекте…).

Весь стек будет состоять из одного EC2, на котором будет NGINX + Prometheus + Grafana.

Из экспортёров на хосте мониторинга будут node_exporter и blackbox_exporter, скорее всего ещё mysql_exporter – собирать метрики с MariaDB бекенда нашего приложения, и какие-то ещё. Но это ещё не точно.

Вообще сбор метрик с приложения и екпортёры к нему будем думать позже.

Планируемый сетап:

  • CloudFormation шаблон для создания окружений
  • Ansible для настройки хостов и создания сервисов
  • Docker Compose – для управления сервисами мониторинга
  • Jenkins – для провижена окружений

Планируемые шаги:

  • т.к. поначалу Jenkins jobs не будет – то написать скрипт для создания/апдейта CloudFormation стеков
    • к нему добавить тестовый CF-шаблон
  • дальше можно начинать создавать CloudFormaition стек для мониторинга, который будет включать в себя:
    • VPC
      • публичная подсеть
      • отдельно ещё ACL
    • EС2: собственно, хост мониторинга, на котором всё будет работать
    • отдельный EBS для данных Promethus и базы Grafana
    • security группа для ограничения доступа к 22, 80, 443
  • дальше – скрипт для Ansible
    • тестовые плейбук и роль
  • Jenkins job-ы для запуска CloudFormation и Ansible

Собственно – этим пока и займёмся, а потом перейдём к Ansible, а ещё поже – непосредственно к настройке сбора и отображения метрик.

В этом посте будут только создание пользователя, скрипта и CloudFormation шаблона.

Скрипт создания AWS CloudFormation стека

Собственно – тут ничего нового, уже делался аналогичный, см. пост BASH: скрипт создания AWS CloudFormation стека, только тут я ещё добавил getops(), см. BASH: функция getopts – используем опции в скриптах.

Для написания скрипта потребуются:

  1. IAM политика с CloudFormation доступом
  2. IAM пользователь, от которого будет выполняться создание стека
  3. минимальный CF шаблон

IAM политика

AWS IAM предоставляет только одну готовую политику – AWSCloudFormationReadOnlyAccess, поэтому создадим свою, с полным доступом к CloudFormation API вызовам и ресурсам.

Переходим в IAM > Policies > Create Policy, в JSON-редакторе добавляем политику:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "btrmcf0",
            "Effect": "Allow",
            "Action": "cloudformation:*",
            "Resource": "*"
        }
    ]
}

Сохраняем её с именем, например, AWSCloudFormationFullAccess.

IAM пользователь

В IAM переходим в Users и создаём пользователя с Programmatic access:

В Permission выбираем Attach existing policies directly, и находим созданную ранее политику:

Сохраняем, и получаем Access и Secret ключи:

Повторяем для уже готовой политики AmazonEC2FullAccess, т.к. нужен будет доступ к EC2 и ключам:

Именованный профиль AWS

Усатанавливаем AWS CLI (рабочая машинка совсем новая):

[simterm]

$ sudo apt install awscli

[/simterm]

Далее настраиваем именованный профиль:

[simterm]

$ aws configure –profile btrm-mon
AWS Access Key ID [None]: AKI***OAA
AWS Secret Access Key [None]: 8fq***90o
Default region name [None]: eu-west-1
Default output format [None]: json

[/simterm]

Пробуем проверить стеки:

[simterm]

$ aws --profile btrm-mon cloudformation list-stacks
{
    "StackSummaries": []
}

[/simterm]

ОК.

Создание тестового стека

Используем шаблон с одним EC2 отсюда ec2_simple_stack.json.

Создаём стек, используя профайл btrm-mon:

[simterm]

$ aws –profile btrm-mon cloudformation create-stack –stack-name btrm-test –template-body file://ec2_simple_stack.json –parameters ParameterKey=AllowLocation,ParameterValue=194.183.169.26/32

[/simterm]

Проверяем:

[simterm]

$ aws –profile btrm-mon cloudformation describe-stacks –stack-name btrm-test –query ‘Stacks[*].StackStatus’ –output text
CREATE_COMPLETE

[/simterm]

ОК, удаляем его, и переходим к скрипту:

[simterm]

$ aws –profile btrm-mon cloudformation delete-stack –stack-name btrm-test

[/simterm]

Скрипт

Сам скрипт доступен в репозитории>>>, тут кратко.

Принимает три обязательных опции с аргументами – имя профайла AWS CLI, имя стека и путь к файлу шаблона:

[simterm]

$ ./create_aws_stack.sh -h

Used to create AWS CLoudFormation stack.

Usage:
-p: (mandatory) AWS CLI profile name for authorization
-s: (mandatory)CloudFormation stack name to be created/updated
-t: (mandatory) path to a template file

[/simterm]

При запуске – скрипт выполняет валидацию шаблона (cf_template_validate()), затем проверяет наличие стека, вызывая cf_stack_check_create_or_update(), которая возвращает список стеков в аккаунте, и сравнивания их с переданным именем стека в параметре -s:

...
# cf_stack_check_create_or_update() will return all stacks in a AWS account as TEXT
# test[] below will check with regex =~ if the $STACK_NAME present in the output from the cf_stack_check_create_or_update()
echo -e "\nChecking if stack $STACK_NAME already present..."
if  [[ $(cf_stack_check_create_or_update $PROFILE_NAME) =~ $STACK_NAME ]]; then
    echo -e "\nStack $STACK_NAME found, preparing Stack Update.\n"
    CREATE_OR_UPDATE="update"
else
    echo -e "\nStack $STACK_NAME not found, running Stack Create.\n"
    CREATE_OR_UPDATE="create"
fi
...

Далее переменная CREATE_OR_UPDATE передаётся аргументом функции cf_stack_exec_create_or_update(), где используется при вызове  aws cloudformation --profile $profile $create_or_update-stack:

...
cf_stack_exec_create_or_update () {

    local profile=$1
    local stack_name=$2
    local template=$3
    local create_or_update=$4

    aws cloudformation --profile $profile $create_or_update-stack --stack-name $stack_name --template-body file://$template
}
...

Пример выполнения скрипта:

[simterm]

$ ./create_aws_stack.sh -p btrm-mon -s btrm-test -t ../../setevoy-aws-templates/ec2_simple_stack.json

Validating template ../../setevoy-aws-templates/ec2_simple_stack.json...

{
    "Parameters": [
        {
            "ParameterKey": "AMIID",
            "DefaultValue": "ami-8a745cf3",
            "Description": "Debian AMI ID",
            "NoEcho": false
        },
        {
            "ParameterKey": "KeyName",
            "DefaultValue": "setevoy-test",
            "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instance",
            "NoEcho": false
        },
        {
            "ParameterKey": "InstanceType",
            "DefaultValue": "t2.nano",
            "Description": "EC2 instance type",
            "NoEcho": false
        },
        {
            "ParameterKey": "AllowLocation",
            "NoEcho": false,
            "Description": "The IP address range that can be used to SSH to the EC2 instances"
        },
        {
            "ParameterKey": "EC2AvailabilityZone",
            "DefaultValue": "eu-west-1a",
            "Description": "AZ for a EC2",
            "NoEcho": false
        }
    ],
    "Description": "AWS CloudFormation simple EC2 stack template."
}

Template OK

Checking if stack btrm-test already present...

Stack btrm-test not found, running Stack Create.

Starting AWS CloudFormation stack creation using:

AWS profile: btrm-mon
stack name: btrm-test
template: ../../setevoy-aws-templates/ec2_simple_stack.json
Stack exist: False

Are you sure to proceed? [y/n] y

Running create or update stack btrm-test...

Stack creation started. Use https://us-west-1.console.aws.amazon.com/cloudformation/home?region=us-west-1#/stacks?filter=active&tab=events to check its status.

[/simterm]

CloudFormation шаблон

Теперь можно создавать сам шаблон.

Тут потребуются такие ресурсы:

  • EIP: нужен статический IP
  • VPC
    • subnet
    • ACL
  • EC2
  • EBS – для данных Prometehus/Grafana

Сам шаблон доступен в репозитории тут – vpc_ec2_stack.json.

Кратко его основные ресурсы.

VPC

Все создаваемые ресурсы будут включены в одну VPC:

...
    "VPC" : {
      "Type" : "AWS::EC2::VPC",
      "Properties" : {
        "CidrBlock" : {"Ref" : "VPCCIDRBlock"},
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "vpc"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
        ]
      }
    },
...

В Tags создаём тег Name со значением из ИмениСтека + vpc, т.е. в результате получается имя VPC вида btrm-mon-dev-vpc:

Этот же подход используется дальше для всех ресурсов.

{"Key" : "Env", "Value" : {"Ref" : "ENV"} } используется для создания AWS Resource Group.

PublicSubnet

VPC состоит из одной публичной подсети, в которой будет работать наш EC2 с сервером мониторинга:

...
    "PublicSubnet" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : {"Ref" : "PublicSubnetCIDR"},
        "AvailabilityZone" : {"Ref" : "AvalabilityZone"},
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "public-net"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
        ]
      }
    },
...

В параметрах шаблона задаётся единая Availability Zone для всех ресурсов, которая потом ими используется:

...
"AvailabilityZone" : {"Ref" : "AvailabilityZone"},
...

И сама зона задаётся как:

{
  "AWSTemplateFormatVersion" : "2010-09-09",

  "Description" : "AWS CloudFormation stack with VPC, ACL, EC2, EBS",

  "Parameters" : {
  ...
    "AvailabilityZone": {
      "Description": "Availability Zone for all resources",
      "Type": "String",
      "Default": "eu-west-1a"
    },
  ...

InternetGateway

Для доступа в инетрнет из публичной сети – создаётся ресурс Internet Gateway:

...
    "InternetGateway" : {
      "Type" : "AWS::EC2::InternetGateway",
      "Properties" : {
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "igw"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
        ]
      }
    },
...

VPC ACL

В дополнение к EC2 Security Group – имеется отдельный Network Access Control List в VPC:

...
    "NetACL": {
      "Type" : "AWS::EC2::NetworkAcl",
      "Properties" : {
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "pub-acl"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
        ],
        "VpcId" : { "Ref" : "VPC"}
      }
    },
...

У которого в дополнение к дефолным “Deny all” пока всего два правила – “Allow all“:

...
    "NetACLInAllAllow": {
      "Type" : "AWS::EC2::NetworkAclEntry",
      "Properties" : {
        "CidrBlock" : "0.0.0.0/0",
        "Egress" : false,
        "NetworkAclId" : { "Ref" : "NetACL"},
        "Protocol" : -1,
        "RuleAction" : "Allow",
        "RuleNumber" : 100
      }
    },

    "NetACLOutAllAllow": {
      "Type" : "AWS::EC2::NetworkAclEntry",
      "Properties" : {
        "CidrBlock" : "0.0.0.0/0",
        "Egress" : true,
        "NetworkAclId" : { "Ref" : "NetACL"},
        "Protocol" : -1,
        "RuleAction" : "Allow",
        "RuleNumber" : 100
      }
    },
...

EC2 SecurityGroup

Тут сейчас основные правила доступа, разрешающие SSH, HTTP/S, ICMP для офиса и домашней сетей:

...
    "EC2SecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "sg"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
        ],
        "VpcId" : {"Ref" : "VPC"},
        "GroupDescription" : "Enable access to the Prometheus",
        "SecurityGroupIngress" : [

          {
            "IpProtocol" : "tcp",
            "FromPort" : "22",
            "ToPort" : "22",
            "CidrIp" : { "Ref" : "JenkinsIP"},
            "Description": "Jenkins/Ansible SSH allow"
          },

          {
            "IpProtocol" : "tcp",
            "FromPort" : "22",
            "ToPort" : "22",
            "CidrIp" : { "Ref" : "HomeAllowLocation"},
            "Description": "Home SSH allow"
          },

          {
            "IpProtocol" : "tcp",
            "FromPort" : "22",
            "ToPort" : "22",
            "CidrIp" : { "Ref" : "OfficeAllowLocation"},
            "Description": "Office SSH allow"
          },

          {
            "IpProtocol" : "tcp",
            "FromPort" : "80",
            "ToPort" : "80",
            "CidrIp" : { "Ref" : "HomeAllowLocation"},
            "Description": "All HTTP"
          },

          {
            "IpProtocol" : "tcp",
            "FromPort" : "443",
            "ToPort" : "443",
            "CidrIp" : { "Ref" : "HomeAllowLocation"},
            "Description": "All HTTPS"
          },

          {
            "IpProtocol" : "tcp",
            "FromPort" : "80",
            "ToPort" : "80",
            "CidrIp" : "0.0.0.0/0",
            "Description": "All HTTP"
          },

          {
            "IpProtocol" : "tcp",
            "FromPort" : "443",
            "ToPort" : "443",
            "CidrIp" : { "Ref" : "OfficeAllowLocation"},
            "Description": "All HTTPS"
          },

          {
            "IpProtocol" : "icmp",
            "FromPort" : "8",
            "ToPort" : "-1",
            "CidrIp" : { "Ref" : "HomeAllowLocation"},
            "Description": "Home ping"
          },

          {
            "IpProtocol" : "icmp",
            "FromPort" : "8",
            "ToPort" : "-1",
            "CidrIp" : { "Ref" : "OfficeAllowLocation"},
            "Description": "Office ping"
          }

        ]
      }
    },
...

"CidrIp" : { "Ref" : "OfficeAllowLocation"}, ссылается на параметр OfficeAllowLocation:

...
    "OfficeAllowLocation": {
      "Description": "The office IP address range that can be used to SSH to the EC2 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": "0.0.0.0/0"
    },
...

И аналогично – остальные сети.

EBS

Отдельно создаётся EBS, который потом подключается к создаваемому EC2:

...
    "EC2DataEBS": {
       "Type":"AWS::EC2::Volume",
       "Properties" : {
          "AvailabilityZone" : {"Ref" : "AvalabilityZone"},
          "Size" : {"Ref" : "EBSsize"},
          "Tags" : [
            {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "data-ebs"] ] } },
            {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
          ],
          "VolumeType" : "gp2"
       }
    },
...

EC2

И, наконец-то – сам EC2 интанс:

...
    "EC2Instance" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "ec2"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
        ],
        "InstanceType" : { "Ref" : "EC2InstanceType" },
        "SecurityGroupIds" : [
          {
            "Ref": "EC2SecurityGroup"
          }
        ],
        "KeyName" : { "Ref" : "KeyName" },
        "ImageId" : { "Ref" : "AMIID"  },
        "AvailabilityZone": {"Ref" : "AvalabilityZone"},
        "SubnetId": { "Ref" : "PublicSubnet" },
        "Volumes" : [
          {
           "VolumeId" : { "Ref" : "EC2DataEBS" },
           "Device" : "/dev/xvdb"
          }
        ],
        "SourceDestCheck": "false"
      }
    },
...

Собственно – всё.

Далее разворачивается стек, используя скрипт выше – и можно приступать к Jenkins и Absible.