상세 컨텐츠

본문 제목

9.주간 C# 서버 챌린지: Docker 배포 및 구조적 로깅

개발 & 프로그래밍/C# 서버 챌린지

by Jiung. 2025. 11. 20. 22:29

본문

반응형

서버는 개발 환경(Visual Studio)에서 잘 돌아가는 것과 실제 운영 환경(Production)에서 잘 돌아가는 것은 전혀 다른 이야기입니다. 서버를 클라우드에 배포하고, 문제가 생겼을 때 원격으로 진단할 수 있어야 진정한 '서비스'가 됩니다.

이번 주에는 서버를 '배포 가능한 상태'로 만들고 '관측 가능하게' 만드는 작업을 진행합니다.


주간 C# 서버 챌린지: Docker 컨테이너화 및 Serilog 구조적 로깅 가이드

안녕하세요! 스튜디오 비를긋다입니다. 이번 챌린지의 주제는 'Docker 컨테이너화 및 구조적 로깅(Structured Logging) 구축' 입니다. "내 컴퓨터에서는 되는데?"라는 말을 없애기 위해 Docker를 사용하고, 텍스트 파일 뒤지기를 멈추기 위해 현대적인 로깅 시스템을 도입해 보겠습니다. 🐳

.NET 8 서버를 프로덕션 수준으로 배포하고 싶으신가요? Dockerfile 멀티 스테이지 빌드부터 Docker Compose 오케스트레이션, Serilog를 활용한 JSON 구조적 로깅 구축까지. 실무에 바로 적용 가능한 전체 소스 코드를 확인하세요.

문제: 서버의 컨테이너화 및 중앙 집중식 로깅 준비

상황 시나리오:

'아르카디아의 그림자'의 출시가 임박했습니다. 운영팀에서는 "서버를 어떻게 배포하나요? exe 파일을 복사하면 되나요?"라고 묻고 있습니다. 또한, "서버가 다운되면 로그는 어디서 보나요? 텍스트 파일은 검색하기 너무 힘들어요."라는 요청도 들어왔습니다.

당신의 임무는 GameServerChatServerDocker 이미지로 만들어 어디서든 동일하게 실행되도록 하고, Serilog를 도입하여 로그를 JSON 형식으로 남겨 기계가 분석할 수 있도록(Machine-readable) 만드는 것입니다.

핵심 요구사항:

  1. 구조적 로깅 (Structured Logging): 기존의 Console.WriteLine을 모두 제거하고, Serilog 라이브러리를 도입합니다. 로그는 단순 텍스트가 아닌 JSON 형식으로 출력되어야 합니다.
  2. Dockerfile 작성: GameServerChatServer 프로젝트를 빌드하고 실행할 수 있는 최적화된 Dockerfile을 각각 작성합니다. (.NET 8 SDK 및 Runtime 이미지 활용)
  3. Docker Compose 구성: docker-compose.yml 파일을 작성하여, 명령 한 번으로 게임 서버, 채팅 서버, (그리고 지난주에 만든) Redis 서비스 레지스트리가 동시에 실행되고 서로 통신할 수 있도록 네트워크를 구성합니다.
  4. 환경 변수 설정: 하드코딩된 설정값(DB 경로, 포트 등)을 appsettings.json 또는 Docker 환경 변수로 주입받도록 수정합니다.

