返回首頁AWS Lambda

Terraform 部署 AWS Lambda 完整教學:IaC 最佳實踐

17 min 分鐘閱讀
#AWS Lambda#Terraform#IaC#DevOps#CI/CD#Serverless

Terraform 部署 AWS Lambda 完整教學:IaC 最佳實踐

手動在 AWS Console 建立 Lambda 很簡單,但當你有 10 個、50 個甚至 100 個 Lambda 函數要管理時,事情就會失控。

更別提每次部署都要手動點擊、環境之間的設定不一致、無法追蹤誰改了什麼。這些問題,Terraform 都能解決。

本文將從基礎到進階,教你如何用 Terraform 管理 AWS Lambda,建立可重複、可追蹤、可協作的基礎設施。

想先了解 Lambda 本身的概念和功能,請參考 AWS Lambda 完整指南


為什麼用 Terraform 管理 Lambda

IaC 的核心優勢

Infrastructure as Code(IaC) 將基礎設施定義成程式碼,帶來以下好處:

優勢傳統手動部署Terraform IaC
版本控制無法追蹤變更Git 完整歷史記錄
環境一致性容易有設定差異同一份程式碼,結果相同
協作審查口頭溝通,易出錯Pull Request Code Review
災難復原重新手動設定terraform apply 重建
文件化另外寫文件,易過時程式碼即文件

Terraform vs CloudFormation vs CDK 比較

terraform-vs-cloudformation-cdk-comparison 圖片說明:Terraform、CloudFormation、AWS CDK 三種 IaC 工具的功能比較表,包含語法類型、多雲支援、學習曲線、社群資源等維度。

特性TerraformCloudFormationAWS CDK
語法HCL(宣告式)YAML/JSONTypeScript/Python/Java
多雲支援支援僅 AWS僅 AWS
學習曲線中等中等較陡(需懂程式語言)
社群資源非常豐富AWS 官方快速成長中
狀態管理需自行管理 StateAWS 自動管理基於 CloudFormation
偵錯體驗較好錯誤訊息較難懂較好

選擇建議:

  • 多雲環境或團隊已熟悉 Terraform → 選 Terraform
  • 純 AWS 且團隊熟悉 TypeScript/Python → 選 CDK
  • 企業環境需要 AWS 官方支援 → 選 CloudFormation

基礎設定

Provider 設定

首先建立 provider.tf

terraform {
  required_version = ">= 1.0"

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

  # 建議:使用遠端 State 儲存
  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 必要資源

一個完整的 Lambda 部署需要三個核心資源:

lambda-terraform-resources-diagram 圖片說明:Terraform 部署 Lambda 的資源關係圖,顯示 aws_lambda_function、aws_iam_role、aws_iam_role_policy_attachment 三者的關聯。

1. IAM 執行角色(Execution Role)

# IAM 角色 - Lambda 的執行身份
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"
        }
      }
    ]
  })
}

# 附加基本執行政策(CloudWatch Logs 權限)
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 函數

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. 程式碼打包

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

完整範例

簡單 Lambda 函數

完整的單一 Lambda 部署範例:

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

# 程式碼打包
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/src"
  output_path = "${path.module}/dist/lambda.zip"
}

# IAM 角色
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 函數
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
    }
  }
}

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

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

Lambda + API Gateway

建立 HTTP API 整合:

# 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 整合
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"
}

# 路由設定
resource "aws_apigatewayv2_route" "default" {
  api_id    = aws_apigatewayv2_api.api.id
  route_key = "$default"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

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

# Lambda 執行權限
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
}

更多 API Gateway 整合細節,請參考 API Gateway 整合概念

Lambda + EventBridge 定時任務

建立每小時執行的排程任務:

# CloudWatch Event Rule(使用 EventBridge)
resource "aws_cloudwatch_event_rule" "schedule" {
  name                = "${var.function_name}-schedule"
  description         = "每小時觸發一次"
  schedule_expression = "rate(1 hour)"

  # 或使用 Cron 表達式
  # 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 執行權限
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
}

