Laravel 有 log 記錄各種錯誤、發生的事情以及開發、debug 需要的訊息,這些 log 在 Laravel 執行在 container 內後都存在 container 內。如果執行在 AWS 的 Laravel 需要 log 資訊來 debug,在開著多台 container 的情況下要連進 container 找到出錯的 log 相當費時費力,而且 container 可能因為 deploy 等因素被關閉,寶貴的 log 可能就這麼隨風而逝……所以我們希望把 log 放在一個比較好查詢及保存的地方,在 AWS 裡就是使用 CloudWatch 這個服務。(本日程式碼)
CloudWatch 是 AWS 提供監控功能的服務,可以收集 log、監控各種服務與系統狀況以及在某些條件下做一些反應。今天要使用收集 log 的部分,我們會收集 container 執行的 log 以及 Laravel application 層的 log。
CloudWatch 收集、儲存 log 的地方稱為 log group,每個 log group 裡會有一到多個 log stream,每個 log stream 分別來自不同來源,像是不同 EC2 instance。
先來讓 Laravel 的 log 可以送到 cloudwatch,方便在出現 error 時能從 cloudwatch 找相關 log 來 debug。如果沒有把 log 送到 cloudwatch,出現問題時只能連進 EC2 instance 再進入一個個 container 檢查 log。container 只有一兩個的時候還好,再多幾個就……青春都耗費在找 log 上了 😰
要把 EC2 instance 或 container 的 log 檔案傳到 cloudwatch,要在 EC2 instance 或 container 上安裝 cloudwatch agent。現在我們要收集的是執行 Laravel 的 container 內的 log,在 Dockerfile 加入以下指令安裝及設定 cloudwatch agent:
# install amazon-cloudwatch-agent
RUN curl -O https://s3.amazonaws.com/amazoncloudwatch-agent/debian/amd64/latest/amazon-cloudwatch-agent.deb && \
dpkg -i -E amazon-cloudwatch-agent.deb && \
rm -f amazon-cloudwatch-agent.deb
# cloudwatch config
COPY docker/cloudwatch/amazon-cloudwatch-agent.json /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
COPY docker/cloudwatch/amazon-cloud-watch.conf /etc/supervisor/conf.d/amazon-cloud-watch.conf
這段指令是依照文件下載及安裝 deb,再把 cloudwatch agent 的設定檔放到對應位置 (ref)。另外我們要用 supervisord 啟動並維持 cloudwatch agent 的運作,所以要放一個 cloudwatch 的設定檔給 supervisord。
在 cloudwatch agent 設定檔 amazon-cloudwatch-agent.json
指定哪些檔案的內容要傳給 cloudwatch、傳到哪個 log group,我們可以把 Laravel 的 log 跟 container 的 crontab log 傳去 cloudwatch:
{
"agent": {
"run_as_user": "root"
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/www/html/storage/logs/laravel.log",
"log_group_name": "/my-app/laravel.log"
},
{
"file_path": "/var/log/cron.log",
"log_group_name": "/my-app/cron.log"
}
]
}
},
"log_stream_name": "{instance_id}/{local_hostname}"
}
}
log_stream_name
是指定 container 送出的 log 所在的 log stream。{instance_id}
跟 {local_hostname}
是變數,{instance_id}
是 EC2 instance 的 instance ID,{local_hostname}
在 container 內會是 container id。log_stream_name
設為 EC2 instance ID 跟 container id 的字串,這樣比較容易知道 log 是哪個 task 的。
跟 Laravel worker 一樣,用 supervisord 啟動與維持 cloudwatch agent 的運作。設定檔 amazon-cloud-watch.conf
:
[program:amazon-cloud-watch]
command=/opt/aws/amazon-cloudwatch-agent/bin/start-amazon-cloudwatch-agent
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=3600
用 terraform 管理 resource 後,針對 cloud resource 的修改都改用 terraform 了~ terraform 定義 log group 的 resource,能夠設定 log group 名稱跟 log 的保留天數(retention_in_days):
resource "aws_cloudwatch_log_group" "app" {
name = "/my-app/laravel.log"
retention_in_days = 30
}
resource "aws_cloudwatch_log_group" "app_cron" {
name = "/my-app/cron.log"
retention_in_days = 14
}
順利的話就能在 cloudwatch 的 log group 裡找到 Laravel container 的 log group:
每個 log stream 來自一個 container,可以進到 log stream 看那個 container 的 log,也能夠從 log group 頁面點 Search all log streams 一次查看所有 log stream。
如果沒那麼順利……過了幾分鐘 cloudwatch log group 內還是沒有 log stream 的話,就要進 container 看看 cloudwatch agent 是不是有什麼問題。先連進 EC2 instance,接著用 docker exec -it [CONTAINER_ID] bash
指令進到 container ,再查看 cloudwatch agent 本身的 log /var/log/amazon/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log
。
ECS task 的 log 功能讓我們可以看到執行 container 時的 output,這個 output 是 container 的 entry script 的 output,log driver awslogs
只是單純把 Docker 得到的 log 傳給 cloudwatch。啟動 ECS task 的 log 功能要在 ecs task definition resource 的 container definition 加上 log configuration:
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = "my-app-ecs-awslogs"
awslogs-region = var.region
awslogs-create-group = "true"
awslogs-stream-prefix = "my-app"
}
}
這樣設定讓 ECS task 在變數 region
指定的 region 建立 my-app-ecs-awslogs
log group,裡面會有像 my-app/my-app/ea1e930c2ad5452d9cbf924699891fff
這樣的 log stream,log stream 名稱格式是 prefix-name/container-name/ecs-task-id
。
接著加入能建立 log group、log stream 以及寫 log 的 IAM role 跟 policy:
resource "aws_iam_role" "ecs_task_exec" {
name = "my-app-ecs-task-exec-role"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : "ecs-tasks.amazonaws.com"
},
"Action" : "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy" "ecs_task_exec" {
role = aws_iam_role.ecs_task_exec.id
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource" : "*"
}
]
})
}
如果前面 awslogs-create-group
設 true
,ecs task 或 EC2 就要有 logs:CreateLogGroup
的權限。
最後把 IAM role 設定給 aws_ecs_task_definition.td
的 execution_role_arn
:
execution_role_arn = aws_iam_role.ecs_task_exec.arn
ECS task 的 execution role 是給 ECS agent 的 role,讓 ECS agent 可以 call AWS 的 API。
ECS task 有另一個 role 是 task role,這個 role 是給 container 使用的。如果 Laravel 需要權限使用 AWS 的服務,可以把需要的權限放在 IAM role 並且設定成 task role。這樣 Laravel 只要是以 ECS service 跑起來,就擁有需要的 AWS 權限,以這個方式給 Laravel 權限可以避免 AWS 的 credential 隨著 docker image 外流。
ECS task log 不像前面自己送到 cloudwatch 的 log 得到 cloudwatch 找 log group,從 ECS service 跟 task 頁面的 Logs tab 就能看到。
ECS service 的 log 會顯示所有 task 的 log:
某個 task 當然就是顯示自己的 log: