Back to HomeAWS Lambda

Terraform AWS Lambda Deployment Complete Tutorial: IaC Best Practices

13 min min read
#AWS Lambda#Terraform#IaC#DevOps#CI/CD#Serverless

Terraform AWS Lambda Deployment Complete Tutorial: IaC Best Practices

Terraform AWS Lambda Deployment Complete Tutorial: IaC Best Practices

Manually creating Lambda in AWS Console is simple, but when you have 10, 50, or even 100 Lambda functions to manage, things get out of control.

Not to mention manually clicking through each deployment, configuration inconsistencies between environments, and inability to track who changed what. Terraform solves all these problems.

This article will teach you from basics to advanced how to manage AWS Lambda with Terraform, building repeatable, traceable, and collaborative infrastructure.

To first understand Lambda concepts and features, see AWS Lambda Complete Guide.


Why Use Terraform to Manage Lambda

Core Advantages of IaC

Infrastructure as Code (IaC) defines infrastructure as code, bringing these benefits:

AdvantageTraditional Manual DeploymentTerraform IaC
Version ControlCannot track changesComplete Git history
Environment ConsistencyEasy to have config differencesSame code, same results
Collaboration ReviewVerbal communication, error-pronePull Request Code Review
Disaster RecoveryManually reconfigureterraform apply to rebuild
DocumentationWrite docs separately, easily outdatedCode is documentation

Terraform vs CloudFormation vs CDK Comparison

FeatureTerraformCloudFormationAWS CDK
SyntaxHCL (Declarative)YAML/JSONTypeScript/Python/Java
Multi-cloud SupportSupportedAWS onlyAWS only
Learning CurveMediumMediumSteeper (need programming language)
Community ResourcesVery richAWS officialGrowing rapidly
State ManagementSelf-manage StateAWS auto-managesBased on CloudFormation
Debugging ExperienceBetterError messages harder to understandBetter

Selection Recommendations:

  • Multi-cloud environment or team familiar with Terraform → Choose Terraform
  • Pure AWS and team familiar with TypeScript/Python → Choose CDK
  • Enterprise environment needing AWS official support → Choose CloudFormation

Basic Setup

Provider Configuration

First create provider.tf:

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  # Recommended: Use remote State storage
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "lambda/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "terraform"
      Project     = "my-lambda-project"
    }
  }
}

Lambda Required Resources

A complete Lambda deployment requires three core resources:

1. IAM Execution Role

# IAM Role - Lambda's execution identity
resource "aws_iam_role" "lambda_role" {
  name = "${var.function_name}-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

# Attach basic execution policy (CloudWatch Logs permissions)
resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

2. Lambda Function

resource "aws_lambda_function" "this" {
  function_name = var.function_name
  role          = aws_iam_role.lambda_role.arn
  handler       = "index.handler"
  runtime       = "nodejs20.x"

  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  memory_size = 256
  timeout     = 30

  environment {
    variables = {
      NODE_ENV = var.environment
    }
  }
}

3. Code Packaging

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/src"
  output_path = "${path.module}/dist/lambda.zip"
}

Complete Examples

Simple Lambda Function

Complete single Lambda deployment example:

# main.tf
locals {
  function_name = "hello-world-${var.environment}"
}

# Code packaging
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/src"
  output_path = "${path.module}/dist/lambda.zip"
}

# IAM Role
resource "aws_iam_role" "lambda" {
  name = "${local.function_name}-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_logs" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# Lambda Function
resource "aws_lambda_function" "hello" {
  function_name = local.function_name
  role          = aws_iam_role.lambda.arn
  handler       = "index.handler"
  runtime       = "nodejs20.x"

  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  memory_size = 128
  timeout     = 10

  environment {
    variables = {
      ENVIRONMENT = var.environment
    }
  }
}

# Outputs
output "function_arn" {
  value = aws_lambda_function.hello.arn
}

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

Lambda + API Gateway

Create HTTP API integration:

# API Gateway HTTP API
resource "aws_apigatewayv2_api" "api" {
  name          = "${var.project_name}-api"
  protocol_type = "HTTP"

  cors_configuration {
    allow_origins = ["*"]
    allow_methods = ["GET", "POST", "PUT", "DELETE"]
    allow_headers = ["Content-Type", "Authorization"]
    max_age       = 300
  }
}

# Lambda integration
resource "aws_apigatewayv2_integration" "lambda" {
  api_id           = aws_apigatewayv2_api.api.id
  integration_type = "AWS_PROXY"

  integration_uri    = aws_lambda_function.api.invoke_arn
  integration_method = "POST"
  payload_format_version = "2.0"
}

# Route configuration
resource "aws_apigatewayv2_route" "default" {
  api_id    = aws_apigatewayv2_api.api.id
  route_key = "$default"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

# Stage deployment
resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.api.id
  name        = "$default"
  auto_deploy = true
}

# Lambda execution permission
resource "aws_lambda_permission" "apigw" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.api.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
}

