понедельник, 18 марта 2019 г.

AWS. Схема создания уникальных имен (тэгов) в ASG.

Не первый раз встречается задача присвоения уникальных имен или идентификаторов для ASG. Ну вот надо и всё :) Задача на первый вгляд кажется простая, чего бы там не поставить в UserData вызов aws ec2 create-tags .... тут и начинается задача. Тэг уникальный должен быть а значит надо как то вычислять его уникальность. Решений в лоб несколько приходит на ум.
Например, читать все тэги что уже есть у группы, сортировать, найти максимальный, прибавить 1 и получить решение задачи. Проблемы сразу вылезают:
 - нумерация будет со временем большими цифрами, особенно если группа часто изменяется по составу.
- вторая более серьезная и будет касаться почти всех остальных решений в лоб - тэги на инстанцах появляются не сразу же, может пройти какое-то время прежде чем тэг появится. А это означает, что при старте более одного нового инстанца в группе почти гарантированно каждый новый инстанц получит один и тот же номер.

Что я придумал делать, что бы избавиться от этих двух проблем.
Решение выглядит так - инстанц стартует с ролью, которой разрешено писать сообщения в SQS. В Userdata простой скрипт:
-----------------------
#!/bin/bash
echo "Send to SQS out INSTANCE ID"

INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
AVZONE=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone);
REGION=${AVZONE::-1}

echo "INSTANCE_ID: ${INSTANCE_ID}"
sqsStatus="aws sqs send-message --queue-url https://sqs.${REGION}.amazonaws.com/XXXXXXXXX/BName --message-body \"${INSTANCE_ID}\" --delay-seconds 10 --region ${REGION}"

if [ "$(eval $sqsStatus)" ]; then
 echo "Message sent"
else
 echo "Error sending message to SQS"
 exit 1
fi
-------------------------
Скрипт просто отправляет в SQS сообщение, в теле которого содержится InstanceID

SQS Standard. Я не смог победить вариант FIFO, она интегрирована сразу с Lambda по получению сообщения, лямбда что получает такое оповещение почему то не получает никогда сообщений. Вызывается лямбда по событию но сообщений в очереди уже почему то нет. Не знаю почему а было бы хорошо!! Ну сделать пришлось так - SQS Standard + Lambda Python + CloudWatch Cron каждые минут 10 проверяю очередь. Стоимость SQS+Lambda+Cloudwatch в таком использовании практически нулевая.

Лямбда:
---------------------
import json
import boto3
import re
queue_url = 'https://sqs.us-east-2.amazonaws.com/XXXXXXXX/Bname'
HostedZoneId = 'XXXXXXXXXX'
sqs_client = boto3.client('sqs')
ec2_resource = boto3.resource('ec2')
ec2_client = boto3.client('ec2')
r53_client = boto3.client('route53')

def add_cname_record(source, target):
    try:
        response = r53_client.change_resource_record_sets(
          HostedZoneId=HostedZoneId,
          ChangeBatch= {
            'Comment': 'add %s -> %s' % (source, target),
            'Changes': [{
            'Action': 'UPSERT',
            'ResourceRecordSet': {
              'Name': source+'.'+r53_client.get_hosted_zone(Id=HostedZoneId)['HostedZone']['Name'],
              'Type': 'CNAME',
              'TTL': 300,
              'ResourceRecords': [{'Value': target}]
            }
          }]
        }
       )
    except Exception as e:
       print(e)
    return

def get_messages_from_queue():
    #sqs_client = boto3.client('sqs')
    messages = []
    while True:
        resp = sqs_client.receive_message(
            QueueUrl=queue_url,
            AttributeNames=['All'],
            MaxNumberOfMessages=10
        )
     
        try:
            messages.extend(resp['Messages'])
        except KeyError:
            break

        #mm = json.dumps(messages)
        #print('MSG: %s' % mm)
        entries = [
            {'Id': msg['MessageId'], 'ReceiptHandle': msg['ReceiptHandle']}
            for msg in resp['Messages']
        ]

        resp = sqs_client.delete_message_batch(
            QueueUrl=queue_url, Entries=entries
        )

        if len(resp['Successful']) != len(entries):
            raise RuntimeError(
                f"Failed to delete messages: entries={entries!r} resp={resp!r}"
        )
    return messages

def get_ec2_name(instanceId,tag):
    ## return NAME of instance by instanceId tag (prefered Name)
    ec2instance = ec2_resource.Instance(instanceId)
    instanceName = ''
    for tags in ec2instance.tags:
        if tags["Key"] == tag:
            instanceName = tags["Value"]
    return instanceName

def set_ec2_tag_bname(instanceId,tagBname):
    ## set TAG tagName for instance by instanceId
    ids = []
    ids.append(instanceId)
    ec2_client.create_tags(
        Resources=ids,
        Tags=[
          {
            'Key': 'BName',
            'Value': tagBname
          }
        ]
    )
    return

