在 EC2 上用 CloudWatch Logs 收集 Container Logs

這篇把我在實物上設定 EC2 container log遇到的問題及做法完整記下來

為什麼要用 CloudWatch Logs

以前要看 container logs 必須 SSH 進 EC2 跑 docker logs,但當環境變多(staging + prod)後就很難管理。CloudWatch Logs 可以讓你直接在 AWS Console 或 CLI 看日誌,不需要每次 SSH。

整體架構

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
EC2 上的 Docker containers
  ├── nginx
  ├── api
  └── postgres
        │  每個 container 的 stdout/stderr
        │  透過 Docker 的 awslogs driver
        │  直接送到 AWS CloudWatch Logs
CloudWatch Logs
  ├── /side-project/dev/nginx
  ├── /side-project/dev/api
  └── /side-project/dev/postgres

要讓這件事成立,需要三個東西:

  1. CloudWatch 有 log group 接收(Terraform 建)
  2. EC2 有權限寫入 CloudWatch(IAM role + instance profile)
  3. Docker containers 知道要送去哪(docker-compose 設定 awslogs driver)

查看 Logs

1
2
3
4
5
6
7
# CLI(需要 AWS profile)
aws logs tail /side-project/dev/api --follow --profile side-project
aws logs tail /side-project/dev/postgres --follow --profile side-project
aws logs tail /side-project/dev/nginx --follow --profile side-project

# Console:
# CloudWatch > Log groups > /side-project/dev/*

Local dev 不走 CloudWatch,直接用 docker compose logs

IAM 概念

IAM Role — 門禁卡

IAM Role 就像一張門禁卡,上面寫著:

  • 誰可以戴這張卡(Trust Policy / Assume Role Policy)
  • 戴了這張卡可以進哪些房間(Permission Policy)
1
2
3
aws_iam_role "ec2"
├── Trust Policy:    只有 EC2 service 可以使用這個 role
└── Permission Policy: 允許寫入 CloudWatch Logs

Instance Profile — 名牌夾

EC2 不能直接掛 IAM Role,必須透過 Instance Profile 這個「容器」間接掛。這是 AWS 的設計限制:

1
IAM Role(門禁卡)──▶ Instance Profile(名牌夾)──▶ EC2 Instance(人)

Terraform 範例:

1
2
3
4
5
6
7
8
9
resource "aws_iam_role" "ec2" { ... }

resource "aws_iam_instance_profile" "ec2" {
  role = aws_iam_role.ec2.name
}

resource "aws_instance" "api" {
  iam_instance_profile = aws_iam_instance_profile.ec2.name
}

注意:對已存在的 EC2 補上 iam_instance_profile 會觸發 instance 重建(AWS 限制)。

PassRole — 交出門禁卡的權限

GitHub Actions 透過 Terraform 幫 EC2 掛 IAM Role。AWS 會要求執行者具備「把門禁卡交出去」的權限:iam:PassRole

如果沒有這個限制,任何有 IAM 權限的人都能把超級管理員 role 掛到任意 EC2。

1
2
3
4
5
6
7
8
9
{
  Action   = ["iam:PassRole"]
  Resource = "arn:aws:iam::*:role/side-project-ec2-*"
  Condition = {
    StringEquals = {
      "iam:PassedToService" = "ec2.amazonaws.com"
    }
  }
}

最佳實務是把 PassRole 獨立成一個 statement,跟其他 IAM 操作(CreateRole 等)分開。

AWS 權限結構

每條 IAM Statement 由三個部分組成:

1
2
3
4
5
{
  Action   = "某個操作"
  Resource = "針對哪些資源"
  Condition = { ... }
}

為什麼有些權限要用 Resource = "*"

logs:DeleteLogGroup 這種操作是針對特定 log group,可以做資源限制:

1
2
3
4
{
  Action   = "logs:DeleteLogGroup"
  Resource = "arn:aws:logs:us-east-1:*:log-group:/side-project/*"
}

logs:DescribeLogGroups 是列表操作,AWS 不會用你指定的 resource pattern 來比對,必須用 *。這是唯讀操作,安全性可接受。

權限類型 Resource 可以限定嗎? 原因
增刪改操作(CreateLogGroup, DeleteLogGroup 等) 可以,限定到 /side-project/* 操作針對具體資源
列表操作(DescribeLogGroups) 不行,必須用 * AWS 不支援列表 API 的資源限定

舊 API vs 新 API

AWS 會更名 API,以下兩組功能相同:

功能 舊 API 新 API
對 log group 加 tag logs:TagLogGroup logs:TagResource
列出 log group 的 tags logs:ListTagsLogGroup logs:ListTagsForResource

Terraform AWS provider 已改用新 API,但為了相容我會兩組都寫。

Bootstrap vs Main Terraform

Bootstrap Main Terraform
在哪裡跑 本地(手動) GitHub Actions(CI/CD)
管什麼 GitHub Actions 的 IAM 權限、S3 state bucket EC2、Security Group、EIP、IAM Role、CloudWatch
為什麼分開 CI/CD 不能自己幫自己加權限(雞生蛋) 需要先有權限才能 apply
多久跑一次 很少,只有改 CI/CD 權限時 每次 push 到 main

流程:

1
2
3
4
5
6
1. 本地跑 bootstrap terraform apply
   → GitHub Actions 拿到新權限

2. Push 到 main
   → infra.yml: terraform apply(建 IAM role、log groups、EC2)
   → deploy.yml: SSH 進 EC2 部署 containers

Docker awslogs Driver

1
2
3
4
5
6
7
8
9
# docker-compose.prod.yml
services:
  api:
    logging:
      driver: awslogs
      options:
        awslogs-group: /side-project/${APP_ENV}/api
        awslogs-region: ${AWS_REGION}
        tag: api
  • driver: awslogs:Docker 不存 logs 在本機,改送到 CloudWatch
  • ${APP_ENV} / ${AWS_REGION}:從 .env 讀取
  • tag:在 CloudWatch 裡辨識 log stream

Conclusion

把容器日誌集中到 CloudWatch 後,排錯速度大幅提升,而且 multi-env 管理也更乾淨。這套做法不只適用 Side-Project,任何 EC2 + Docker 的部署都能直接套用。

參考連結

0%