# EC2 Docker container logs to CloudWatch

사이드 프로젝트나 회사에서 가끔 EC2 에 간단한 어플리케이션을 뛰우기 위해 도커 Container 를 통해 서버를 뛰울때가 있다. 이럴때면 항상 문제가 되는 점이 이 Container 에 대한 log 를 쉽게 확인할 수 없다는 점이다. 따라서 이를 logStream 을 통해 cloudWatch 로 보내거나 다른 log 서비스로 보내야 한다. 오늘은 Docker container 에서 아주 쉽게 CloudWatch 로 보내는 방법을 작성해보려고 한다.

### 들어가기에 앞서

사실 여기서 **CloudWatch Log driver** 를 설정하는게 좋은데, 아무래도 이 글을 방문한 사람들은 대다수 logging driver 를 configure 하는데 익숙하지 않을 것 이라고 생각하기 때문에 조금 raw 한 방식으로 가보려고 한다. 만약 자신이 Docker 를 공부해보고 좀 알아가보고 싶다면 [logging driver](https://docs.docker.com/engine/logging/drivers/awslogs/) 쪽을 공부해가며 적용해보길 바란다.

## Docker logs

기본적으로 Docker 는 log 를 남기도록 되어있다. 이건 logging driver 를 어떻게 설정하냐에 따라 달라질 수 있는데, 기본적으로는 [JSON File logging driver](https://docs.docker.com/engine/logging/drivers/json-file/) 를 설정하도록 되어있다. 해당 driver 는 docker logs 를 아래와 같이 json 형태로 남겨준다. 혹시 모르는 사람이 있을 수도 있으니 로그를 10줄 확인하는 커맨드를 함께 작성해보자.

```bash
docker ps -a

>>> CONTAINER ID   IMAGE                                                           COMMAND                  CREATED      STATUS      PORTS                                                                                  NAMES
6396bb7d8e98   example-container   "docker-entrypoint.s…"   4 days ago   Up 4 days   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp, 0.0.0.0:6543->6543/tcp, :::6543->6543/tcp   elegant_kapitsa
```

보통 container id 기반으로 docker log 를 조회하므로 열줄만 확인하기 위해서는 아래처럼 커맨드를 작성하면 된다.

```bash
docker logs --tail 10 6396bb7d8e98

2025-05-25 13:07:00 [info] [1UQ9FYDqJEjCEcw2FmYe2] start notification batch 
2025-05-25 13:07:00 [info] [WXNxA-WZMSu5srV_BWdGp] no notification query to send 
2025-05-25 13:08:00 [info] [VzPfGxoI0jdWIba-IfH5K] start notification batch 
2025-05-25 13:08:00 [info] [QBu5oW6J2z1fbCZx6FfQI] no notification query to send 
2025-05-25 13:09:00 [info] [1Aso2fvtff4HcPvUO2Hg5] start notification batch 
2025-05-25 13:09:00 [info] [I_kbpV0kylgdt0pxdjDBQ] no notification query to send 
2025-05-25 13:10:00 [info] [8OuDbyiy-TPnvSHP2WEzz] start notification batch 
2025-05-25 13:10:00 [info] [eOHYte-dKFiUaeDKptlGk] start notification batch 
2025-05-25 13:10:00 [info] [Lg0JMayauwq5OZ9I9gQ4u] no notification query to send 
2025-05-25 13:10:00 [info] [tuunzsY20t8xosT_5WnI9] no notification query to send 
```

그러면 위와 같이 현재 어플리케이션의 로그가 잘 찍히는 것을 확인해볼수 있다. 이제 해당 docker container 가 로그를 어디에 남기는지를 확인해야 하는데, 이건 docker 명령어 중 하나인 `docker inspect` 로 확인가능하다. 공식문서에도 친절히 [도커 컨테이너 로그 Path 찾는법](https://docs.docker.com/reference/cli/docker/inspect/#get-an-instances-log-path)이 나와있으니 항상 공식문서를 잘 확인해보자.

```bash
docker inspect --format='{{.LogPath}}' 6396bb7d8e98

/var/lib/docker/containers/6396bb7d8e9816cec19a19fa6d01ce66f44b4b122c8c3c03aa45c422710796e0/6396bb7d8e9816cec19a19fa6d01ce66f44b4b122c8c3c03aa45c422710796e0-json.log
```

우리의 컨테이너 id 를 넘겨주게 되면 위와 같이 **docker log 가 위치한 경로(Path)** 가 출력되게 된다. 아마도 오랫동안 열려있었다면 상당히 클 것이므로 Linux 에서 지원해주는 `tail` 명령어를 사용해보도록 하자. 일단 tail 이 뭔지 모르는 사람이 있을수도 있으니 `tail —help` 를 통해 어떤 역할을 해주는지 좀 보자

```bash
Print the last 10 lines of each FILE to standard output.
With more than one FILE, precede each with a header giving the file name.

With no FILE, or when FILE is -, read standard input.

Mandatory arguments to long options are mandatory for short options too.
  -c, --bytes=[+]NUM       output the last NUM bytes; or use -c +NUM to
                             output starting with byte NUM of each file
  -f, --follow[={name|descriptor}]
                           output appended data as the file grows;
                             an absent option argument means 'descriptor'
  -F                       same as --follow=name --retry
  -n, --lines=[+]NUM       output the last NUM lines, instead of the last 10;
                             or use -n +NUM to output starting with line NUM
      --max-unchanged-stats=N
                           with --follow=name, reopen a FILE which has not
                             changed size after N (default 5) iterations
                             to see if it has been unlinked or renamed
                             (this is the usual case of rotated log files);
                             with inotify, this option is rarely useful
      --pid=PID            with -f, terminate after process ID, PID dies
```

대충 읽어보면 **STDOUT(standard output)** 의 마지막 10번째 줄을 출력해준다고 한다. 모든 로그 파일을 열게 되면 상당히 길어지고, 부담이 될수 있으므로 우리는 이 `tail` 을 사용해서 마지막 10번째 줄만 확인해볼 것이다.

```bash
tail /var/lib/docker/containers/6396bb7d8e9816cec19a19fa6d01ce66f44b4b122c8c3c03aa45c422710796e0/6396bb7d8e9816cec19a19fa6d01ce66f44b4b122c8c3c03aa45c422710796e0-json.log
```

```bash
[ec2-user@ip-0-0-0-0 ~]$ sudo tail /var/lib/docker/containers/6396bb7d8e9816cec19a19fa6d01ce66f44b4b122c8c3c03aa45c422710796e0/6396bb7d8e9816cec19a19fa6d01ce66f44b4b122c8c3c03aa45c422710796e0-json.log
{"log":"2025-05-25 13:17:00 [\u001b[32minfo\u001b[39m] [_nGw3N58VfZ3bSROjvS3t] start notification batch \n","stream":"stdout","time":"2025-05-25T04:17:00.002011521Z"}
{"log":"2025-05-25 13:17:00 [\u001b[32minfo\u001b[39m] [efRuI9tyoFaqP5Tzkc9VJ] no notification query to send \n","stream":"stdout","time":"2025-05-25T04:17:00.029296098Z"}
{"log":"2025-05-25 13:18:00 [\u001b[32minfo\u001b[39m] [5CTslcZhX7PlEaMg5gJ5d] start notification batch \n","stream":"stdout","time":"2025-05-25T04:18:00.002467655Z"}
{"log":"2025-05-25 13:18:00 [\u001b[32minfo\u001b[39m] [YJ5IPVhbxbBdWkG5u-lYC] no notification query to send \n","stream":"stdout","time":"2025-05-25T04:18:00.034712075Z"}
{"log":"2025-05-25 13:19:00 [\u001b[32minfo\u001b[39m] [Knbdfcav4bvY7ISA5CFHq] start notification batch \n","stream":"stdout","time":"2025-05-25T04:19:00.001702587Z"}
{"log":"2025-05-25 13:19:00 [\u001b[32minfo\u001b[39m] [yPOFrwgGJYrzBoMptCvD2] no notification query to send \n","stream":"stdout","time":"2025-05-25T04:19:00.03466593Z"}
{"log":"2025-05-25 13:20:00 [\u001b[32minfo\u001b[39m] [mY9a7bqB2ui9ikC-dxw6o] start notification batch \n","stream":"stdout","time":"2025-05-25T04:20:00.002331836Z"}
{"log":"2025-05-25 13:20:00 [\u001b[32minfo\u001b[39m] [wkUPGnu2nBAW-5BnRN5iV] start notification batch \n","stream":"stdout","time":"2025-05-25T04:20:00.004497565Z"}
{"log":"2025-05-25 13:20:00 [\u001b[32minfo\u001b[39m] [Me84S199I61hQFKCuklRQ] no notification query to send \n","stream":"stdout","time":"2025-05-25T04:20:00.035242873Z"}
{"log":"2025-05-25 13:20:00 [\u001b[32minfo\u001b[39m] [9ERMLXHUyd3h-qhjKblqg] no notification query to send \n","stream":"stdout","time":"2025-05-25T04:20:00.035332976Z"}
```

위와 같이 Log 가 **json 형태**로 남겨져 있는 것을 확인할 수 있다. 이제 어떻게 남겨져 있는지 까지 확인해 봤으니 이걸 CloudWatch 로 연결해보도록 하자.

## Connect to Cloud watch

AWS 에서는 기본적으로 아마존 서비스 내에서도 특정 서비스에서 다른 서비스로 특별한 행동을 취할때 권한이 필요하다. 예를 들면, 우리가 EC2 서비스에서 CloudWatch 라는 서비스로 로그를 보내기 위해서는 EC2 서비스가 CloudWatch 에 대한 행동을 할 수 있는 권한이 필요하다. 그것을 위해 보통 **IAM** 을 이용한다.

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748147096286/ca4e2932-c491-432d-aa11-1c8bc2164326.png align="center")

  
따라서 IAM 을 위와 같이 만들고 EC2의 `Modify IAM role` 을 이용해서 추가해주도록 한다. 이제 EC2 가 CloudWatch 에 대해 로그 그룹, 로그 스트림을 만들수 있고, 로그 이벤트를 발송할수도 있는 최소한의 권한을 가지게 된다.

### JSON 설정

```json
{
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/lib/docker/containers/6396bb7d8e9816cec19a19fa6d01ce66f44b4b122c8c3c03aa45c422710796e0/6396bb7d8e9816cec19a19fa6d01ce66f44b4b122c8c3c03aa45c422710796e0-json.log",
            "log_group_name": "docker-messaging-server",
            "log_stream_name": "{instance_id}",
            "timestamp_format": "%Y-%m-%dT%H:%M:%S.%fZ",
            "timezone": "UTC"
          }
        ]
      }
    }
  }
}
```

이제 **json** 파일을 통해 설정을 진행해야 한다. 일단 가볍게 위의 json file 을 살펴보면 크게 아래와 같은 속성이 존재한다. (**해당 json 파일을 vi or vim 을 통해 /opt/aws/amazon-cloudwatch-agent/bin/config.json 에 만들어주면 된다**)

* **file\_path**: 로그가 놓여진 경로(이전에 docker inspect 로 확인했던 경로)
    
* **log\_group\_name**: 로그가 그룹핑 될 그룹
    
* **log\_stream\_name**: 로그 그룹안에서 우리가 전송할 로그가 전송될 스트림(알아보기 쉽게하기 위해 instance\_id 를 이용하도록 했다)
    

위 처럼 **속성파일**을 적어주면 이제 대략적인 준비는 끝났다. 이제 CloudWatch Agent 를 실행시켜서 해당 config 정보를 이용해서 로그를 CloudWatch 로 보내도록 하자.

```bash
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config \
  -m ec2 \
  -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json \
  -s
```

가볍게 설명하면 `fetch_config` 라는 action 을 하는데 `mode` 는 ec2 모드이고, config 파일 경로는 `/opt/aws/amazon-cloudwatch-agent/bin/config.json` 에 존재한다는 뜻이다. 이제 커맨드가 입력되고 잘 되었다면 Error 가 보이지않고, Success 만 보일것이다.

### 검증

CloudWatch 에 로그가 늦게 들어오는건지 이게 실패한건지 헷갈릴때가 있을 것이다. 따라서 아래 커맨드를 입력해서 잘 가고 있는지 확인해보자. 보통 잘 안가고 있다면 어떤 에러가 해당 로그에 존재하고 있을 것이다.

```bash
[ec2-user@ip-0-0-0-0 ~]$ sudo tail -f /opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log
```

```bash
2025-05-25T03:45:22Z I! [inputs.logfile] turned on logs plugin
2025-05-25T03:45:22Z I! {"caller":"service@v0.115.0/service.go:166","msg":"Setting up own telemetry..."}
2025-05-25T03:45:22Z I! {"caller":"service@v0.115.0/service.go:215","msg":"Skipped telemetry setup."}
2025-05-25T03:45:22Z I! {"caller":"service@v0.115.0/service.go:238","msg":"Starting CWAgent...","Version":"1.300054.1","NumCPU":2}
2025-05-25T03:45:22Z I! {"caller":"extensions/extensions.go:39","msg":"Starting extensions..."}
2025-05-25T03:45:22Z I! {"caller":"extensions/extensions.go:42","msg":"Extension is starting...","kind":"extension","name":"entitystore"}
```

## 마치며

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748147792417/6e9ec55c-c392-4466-a2ea-804c63754c54.png align="center")

이제 cloud watch 의 docker-messaging-server 라는 log group 하위에 인스턴스 id 로 로그 스트림이 생성된것을 확인해볼수 있다. 조금 더 견고하게 로그를 관리하기 위해서는 log 가 너무 많이 쌓일 수 있으므로 retention 등등을 고려해보는 것도 좋은 공부가 될 것이다. 항상 공식문서를 잘 읽고 적용해보는 습관을 길러보자.
