Zatrzymywanie instancji EC2 przy użyciu Python oraz boto3

Posiadasz środowisko developerskie na AWS z wykorzystaniem EC2, ale płacisz nawet wtedy kiedy nie pracujesz? Jednym ze sposób na zmniejszenie kosztów jest automatyzacja zatrzymania EC2 i odpalanie ich tylko wtedy, kiedy potrzebujemy. W tym artykule pokaże Ci jak przy pomocy Python oraz boto3 zautomatyzować ten proces.

Do zabawy potrzebujemy mieć zainstalowany oraz skonfigurowany AWS CLI oraz środowisko Python 3, obecnie AWS Lambda wspiera maksymalnie Python 3.9. Dodatkowo musimy posiadać jakieś instancje EC2, które mają przypisany tag „Environment” na dev, ponieważ będziemy wyszukiwali instancji właśnie z takim tagiem i tylko je będziemy zatrzymywać o określonej godzinie.

Tworzenie lambdy

W pierwszej kolejności utwórzmy sobie plik trust-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Następnie musimy wykonać polecenie, które utworzy nam rolę z powyższą polityką, robimy to przy pomocy polecenia

aws iam create-role --role-name lambda-ex-role --assume-role-policy-document file://trust-policy.json

W odpowiedzi powinniśmy dostać zwrotkę jak poniżej

{
    "Role": {
        "Path": "/",
        "RoleName": "lambda-ex-role",
        "RoleId": "AROASSTMFYGKNYODE54TA",
        "Arn": "arn:aws:iam::111111111111:role/lambda-ex-role",
        "CreateDate": "2022-11-04T11:22:25+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

Zapisz sobie Arn lambdy, ponieważ niedługo będzie potrzebny 🙂

Teraz musimy utworzyć politykę dla lambdy, która pozwoli jej zatrzymywać instancje EC2 oraz pobierać o niej informację, bez tego nic nie zadziała.
Tworzymy plik lambda-ex-role.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": ["ec2:Stop*", "ec2:DescribeInstances"],
      "Resource": "*"
    }
  ]
}

Teraz musimy przypisać powyższą politykę do naszej roli, więc wykonujemy polecenie

aws iam put-role-policy --role-name lambda-ex-role --policy-name ExecutionPolicy --policy-document file://lambda-ex-role.json

Mamy utworzoną rolę z odpowiednimi uprawnieniami, teraz czas na kod Python. Tworzymy sobie plik ec2_instance.py i wklejamy do niego poniższy kod:

import boto3

def lambda_handler(event, context):
    ec2_client = boto3.client('ec2')

    instances = ec2_client.describe_instances()

    result = instances['Reservations']

    instances_to_stop = []

    for instances in result:
        for instance in instances['Instances']:
            tags = instance['Tags']

            for tag in tags:
                if tag['Key'] == "Environment" and tag['Value'] == "dev":
                    instance_id = instance['InstanceId']
                    instances_to_stop.append(instance_id)

    ec2_client.stop_instances(
        InstanceIds=instances_to_stop
    )

Powyższy kod pobiera wszystkie instancje z naszego regionu, sprawdzając które z nich, mają ustawiony tag Environment na true, wszystkie instancje które znajdzie dodaje do listy, a końcową listę przekazuje do stop_instances, która zatrzymuje wszystkie instancje z listy. Mamy przygotowany skrypt, teraz musimy go spakować do formatu zip, przy pomocy polecenia

zip ec2_instance.zip ec2_instance.py

Ostatnim krokiem jest deploy naszej lambdy na AWS, więc wykonujemy poniższe polecenie

aws lambda create-function --function-name stop_instance --zip-file fileb://ec2_instance.zip --handler ec2_instance.lambda_handler --runtime python3.9 --role arn:aws:iam::111111111111:role/lambda-ex-role

W odpowiedzi, powinniśmy dostać podobne informację do poniższych