更多事件驅動架構內容,請參考 EventBridge 事件驅動架構

想設計更完善的 IaC 架構?預約架構諮詢,讓專家幫你規劃。


Terraform Lambda Module

使用現成 Module

社群維護的 terraform-aws-modules/lambda 是最受歡迎的 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 設定
  vpc_subnet_ids         = var.private_subnet_ids
  vpc_security_group_ids = [aws_security_group.lambda.id]

  # 環境變數
  environment_variables = {
    DATABASE_URL = var.database_url
    NODE_ENV     = var.environment
  }

  # CloudWatch Logs
  cloudwatch_logs_retention_in_days = 14

  # 效能設定
  memory_size = 512
  timeout     = 60

  # 併發限制
  reserved_concurrent_executions = 100

  tags = var.common_tags
}

Module 的優點:

  • 自動建立 IAM Role 和 Policy
  • 內建程式碼打包功能
  • 支援 Layers、VPC、Alias 等進階功能
  • 持續維護更新

自建 Module 結構

terraform-lambda-module-structure 圖片說明:自建 Terraform Lambda Module 的目錄結構圖,展示 modules/lambda 下的 main.tf、variables.tf、outputs.tf 等檔案組織方式。

當你需要客製化更多邏輯時,可以自建 Module:

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

variables.tf

variable "function_name" {
  description = "Lambda 函數名稱"
  type        = string
}

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

variable "runtime" {
  description = "執行環境"
  type        = string
  default     = "nodejs20.x"
}

variable "source_dir" {
  description = "程式碼目錄"
  type        = string
}

variable "memory_size" {
  description = "記憶體大小 (MB)"
  type        = number
  default     = 256
}

variable "timeout" {
  description = "執行超時 (秒)"
  type        = number
  default     = 30
}

variable "environment_variables" {
  description = "環境變數"
  type        = map(string)
  default     = {}
}

variable "tags" {
  description = "資源標籤"
  type        = map(string)
  default     = {}
}

outputs.tf

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

output "function_name" {
  description = "Lambda 函數名稱"
  value       = aws_lambda_function.this.function_name
}

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

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

變數與輸出設計原則

  1. 必要參數不設預設值:強制呼叫者提供
  2. 合理預設值:常用設定提供預設,減少重複程式碼
  3. 類型明確定義:使用 type 強制類型檢查
  4. 描述清楚:每個變數都有 description
  5. 輸出有用資訊:ARN、Name、Invoke URL 等常用屬性

進階技巧

管理 Lambda 程式碼

方式一:archive_file(小型專案)

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

方式二:S3 儲存(大型專案)

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

  # ...
}

方式三:Container Image(複雜相依)

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
}

版本與別名管理

版本與別名讓你實現藍綠部署和流量切換。詳細的版本管理概念,請參考 Lambda 版本與別名說明

# 發布新版本
resource "aws_lambda_function" "this" {
  # ...
  publish = true  # 每次更新自動發布新版本
}

# 建立別名
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

  # 流量切換(金絲雀部署)
  routing_config {
    additional_version_weights = {
      "${aws_lambda_function.this.version}" = 0.1  # 10% 流量到新版本
    }
  }
}

環境變數與 Secrets 管理

lambda-secrets-management-diagram 圖片說明:Lambda 取得 Secrets 的三種方式比較:環境變數(簡單但不安全)、SSM Parameter Store(適中)、Secrets Manager(最安全但較貴)。

方式一:SSM Parameter Store

# 建立參數
resource "aws_ssm_parameter" "api_key" {
  name  = "/${var.environment}/lambda/api-key"
  type  = "SecureString"
  value = var.api_key
}

# Lambda 取得參數的權限
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
    }]
  })
}

# 傳遞參數名稱給 Lambda
resource "aws_lambda_function" "this" {
  # ...
  environment {
    variables = {
      API_KEY_PARAM = aws_ssm_parameter.api_key.name
    }
  }
}

方式二: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
    }]
  })
}