def list_instanceIds_by_tag_value(tagkey, tagvalue):
    # When passed a tag key, tag value this will return a list of InstanceIds that were found.
    #print(tagkey)
    #print(tagvalue)
    response = ec2_client.describe_instances(
        Filters=[
            {
                'Name': 'tag:'+tagkey,
                'Values': [tagvalue]
            },
            {'Name': 'instance-state-name', 'Values': ['running']}
        ]
    )
    instancelist = []
    for reservation in (response["Reservations"]):
        for instance in reservation["Instances"]:
            instancelist.append(instance["InstanceId"])
    return instancelist

def list_instanceBnameIndex_by_tag_value(tagkey, tagvalue):
    # return list of digits each BNAME tag
    #print(tagkey)
    #print(tagvalue)
    response = ec2_client.describe_instances(
        Filters=[
            {
                'Name': 'tag:'+tagkey,
                'Values': [tagvalue+'*']
            },
            {'Name': 'instance-state-name', 'Values': ['running']}
        ]
    )
    instancelist = []
    for reservation in (response["Reservations"]):
        for instance in reservation["Instances"]:
            for tags in instance["Tags"]:
              if tags["Key"] == tagkey:
                 if tags["Value"]:
                   bname = re.findall('([0-9]+)$',tags["Value"])
                   bname = int(''.join(bname))
                   #print('II: %s' % bname)
                   instancelist.append(bname)
    instancelist.sort()

    return instancelist

def lambda_handler(event, context):
    #print('Start poll SQS')
    messages = get_messages_from_queue()
    if not messages:
        print('SQS is empty')
        return
   
    for message in messages:
        instanceId = message['Body']
        print('InstanceID: %s' % instanceId)
        if get_ec2_name(instanceId,'BName'):
           print('%s has BNAME!' % instanceId)
        else: 
           ids = []
           ids.append(instanceId)
           instanceName = get_ec2_name(instanceId,'Name')
           print('Tag Name of instance: %s' % instanceName)
           indexCurrentBname = []
           indexCurrentBname = list_instanceBnameIndex_by_tag_value('BName',instanceName)
           print('indexCurrentBname instances: %s' % indexCurrentBname)
           if not indexCurrentBname:
              print('No BNAME tags for %s. Set to 1' % instanceName)
              myFreeNumber = 1
           else:
              #print('IndexCurrentBname: %s' % indexCurrentBname)
              countAllInstancesByTagName = len(list_instanceIds_by_tag_value('Name', instanceName))
              print('countAllInstancesByTagName = %s' % countAllInstancesByTagName)
              fullArrayOfElements = list(range(1,countAllInstancesByTagName+1))
              #countFullArrayElement = len(list_instanceIds_by_tag_value('Name',instanceName))+1
              #fullArrayOfElements = list(range(1,countFullArrayElement))
              print('fullArrayOfElements %s' % fullArrayOfElements)

              for item in indexCurrentBname:
                  if item in fullArrayOfElements:
                     fullArrayOfElements.remove(item)

              myFreeNumber = min(fullArrayOfElements)
              freeNumbers = []
              for item in fullArrayOfElements:
                 freeNumbers.append(item)

              print('Current list of free elements: %s' % freeNumbers)
           print('Current free min number is: %s' % myFreeNumber)
           myTagName = instanceName+str(myFreeNumber)
           myTagName = myTagName.lower().replace(" ","")
           print('MyTagName will %s' % myTagName)
           set_ec2_tag_bname(instanceId,myTagName)
           add_cname_record(myTagName,ec2_resource.Instance(instanceId).private_dns_name)
         

-------------------------------------------------
Я не спец в питоне, пишу время от времени.
Что тут я задумал - читаю SQS до 10 сообещний максимум. В цикле отрабатываю каждое:
- тэг Name у каждого инстанца я не меняю (в задаче так было сказано). Зато я беру Name как основу для построения дополнительного тэга BName, который формируется как NameXX, где XX это уникальный номер в группе. Номер я ставлю минимальный свободный из списка номеров от 1 до текущего максимального в тэге BName. Так я не даю вырастать номерам до огромных цифр. Это решение позволило уникализировать и имена инстанцев в prometheus, ansible. В Ansible я использую связку -i /etc/ec2.py и hosts=tag_BName_yyyyyyyXX, для отработки на конкретном хосте или tag_Nane_yyyyyy что бы применить для всей группы хостов что либо.


Роль у хостов должна содержать право писать в SQS:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "sendSQSusBname",
            "Effect": "Allow",
            "Action": "sqs:SendMessage",
            "Resource": [
                "arn:aws:sqs:us-east-2:XXXXXX:Bname",
                "arn:aws:sqs:ap-northeast-1:XXXXXX:Bname"
            ]
        }
    ]
}









Комментариев нет: