Не первый раз встречается задача присвоения уникальных имен или идентификаторов для 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 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"
]
}
]
}
Например, читать все тэги что уже есть у группы, сортировать, найти максимальный, прибавить 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 сообщение, в теле которого содержится InstanceIDSQS 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"
]
}
]
}
Комментариев нет:
Отправить комментарий