[42Seoul] ft_transcendence 뉴 트센 회고록 (1 ~ 3편)

tpwhzla·2024년 3월 15일

42Seoul

목록 보기
14/16

트센 끝

뉴 트센 회고록은 인생 최초로 웹 프로젝트에 도전하는 데브옵스 엔지니어 지망생 + 프로젝트 팀장의 시각으로 작성된 글임을 밝힙니다.

1년 4개월 간의 공통 과정 도전을 마쳤다.
총 다섯 명의 팀원을 구성했고, 2명의 프론트엔드, 2명의 백엔드, 1명의 풀스택(시니어) 개발자로 프로젝트를 진행했다.

함께 ft_irc 프로젝트를 진행한 집현전(42서울 도서관 서비스) 백엔드를 담당하고 계신 분께서 처음부터 함께 해주셨고, 풀스택 개발과 동시에 시니어 역할까지 맡아주셨다. 프로젝트 내내 질문에 대해 막힘 없이 대답해주셨고, 모든 팀원들이 많이 의지할 수 있는 시니어 포지션으로서의 역할을 너무 잘 수행해주셔서 감사했다.

사실상 프로젝트의 전체적인 틀에서 기술적인 모든 부분을 커버해주셨기 때문에, 과중한 부담을 짊어지게 하기가 죄송스러웠다.
따라서 프로젝트 전체의 아키텍쳐를 설계하는 부분이나 인프라 관련 부분들을 내가 담당했고, 팀장의 역할도 내가 수행함으로서 나의 부족한 개발 실력(?) 을 조금이나마 커버 하고자 노력했다.

0주차 논의 내용, 첫 설계는 이러했는데 지금 보니 확실히 많은 내용이 달랐다.

1월 4일에 프로젝트를 시작했고, 3월 14일까지 약 두 달 반 정도의 시간이 걸렸다.

모종의 이유로 프론트엔드 포지션으로 선발했던 분들의 역할을 조금 다르게 했고, 자연스럽게 '세부적으로' 역할을 나누게 되었다.
앞서 언급했던 ft_irc를 함께 진행했던 분께서 시니어이자 풀스택 포지션을, 백엔드 전반을 맡아주실 분과, 프론트엔드 전반을 맡아주실 분,
그리고 팀원 모집 막바지에 합류한 나와 친한 형이 디자인, UI/UX 를 맡아주었다. (조금 지나고서야 알게 되었는데, 전혀 몰랐지만 이 형은 디자인에 재능이 있는 것 같다... 2년을 알고 지냈는데 전혀 몰랐다. 신기)

Docker-compose로 VM 속 도커 환경을 구성하여 블로그 페이지를 만드는 과제인 Inception 을 재밌게 했던 내가 데브옵스 엔지니어의 역할을 맡았다.

독일에서 프로젝트를 진행하더라도 도커, 쿠버네티스, Azure, CI/CD 관련 기술을 놓고 싶지 않았다.

개발 역시 재밌지만, 인프라만큼 재밌지는 않았다. 뉴 트센은 Devops 관련 과제가 3개나 있다.

Cybersecurity 모듈 사이에 끼어 있긴 하지만, Hashicorpvault나 Modsecurity 관련된 부분은 Devops 엔지니어가 수행해도 될만한 역할이라고 생각한다.

0주차 논의 내용에 기술 스택 중 Redis가 쓰여져 있는데, Redis를 채널 레이어로서의 기능 (웹소켓 실시간 통신을 할 때 그룹, 채널용) 으로 사용하는 것인지, Redis 자체가 DB이기 때문에 PostgreSQL 외 다른 DB로 봐야 하는지 말이 오갔지만, 결국 Django channels에 있는 InmemoryChannelLayer를 활용하여 구현했다. Redis는 앞으로도 쓸 기회가 많으니, 기회가 되면 꼭 써보려고 한다.

Elasticsearch, logstash, Kibana의 경우에는 10점짜리 Major Module인데, 과제에서 요구하는 정도가 어느정도인지는 모르겠으나 클러스터 아이맥에서 docker-compose up --build 명령어 하나만으로 모든 서비스가 실행되어야하는 특성 상 우리가 원하는만큼의 엄청난 퍼포먼스를 보이긴 힘들지 않나 싶다.

이너서클의 마지막 과제에 담기에는 확실히 오버스펙이기도 하다.하지만 Grafana + Prometheus와 함께 가장 재미있게 했던 모듈이기도 하다.

클러스터에 이 모듈을 선택한 사람이 거의 없는 것 같던데, 아마 ELK Stack을 트센(클러스터 아이맥과 같은 제한된 리소스와 sudo도 못 쓰는 폐쇄된 환경)에서 구성하기에 지친 사람들이 찾아와서 읽어준다면, 충분히 기쁠 것 같다.

1주차 논의 내용, 지금 보니 우리 많이 귀여웠다... 안 그래도 기능도 많은 트센인데 이 땐 더 많았구나... 와우...

1주차 까지는 채팅 기능이 있었다. 지금 생각하니 채팅보다 훨씬 어려운 실시간 게임 방을 구현했다... 정말 우리 팀원분들 고생 많았다...!