샘플 솔루션 (C# 12 / .NET 8 & Docker)

사전 준비:

  • Docker Desktop 설치 필요
  • NuGet 패키지 추가: Serilog, Serilog.AspNetCore, Serilog.Sinks.Console, Serilog.Formatting.Compact

1. 구조적 로깅 적용 (Program.cs)

using Serilog;
using Serilog.Formatting.Compact;

// 1. Serilog 초기화 (JSON 포맷터 사용)
Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.Console(new RenderedCompactJsonFormatter()) // 사람이 읽기 좋은 Compact JSON 형식
    .CreateLogger();

try
{
    Log.Information("Starting GameServer...");

    var builder = WebApplication.CreateBuilder(args);

    // 2. .NET 호스트에 Serilog 주입
    builder.Host.UseSerilog(); 

    // ... 기존 서비스 등록 코드 ...

    var app = builder.Build();

    // 3. 기존 Console.WriteLine 대체 예시
    // 기존: Console.WriteLine($"Player {playerId} logged in.");
    // 변경: 구조적 로깅 (PlayerId가 검색 가능한 필드로 저장됨)
    Log.Information("Player {PlayerId} logged in from {IpAddress}", playerId, ipAddress);

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "GameServer terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}

2. Dockerfile (GameServer/Dockerfile)

멀티 스테이지 빌드(Multi-stage build)를 사용하여 이미지 크기를 최소화합니다.

# 1. 빌드 스테이지 (SDK 포함)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# 프로젝트 파일 복사 및 복원 (캐시 효율성을 위해)
COPY ["GameServer/GameServer.csproj", "GameServer/"]
COPY ["Protos/chat.proto", "Protos/"]
RUN dotnet restore "GameServer/GameServer.csproj"

# 전체 소스 복사 및 빌드
COPY . .
WORKDIR "/src/GameServer"
RUN dotnet publish "GameServer.csproj" -c Release -o /app/publish /p:UseAppHost=false

# 2. 실행 스테이지 (Runtime만 포함 - 가벼움)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "GameServer.dll"]

(ChatServer의 Dockerfile도 위와 유사하게 작성합니다.)

3. Docker Compose (docker-compose.yml)

version: '3.8'

services:
  # Redis (서비스 레지스트리 용)
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"
    networks:
      - game-network

  # 채팅 서버
  chat-server:
    build:
      context: .
      dockerfile: ChatServer/Dockerfile
    environment:
      - ASPNETCORE_URLS=http://+:5000
      - RedisAddress=redis:6379
    ports:
      - "5000:5000"
    networks:
      - game-network
    depends_on:
      - redis

  # 게임 서버
  game-server:
    build:
      context: .
      dockerfile: GameServer/Dockerfile
    environment:
      - ASPNETCORE_URLS=http://+:7777
      # Docker 내부 DNS를 사용하여 'chat-server' 호스트명으로 접근
      - ChatServerUrl=http://chat-server:5000 
    ports:
      - "7777:7777"
    networks:
      - game-network
    depends_on:
      - chat-server

networks:
  game-network:
    driver: bridge

실용적인 사용 시나리오

  • 원클릭 실행: 개발자가 프로젝트에 합류했을 때, 복잡한 환경 설정 없이 docker-compose up 명령어 하나만 입력하면 Redis, DB, 채팅 서버, 게임 서버가 모두 실행되는 완벽한 로컬 개발 환경이 구축됩니다.
  • 로그 분석: JSON으로 출력된 로그는 나중에 ELK Stack(Elasticsearch, Logstash, Kibana)이나 Datadog 같은 로그 수집 시스템으로 바로 전송할 수 있습니다. 텍스트 파싱 없이 PlayerId == 12345 같은 쿼리로 특정 유저의 로그만 필터링하는 것이 가능해집니다.
  • 일관된 배포: 개발자의 로컬 환경(Windows)과 서버 운영 환경(Linux)의 차이로 인한 버그가 사라집니다. Docker 이미지가 동일하게 동작하기 때문입니다.

학습 목표

  • 컨테이너 가상화: Docker의 기본 개념과 이미지를 빌드하고 실행하는 과정을 이해합니다.
  • 오케스트레이션 기초: Docker Compose를 사용하여 여러 개의 컨테이너를 하나의 서비스 그룹으로 묶고 관리하는 방법을 익힙니다.
  • 컨테이너 네트워킹: Docker 내부 네트워크에서 서비스들이 서로를 어떻게 찾고(DNS) 통신하는지 이해합니다.
  • 구조적 로깅: 사람이 읽는 로그(Unstructured)와 기계가 읽는 로그(Structured)의 차이를 이해하고, 로그 데이터를 자산으로 활용하는 방법을 배웁니다.

솔루션 품질 평가 기준

  • 이미지 효율성: Docker 이미지가 불필요한 파일(소스 코드 등)을 포함하지 않고, 실행에 필요한 파일만 포함하여 가벼운가? (멀티 스테이지 빌드 사용 여부)
  • 구성 자동화: docker-compose up 실행 시 수동 개입 없이 모든 서버가 정상적으로 뜨고 서로 연결되는가?
  • 로그 유용성: 로그에 타임스탬프, 로그 레벨, 그리고 PlayerId 같은 문맥 정보(Context)가 JSON 필드로 잘 포함되어 있는가?

보너스 목표

JSON 로그를 눈으로 읽기는 힘듭니다. 로그를 수집하고 시각화해 주는 도구를 컨테이너로 띄워 연동해 보세요.

과제: Seq를 이용한 중앙 집중식 로그 모니터링 대시보드 구축 📊

요구사항:

  1. docker-compose.ymlSeq 서비스를 추가합니다. (이미지: datalust/seq)
  2. C# 프로젝트에 Serilog.Sinks.Seq 패키지를 추가합니다.
  3. Program.cs의 로거 설정에 .WriteTo.Seq("http://seq:5341")를 추가합니다. (Docker 내부 주소 사용)
  4. 모든 컨테이너를 재시작한 후, 브라우저로 Seq 대시보드(http://localhost:8081)에 접속하여 게임 서버와 채팅 서버의 로그가 한곳에 모이는 것을 확인합니다.
  5. 고급: Serilog의 LogContext를 사용하여, 특정 요청 처리 구간(Scope) 내에서 발생하는 모든 로그에 자동으로 CorrelationId(요청 추적 ID)를 붙여보세요.
// Program.cs 예시
var seqUrl = Environment.GetEnvironmentVariable("SeqUrl") ?? "http://localhost:5341";

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.Console() // 콘솔에도 남기고
    .WriteTo.Seq(seqUrl) // Seq로도 전송
    .CreateLogger();
version: '3.8'

services:
  # 1. Redis (서비스 레지스트리 용)
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"
    networks:
      - game-network

  # 2. Seq (중앙 집중식 로그 서버) - [새로 추가됨]
  seq:
    image: datalust/seq
    environment:
      - ACCEPT_EULA=Y  # 필수: 라이선스 동의
    ports:
      - "5341:5341"    # 로그 수집 포트 (Ingestion)
      - "8081:80"      # 대시보드 UI 포트 (브라우저 접속용)
    volumes:
      - seq-data:/data # 로그 데이터를 영구 저장하기 위한 볼륨
    networks:
      - game-network

  # 3. 채팅 서버
  chat-server:
    build:
      context: .
      dockerfile: ChatServer/Dockerfile
    environment:
      - ASPNETCORE_URLS=http://+:5000
      - RedisAddress=redis:6379
      # Seq 서버 주소 (Docker 네트워크 내부 DNS 이름 'seq' 사용)
      - SeqUrl=http://seq:5341 
    ports:
      - "5000:5000"
    networks:
      - game-network
    depends_on:
      - redis
      - seq # Seq가 먼저 켜져야 함

  # 4. 게임 서버
  game-server:
    build:
      context: .
      dockerfile: GameServer/Dockerfile
    environment:
      - ASPNETCORE_URLS=http://+:7777
      - ChatServerUrl=http://chat-server:5000
      # Seq 서버 주소 주입
      - SeqUrl=http://seq:5341 
    ports:
      - "7777:7777"
    networks:
      - game-network
    depends_on:
      - chat-server
      - seq

networks:
  game-network:
    driver: bridge

volumes:
  seq-data: # Seq 데이터 저장을 위한 로컬 볼륨 정의

 

이제 여러분의 서버는 클라우드 환경 어디에 던져놔도 생존할 수 있는 현대적인 애플리케이션의 모습을 갖췄습니다. Docker의 바다로 항해를 시작해 보세요! 🚢

 

 

Getting Started

Simple .NET logging with fully-structured events. Contribute to serilog/serilog development by creating an account on GitHub.

github.com

 

Home

Docker Documentation is the official Docker library of resources, manuals, and guides to help you containerize applications.

docs.docker.com

 

GitHub - datalust/serilog-sinks-seq: A Serilog sink that writes events to the Seq structured log server

A Serilog sink that writes events to the Seq structured log server - datalust/serilog-sinks-seq

github.com

 

반응형

관련글 더보기