output "api_endpoint" {
  value = aws_apigatewayv2_stage.default.invoke_url
}

For more API Gateway integration details, see API Gateway Integration Concepts.

Lambda + EventBridge Scheduled Tasks

Create hourly scheduled task:

# CloudWatch Event Rule (using EventBridge)
resource "aws_cloudwatch_event_rule" "schedule" {
  name                = "${var.function_name}-schedule"
  description         = "Trigger once per hour"
  schedule_expression = "rate(1 hour)"

  # Or use Cron expression
  # schedule_expression = "cron(0 * * * ? *)"
}

# Event Target
resource "aws_cloudwatch_event_target" "lambda" {
  rule      = aws_cloudwatch_event_rule.schedule.name
  target_id = "TriggerLambda"
  arn       = aws_lambda_function.scheduled.arn
}

# Lambda execution permission
resource "aws_lambda_permission" "eventbridge" {
  statement_id  = "AllowEventBridgeInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.scheduled.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.schedule.arn
}

For more event-driven architecture content, see EventBridge Event-Driven Architecture.

Want to design a more robust IaC architecture? Book architecture consultation and let experts help you plan.


Terraform Lambda Module

Using Existing Modules

The community-maintained terraform-aws-modules/lambda is the most popular Lambda Module:

module "lambda_function" {
  source  = "terraform-aws-modules/lambda/aws"
  version = "~> 6.0"

  function_name = "my-lambda-function"
  description   = "My awesome lambda function"
  handler       = "index.handler"
  runtime       = "nodejs20.x"

  source_path = "./src"

  # VPC configuration
  vpc_subnet_ids         = var.private_subnet_ids
  vpc_security_group_ids = [aws_security_group.lambda.id]

  # Environment variables
  environment_variables = {
    DATABASE_URL = var.database_url
    NODE_ENV     = var.environment
  }

  # CloudWatch Logs
  cloudwatch_logs_retention_in_days = 14

  # Performance settings
  memory_size = 512
  timeout     = 60

  # Concurrency limit
  reserved_concurrent_executions = 100

  tags = var.common_tags
}

Module Advantages:

  • Automatically creates IAM Role and Policy
  • Built-in code packaging functionality
  • Supports Layers, VPC, Alias and other advanced features
  • Continuously maintained and updated

Custom Module Structure

When you need more customized logic, you can build your own Module:

modules/
└── lambda/
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    └── iam.tf

variables.tf

variable "function_name" {
  description = "Lambda function name"
  type        = string
}

variable "handler" {
  description = "Lambda handler"
  type        = string
  default     = "index.handler"
}

variable "runtime" {
  description = "Runtime environment"
  type        = string
  default     = "nodejs20.x"
}

variable "source_dir" {
  description = "Code directory"
  type        = string
}

variable "memory_size" {
  description = "Memory size (MB)"
  type        = number
  default     = 256
}

variable "timeout" {
  description = "Execution timeout (seconds)"
  type        = number
  default     = 30
}

variable "environment_variables" {
  description = "Environment variables"
  type        = map(string)
  default     = {}
}

variable "tags" {
  description = "Resource tags"
  type        = map(string)
  default     = {}
}

outputs.tf

output "function_arn" {
  description = "Lambda function ARN"
  value       = aws_lambda_function.this.arn
}

output "function_name" {
  description = "Lambda function name"
  value       = aws_lambda_function.this.function_name
}

output "invoke_arn" {
  description = "Lambda invoke ARN"
  value       = aws_lambda_function.this.invoke_arn
}

output "role_arn" {
  description = "IAM role ARN"
  value       = aws_iam_role.lambda.arn
}

Variable and Output Design Principles

  1. Required parameters have no defaults: Force callers to provide
  2. Reasonable defaults: Common settings have defaults to reduce repeated code
  3. Clearly defined types: Use type to enforce type checking
  4. Clear descriptions: Every variable has a description
  5. Output useful information: ARN, Name, Invoke URL and other commonly used attributes

Advanced Techniques