나는 이 때부터 Docker-compose로 프로젝트의 전체 아키텍쳐를 설계하기 시작했다. 백엔드 팀원분들이 DB 구조를 어떻게 설계할지 고민했고, ERD Cloud에 설계된 내용을 그렸다.

프론트엔드 개발자 분들은 프론트엔드 페이지는 순수 Vanila Javascript로만 이루어져야 했기에, Vanila javascript로 구조를 설계하는 것과 어떻게 프로젝트를 진행해야 하는지를 공부하셨고, 디자인 전반을 맡아준 팀원은 Figma에 열심히 페이지들을 그리기 시작했다.

전체적인 인프라 설계 과정은 이러했다.

맨 앞단에 nginx를 두고, TLS 인증서를 발급받은 뒤 URI 분기로 api 요청을 백엔드 서버인 Django로, 웹소켓 연결 역시 Django로 요청을 보내도록 하였다.

모든 서비스의 환경변수를 Hashicorpvault에서 참조하도록 하지는 않았으나, Django에서 사용한 환경변수는 모두 Hashicorpvault에서 참조하도록 수정했다.

즉, Django는 env 파일에 hashicorp vault의 token만 담아도 비밀에 접근할 수 있게 설계했다. (올바른 사용 방법이라 생각하지는 않지만. 커맨드 한 줄만으로 모든 컨테이너가 다 실행되어야 한다는 Mandatory의 설명 때문에 이렇게 설계하게 되었다.)

ELK 스택은 Nginx의 access_log 를 json 형태로 volume 마운트 해둔 파일에서 가져오고, Django에서도 텍스트 형태로 파일에 로그가 찍히도록 하여 그 파일을 바탕으로 Elasticsearch에 log 정보를 전달, kibana로 시각화하였다.

nginx에 geoIP 같은 걸 도입하면 훨씬 재밌는 시각화를 많이 할 수 있었을 텐데...! 아쉬움이 남는다.

Python은 전반적인 게임 로직을 담당하고 있기에, 매 게임이 시작될 때마다 INFO Log로
[시작] 게임 유형 로그를 남기도록 하였다.

그리고 Logstash pipeline을 구성해서 앞에 [시작] 키워드를 지우고, 뒤에 있는 Value 값들로 시각화 데이터를 구성했다.

### logstash.conf
input {
	file {
		path => "/usr/share/logstash/log/nginx_logs/access_json.log"
		start_position => "beginning"
		codec => json
		tags => ["nginx"]
		id => "nginx_log"
	}
	file {
		path => "/usr/share/logstash/log/django_logs/django.log"
		start_position => "beginning"
		tags => ["django"]
		id => "django_log"
	}
}

filter {
	if "django" in [tags] {
		grok {
			match => {"message" => "\[시작\] %{WORD:game_type}" }
		}
		if "_grokparsefailure" in [tags] {
			drop { }
		}
		mutate {
			remove_field => ["@version", "@timestamp", "host", "path"]
			gsub => ["message", "\[시작\]\s*", ""]
		}
	}
    if "nginx" in [tags] {
		mutate {
			convert => { "요청에 걸린 시간" => "float" }
		}
		if [요청에 걸린 시간] <= 0.0 {
			drop { }
		}
    }
}

output {
	if "django" in [tags] {
		elasticsearch {
			hosts => "https://elasticsearch:9200"
			index => "django-logs-%{+YYYY.MM.dd}"
			user => "elastic"
			password => "${ELASTIC_PW}"
			ssl => true
			ssl_certificate_verification => false
			keystore => "/usr/share/logstash/config/certs/elastic-certificates.p12"
			keystore_password => "${ES_CERTS_PW}"
			truststore => "/usr/share/logstash/config/certs/elastic-certificates.p12"
			truststore_password => "${ES_CERTS_PW}"
		}
	}
	if "nginx" in [tags] {
		elasticsearch {
			hosts => "https://elasticsearch:9200"
			index => "nginx-logs-%{+YYYY.MM.dd}"
			user => "elastic"
			password => "${ELASTIC_PW}"
			ssl => true
			ssl_certificate_verification => false
			keystore => "/usr/share/logstash/config/certs/elastic-certificates.p12"
			keystore_password => "${ES_CERTS_PW}"
			truststore => "/usr/share/logstash/config/certs/elastic-certificates.p12"
			truststore_password => "${ES_CERTS_PW}"
		}
	}
	elasticsearch {
		hosts => "https://elasticsearch:9200"
		index => "tscen-logs-%{+YYYY.MM.dd}"
		user => "elastic"
		password => "${ELASTIC_PW}"
		ssl => true
		ssl_certificate_verification => false
		keystore => "/usr/share/logstash/config/certs/elastic-certificates.p12"
		keystore_password => "${ES_CERTS_PW}"
		truststore => "/usr/share/logstash/config/certs/elastic-certificates.p12"
		truststore_password => "${ES_CERTS_PW}"
	}
}

