NRISE를 지탱하는 기술 — 인프라
김대근 CTO | 2022년 11월 22일
NRISE는 국내 소셜 디스커버리 시장을 선도하는 위피(WIPPY)와 홈트레이닝 서비스 콰트(QUAT)를 운영하고 있습니다. 서비스는 다르지만 지탱하고 있는 기반 기술은 크게 다르지 않습니다. NRISE 역시 클라우드 플랫폼을 사용하며 Amazon Web Services (AWS)를 사용합니다.

우리는 고객에게 높은 가치를 제공하는 것을 최우선으로 하기에 안정적이고 편이한 인프라스트럭처를 갖추어 프로덕트 엔지니어분들이 고객에 더 집중할 수 있도록 합니다. 우리는 어떤 선택을 했고 변화하고 있는지 기록합니다.
(2021년 11월) 1년 전만 해도 EC2 위에 PostgreSQL, MongoDB, ElasticSearch, Redis, RabbitMQ 서버를 운영했으며 위피와 콰트 역시 EC2와 Auto Scaling Group (ASG)으로 간단하지만, 배포는 손이 많이 가는 형태였습니다.
입사 후 지켜보며 느꼈던 문제와 불편함은
- Fully-managed services (ex: AWS Aurora)를 사용하지 않아 High Availability (HA)구성이나 시스템 업그레이드 등 운영의 번거로움
- 기존 배포 도구의 불편함과 장애 대응
- 모니터링 도구의 부재로 장애 인지와 대응의 어려움
- 기술부채와 개발환경 부재 등으로 빈번한 장애
- AWS 자원 히스토리 파악의 어려움
등이 있었습니다.
Kubernetes (EKS) 전환과 fully-managed 서비스 전환을 빠르게 진행하는 것을 목표로 했고 대략 한달 정도의 시간동안 아래 작업을 진행했습니다.
- AWS Aurora, ElastiCache, DocumentDB, OpenSearch 전환
- Savings Plans, Reserved Instances, Spot Instances
- Application Performance Management (APM) 도구인 DataDog 도입
- Hashicorp Terraform 으로 Infrastructure as Code (IaC)
- Kubernetes (AWS EKS) & Istio & Karpenter
- Helm 3 & Helmfile
- ArgoCD, Rollouts, ApplicationSet, Notifications
- GitHub Actions 통한 배포
- EKS 전환에 앞서 WIPPY 성능 최적화 (Karpenter, Spot Instances 등은 2022년 4월 도입)
AWS
NRISE에서는 총 3개의 AWS 계정을 사용합니다. 운영, 개발, 데이터 환경을 나누어서 용도에 맞게 사용합니다. 기존에 없던 개발환경과 데이터 분석 환경을 추가했습니다.
Infrastructure as Code (IaC)
기존엔 IaC 작업이 되어있지 않았기 때문에 fully-managed 서비스 생성에 앞서 가볍게 IaC 작업도 선행하였습니다. 하나의 거대한 워크스페이스에 담아두지는 않고 용도별로 쪼개서 관리합니다. 테라폼 클라우드를 사용하고 워크스페이스는 아래와 같은 디렉토리 구조로 분류해서 관리합니다. (상위 디렉토리만 표현했습니다.)
.
├── README.md
└── aws
├── eks
│ ├── data-apne2
│ ├── dev-apne2
│ ├── prod-apne1
│ ├── prod-apne2
│ └── shared
├── eks-irsa
│ ├── data-apne2
│ ├── dev-apne2
│ ├── prod-apne1
│ ├── prod-apne2
│ └── shared
├── global
│ ├── cloudfront
│ ├── eks-iam
│ ├── iam-user
│ └── route53
├── products
│ ├── data
│ ├── platform
│ ├── quat
│ ├── shared
│ └── wippy
├── projects
└── regional
├── acm
├── key_pair
├── kms
├── shared
└── vpc
각 분류별 목적은
products : WIPPY, QUAT, 플랫폼, 데이터 제품별 rds, cache 등 제품별로 공유하는 리소스 global : iam, route53, cloudfront 등 region 을 가리지 않는 리소스 regional : vpc, kms, acm 등 region 별로 공유해서 사용하는 리소스 projects : product, global, regional 등을 참조하는 리소스 단방향으로 참조하고 재사용할 수 있게 층을 나눕니다. 그리고 몸집을 줄이는 형태로 유지합니다. 필요에 따라서는 output 만 있는 워크스페이스를 만들어서 쓰기도 합니다.
그 외에도 몇가지 컨벤션이 있습니다. 각 워크스페이스는 main.tf 파일을 가지고 있고 README 역할을 하기에 리소스를 선언하지 않고 아래 정보만 다룹니다.
각종 프로바이더의 버전 워크스페이스 정보 참조하는 리소스 (global, regional, product 등) 선언 각 워크스페이스별 로컬변수 정보 (태그, 계정, 리전, 리소스 prefix 등)
...
data "terraform_remote_state" "iam" {
backend = "remote"
config = {
organization = "<organization>"
workspaces = {
name = "eks-iam-prod"
}
}
}
# 다른 tf 파일에서 아래처럼 접근합니다.
# data.terraform_remote_state.vpc.outputs.vpc_id
# data.terraform_remote_state.vpc.outputs.vpc_cidr_block
# data.terraform_remote_state.vpc.outputs.private_subnets
data "terraform_remote_state" "vpc" {
backend = "remote"
config = {
organization = "<organization>"
workspaces = {
name = "nrise-vpc-prod"
}
}
}
locals {
...
env = "prod"
region_code = "apne1"
name_prefix = "eks-${local.env}-${local.region_code}"
aws_region = "ap-northeast-1"
aws_arn = data.aws_caller_identity.current.arn
aws_user_id = data.aws_caller_identity.current.user_id
aws_account_id = data.aws_caller_identity.current.account_id
default_tags = {
# 비용 추적 용도로도 사용합니다.
Environment = local.env
Service = "quat"
Team = "quat"
"nrise.net/managed-by" = "terraform"
# AWS 리소스 태그만으로 워크스페이스 이름을 알 수 있도록 하기 위함
"nrise.net/workspace" = "eks-prod-apne1"
# AWS 리소스 태그만으로 repo 내 디렉토리 위치
"nrise.net/work-dir" = "aws/eks/prod-apne1"
}
...
}
data "aws_caller_identity" "current" {}
그 외에는 조금 자유롭게 사용하고 있습니다.
Kubernetes (AWS EKS)
EKS Clusters와 IAM Role for Service Account (IRSA) 설정은 Terraform으로 관리하고 그 외 add-ons 설치 및 설정은 helmfile로 합니다.
Helm
클러스터내에 설치하는 모든 애플리케이션은 helm을 이용합니다.
NRISE 서비스의 애플리케이션은 ArgoCD와 helm을 이용 그 외 애플리케이션은 helmfile을 이용 적용하기 전에 변경사항을 확인하는 diff 플러그인과 코드 내 민감정보를 포함해야하는 경우에는 secrets플러그인(sops)으로 암호화해서 사용합니다.
Helmfile
Terraform helm provider 에서 불편하고 helm만 쓰기에는 부족해서 우연히 19년에 발견하고 편히 쓰게된 제품입니다. 여러 release를 다룰 수 있고 kubectl context 조작 실수 등을 방지하는 차원에서 사용합니다. 설정할 수 있는 옵션이 굉장히 많습니다.
각종 add-ons는 아래 명령어처럼 설치합니다.
helmfile -e <env> diff [--skip-deps] [-l ~]
helmfile -e <env> apply [--skip-deps] [-l ~]
아래 helmfile.yaml 은 사용하고 있는 것 중 하나이고 용도별로 여러 파일로 쪼개져서 관리하고 있습니다. 몇가지 생략한 정보는 있지만 지금까지 쓰면서 정착한 방식입니다.
# helmfile.yaml
repositories:
- name: eks
url: https://aws.github.io/eks-charts
- name: argo
url: https://argoproj.github.io/argo-helm
- name: karpenter
url: https://charts.karpenter.sh
...
# helmfile.yaml 마다 반복적으로 사용하는 부분이라서 실제로는 bases 설정을 씁니다.
# bases:
# - ../../shared/environments.yaml
# context 변경없이 env 값으로 클러스터를 지정할 때 사용합니다.
helmDefaults:
kubeContext: {{ .Environment.Name }}
environments:
prod-apne1:
values:
- ../../shared/values.default.yaml
- ../../shared/values.prod-apne1.yaml
prod-apne2:
values:
- ../../shared/values.default.yaml
- ../../shared/values.prod-apne2.yaml
dev-apne2:
values:
- ../../shared/values.default.yaml
- ../../shared/values.dev-apne2.yaml
data-apne2:
values:
- ../../shared/values.default.yaml
- ../../shared/values.data-apne2.yaml
# helm release
releases:
- name: termination-handler
chart: eks/aws-node-termination-handler
values:
- ./config/termination-handler/values.yaml
- name: aws-vpc-cni
chart: eks/aws-vpc-cni
namespace: kube-system
values:
- ./config/aws-vpc-cni/values.yaml
- ./config/aws-vpc-cni/values.{{ .Environment.Name }}.yaml
- name: external-dns
chart: bitnami/external-dns
values:
- ./config/external-dns/values.yaml
- ./config/external-dns/values.{{ .Environment.Name }}.yaml
missingFileHandler: Warn
- name: karpenter
chart: karpenter/karpenter
values:
- ./config/karpenter/values.yaml
- name: argo-rollouts
chart: argo/argo-rollouts
values:
- ./config/argo-rollouts/values.yaml
missingFileHandler: Warn
...
missingFileHandler: Error
Karpenter
2022년 4월부터 도입한 후 만족도가 높은 제품입니다. cluster-autoscaler 의 단점을 모두 해결해주는 제품이 아닌가 싶습니다. 이 주제로 따로 다뤄보도록 하겠습니다. 7개월 이상 사용하면서 아직 문제점이나 불편함은 없었어요.
Kubernetes Event-driven Autoscaling (KEDA)
NRISE 백엔드 챕터는 Python을 주로 사용합니다. 자연스럽게 RabbitMQ를 백엔드로 Celery Worker도 운영하고 있습니다. RabbitMQ에 쌓이고 있는 소진 속도를 보고 worker를 autoscaling 할 때 사용하기도 합니다. 그 외에 다양한 scaler를 제공하며 kafka, sqs, activemq, kinesis, pulsar 같이 익숙한 제품들이 있습니다.
그 외
- external-dns
- aws-node-termination-handler
- istio / alb ingress controller / nginx ingress controller
- etc.
ArgoCD