DevOps 流程需要優化? CI/CD、多環境部署、Secrets 管理都有很多眉角。 預約架構諮詢,讓我們幫你設計最佳 DevOps 流程。


CI/CD 整合

GitHub Actions 範例

# .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 範例

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

自動部署流程設計

ci-cd-terraform-lambda-flow 圖片說明:Terraform Lambda CI/CD 部署流程圖,從程式碼推送到 GitHub,經過 Build、Test、Plan、Apply 階段,最終部署到 AWS。

建議的流程:

  1. 開發階段:本地開發 + terraform plan 預覽
  2. Code Review:Pull Request + Plan 結果自動留言
  3. 合併後:自動執行 terraform apply
  4. 監控:CloudWatch 警示 + Slack 通知

多環境策略:

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

或使用 Terraform Workspace:

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

常見問題與解法

程式碼更新不生效

症狀:修改了 Lambda 程式碼,terraform apply 顯示沒有變更。

原因:Terraform 只追蹤 filenamesource_code_hash,檔案路徑沒變就不會偵測到變更。

解法:確保使用 source_code_hash

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

IAM 權限問題

症狀:Lambda 執行時出現 AccessDenied 錯誤。

解法

  1. 確認 IAM Role 有正確的 Trust Policy
  2. 檢查是否附加了必要的 Policy
  3. 使用最小權限原則,只給必要權限
# 額外權限範例:允許存取 DynamoDB
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 設定問題

症狀:Lambda 放入 VPC 後無法存取網路。

原因:VPC Lambda 沒有公網存取能力。

解法

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

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

# 確保 Private Subnet 有 NAT Gateway 路由
# 或使用 VPC Endpoints 存取 AWS 服務

想深入了解 Lambda@Edge 的 Terraform 部署,可參考 Lambda@Edge Terraform 部署


常見問題 FAQ

Terraform Lambda 部署需要多久?

首次部署約 1-3 分鐘,後續更新通常在 30 秒內完成。主要時間花在程式碼上傳和 IAM 權限傳播。使用 S3 儲存程式碼可以加速大型專案的部署。

Terraform State 應該存在哪裡?

強烈建議使用 S3 + DynamoDB 作為遠端 Backend。本地 State 檔案在團隊協作時會造成衝突,且有遺失風險。設定方式請參考本文的 Provider 設定段落。

如何處理 Lambda 程式碼和基礎設施的版本同步?

建議將 Lambda 程式碼和 Terraform 放在同一個 Repository,使用統一的 CI/CD Pipeline 部署。這樣可以確保程式碼變更和基礎設施變更同步進行,避免版本不一致。

AWS CDK 和 Terraform 哪個更適合部署 Lambda?

如果團隊熟悉 TypeScript/Python 且專案只用 AWS,CDK 是不錯的選擇,程式語言的靈活性可以減少重複程式碼。如果需要管理多雲環境或團隊已經有 Terraform 經驗,Terraform 會是更好的選擇。


結語

Terraform 管理 Lambda 的核心價值在於 可重複性可追蹤性

從本文學到的重點:

  • 基礎設定:Provider、IAM Role、Lambda Function 三件套
  • Module 使用:善用社群 Module 減少重複程式碼
  • 進階技巧:版本管理、Secrets 處理、多種程式碼部署方式
  • CI/CD 整合:自動化部署流程設計

下一步建議:

  1. 從一個簡單的 Lambda 開始實作
  2. 逐步加入 Module 和 CI/CD
  3. 建立團隊的 Terraform 最佳實踐規範

IaC 架構需要專業規劃?

如果你正在:

  • 導入 Terraform 管理 AWS 資源
  • 設計 CI/CD 自動部署流程
  • 規劃多環境的基礎設施

預約架構諮詢,我們會在 24 小時內回覆你。 正確的 IaC 設計能大幅降低維運成本與風險。


需要專業的雲端建議?

無論您正在評估雲平台、優化現有架構,或尋找節費方案,我們都能提供協助

預約免費諮詢

相關文章