개요
오늘은 VPC Flow Log를 사용하면서 겪었던 이슈를 정리해보려고 한다. 그중에서도 VPC Flow Log를 중점적으로 다루기보다는 ECS의 네트워크 모드, ECS와 Flow Log의 연동, ECS와 OS의 임시 포트 개념에 대해 정리할 예정이다.
VPC Flow Log
VPC Flow Log는 VPC에서 네트워크 인터페이스에서 전송되거나 수신되는 IP 트래픽에 대한 정보를 수집할 수 있는 기능이다. VPC에서 이 기능을 활성화할 수 있으며, 서브넷 단위로도 활성화가 가능하다. 공식문서에서는 Subnet에서 Flow log를 활성화한 뒤 S3에 저장하는 아키텍처를 보여주고 있다.
Flow log를 활성화하게 되면 공백으로 구분되는 문자열 형태로 로그가 저장되는데, 이 로그에는 src, dest, protocol 같은 정보들이 저장된다. 어떤 정보를 보일지는 flow log 생성 시에 선택할 수 있으며, 기본적인 format은 다음과 같다.
${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status}
그런데 사실 이 정보만으로는 추적하는데 충분하진 않았어서 가능하면 많은 정보를 포함하는 것을 추천한다. 공식문서에 사용 가능한 필드 정보들이 나와있으니 참고해서 Custom format
으로 추가하면 된다.
https://docs.aws.amazon.com/vpc/latest/userguide/flow-log-records.html
VPC flow logs에서 ECS metadata도 지원하는데, 2024년 5월부터 버전 7로 지원하게 된 것으로 보인다. 나는 ECS의 어느 태스크에서 통신이 일어났는지 파악하는 게 주목적이었어서 이 필드도 활성화했는데, 결과적으로는 현재 아키텍처 기준으로는 제대로 동작하지 않았다 😢 이 내용은 추후에 다뤄보도록 하겠다.
VPC Flow Log 분석
아무튼 나는 VPC Flow Log를 활성화하기 위해 저장소를 S3로 두고 Parquet 포맷으로 저장하도록 설정했다. 디버깅 시에 어떤 필드가 유용할지 모르겠어서 일단 모든 필드를 추가해두고 생성했다.
연동이 완료되면 이후에 S3 버킷에 데이터가 쌓이는데, 이걸 확인하기 위해 Athena integration
기능을 사용해 테이블을 추가했다. 이 기능을 사용하면 Athena와 연동 하기 위한 CloudFormation 템플릿을 제공해 주기 때문에 간편하게 연동이 가능하다.
VPC Flow Log의 사용법에 대해서는 굳이 다루지 않을 예정이다. 사용하다보면 익혀지기도 하고 오늘 포스팅하려던 주된 목표는 Flow Log의 사용법이 아니라 도입하는 과정에서 겪은 트러블 슈팅 기록이기 때문이다.
ECS Network Mode
트러블 슈팅 내용을 기록하기 전에 ECS 네트워크 모드에 대해 정리해보려고 한다. ECS Network Mode는 총 5가지(awsvpc, host, bridge, none, default)로 나뉘는데, default는 윈도용으로, none 모드는 외부 네트워크 연결을 하지 않을 때 사용한다. 나머지 3가지 모드(awsvpc, host, bridge)에 대해 간단하게 알아보겠다.
awsvpc
awsvpc 모드를 사용하면 컨테이너마다 ENI와 private IPv4가 할당된다. ENI가 할당되는 만큼 애플리케이션 간 통신 방식을 더 많이 제어할 수 있으며, VPC Flow Logs 같은 기능을 사용해서 트래픽을 모니터링할 수도 있다. ECS가 ENI를 만들고 EC2 인스턴스에 할당하고, ENI를 통해 네트워크 트래픽을 보내고 받게 된다. 만약 Fargate를 사용한다면 awsvpc
를 사용하게 된다.
그림에 보이는 것처럼 인스턴스에 태스크가 2개 떠있다고 하면, 각각의 태스크는 Primary IP(172.31.16.1
, 172.31.16.2
)와 ENI를 할당받게 된다.
awsvpc 모드를 사용할 때 리눅스 환경에서 고려할 사항은 다음과 같다.
- 태스크와 서비스에는 ECS service-linked 역할이 필요함
aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com
- ECS용 인스턴스 타입에 따라 ENI 개수가 제한되지만, ENI Trunking Mode라는 것을 사용하게 되면 개수를 확장할 수 있음
- 태스크의 ENI에는 Public IP가 제공되지 않기 때문에 인터넷에 연결하려면 NAT를 사용하도록 구성된 Private Subnet에서 시작되어야 함
- ENI는 수동으로 분리하거나 수정할 수 없음 (해제 시에는 태스크를 중지해야 함)
- TG로는 항상 IP를 지정해야함 (EC2 Instance로 설정하면 기본적으로 Primary ENI가 할당되기 때문)
이 외에도 몇 가지 요구사항이 더 있지만 따로 기술하진 않았다. 자세한 내용은 공식문서를 참고하면 된다.
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking-awsvpc.html
awsvpc 모드를 사용하게 되면 아래 테라폼 코드로 작성한 것 같이 network_configuration
를 필수로 지정해야 한다. ECS가 ENI를 생성할 때, local.private_subnet_ids
로 지정된 프라이빗 서브넷에 ECS 태스크가 배포되며 aws_security_group.this["xxx_api"]
보안 그룹이 ECS 태스크에 연결된다. 즉, 태스크마다 별도의 보안그룹을 설정할 수 있어 보안적으로 우수하다.
resource "aws_ecs_task_definition" "example" {
...
network_mode = "awsvpc"
...
}
resource "aws_ecs_service" "example" {
...
network_configuration {
subnets = local.private_subnet_ids
security_groups = [aws_security_group.this["xxx_api"].id]
assign_public_ip = false
}
...
}
host
host 모드를 사용하면 컨테이너 포트를 EC2 인스턴스의 ENI에 직접 매핑하게 된다. 그림과 같이 구성이 되어 있다고 가정했을 때, 172.31.16.1
로 요청을 보내면 3000번 포트에서 트래픽을 수신하게 된다. 즉, 별도 IP를 할당받지 않고 호스트 EC2 인스턴스와 IP를 공유하고 ENI를 직접 사용하는 방식이다.
이 방법을 사용하게 되면 하나의 호스트에서 하나의 태스크만 실행해야 하며, 보안적으로도 좋지 않기 때문에 기본적으로는 권장하지 않는다.
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/networking-networkmode-host.html
resource "aws_ecs_task_definition" "example" {
...
network_mode = "host"
...
}
bridge
bridge 모드를 사용하면 호스트와 태스크 사이를 이어주는 가상 네트워크 브리지(docker0)라는 것을 사용하게 된다. 이후에 태스크가 만들어지면 내부 IP가 할당되게 되는데(172.17.0.0/16
), 각 태스크는 호스트와 태스크를 연결하는 가상 이더넷 쌍을 통해 브리지에 연결하게 된다(= veth)
이 모드에서는 정적 포트 매핑과 동적 포트 매핑을 선택할 수 있는데, 정적 포트 매핑을 사용하면 host 모드와 동일하게 각 호스트 인스턴스에서 동일한 포트를 동시에 하나만 실행할 수 있고, 동적 포트 매핑을 사용하면 하나의 인스턴스에서 여러 개의 동일한 컨테이너 포트를 사용할 수 있다.
첫 번째 그림은 정적 포트 매핑을 사용한 예시이다. 만약 172.31.16.1
의 80번으로 트래픽을 전송한다면, 컨테이너에서는 3000번 포트에서 인바운드 트래픽을 인식하게 된다. 즉, 단일 컨테이너만 80 포트에 매핑할 수 있기 때문에 host 모드와 똑같이 하나의 호스트에서 한 개의 태스크만 실행할 수 있다.
두 번째 그림은 동적 포트 매핑을 사용한 예시이다. 도커의 임시 포트 범위에서 무작위로 사용되지 않는 포트를 선택해서 컨테이너의 공개 호스트 포트로 할당한다. 그림에서 보이는 것처럼 첫 번째 인스턴스의 첫 번째 컨테이너는 47760이라는 임시 포트를 할당받았고, 두 번째 컨테이너는 45283이라는 포트를 할당받은 것을 볼 수 있다. 이후에 외부 트래픽이 EC2 인스턴스의 IP와 포트에 도착하게 되면 포트 매핑 테이블을 참조해서 트래픽을 적절한 컨테이너로 전달하고, 변환된 트래픽은 컨테이너 내부 포트로 전달된다. 이렇게 되면 여러 컨테이너가 하나의 호스트에서 자체 포트를 할당받아 여러 태스크가 실행 가능해지는 것이다.
resource "aws_ecs_task_definition" "example" {
# ...
network_mode = "bridge"
# ...
}
여기에서 hostPort를 명시적으로 설정하면 정적 포트 매핑을 사용하는 것이고, 0으로 설정하면 동적 포트 매핑을 사용하게 된다.
resource "aws_ecs_task_definition" "example" {
container_definitions = jsonencode([
{
...
portMappings = [
{
containerPort = 8080
hostPort = 0
protocol = "tcp"
}
]
...
}
실제로 bridge 모드를 사용하고 있는 인스턴의 포트를 확인해 보면 다양한 포트로 매핑이 되어 있는 것을 확인할 수 있다.
VPC Flow Log with ECS Attributes
위에 설명한 것처럼 VPC flow log의 속성을 추가할 때, ECS 관련 속성을 추가할 수 있게 업데이트되어 추가한 뒤 테스트 해보았다. 구성 시에 참고한 링크는 아래와 같다.
그러나 ECS 관련 정보가 나오지 않았다. 아마 ECS Fargate를 사용하고 있었거나 네트워크 모드에 대해 알고 있었으면 쉽게 해결할 수 있었을 텐데, EC2 on EC2를 사용하고 있었고, 네트워크 모드에 대해 잘 몰라서 이유를 찾느라 좀 헤맸다. 이유는 Flow log limitations에 친절하게 설명이 되어 있다 (진작에 읽어볼걸…^_^)
https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs-limitations.html
Only ECS tasks launched in
awsvpc
network mode are supported.
나는 bridge 모드를 사용하고 있었기 때문에 정보가 표시되지 않는 것이었다. VPC Flow Log 자체가 VPC 내에서 ENI 간에 이동하는 트래픽을 기록하도록 설계된 서비스인 만큼 bridge 모드를 사용하면 하나의 ENI만 사용하기 때문에 컨테이너별 트래픽을 구분할 수 없다. 이것을 해결하기 위해서는 awsvpc 모드로 변경하면 된다.
이후에는 ECS 관련 값들이 잘 나오는 것을 확인할 수 있다.
임시 포트에 대한 착각
bridge 모드를 사용하는 것을 유지한 채로 아웃바운드로 나갈 때 포트 번호를 추적하는 방법을 찾다가, 삽질을 하며 알게 된 내용도 함께 정리해보려고 한다.
테스트 컨테이너에서 일부러 아웃바운드 통신을 발생시키고, 로그를 확인해 보았다. 참고로 현재 열려 있는 임시 포트는 다음과 같다.
- 3000/tcp -> 0.0.0.0:32775
- 3000/tcp -> [::]:32775
SELECT az_id, bytes, srcaddr, srcport, dstaddr, dstport, flow_direction, pkt_srcaddr, pkt_dstaddr, tcp_flags, start, "end"
FROM "~"
WHERE srcaddr = '10.xxx.xxx.166' # 테스트 컨테이너의 호스트
AND dstaddr = '23.xxx.xxx.138' # outbound 통신 대상
ORDER BY start
출발지 IP(srcaddr)와 도착지 IP(dstaddr)로 조건을 걸어 확인해 보니, 총 4개의 로그가 남은 것을 볼 수 있다. SYN(2)와 FIN(1)을 포함해 TCP 통신이 이루어진 것을 확인할 수 있는데, 여기에서 srcport가 40728로 나온다. 즉, 실제로 요청을 보낸 포트와는 다른 포트 번호인 것이다.
여기에서 둘 다 임시포트라서 개념을 헷갈렸는데, 32775번 포트는 컨테이너와 매핑된 리스닝 포트로, 외부에서 컨테이너에 접근할 때 사용하는 포트이다. 또한 40728번 포트는 컨테이너에서 외부로 요청을 보낼 때 OS에서 자동으로 할당한 임시포트이다. 아웃바운드 요청에 대한 응답(SYN+ACK)도 마찬가지로 32775번 포트를 거치지 않고 40728번 포트를 거치기 때문에 다른 포트로 보이는 것이다.
awsvpc 모드를 사용하는 인스턴스도 동일하다. 다만 차이점은 출발지 IP가 호스트의 IP가 아니라 태스크에 할당된 IP로 표시되기 때문에 이 IP를 가지고 어떤 태스크인지 찾을 수 있는 것이다.
참고링크
'DevOps > AWS' 카테고리의 다른 글
AWS Network Firewall & TCP 패킷만 캡처되는 이슈 트러블 슈팅 (1) | 2025.05.01 |
---|---|
AWS VPC Endpoint (Interface, Gateway) (0) | 2025.04.12 |
ECS Graceful Shutdown (dumb-init) (4) | 2025.02.01 |
Secret Manager를 사용해서 Spring Boot 프로젝트의 property값 관리하기(2) - EC2 (0) | 2021.10.29 |
Secret Manager를 사용해서 Spring Boot 프로젝트의 property값 관리하기(1) - 로컬 환경 (2) | 2021.10.15 |