1년 전까지만 해도 로컬에서 fabric을 통해 rolling upgrade하는 과정에서 정확한 probe가 이루어지지 않아 가끔 장애가 발생하기도 했습니다. 가시성도 떨어지고 동작확인도 쉽지않은 구조였습니다. Git Repo 를 Single Source of Truth (SSOT)로 하여 GitOps 구현체인 ArgoCD를 도입했습니다.
물론 ArgoCD 외에 ArgoCD ApplicationSets, Rollouts을 함께 사용합니다. 일일이 수동으로 애플리케이션을 등록하지 않고 git generator를 이용해서 자동 생성하는 방식을 취합니다.
ArgoCD ApplicationSets
NRISE 제품 중 WIPPY와 연관된 서버를 띄울 때 사용하는 ApplicationSet을 사례로 소개합니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: wippy-apps
spec:
generators:
- git:
repoURL: <git repo url>
revision: main
files:
- path: appsets/prod/wippy/apps/**/config.json # (1)
template:
metadata:
name: '{{release}}-prod'
namespace: prod
annotations:
notifications.argoproj.io/subscribe.slack: alarm-deploy-prod
spec:
project: default
...
source:
repoURL: <git repo url>
targetRevision: main
path: '{{chart}}'
helm:
releaseName: '{{release}}'
valueFiles: # (2)
- ../../../appsets/prod/wippy/values/wippy-defaults.yaml
- ../../../appsets/prod/wippy/values/{{release}}/defaults.yaml
version: v3
ignoreDifferences: # (3)
- group: networking.istio.io
kind: VirtualService
jsonPointers:
- /spec/http/0
- git.repoURL 에 명시한 리포지토리와 동기화 과정에서appsets/prod/wippy/apps/**/config.json 패턴과 일치하는 경우 ArgoCD Application으로 자동 생성합니다.
- helm 차트를 릴리스할 때 사용하는 value files 입니다. 모든 애플리케이션이 공유해서 사용하는 wippy-defaults.yaml 와 각 release 에서만 사용하는 defaults.yaml 파일을 두고 사용합니다.
- Argo Rollouts를 Istio와 함께 사용하는데 변화를 무시하는 설정입니다.
Rollouts
NRISE는 canary 배포 전략을 사용합니다. Istio와도 잘 어울리고 적절한 검증을 하면서 롤백할 수 있는 시간적인 여유 확보가 가능하여 애플리케이션 배포할 때 활용합니다. 아직까지 canary step 으로 AnalysisTemplate을 활용하고 있지는 않습니다. 트래픽이 충분하게 발생하지 않는 기능의 변경사항인 경우 100% 신뢰하기 어렵기때문에 매뉴얼로 모니터링하는 것을 권하고 있습니다.
GitHub Actions/Packages
Jenkins와 같은 도구를 쓰지 않고 GitHub Actions에서 모든 배포와 관련된 작업을 수행합니다. ECR에 푸시권한과 특정 버킷 저장 외 권한은 열어두지 않습니다. (민감한 AWS 접근 권한이 필요한 경우 self-hosted agent를 활용하겠지만 아직까지는 사례가 없었습니다)
GitHub Actions 코드는 간단하게 아래와 같은 순서로 진행합니다.
- 코드 체크아웃
- 컨테이너 이미지 빌드하고 ECR 푸시
- Single Source of Truth (SSOT)으로 사용하고 있는 Git Repo에 컨테이너 이미지 태그 정보를 갱신하는 PullRequest 등록
- 배포가 필요한 시점에 엔지니어가 PR 머지
PR이 머지되면 ArgoCD에 의해 배포가 곧 자동으로 시작됩니다.
결국 엔지니어는 main 브랜치에 코드를 머지한 이후에는 SSOT으로 사용하는 Git Repo PR 머지 외에는 별도 수행하는 작업은 없습니다. 언젠가는 필요하겠지만 규모가 크지는 않아서 별도의 시스템을 갖추고 있지는 않습니다.
마무리
저의 입사 1주년을 자축하며, 1인 팀이지만 SRE 엔지니어로 했던 것을 기록합니다. NRISE에서 Cloud Platform, Kubernetes Cluster, Application Delivery 중점으로 가볍게 소개하는 포스팅이어서 깊지않지만 머릿말 차원에서 기록합니다. 많은 곳에서 Kubernetes 도입하고 있지만 모든 스타트업에서 반드시 써야하는 도구는 아닙니다. 백엔드/프론트엔드 엔지니어분들이 인프라보다 스쿼드와 제품 집중할 수 있도록 하는 것이 중요하기 때문입니다.
위피, 콰트, 플랫폼 백엔드 엔지니어를 적극 채용 중 입니다. NRISE에서 함께 성장하고 싶다면 지원해주세요!