Python container와 Nginx container의 로그들은 실시간으로 로컬에 마운트되고, I/O가 될 때마다 Logstash에 함께 마운트된 파일이 로그를 인식한다.

이 과정에서 Logstash.conf 를 거치게 되는데, 받아온 로그 데이터들을 filter 단에서 정제하게 된다.

nginx의 access log에서는 요청에 걸린 평균 시간을 구하기 위해 $request_time 을 사용했는데, 소수점으로 인식되지 않는 문제가 있어 Metric 중 Average를 적용할 수 없어 filter를 저렇게 설정했다.

Django 로그에서는 [시작] 으로 찍힌 로그 외에는 모두 Drop되게 했고, [시작] 을 공백으로 치환하여 게임 시작 시 찍히는 로그의 종류인
RANK, TOURNAMENT, LOCAL, PVP 만 찍히도록 하였다. 우리 프로젝트에서의 게임 종류가 4가지였기 때문에 가능한 설계였다.
실제 게임 사이트를 운영하여 유저들에게 재미있는 통계를 공개할 때에도 이런 방식으로 로그를 구성해보지 않을까 싶다.

Django와 Nginx에서 받아온 로그들에 인덱스 패턴을 할당함으로서 서비스 별로 다른 대시보드를 구성할 수도 있었다.
독일을 가게 되면 차량 관련 더욱 다양한 시계열 데이터와 로그들을 핸들링하게 될 텐데, 미리 연습하는 단계 정도로 생각하고 컨테이너를 띄워봤는데 이번 프로젝트에서 가장 재밌었다.


  elasticsearch:
    build:
      context: ./elk/elasticsearch
      dockerfile: Dockerfile
    volumes:
      - "${ELASTIC_DATA}:/usr/share/elasticsearch/data"
      - "${ELASTIC_CERTS}:/usr/share/elasticsearch/config/certs"
    environment:
      discovery.type : single-node
      ELASTIC_PW : ${ELASTIC_PW}
      ES_JAVA_OPTS: "-Xms2g -Xmx2g"
      ES_CERTS_PW: ${ES_CERTS_PW}
    ports:
      - 9200:9200
      - 9300:9300
    networks:
      - app_network

  logstash:
    build: ./elk/logstash
    container_name: logstash_container
    environment:
      LS_JAVA_OPTS: "-Xms2g -Xmx2g"
      ELASTIC_PW: ${ELASTIC_PW}
      ES_CERTS_PW: ${ES_CERTS_PW}
    volumes:
      - "${ELASTIC_CERTS}:/usr/share/logstash/config/certs"
      - "${NGINX_LOGS}:/usr/share/logstash/log/nginx_logs"
      - "${DJANGO_LOGS}:/usr/share/logstash/log/django_logs"
    ports:
      - 5333:5333
    depends_on:
      - elasticsearch
    networks:
      - app_network

  kibana:
    build: ./elk/kibana
    container_name: kibana_container
    volumes:
      - "${KIBANA_DATA}:/usr/share/kibana/data"
      - "${ELASTIC_CERTS}:/usr/share/kibana/config/certs"
    environment:
      ELASTICSEARCH_URL: https://elasticsearch:9200
      ELASTIC_PW: ${ELASTIC_PW}
      ES_CERTS_PW: ${ES_CERTS_PW}
    ports:
      - 5601:5601
    depends_on:
      - elasticsearch
    networks:
      - app_network

docker-compose.yml 파일 중 ELK 스택 컨테이너를 구성하는 단락

Elasticsearch의 내부 데이터를 로컬 볼륨에 마운트해놓는다, Kibana 설정도 마찬가지로 Local 내부 볼륨에 마운트 해두어야 컨테이너를 올렸다 내리더라도 내가 설정했던 대시보드, 인덱스 패턴, Privacy 관련 정책들이 모두 유지된다. 실제 프로덕션 환경에서는 이런 방식으로 사용할리가 없다만, 일단은 아이맥에서 하는 거니까! 트센 끝나면 쿠버네티스부터 CI/CD, 리소스 관리까지 싹 다 공부할 거니까! 라는 안일한 생각으로 기술 부채로 남겨둔 채 compose 파일을 작성했다.

뉴트센은 모든 서비스를 TLS 인증서를 사용하여 https로 접속이 가능하게 설계를 해야 하는데, ELK 스택은 모두 Elasticsearch에서 자체 발급한 인증서를 사용해야 해서 이 부분에서 애를 먹었다.

워낙 초창기에 했던 작업이어서 ELK Stack 간의 의존성을 어떻게 해야할지도 몰랐고, 인증서 발급을 스크립트화 해서 마운트 해야 하는지도 몰랐고... 이 인증서 자체가 트센 통과 하루 전 날 새벽까지 날 꾸준히 힘들게 했지만, 인프라 엔지니어로 살아가기 위해서는 인증서를 완벽히 이해하는 게 아주아주아주아주아주아주아주아주아주아주아주아주아주아주 중요하다는 것을 다시 한 번 깨닫는 계기가 되었다.

profile
DevOps / Infrastructure / Cloud Native / Platform Engineering

0개의 댓글