Managing Lambda Code

Method 1: archive_file (small projects)

data "archive_file" "lambda" {
  type        = "zip"
  source_dir  = "${path.module}/src"
  output_path = "${path.module}/dist/lambda.zip"
  excludes    = ["node_modules", "*.test.js"]
}

Method 2: S3 Storage (large projects)

resource "aws_s3_object" "lambda_code" {
  bucket = aws_s3_bucket.deployment.id
  key    = "lambda/${var.function_name}/${var.code_version}.zip"
  source = "${path.module}/dist/lambda.zip"
  etag   = filemd5("${path.module}/dist/lambda.zip")
}

resource "aws_lambda_function" "this" {
  function_name = var.function_name
  role          = aws_iam_role.lambda.arn
  handler       = "index.handler"
  runtime       = "nodejs20.x"

  s3_bucket = aws_s3_bucket.deployment.id
  s3_key    = aws_s3_object.lambda_code.key

  # ...
}

Method 3: Container Image (complex dependencies)

resource "aws_lambda_function" "container" {
  function_name = var.function_name
  role          = aws_iam_role.lambda.arn
  package_type  = "Image"
  image_uri     = "${aws_ecr_repository.lambda.repository_url}:${var.image_tag}"

  memory_size = 1024
  timeout     = 300
}

Version and Alias Management

Versions and aliases let you implement blue-green deployment and traffic switching. For detailed version management concepts, see Lambda Version and Alias Explanation.

# Publish new version
resource "aws_lambda_function" "this" {
  # ...
  publish = true  # Auto-publish new version on each update
}

# Create alias
resource "aws_lambda_alias" "live" {
  name             = "live"
  description      = "Production traffic"
  function_name    = aws_lambda_function.this.function_name
  function_version = aws_lambda_function.this.version

  # Traffic switching (canary deployment)
  routing_config {
    additional_version_weights = {
      "${aws_lambda_function.this.version}" = 0.1  # 10% traffic to new version
    }
  }
}

Environment Variables and Secrets Management

Method 1: SSM Parameter Store

# Create parameter
resource "aws_ssm_parameter" "api_key" {
  name  = "/${var.environment}/lambda/api-key"
  type  = "SecureString"
  value = var.api_key
}

# Lambda permission to get parameter
resource "aws_iam_role_policy" "ssm_read" {
  name = "ssm-read"
  role = aws_iam_role.lambda.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "ssm:GetParameter",
        "ssm:GetParameters"
      ]
      Resource = aws_ssm_parameter.api_key.arn
    }]
  })
}

# Pass parameter name to Lambda
resource "aws_lambda_function" "this" {
  # ...
  environment {
    variables = {
      API_KEY_PARAM = aws_ssm_parameter.api_key.name
    }
  }
}

Method 2: Secrets Manager

data "aws_secretsmanager_secret" "db_credentials" {
  name = "prod/database/credentials"
}

resource "aws_iam_role_policy" "secrets_read" {
  name = "secrets-read"
  role = aws_iam_role.lambda.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "secretsmanager:GetSecretValue"
      ]
      Resource = data.aws_secretsmanager_secret.db_credentials.arn
    }]
  })
}

Need DevOps process optimization? CI/CD, multi-environment deployment, Secrets management all have many nuances. Book architecture consultation and let us help design the optimal DevOps process.


CI/CD Integration

GitHub Actions Example

# .github/workflows/deploy-lambda.yml
name: Deploy Lambda

on:
  push:
    branches: [main]
    paths:
      - 'lambda/**'
      - 'terraform/**'

env:
  AWS_REGION: ap-northeast-1
  TF_VERSION: 1.6.0

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Build Lambda
        run: |
          cd lambda
          npm ci
          npm run build

      - name: Terraform Init
        run: |
          cd terraform
          terraform init

      - name: Terraform Plan
        id: plan
        run: |
          cd terraform
          terraform plan -no-color -out=tfplan
        continue-on-error: true

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && steps.plan.outcome == 'success'
        run: |
          cd terraform
          terraform apply -auto-approve tfplan

GitLab CI Example

# .gitlab-ci.yml
stages:
  - validate
  - plan
  - apply

variables:
  TF_ROOT: ${CI_PROJECT_DIR}/terraform
  AWS_DEFAULT_REGION: ap-northeast-1

.terraform_template: &terraform_template
  image: hashicorp/terraform:1.6
  before_script:
    - cd ${TF_ROOT}
    - terraform init

