Créez un Python Lambda pour économiser sur votre facture cloud avec Terraform
Pour bien utiliser le cloud, il faut parler le langage du cloud. Et le langage du cloud est le code.
Si nous voulons automatiser la gestion de nos ressources à grande échelle, nous devons utiliser du code, tel que Terraform, pour simplifier la création et la gestion des ressources. Nous devons donner la priorité à l'utilisation sans serveur, comme Lambdas, pour maintenir les coûts bas. Et nous devons pouvoir utiliser des langages de programmation, tels que Python, pour modifier dynamiquement les ressources en cours d'exécution.
Dans ce projet, nous verrons comment nous pouvons utiliser Terraform, Lambdas et Python pour créer une tâche répétable qui nous protège des frais inutiles sur nos factures cloud. Nous utiliserons Terraform pour créer un Lambda exécutant un script Python qui arrêtera toutes les instances avec une balise particulière, et nous programmerons ce Lambda pour qu'il s'exécute quotidiennement.
Mieux encore, cela se fera entièrement en code. Alors allons-y.
Conditions préalables
- Compréhension intermédiaire de Terraform — Je vais vous expliquer mes modèles, mais vous devez savoir comment fonctionnent les ressources et les modules pour tirer le meilleur parti de cette procédure pas à pas.
- Compréhension débutante de Python — Nous utiliserons les conditions, les boucles For et les listes dans le script Lambda.
- Une certaine connaissance de Boto3 — J'utiliserai le kit SDK AWS pour Python ( Boto3) tout au long du script Lambda.
La configuration se compose de plusieurs fichiers et dossiers :
providers.tf: Ce fichier définit le fournisseur AWS, qui est requis pour que Terraform puisse créer et gérer des ressources AWS.main.tf: Ce fichier illustre les ressources du projet, notamment un module de gestion des autorisations IAM et un autre module de création de la fonction Lambda.iam: Ce module définit les ressources IAM requises pour la fonction Lambda, y compris un rôle IAM avec des autorisations pour démarrer et arrêter des instances EC2 et une stratégie IAM qui peut être attachée au rôle.lambda: ce module définit la fonction Lambda, y compris son nom, son code, ainsi que le rôle IAM et l'attachement de stratégie définis dans leiammodule.python: un fichier qui contient le script qui sera utilisé par lelambdaModule.
Fournisseurs.tf
Utilisez us-east-1 pour ce projet, mais n'hésitez pas à changer de région en fonction de l'endroit où vous vous trouvez.
# -- root/variables.tf --
#Declare the AWS provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-1"
}
# -- root/main.tf --
#Defines variable I wll use to name the Lambda I'm creating
locals {
lambda_name = "stop-dev-instances"
}
#Module containing IAM permissions used by Lambda
module "iam" {
source = "./iam"
}
#Defines a data resource of type "archive_file" named "zip_the_python_code".
data "archive_file" "zip_the_python_code" {
type = "zip"
# Creates a zip archive file by combining the contents of the "python" directory and saving it to the specified output path
source_dir = "${path.module}/python/"
# The output path for the zip file is being set to a file
# Named after the value of the "lambda_name" local variable in the "python" directory
output_path = "${path.module}/python/${local.lambda_name}.zip"
}
#Module creating the actual Lambda
module "lambda" {
source = "./lambda"
lambda_name = local.lambda_name
#The filename of the zip file containing the code for the lambda function
filename = "${path.module}/python/${local.lambda_name}.zip"
#The IAM role that the lambda function should assume
lambda_role_arn = module.iam.lambda_role_arn
#The IAM policy attached to that role
role_policy_attachment = module.iam.role_policy_attachment
}
Dans ce module, nous donnerons à Lambda les autorisations dont il a besoin pour arrêter et démarrer les utilisations d'EC2. Pour ce faire, nous devons permettre à Lambda d'assumer un rôle IAM avec les autorisations appropriées.
iam/main.tf
# -- iam/main.tf --
#Policy for Lambda to assume IAM role
resource "aws_iam_role" "lambda_role" {
name = "assume-lambda-role"
#References a file from the iam directory that has the Lambda's assume role IAM policy
assume_role_policy = file("${path.module}/iam_role.txt")
}
#Policy for the IAM role to use
resource "aws_iam_policy" "iam_policy_for_lambda_role" {
name = "aws-policy-for-assume_role"
description = "AWS IAM Policy for managing assume-lambda-role"
#References a file from the iam directory that has the IAM policy for the role assumed
policy = file("${path.module}/iam_policy.txt")
}
#Attach the IAM policy to the role
resource "aws_iam_role_policy_attachment" "role_policy_attachment" {
role = aws_iam_role.lambda_role.name
policy_arn = aws_iam_policy.iam_policy_for_lambda_role.arn
}
Stratégie permettant à Lambda d'assumer le rôle IAM :
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
Stratégie permettant au rôle IAM de rechercher et d'arrêter les instances EC2 :
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:StopInstances"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:235447109042:*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:235447109042:log-group:/aws/lambda/createEnvInstances:*"
}
]
}
Ces valeurs seront transmises au module Lambda via des variables dans le fichier main.tf du module racine.
#-- iam/outputs.tf --
output "lambda_role_arn" {
value = aws_iam_role.lambda_role.arn
}
output "role_policy_attachment" {
value = aws_iam_role_policy_attachment.role_policy_attachment
}
Ce module ingère les politiques IAM précédemment créées et un script Python, que nous examinerons plus tard, pour arrêter toutes les instances EC2 qui sont étiquetées Dev. Nous utiliserons également CloudWatch pour planifier l'exécution quotidienne de Lambda.
main.tf
# -- lambda/main.tf --
#Create Lambda function that implements Python code
#This function will stop the instances labeled Dev
resource "aws_lambda_function" "lambda_function" {
#Uses the Python file that is zipped in main.tf
filename = var.filename
function_name = "stop-Dev-instances"
#Attach IAM role to Lambda
role = var.lambda_role_arn
handler = "lambda_function.lambda_handler"
runtime = "python3.8"
timeout = 60
#Wait until IAM Policy is attached to IAM role before creating
depends_on = [var.role_policy_attachment]
}
# Create the daily stop schedule
resource "aws_cloudwatch_event_rule" "every_day" {
name = "daily"
schedule_expression = "rate(1 day)"
}
# Allow CloudWatch to invoke stop_dev_lambda Function
resource "aws_lambda_permission" "allow_cloudwatch_to_invoke" {
function_name = aws_lambda_function.lambda_function.function_name
statement_id = "CloudWatchInvoke"
action = "lambda:InvokeFunction"
#Uses the daily CloudWatch stop schedule we just created
source_arn = aws_cloudwatch_event_rule.every_day.arn
principal = "events.amazonaws.com"
}
# Set the stop_dev_lambda to perform when the every_day is triggered
resource "aws_cloudwatch_event_target" "invoke_lambda" {
rule = aws_cloudwatch_event_rule.every_day.name
arn = aws_lambda_function.lambda_function.arn
depends_on = [aws_cloudwatch_event_rule.every_day, aws_lambda_function.lambda_function]
}
Déclarez les variables qui seront transmises du module racine au module Lambda :
# -- lambda_name/variables.tf --
variable "filename" {
type = string
}
variable "lambda_name" {
type = string
}
variable "lambda_role_arn" {
type = string
}
variable "role_policy_attachment" {}
Si vous vous souvenez, dans le module racine, nous avions le code ci-dessous pour créer un fichier zip que nous avons ensuite utilisé comme valeur filenamedans notre module Lambda :
#Defines a data resource of type "archive_file" named "zip_the_python_code".
data "archive_file" "zip_the_python_code" {
type = "zip"
# Creates a zip archive file by combining the contents of the "python" directory and saving it to the specified output path
source_dir = "${path.module}/python/"
# The output path for the zip file is being set to a file
# Named after the value of the "lambda_name" local variable in the "python" directory
output_path = "${path.module}/python/${local.lambda_name}.zip"
}
import json
def lambda_handler(event, context):
import logging
import boto3
#make logging executable in Lambda and locally
if len(logging.getLogger().handlers) > 0:
# the Lambda environment pre-configures a handler logging to stderr. If a handler is already configured,
# `.basicConfig` does not execute. Thus we set the level directly.
# Reference: https://stackoverflow.com/questions/37703609/using-python-logging-with-aws-lambda
logging.getLogger().setLevel(logging.INFO)
else:
logging.basicConfig(level=logging.INFO)
#Set the boto3 client to modify us-east-1 resources
#change this if your region isn't us-east-1
ec2_client=boto3.client("ec2", region_name='us-east-1')
#capture all the instance reservations in us-east-1
list_instances=ec2_client.describe_instances()
reservations=list_instances["Reservations"]
#declare list that will collect ids of instances to stop
stop_list=[]
#iterate through instance reservations
for r in reservations:
instances=r["Instances"]
#iterate through instances within the reservation
for i in instances:
instance_id = i['InstanceId']
#determine whether instance is running AND has a Environment tagged as Dev
#add matching instance_ids to stop_list
if i['State']['Name'] == 'running':
tags=i['Tags']
to_stop=False
for t in tags:
if t['Key']=='Environment' and t['Value']=='Dev':
to_stop=True
if to_stop:
logging.info(f"{instance_id} is being stopped", )
stop_list.append(instance_id)
else:
logging.info(f"{instance_id} is not tagged as Environment:Dev and will not be stopped")
else:
logging.info(f"{instance_id} is not running and will not be stopped")
logging.info(f"Stop List: {stop_list}")
#stop all instance_ids on the stop_list
if(len(stop_list)>0):
result=ec2_client.stop_instances(InstanceIds=stop_list)
#log the outcome of running ec2_clinet.stop_instances
logging.info(f"Result: {result}")
else:
#log that the list is empty if there are no Dev instances to stop
logging.info("Stop List is empty. Nothing to stop")
Il est temps de postuler
Vérifiez que vous avez créé tous les dossiers et fichiers ci-dessous dans le répertoire :
Après avoir terminé le répertoire, il est temps d'appliquer vos modifications au terminal. Vous devriez connaître ces commandes si vous avez déjà utilisé Terraform, mais sinon, les voici :
#make sure the directory has been initialized
terraform init
#validate that the configuration files are syntatically valid and internally consistent
terraform validate
#see what changes Terraform will make when you run your template
terraform plan
#deploy your infrastructure
terraform apply
Pour ce Lambda, accédez au menu Lambda dans votre console AWS. Vous devriez voir une fonction avec son nom :
Cliquez sur Lambda et cliquez sur Test :
Conservez toutes les valeurs par défaut et utilisez ce que vous voulez pour le nom de l'événement :
Lorsque vous appuyez à nouveau sur Test , vous verrez les résultats de l'exécution . Dans la section Function Logs , vous devriez voir ceci :
Comme nous n'avons aucune Devinstance en cours d'exécution, c'est le résultat attendu.
Pour tester Devdes instances en cours d'exécution, vous pouvez soit démarrer manuellement une instance avec Environment Devdes balises, soit utiliser le script Terraform ci-dessous pour créer rapidement des instances de test :
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "test" {
count = 3
ami = "ami-0093a6022697a73aa"
instance_type = "t2.micro"
tags = {
Name = "Dev-${count.index}"
Environment = "Dev"
}
}
Revenez maintenant à Lambda et exécutez à nouveau Test .
Cette fois, dans les résultats d'exécution, nous verrons que la liste d'arrêt contient les trois ID d'instance tagués avec la Dev Environmentbalise :
Et si nous regardons la console EC2, les instances ont été arrêtées :
Emballer
Comme vous l'avez vu dans cette démo, en utilisant ensemble Lambda, Python et Terraform, vous pouvez créer des processus automatisés qui répondent aux événements, exécutent du code écrit en Python et gèrent les ressources de l'infrastructure de manière fiable et rentable. Nous avons effectué une tâche relativement simple consistant à arrêter des instances selon un calendrier quotidien. Pourtant, vous pouvez imaginer comment ces trois outils pourraient être utilisés ensemble pour des tâches telles que le traitement des données, les sauvegardes et la surveillance.
Si vous souhaitez voir le code que j'ai utilisé dans le cours, le référentiel peut être trouvé ici :https://github.com/nickcmiller/tf-stop-instances
![Qu'est-ce qu'une liste liée, de toute façon? [Partie 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































