Dlaczego AWS nie wyłączy naszych zasobów? Ratujemy nasze karty kredytowe.
Artykuł ukazał się pierwotnie na blogu Chmurowiska.
Podczas prawie każdego szkolenia, przy okazji omawiania kosztów lub budżetów w chmurze, pada pytanie, dlaczego nie możemy ustawić jakiegoś maksymalnego poziomu, powyżej którego Amazon nie wyłączy naszych zasobów. Spali byśmy spokojniej, nie obawiając się o deficyt na naszej karcie kredytowej.
Na pierwszy rzut oka takie rozwiązanie wygląda idealnie. W tym przypadku jest jednak kilka „ale”.
Co wyłączyć?
Załóżmy, że Amazon chciałby wprowadzić taką politykę i w momencie gdy na przykład przekroczymy 100$, nasze zasoby zaczęłyby znikać. Ale co kasować?
- Maszyny wirtualne, na których mam uruchomione aplikacje obsługujące klientów?
- Pliki w S3? Przecież tam jest jedyna kopia naszych zdjęć z pierwszych wakacji z naszą ukochaną.
No dobra, to nic nie kasujemy. Ograniczamy użycie…
- Lambda? Tam też trafiają użytkownicy naszych usług. Zresztą to prawie nic nie kosztuje.
- Nie pozwalamy się rozrastać naszemu sieciowemu systemowi plików uruchomionemu w usłudze EFS? Jasne… Nowe, opiewające na 1 milion dolarów zamówienia od naszych klientów, przesłane do naszego API Gateway, zwrócą im do przeglądarki coś ze statusem 500. Super, zaoszczędziliśmy dolara.
Po tych argumentach pada zawsze stwierdzenie, że powinniśmy więc mieć możliwość skonfigurowania tego, co ma się stać w momencie przekroczenia budżetu. Mamy jednak ponad 100 usług. Nie za bardzo wyobrażam sobie interfejs do konfiguracji takiego workflow, które by było uruchamiane w takiej chwili.
No dobrze, mogą mam przysłać kilkanaście emaili i, po na przykład 21 dniach, zabrać się za czyszczenie naszego konta. Ale my w tym czasie jesteśmy na wakacjach… Albo w szpitalu. Odcięci od świata. Ciekawe jakie emocje poczujemy po powrocie?
Załóżmy, że prowadzimy firmę, w której programiści mogą sobie sami tworzyć wirtualne maszyny w oparciu o usługę EC2. Nie chcemy jednak, żeby ich działania wymknęły się spod jakiejkolwiek kontroli. Musimy więc jakoś kontrolować swoje wydatki. Najlepiej w sposób automatyczny. Możemy przecież być na urlopie 🙂
Alarm!!!
OK. Nie jest jednak tak źle. Jeżeli trochę się postaramy, to nie powinniśmy obudzić się z ręką… Sami wiecie gdzie. Opcje monitoringu w AWS są naprawdę rozbudowane, a jedną z możliwości jest poproszenie o informację, jeżeli nasz rachunek przekroczył jakiś określony poziom. W tym celu utworzymy alarm, który powiadomi nas, gdy przekroczymy jakiś założony pułap płatności za usługi w AWS.
- W usłudze CloudWatch przechodzimy do Billing Alarms i tworzymy nowy alarm.
- Wpisujemy interesującą na kwotę i podajemy adres email, na który ma przyjść powiadomienie w momencie przekroczenia założonego budżetu.
- Kolejnym krokiem jest potwierdzenie subskrybowania listy mailingowej. W otrzymanym od Amazonu mailu potwierdzamy chęć otrzymywania maili
i po chwili nasz alarm będzie gotowy.
W tym momencie jesteśmy już w pewnym stopniu zabezpieczeni przed nieoczekiwanymi płatnościami za usługi, z których korzystamy. Gdy przekroczymy 10$ dostaniemy maila.
Musimy jednak zwracać uwagę na przychodzące powiadomienia i na nie reagować. Niektórym to wystarczy. My jednak stworzymy sobie…
Plan awaryjny
Mamy więc już nasz alarm. Poza wysłaniem maila możemy oczywiście pod niego podpiąć także konkretne działania. Na przykład nasza notyfikacja może wywołać funkcję Lambda.
Jednym z zastosowań funkcji Lambda jest zarządzane środowiskiem w AWS. Jeżeli dodamy do tego Step Functions to już naprawdę jedynym ograniczeniem, jest nasza wyobraźnia.
Scenariusz
Naszej firma zatrudnia grupę programistów. Każdy z nich pracuje z AWS na swoim koncie jako IAMUser. Wszyscy należą do grupy IAMDevelopers, która ma „przyklejoną” odpowiednią politykę zawierającą uprawnienia dla tej grupy.
Założyliśmy, że nasi programiści mogą sobie sami tworzyć maszyny wirtualne. W tym celu podpięliśmy do ich grupy politykę AmazonEC2FullAccess, która to umożliwia. ARN (czyli Amazon Resource Name, taki amazonowy url do zasobu) dla tej polityki to:
arn:aws:iam::aws:policy/AmazonEC2FullAccess
Przyda nam się to później.
Naszym celem jest automatyczna obrona przed za dużym rachunkiem. Musimy przyjąć więc jakieś założenia, co ma się dziać w momencie, gdy taki alarm zostanie wywołany. Co chcemy osiągnąć.
Pisząc ten tekst, co chwilę wpadam na nowy pomysł. Skupię się jednak na dwóch, które pozwolą mi zaprezentować jakie mamy możliwości i co powinniśmy zrobić, żeby nie bać się o nasze rachunki z Amazonu.
Ostrzegam, nie biorę pod uwagę zakupów w „księgarni”.
Ale do rzeczy. Plan jest taki, że po otrzymaniu notyfikacji o alarmie, nasi programiści nie mogą tworzyć nowych maszyn wirtualnych, a istniejące maszyny zostają zatrzymane. Ten drugi krok jest trochę drastyczny, ale chcę na jego przykładzie pokazać możliwości automatyzacji.
Czas mija. Nasi programiści pracują sobie spokojnie. Niestety zasoby AWS nie są darmowe i został wywołany alarm. Od tego momentu nie chcemy już, aby nasi programiści tworzyli nowe zasoby w AWS, a także chcemy zatrzymać pracujące maszyny EC2. Mamy do wykonania dwie czynności. Podmianę polityki z uprawnieniami dla grupy programistów, oraz zatrzymanie maszyn wirtualnych. Najlepiej zrobić to za pomocą funkcji Lambda, które bardzo dobrze nadają się do zarządzania zasobami w środowisku AWS.
Nasze rozwiązanie będzie się składało z trzech funkcji Lambda. Dlaczego trzech? Do naszej notyfikacji stworzonej w usłudze Simple Notification Service, oprócz naszego emaila, podepniemy także wywołanie funkcji Lambda, która odczyta potrzebne dane z konfiguracji oraz wywoła maszynę stanów utworzoną za pomocą Step Functions. A tam z kolei wywołamy nasze dwie funkcje odpowiedzialne za zmianę polityki i wyłączenie maszyn wirtualnych. Przekażemy także do nich niezbędne dane.
Nasze rozwiązanie umieścimy w regionie N.Virginia.
Składniki
Funkcje Lambda
- BudgetAlarmMasterLambda – wywołana przez alarm będzie pobierała ze zmiennych środowiskowych niezbędne dane, oraz będzie inicjowała wykonanie naszej Step Function,
- ChangeDeveloperPermissionsLambda – funkcja będzie zamieniała politykę przypisaną do grupy Developers na AmazonEC2ReadOnlyAccess,
arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess
- StopDevEC2Instances – funkcja będzie odpowiedzialna za zatrzymanie maszyn wirtualnych.
Step Function
- BudgetAlarmStepfunction – nasza maszyna stanów odpowiedzialna za zarządzanie wykonaniem wszystkich zadań.
BudgetAlarmMasterLambda
Funkcja Lambda potrzebuje uprawnień. W tym celu utworzymy nową rolę, w której pozwolimy na zapis logów oraz dodamy pełne prawa do StepFunctions. Nazwijmy naszą rolę BudgetAlarmMasterLambdaRole, a w polityce umieśćmy odpowiednie uprawnienia:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": "states:*",
"Resource": "*"
}
]
}
Jako runtime wybierzmy Pythona
i możemy przejść dalej.
Jednym z zadań tej funkcji będzie odczyt konfiguracji. Musimy jakoś podać ARN do dwóch polityk. Jedną którą chcemy odpiąć i drugą, która pozwoli tylko na sprawdzenie stanu maszyn wirtualnych. Oprócz tego, przekażemy nazwę grupy użytkowników, którym chcemy zabrać uprawnienia do tworzenia nowych instancji maszyn wirtualnych. Musimy także jakoś poinformować funkcję, którą maszynę stanów powinna zainicjować. Potrzebujemy więc czterech zmiennych środowiskowych.
Nie mamy jeszcze utworzonej naszej maszyny stanów, podajemy więc jako StepFunctionArn cokolwiek. Później to zmienimy.
Przechodzimy do kodu, który nie jest zbyt skomplikowany. Odczytuje zmienne środowiskowe oraz wywołuje step function przekazując do niej dane niezbędne do działania pozostałych funkcji Lambda.
import os
import boto3
import json
from botocore.exceptions import ClientError
def lambda_handler(event, context):
try:
stepFunctionsClient = boto3.client('stepfunctions')
data = {}
data['OldPolicyArn']=os.environ['OldPolicyArn']
data['NewPolicyArn']=os.environ['NewPolicyArn']
data['DevsGroupName'] = os.environ['DevsGroupName']
stepFunctionsMachineMachineArn = os.environ['StepFunctionArn']
response = stepFunctionsClient.start_execution(
stateMachineArn=stepFunctionsMachineMachineArn,
input=json.dumps(data)
)
return 200
except ClientError as e:
print("Error: {0}".format(e))
return 500
Mamy gotową naszą główna funkcję. Pozostały funkcje robotnice.
BudgetAlarmChangePolicyLambda
W tym miejscu podmienimy polityki i zabierzemy developerom możliwość tworzenia nowych instancji maszyn wirtualnych.
Ta funkcja, zamiast uprawnień do Step Functions, musi mieć możliwość dostępu do usługi IAM, czyli do zarządzania uprawnieniami. W tym celu stworzymy nową rolę o nazwie BudgetAlarmChangePolicyLambdaRole. Polityka powinna wyglądać następująco:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": "iam:*",
"Resource": "*"
}
]
}
Także kod tej funkcji nie jest trudny do zrozumienia.
import boto3
import json
from botocore.exceptions import ClientError
def lambda_handler(context,event):
try:
iam = boto3.resource('iam')
dev_group = iam.Group(context['DevsGroupName'])
dev_group.detach_policy(PolicyArn=context['OldPolicyArn'])
dev_group.attach_policy(PolicyArn=context['NewPolicyArn'])
return 200
except ClientError as e:
print ("Error: {0}".format(e))
return 500
Po zapisaniu funkcji przechodzimy do ostatniej Lambdy.
BudgetAlarmStopInstancesLambda
W naszym przypadku wyłączymy każdą maszynę wirtualną pracującą na koncie. Przyjmujemy, że programiści mają osobne konto AWS, na którym pracują. W rzeczywistości moglibyśmy ograniczyć listę zatrzymywanych maszyn. Możemy do tego użyć na przykład tagów i odpowiednio przefiltrować dane. Nie będziemy także przejmowali się maszynami pracującymi w grupach autoscalingowych.
Jak zwykle musimy utworzyć dla naszej lambdy rolę z odpowiednimi uprawnieniami. Nazwijmy naszą rolę BudgetAlarmStopInstancesLambdaRole, a politykę dla niej wypełnijmy następująco.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ec2:*"
],
"Resource": "*"
}
]
}
Tym razem poza zapisem logów, dajemy prawo do zarządzania maszynami wirtualnymi.
Funkcja zatrzymująca nasze maszyny wirtualne będzie trochę bardziej skomplikowana. Wcześniej używaliśmy IAM, który jest usługą globalną. Aby zatrzymać maszyny wirtualne musimy podłączyć się do każdego regionu, pobrać listę maszyn i je zatrzymać lub terminować w przypadku instancji spot.
Zwiększamy limit czasu dla tej funkcji do 90 sekund i wpisujemy poniższy kod. Tym razem dodałem kilka komentarzy, żeby był łatwiejszy w analizie.
import boto3
'''Method returns list of all AWS regions'''
def list_all_regions():
client = boto3.client('ec2')
regions = [region['RegionName'] for region in client.describe_regions()['Regions']]
return regions
'''Method stops all EC2 instances in a given region'''
def stop_all_ec2_instances(region):
ec2client = boto3.client('ec2', region_name=region)
#Get all EC2 instances
response = ec2client.describe_instances()
#List of spot instances that must be terminated instead of stopped
instances_to_terminate = []
for reservation in response["Reservations"]:
for instance in reservation["Instances"]:
instance_id = instance['InstanceId']
#List of instances to stop
instances_to_stop = []
instances_to_stop.append(instance_id)
try:
ec2client.stop_instances(InstanceIds=instances_to_stop)
except Exception as error:
if error.message.find('is a spot instance'):
instances_to_terminate.append(instance_id)
if len(instances_to_terminate) > 0:
ec2client.terminate_instances(InstanceIds=instances_to_terminate)
def lambda_handler(event, context):
#Get all AWS regions
regions = list_all_regions()
for region in regions:
#Stop or terminate spot EC2 instances
stop_all_ec2_instances(region)
W tym momencie mamy gotowe wszystkie funkcje Lambda.
Musimy je teraz połączyć za pomocą
Step Function
Step Functions to usługa pozwalająca między innymi na koordynowanie funkcji Lambda. W naszym przypadku pomoże nam wywołać poszczególne elementy naszego rozwiązania.
Na początku potrzebujemy znać adresy ARN naszych dwóch funkcji odpowiedzialnych za zmianę polityki oraz wyłączenie maszyn wirtualnych. Wchodząc do ustawień każdej, w prawym górnym rogu znajdziecie ciąg znaków podobny do arn:aws:lambda:us-east-1:xxxxxxxxxx:function:BudgetAlarmStopInstancesLambda i arn:aws:lambda:us-east-1: xxxxxxxxxx:function:BudgetAlarmStopInstancesLambda Zapiszcie je sobie, będą potrzebne za moment przy tworzeniu naszej maszyny stanów.
Ponownie utworzymy rolę. Nazwijmy ją BudgetAlarmStepFunctionRole i dodajmy do niej politykę umożliwiającą wywoływanie funkcji lambda
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction",
"states:*"
],
"Resource": [
"*"
]
}
]
}
Przechodzimy do Services->Step Functions i tworzymy nową maszynę stanów. Wybieramy dla niej utworzoną chwilę wcześniej rolę, nazwywamy ją np. BudgetAlarmStepFunction i przechodzimy do jej edycji.
Naszym celem jest osiągnięcie schematu jak na rysunku poniżej.
Utworzona maszyna stanów, uruchomiona przez naszą główną funkcję lambda wykona równolegle dwie czynności. Wywoła funkcję zabierającą uprawnienia grupie programistów oraz, po odczekaniu określonego czasu wywoła drugą funkcję, która zastopuje nasze maszyny wirtualne.
Step Functions opisujemy także za pomocą JSON-a. W naszym przypadku, definicja wygląda następująco
{
"StartAt": "BudgetActions",
"States": {
"BudgetActions": {
"Type": "Parallel",
"Next": "Final State",
"Branches": [
{
"StartAt": "Wait",
"States": {
"Wait": {
"Type": "Wait",
"Seconds": 300,
"Next": "StopEC2Instances"
},
"StopEC2Instances": {
"Type": "Task",
"Resource" : "arn:aws:lambda:us-east-1:xxxxxxxxxx:function:BudgetAlarmStopInstancesLambda",
"End": true
}
}
},
{
"StartAt": "ChangePermissions",
"States": {
"ChangePermissions": {
"Type": "Task",
"Resource" : "arn:aws:lambda:us-east-1:xxxxxxxxxx:function:BudgetAlarmChangePolicyLambda",
"End": true
}
}
}
]
},
"Final State": {
"Type": "Pass",
"Result" : "Final state",
"End": true
}
}
}
Pamiętajcie tylko, aby odpowiednio wpisać adresy zasobów (wartość dla klucza Resource). Po zapisie zapamiętujemy ARN.
Sposób wykonania
Mamy gotowe wszystkie nasz składniki. Trzeba je jeszcze tylko połączyć w całość i umożliwić ich wykonanie.
Wracamy do naszej pierwszej lambdy, BudgetAlarmMasterLambda. Musimy uzupełnić braki i wpisać do jej zmiennych adres naszej step function. Otwieramy jej zmienne środowiskowe i wpisujemy zapisany ARN. W efekcie, nasze zmienne powinny być podobne do tych poniżej.
Jak zapewnie pamiętacie mamy utworzone powiadomienie o przekroczonym budżecie. Przechodzimy więc do serwisu SNS (Simple Notification Service) i odszukujemy nasz topic
W tym momencie jedyna subskrypcja to powiadomienia email. Musimy jako subskrybenta dodać naszą główną funkcję Lambda.
Klikamy Create Subscription, wybieramy naszą funkcję master
i zatwierdzamy subskrypcję.
Nasze rozwiązanie w tym momencie powinno być gotowe. Gdybyśmy przeszli do konfiguracji funkcji BudgetAlarmMasterLambda to zobaczymy, że jako jej trigger ustawiona jest usługa SNS, czyli to co przed chwilą konfigurowaliśmy.
Testujemy
Przypomnijmy sobie założenia. Po otrzymaniu notyfikacji, nasz system powinien:
- Poprzez zamianę podpiętej polityki zabrać grupie Developers uprawnienia do tworzenia maszyn wirtualnych
- Zatrzymać wszystkie pracujące na naszym koncie maszyny wirtualne
Przed przystąpieniem do testów uruchomiłem:
- dwie instancje EC2 w regionie N.Virginia
- jedną instancję EC2 w regionie Ireland
- jedną instancję EC2 w regionie Frankfurt
Uprawnienia dla grupy Developers wyglądają następująco
Jak widać, mają oni pełne uprawnienia do usługi EC2.
W celu przetestowania działania naszego rozwiązania, przechodzimy ponownie do usługi SNS i w naszym topicu opublikujemy notyfikację.
Zarówno temat, jak i treść notyfikacji nie mają dla nas żadnego znaczenia.
Klikamy Publish message i przechodzimy do naszej step function, żeby sprawdzić jej działanie.
Po chwili zobaczymy, że nasz maszyna stanów czeka z wykonaniem funkcji zatrzymującej maszyny wirtualne, zakończyła natomiast działanie nasza funkcja lambda odpowiedzialna za zamianę polityk dla grupy Developers
Sprawdźmy więc jaka polityka podpięta jest pod grupę Developers i… Bingo 🙂
Nasi programiści nie mogą już tworzyć nowych zasobów EC2. O to chodziło.
Dobrze, a co z działającymi maszynami. Sprawdźmy czy nasza step function zakończyła działanie?
Jeżeli macie wszystkie stany zaznaczone na zielono, to znaczy, że funkcja wykonała się prawidłowo. Sprawdźmy więc co z naszymi maszynami wirtualnymi.
Virginia:
Frankfurt:
Irlandia:
Jak widać, wszystkie nasze maszyny wirtualne zostały zatrzymane.
Podsumowanie
Zaproponowane rozwiązanie składa się z dwóch części.
Podstawową funkcjonalność i podstawowy poziom bezpieczeństwa uzyskamy po zaimplementowaniu czynności opisanych w części Alarm. Już to powinno nas ochronić przed niespodziewanymi wydatkami. Od początku mojej przygody z AWS miałem zaimplementowany taki alarm i raz mnie ostrzegł. Zapomniałem usunąć Elastic Load Balancer.
W pozostałej części opisuję konkretne rozwiązanie, które automatycznie pomoże nam zatrzymać wzrost naszych rachunków za usługi wykorzystywane w AWS. Zaproponowane rozwiązanie działa, ale jest przede wszystkim propozycją i przykładem. Propozycją, ponieważ pokazuje przede wszystkim możliwości. A już od naszych potrzeb i naszej wyobraźni zależy jak to rozwiniemy. Każdy ma przecież inne zasoby w AWS i inne potrzeby i możliwości automatyzacji.