{
    "FunctionName": "stop_instance",
    "FunctionArn": "arn:aws:lambda:eu-central-1:111111111111:function:stop_instance",
    "Runtime": "python3.9",
    "Role": "arn:aws:iam::111111111111:role/lambda-ex-role",
    "Handler": "ec2_instance.lambda_handler",
    "CodeSize": 473,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-11-04T12:04:52.808+0000",
    "CodeSha256": "fP1gElIpXUW4pJcNq2A38uF9V3eg6NR+/AD91yxrZ5Q=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "c0c17255-0cf7-4cf6-91a6-6e3d6b1b667d",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}

Ostatnim krokiem, jaki nam został i jest związany ze sprawdzeniem działania funkcji to jej wywołanie, robimy to przy pomocy polecenia

aws lambda invoke --function-name stop_instance response.json

W odpowiedzi w pliku reponse.json powinien być wynik lamby, w naszym wypadku powinien być to napis null, jeżeli jest to wszystko działa poprawnie 🙂

Konfiguracja CloudWatch Events

Mamy funkcję, ale musimy dodać jej automatyczne wywoływanie o określonej godzinie, w tym celu wykorzystamy AWS CloudWatch Events. Tworzymy sobie nową regułę

aws events put-rule --name stop-ec2-rule --schedule-expression 'cron(30 15 * * ? *)'

Ustawmy ją testowo na 15:30, każdego dnia. W odpowiedzi powinniśmy dostać ARN do naszej reguły

{
    "RuleArn": "arn:aws:events:eu-central-1:111111111111:rule/stop-ec2-rule"
}

Następnie wykonujemy poniższe bardzo długie polecenie, a miejscu source arn musimy wkleić arn, które zostało wyświetlone w poprzednim poleceniu

aws lambda add-permission --function-name stop_instance --statement-id stop-ec2-event --action 'lambda:InvokeFunction' --principal events.amazonaws.com --source-arn RULE_ARN

Powyższe zapytanie zezwala CloudWatch na wywołanie naszej funkcji.
Tworzymy sobie plik target.json, w którym umieścimy ARN naszej funkcji, którą ma nasza reguła wywołać

[
    {
      "Id": "1", 
      "Arn": "arn:aws:lambda:eu-central-1:1111111111:function:stop_instance"
    }
  ]

Uruchamiamy polecenie, które przypisze naszą lambdę, jako target dla CloudWatch

aws events put-targets --rule stop-ec2-rule --targets file://targets.json

I to wszystko 🙂 Dzięki temu rozwiązaniu, mając nawet i 100 maszyn developerskich wszystkie zostaną zatrzymane o określonej godzinie. Nasza funkcja uruchamia się codziennie o 15:30, nie ma najmniejszego problemu, aby jeszcze bardziej ograniczyć jej wywoływanie w poszczególnie dni tygodnia, ale w tej kwestii odsyłam do dokumentacji AWS https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html gdzie jest wszystko bardzo szczegółowo opisane.

Jak duże oszczędności?

To jak dużo zaoszczędzimy na zatrzymywaniu instancji EC2, w głównej mierze zależy od tego, jakiego typu instancje używamy. Załóżmy, że mamy infrastrukturę liczącą 10 maszyn t3.medium, które mają 4GB RAM, 2 core oraz 50GB SSD. Miesięczny koszt takiej infrastruktury wynosi około $405 bez VAT według kalkulatora AWS, w momencie kiedy włączamy maszyny tylko w czasie pracy, czyli 160h miesięcznie, koszt takiej infrastruktury spada do $136, więc jest to różnica $269, co według mnie jest ogromną różnicą.

Według mnie warto zatrzymywać developerskie maszyny, ponieważ w przypadku w/w infrastruktury, albo płacimy $405 dolarów za miesiąc przy działaniu maszyn 24/h, albo je wyłączamy automatycznie i w tej cenie mamy opłaconą infrastrukturę na mniej więcej 2.5 miesiąca. W skali roku różnica wynosi $3226, więc jest o co walczyć 🙂

Related Posts