validate:
  <<: *terraform_template
  stage: validate
  script:
    - terraform validate
    - terraform fmt -check

plan:
  <<: *terraform_template
  stage: plan
  script:
    - terraform plan -out=plan.tfplan
  artifacts:
    paths:
      - ${TF_ROOT}/plan.tfplan
    expire_in: 1 hour

apply:
  <<: *terraform_template
  stage: apply
  script:
    - terraform apply -auto-approve plan.tfplan
  dependencies:
    - plan
  only:
    - main
  when: manual

Auto-deployment Process Design

Recommended Process:

  1. Development phase: Local development + terraform plan preview
  2. Code Review: Pull Request + Plan results auto-commented
  3. After merge: Auto-execute terraform apply
  4. Monitoring: CloudWatch alerts + Slack notifications

Multi-environment Strategy:

terraform/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   │   ├── main.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       └── terraform.tfvars
└── modules/
    └── lambda/

Or use Terraform Workspace:

terraform workspace new dev
terraform workspace new prod
terraform workspace select prod
terraform apply -var-file="prod.tfvars"

Common Issues and Solutions

Code Updates Not Taking Effect

Symptom: Modified Lambda code, terraform apply shows no changes.

Cause: Terraform only tracks filename and source_code_hash; if file path doesn't change, it won't detect changes.

Solution: Ensure using source_code_hash:

resource "aws_lambda_function" "this" {
  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256  # Key!
  # ...
}

IAM Permission Issues

Symptom: Lambda execution shows AccessDenied error.

Solution:

  1. Confirm IAM Role has correct Trust Policy
  2. Check if necessary Policies are attached
  3. Use principle of least privilege, only give necessary permissions
# Additional permission example: Allow DynamoDB access
resource "aws_iam_role_policy" "dynamodb" {
  name = "dynamodb-access"
  role = aws_iam_role.lambda.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query"
      ]
      Resource = aws_dynamodb_table.this.arn
    }]
  })
}

VPC Configuration Issues

Symptom: Lambda placed in VPC cannot access network.

Cause: VPC Lambda has no public internet access capability.

Solution:

resource "aws_lambda_function" "vpc" {
  # ...

  vpc_config {
    subnet_ids         = var.private_subnet_ids  # Use Private Subnet
    security_group_ids = [aws_security_group.lambda.id]
  }
}

# Ensure Private Subnet has NAT Gateway routing
# Or use VPC Endpoints to access AWS services

For in-depth Lambda@Edge Terraform deployment, see Lambda@Edge Terraform Deployment.


FAQ

How long does Terraform Lambda deployment take?

First deployment takes about 1-3 minutes, subsequent updates usually complete within 30 seconds. Main time is spent on code upload and IAM permission propagation. Using S3 to store code can speed up large project deployments.

Where should Terraform State be stored?

Strongly recommend using S3 + DynamoDB as remote Backend. Local State files cause conflicts in team collaboration and have loss risk. See the Provider configuration section of this article for setup method.

How to handle Lambda code and infrastructure version synchronization?

Recommend putting Lambda code and Terraform in the same Repository, using a unified CI/CD Pipeline for deployment. This ensures code changes and infrastructure changes are synchronized, avoiding version inconsistencies.

Is AWS CDK or Terraform more suitable for Lambda deployment?

If the team is familiar with TypeScript/Python and the project only uses AWS, CDK is a good choice—the programming language flexibility can reduce repetitive code. If you need to manage multi-cloud environments or the team already has Terraform experience, Terraform is the better choice.


Conclusion

The core value of managing Lambda with Terraform is repeatability and traceability.

Key takeaways from this article:

  • Basic setup: Provider, IAM Role, Lambda Function trio
  • Module usage: Leverage community Modules to reduce repetitive code
  • Advanced techniques: Version management, Secrets handling, various code deployment methods
  • CI/CD integration: Automated deployment process design

Next steps:

  1. Start implementing with a simple Lambda
  2. Gradually add Module and CI/CD
  3. Establish team's Terraform best practices standards

Need Professional IaC Architecture Planning?

If you're:

  • Adopting Terraform to manage AWS resources
  • Designing CI/CD auto-deployment processes
  • Planning multi-environment infrastructure

Book architecture consultation, and we'll respond within 24 hours. Proper IaC design significantly reduces operations costs and risks.

Need Professional Cloud Advice?

Whether you're evaluating cloud platforms, optimizing existing architecture, or looking for cost-saving solutions, we can help

Book Free Consultation

Related Articles