LAB06_AWS_Lambda Function with IAM Execution Role

Lambda Function with IAM Execution Role

☁ AWS ⭐ Beginner Lambda IAM archive_file CloudWatch Logs

Scenario

Your team wants to run a small Python function that logs a greeting message whenever it is invoked. Rather than uploading a zip file manually, you want Terraform to package and deploy the function code automatically from a local file.

Provision the Lambda function, its IAM execution role (with basic logging permissions), and verify it runs by invoking it from the CLI.

Your Objectives
  • Write a small Python handler in a local lambda_src/handler.py file.
  • Use the archive_file data source to zip the source directory automatically.
  • Create an aws_iam_role with a trust policy for lambda.amazonaws.com.
  • Attach the AWSLambdaBasicExecutionRole managed policy so the function can write to CloudWatch Logs.
  • Create an aws_lambda_function pointing to the zip file, using Python 3.12 runtime.
  • Output the function ARN and invoke URL for testing.

Additional Context

The archive_file data source (from the hashicorp/archive provider) creates a zip of your source directory each time Terraform runs. Terraform tracks the output_base64sha256 of the archive — if the source code changes, the hash changes, which triggers a Lambda function update on the next apply. This is a common zero-friction deployment pattern.

lambda_src/handler.py

# lambda_src/handler.py  (Python source — not HCL)
import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    name = event.get("name", "World")
    message = f"Hello, {name}! Welcome to Terraform + Lambda."
    logger.info(message)
    return {
        "statusCode": 200,
        "body": json.dumps({"message": message})
    }

versions.tf

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws     = { source = "hashicorp/aws",     version = "~> 5.0" }
    archive = { source = "hashicorp/archive", version = "~> 2.4" }
  }
}

variables.tf

variable "aws_region"     { type = string; default = "us-east-1" }
variable "function_name"  { type = string; default = "hello-world" }
variable "environment"    { type = string; default = "dev" }

main.tf

# main.tf

provider "aws" {
  region = var.aws_region
}

# Archive the lambda_src/ directory into a zip file.
# Terraform re-zips on every plan. If the source changes, output_base64sha256
# changes, which triggers a new Lambda deployment automatically.
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda_src"
  output_path = "${path.module}/lambda.zip"
}

# Trust policy — allow the Lambda service to assume this role.
data "aws_iam_policy_document" "lambda_assume" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

# Lambda execution role.
resource "aws_iam_role" "lambda" {
  name               = "${var.function_name}-role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
  tags               = { ManagedBy = "terraform", Environment = var.environment }
}

# Attach AWS managed policy: allows writing logs to CloudWatch.
resource "aws_iam_role_policy_attachment" "lambda_logs" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# The Lambda function itself.
resource "aws_lambda_function" "hello" {
  function_name = var.function_name
  runtime       = "python3.12"
  handler       = "handler.lambda_handler"   # filename.function_name
  role          = aws_iam_role.lambda.arn

  # Point to the zip created by the archive_file data source.
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  timeout     = 30   # seconds
  memory_size = 128  # MB

  environment {
    variables = {
      ENV = var.environment
    }
  }

  tags = { ManagedBy = "terraform", Environment = var.environment }

  # Ensure the role exists before the function is created.
  depends_on = [aws_iam_role_policy_attachment.lambda_logs]
}

outputs.tf

output "function_arn"  { value = aws_lambda_function.hello.arn }
output "function_name" { value = aws_lambda_function.hello.function_name }

Workflow Commands

mkdir -p lambda_src
# (create lambda_src/handler.py with the Python code above)

terraform init
terraform apply

# Invoke the function and see the response
aws lambda invoke \
  --function-name $(terraform output -raw function_name) \
  --payload '{"name":"Terraform"}' \
  --cli-binary-format raw-in-base64-out \
  response.json

cat response.json
# {"message": "Hello, Terraform! Welcome to Terraform + Lambda."}

terraform destroy
✓ Expected Output After Apply
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

function_arn  = "arn:aws:lambda:us-east-1:123456789012:function:hello-world"
function_name = "hello-world"