Erstellen Sie mit Terraform ein Python-Lambda, um Ihre Cloud-Rechnung zu sparen

Jan 02 2023
Um die Cloud gut nutzen zu können, müssen Sie die Sprache der Cloud sprechen. Und die Sprache der Cloud ist Code.

Um die Cloud gut nutzen zu können, müssen Sie die Sprache der Cloud sprechen. Und die Sprache der Cloud ist Code.

Wenn wir die Verwaltung unserer Ressourcen im großen Maßstab automatisieren wollen, müssen wir Code wie Terraform verwenden, um die Erstellung und Verwaltung von Ressourcen zu vereinfachen. Um die Kosten niedrig zu halten, müssen wir der Verwendung serverloser Systeme wie Lambdas Priorität einräumen. Und wir müssen in der Lage sein, Programmiersprachen wie Python zu verwenden, um laufende Ressourcen dynamisch zu ändern.

In diesem Projekt werden wir sehen, wie wir Terraform, Lambdas und Python verwenden können, um eine wiederholbare Aufgabe zu erstellen, die uns vor unnötigen Gebühren für unsere Cloud-Rechnungen schützt. Wir werden Terraform verwenden, um ein Lambda zu erstellen, das ein Python-Skript ausführt, das alle Instanzen mit einem bestimmten Tag stoppt, und wir werden die tägliche Ausführung dieses Lambda planen.

Das Beste daran ist, dass dies vollständig im Code erfolgt. Also lasst uns loslegen.

Voraussetzungen

  • Mittleres Verständnis von Terraform – Ich erkläre meine Vorlagen, aber Sie sollten wissen, wie Ressourcen und Module funktionieren, um diese exemplarische Vorgehensweise optimal nutzen zu können.
  • Anfängerverständnis von Python – Wir verwenden Bedingungen, For-Schleifen und Listen im Lambda-Skript.
  • Einige Kenntnisse von Boto3 – ich werde im gesamten Lambda-Skript das AWS SDK für Python (Boto3) verwenden.

Das Setup besteht aus mehreren Dateien und Ordnern:

  • providers.tf: Diese Datei definiert den AWS-Anbieter, der erforderlich ist, damit Terraform AWS-Ressourcen erstellen und verwalten kann.
  • main.tf: Diese Datei veranschaulicht die Ressourcen für das Projekt, einschließlich eines Moduls zum Verwalten von IAM-Berechtigungen und eines weiteren Moduls zum Erstellen der Lambda-Funktion.
  • iam: Dieses Modul definiert die für die Lambda-Funktion erforderlichen IAM-Ressourcen, einschließlich einer IAM-Rolle mit Berechtigungen zum Starten und Stoppen von EC2-Instanzen und einer IAM-Richtlinie, die an die Rolle angehängt werden kann.
  • lambda: Dieses Modul definiert die Lambda-Funktion, einschließlich ihres Namens, Codes sowie der im Modul definierten IAM-Rolle und Richtlinienanhänge iam.
  • python: eine Datei, die das Skript enthält, das vom lambdaModul verwendet wird.

Anbieter.tf

Verwenden Sie für dieses Projekt us-east-1, können Sie diese Region jedoch je nach Standort ändern.

# -- 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
}

In diesem Modul erteilen wir Lambda die Berechtigungen, die es zum Stoppen und Starten von EC2-Verwendungen benötigt. Dazu müssen wir Lambda ermöglichen, eine IAM-Rolle mit den entsprechenden Berechtigungen zu übernehmen.

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
}

Richtlinie, die es Lambda ermöglicht, die IAM-Rolle zu übernehmen:

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

Richtlinie, die es der IAM-Rolle ermöglicht, EC2-Instanzen zu finden und zu stoppen:

{
    "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:*"
        }
    ]
}

Diese Werte werden im Lambda-Modul über Variablen in der main.tf im Root-Modul übergeben.

#-- 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
}

Dieses Modul nimmt die zuvor erstellten IAM-Richtlinien und ein Python-Skript auf, das wir anschließend überprüfen werden, um alle EC2-Instanzen mit der Bezeichnung zu stoppen Dev. Wir werden CloudWatch auch verwenden, um die tägliche Ausführung von Lambda zu planen.

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]
}

Deklarieren Sie die Variablen, die vom Root-Modul an das Lambda-Modul übergeben werden:

# -- lambda_name/variables.tf --

variable "filename" {
  type = string
}

variable "lambda_name" {
  type = string
}

variable "lambda_role_arn" {
  type = string
}

variable "role_policy_attachment" {}

Wenn Sie sich erinnern, hatten wir im Root-Modul den folgenden Code zum Erstellen einer ZIP-Datei, die wir später als filenameWert in unserem Lambda-Modul verwendeten:

#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")

Zeit, sich zu bewerben

Stellen Sie sicher, dass Sie alle folgenden Ordner und Dateien im Verzeichnis erstellt haben:

Nachdem Sie das Verzeichnis ausgefüllt haben, ist es an der Zeit, Ihre Änderungen am Terminal zu übernehmen. Sie sollten diese Befehle kennen, wenn Sie Terraform schon einmal verwendet haben. Wenn nicht, finden Sie sie hier:


#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

Navigieren Sie zu diesem Lambda zum Lambda-Menü in Ihrer AWS-Konsole. Sie sollten eine Funktion mit ihrem Namen sehen:

Klicken Sie in das Lambda und dann auf Test :

Behalten Sie alle Standardeinstellungen bei und verwenden Sie einen beliebigen Namen für den Ereignisnamen :

Wenn Sie erneut auf „Testen“ klicken , werden die Ausführungsergebnisse angezeigt . Im Abschnitt „Funktionsprotokolle“ sollten Sie Folgendes sehen:

Da keine DevInstanzen ausgeführt werden, ist dies das erwartete Ergebnis.

Um laufende DevInstanzen zu testen, können Sie entweder manuell eine Instanz mit Tags hochfahren Environment Devoder das folgende Terraform-Skript verwenden, um schnell einige Testinstanzen zu erstellen:

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"
  }
}

Gehen Sie nun zurück zu Lambda und führen Sie Test erneut aus.

Diesmal sehen wir in den Ausführungsergebnissen, dass die Stoppliste die drei Instanz-IDs enthält, die mit dem Dev EnvironmentTag versehen sind:

Und wenn wir uns die EC2-Konsole ansehen, wurden die Instanzen gestoppt:

Zusammenfassung

Wie Sie in dieser Demo gesehen haben, können Sie durch die gemeinsame Verwendung von Lambda, Python und Terraform automatisierte Prozesse erstellen, die auf Ereignisse reagieren, in Python geschriebenen Code ausführen und Infrastrukturressourcen auf zuverlässige und kostengünstige Weise verwalten. Wir haben eine relativ einfache Aufgabe durchgeführt und Instanzen nach einem täglichen Zeitplan gestoppt. Dennoch können Sie sich vorstellen, wie diese drei Tools zusammen für Aufgaben wie Datenverarbeitung, Backups und Überwachung verwendet werden könnten.

Wenn Sie den Code sehen möchten, den ich im Kurs verwendet habe, finden Sie das Repo hier:https://github.com/nickcmiller/tf-stop-instances