k8s로 구축한 API 서비스 EKS에 배포하기- 1부

이용택
GDSC Soongsil
Published in
18 min readJan 14, 2022

안녕하세요! GDSC Soongsil server/cloud팀 이용택입니다.

저희 cloud 파트에서는 쿠버네티스 스터디를 진행하였는데요, 스터디를 통해 공부한 내용과, 이를 활용해본 과정을 공유하려고 합니다.

저희의 목표는 API 서비스를 k8s로 구축하고, github Action을 통해 EKS에 배포 하는 것입니다.

우선 이를 하기 전에, 1부에서는 k8s에 대해 간략(?)하게 소개하려고 합니다.

0. Kubernetes

왜 쿠버네티스를 사용할까?

인기있는 게임이나 인기있는 가수의 콘서트 등 트래픽이 많이 몰릴경우 서버가 터져버리곤 합니다. 소비자 입장에서는 화가 나지만🤬, 이러한 서버들을 관리하는 기업들 입장에서는 트래픽 양을 수동으로 정확히 예측하기에는 매우 어려울 것이고, 이에 따라 정확한 자원을 준비하기는 어려울 것입니다.

서버 터짐ㅠㅠ

이는 여러 서비스를 운영하면 문제가 더 생깁니다.

예를 들어 한 회사의 A서비스에서 10만명의 트래픽을 예측하고 그에 따른 서버를 만들었지만 5천명 밖에 몰리지 않아 자원을 낭비하였고, 반대로 5천명이 몰릴것이라 예상한 B서비스에 10만명이 몰렸다면 서버가 터져버리고 말것입니다.

물론 이러한 상황에 대비해서, 백업 서버를 두어 즉각적으로 대응할 수 있게 만들어 문제 상황을 해결할 수 있습니다. 하지만 그러기에는 더 많은 서버를 요구하기에 비용적으로 낭비입니다.

이러한 문제점들을 해결해 주는 것이 바로 쿠버네티스입니다!

k8s의 기능

위 예제와 같이 k8s를 쓰지 않았을 경우에는, 서비스A를 위해서 서버 3개와, 서버가 터질것을 대비하여 관리하는 예비 서버 1개를 포함해 4개를 운영하고, 서비스 B를 위해서는 서버 3개와 예비용 서버1개를 포함한 4개, 총 8개의 서버를 운영하는 서비스가 있으며, 현재 서비스 A의 트래픽은 서버 3대분량이고, 서비스 B의 트래픽은 서버 1대 분량이라고 가정하겠습니다.

그렇게 되면 서버는 8대이지만, 사실상 사용하는 서버는 4대 뿐이고 4대는 사용하지 않아 낭비가 발생하고 있습니다.

하지만, 쿠버네티스를 적용하게 되면 쿠버네티스가 auto scaling을 통해 트래픽양에 따라 알아서 서비스의 자원량을 변경해주어서 (서비스에 따라 물리적으로 서버가 나눠져있지 않음) 4대의 서버만으로도 트래픽을 감당할 수 있습니다.

또한 auto-healing을 통해 각각의 서비스마다 백업 서버를 두지 않고도 한개의 서버만으로 상황에 맞춰 필요한 서비스에 백업을 해주어, 적은 서버로 트래픽양을 조절할 수 있습니다.

이러한 예시들은 쿠버네티스의 기능 중 하나이고, 배포를 쉽게 관리하게 해주는 등 쿠버네티스에는 여러기능에 대해 운영 자동화를 지원합니다. 이는 서비스 운영을 편하게 하여 서비스 효율이 좋아지게 합니다.

위의 예시처럼 서버가 적어지면, 유지보수 비용이 적어지고 그만큼 다른 부분에서 발전할 수 있는 시간이 확보됩니다.

그래서 쿠버네티스를 사용합니다!

쿠버네티스가 제공하는 운영 자동화를 통해 관리자들이 이전보다는 조금은 더 발 뻗고 자고 있을 것 같네요 😴

쿠버네티스는 컨테이너화된 애플리케이션을 쉽게 배포하고 관리를 자동화 할 수 있게 해주는 오픈소스 플랫폼입니다.

1. pod

우선 쿠버네티스의 가장 기본적인 단위인 pod부터 소개하려고 합니다.

pod란 영어로 강낭콩 열매 혹은 고래의 무리 라는 뜻을 가집니다.

쿠버네티스에서의 pod 또한 이러한 의미를 지니고 있습니다.

쿠버네티스에서는 고래나 강낭콩이 아닌, 컨테이너의 무리라고 보시면 될것 같네요.

컨테이너🐳

pod란, 쿠버네티스에서 가장 기본적인 배포 단위로, 하나 이상의 컨테이너를 포함하는 단위입니다.

왜 pod 단위가 기본적인 배포단위일까?

그러면, 왜 쿠버네티스에서는 컨테이너별로 배포하지 않고, 여러개의 컨테이너를 Pod 단위로 묶어서 배포하는 걸까요?

Pod에서는 컨테이너끼리 서로 상호작용 할 수 있기 때문입니다.

1. pod 내의 컨테이너끼리는 IP와 포트를 공유가 가능

하나의 Pod 안에서는 두개의 컨테이너가 서로 localhost로 통신이 가능합니다.

컨테이너끼리 통신!

예를 들어, 컨테이너 A가 8080, 컨테이너 B가 20200 포트를 사용중이라고 하겠습니다.

그러면 pod에서는 A가 B를 호출할 때에는 localhost:20200, B가 A를 호출할 때에는 localhost:8080으로 호출을 하여 서로 통신할 수가 있습니다.

2. pod 내의 컨테이너 끼리는 디스크 볼륨도 공유 가능

pod 내에서는 컨테이너끼리 서로의 다른 파일을 읽어올 수 있습니다.

각각 다른 컨테이너인 A컨테이너의 파일을 B컨테이너가 읽어올 수 있게 되는 것입니다.

volume

위에서 언급하기도한 디스크 볼륨이란 컨테이너 안에서 영구적으로 보관할 수 있는 저장소입니다.

pod가 기동 될 때, 컨테이너에서 로컬 디스크를 생성하게 되는데 이 로컬 디스크는 pod 가 종료되면 다같이 사라집니다. 이러한 특성 때문에 항상 데이터를 유지해야 하는 DB 같은 경우는 해당 로컬 디스크는 적합하지 않습니다.

그래서 이런 경우에는 영구적으로 보관할 수 있는 스토리지인 볼륨을 사용합니다.

니껀 내꺼 내껀 내꺼

예를 들어, 다음과 같이 웹서버 컨테이너와 로그를 관리하는 컨테이너가 있다고 하겠습니다.

이때 서비스가 돌아간다면, 웹서버에 접근한 로그들도 쌓고 자체적인 로그 컨테이너에서도 로그를 쌓아나가 소비자들의 행적들을 기록해야 합니다.

그런데 만약에 이를 컨테이너별로 따로 따로 기록한다면 관리하기가 너무 힘들 것입니다. (원하는 로그를 찾을 때도 번거로울 것 같네요)

그래서 이러한 경우에 volume을 사용한다면, 같은 스토리지에 로그 데이터를 쌓을 수 있어 번거로움을 조금 해소할 수 있습니다.

label

pod는 생성될 때마다 이에 접근 할 수 있는(쿠버네티스 클러스터에서만 접근 가능한) ip 주소가 생깁니다.

다만 이 주소는 pod에 장애가 생겨서 꺼져버린다면, 다시 기동될 때 새로 바뀌게 됩니다. (고정적이지 않음)

따라서 해당 pod가 고장이 나서 주소가 바뀌더라도 고정적으로 이를 가리킬 수 있는 장치가 필요한데, 이를 위해 label을 사용합니다.

pod에 이름을 지어준다고 생각하면 더 쉬울거 같네요. 🙃

label은 key: value의 형태로 정의하고, 하나의 pod는 여러개의 라벨을 지정할 수 있습니다.

pod에 별명을 붙이자

예를 들어, 위와 같이 키값에 type:을 지정하여 웹 프론트 인지, 서버인지, db인지를 나타낼 수 있고,

키값을 lo:를 지정하여 테스트 환경인지, 아니면 상용(배포) 환경인지를 나타내어 구분 할 수 있습니다.

이렇게 라벨링을 하게 되면 각각 테스트 환경, 상용 환경의 프론트/서버/db 환경을 만들어 구분하여 사용할 수 있게됩니다.

Node Schedule

이제 이렇게 라벨을 통해 이름이 생긴 pod들이 작동하려면, 결국 여러 node들 중에 한 node에 올려져야 합니다.

노드란, 쿠버네티스 클러스터를 구성하는 머신들을 노드(node) 라고 부릅니다.

이러한 노드에 pod를 올리는 방식은 두가지가 있습니다.

1. 사용자가 직접 연결할 노드를 설정

사용자가 직접 파드를 노드에 설정하는 방식은, pod에서 label을 사용하는 방식과 똑같다 보시면 됩니다.

node에 라벨을 달아주고, pod를 생성할때 nodeSelector 옵션을 주어서 해당 노드의 라벨 키:밸류 값을 넣어주면 됩니다.

pod nodeSelector yaml

다음 yaml 파일에서, nodeSelector의 nodename: node1이 연결할 노드에 달아준 label입니다.

이를 명시해줌으로서 pod 생성 시에 노드가 연결됩니다.

2. 쿠버네티스가 설정

쿠버네티스 스케줄러가 자동으로 자원량(CPU, 메모리)를 고려하여 알맞은 파드를 노드에 할당해 주기도 합니다.

사용량에 따라 할당!

예를들어, 다음과 같이 파드 생성시 메모리 사용량이 2Gi로 명시된 파드를 노드에 연결한다면,

메모리 1Gi 짜리 노드가 아닌 메모리가 넉넉한 3.8Gi짜리 노드로 쿠버네티스 스케줄러가 파드를 할당해 줍니다.

만약에 이렇게 스케줄러가 자동적으로 할당하지 않고 고정적으로 할당되어 있다면, 하나의 pod에서 부하가 생길 시 그 pod가 무한적으로 노드의 자원을 사용하여 한계치가 있는 메모리 사용량을 감당하지 못하고 그 노드 안의 다른 pod들이 같이 죽어버리게 됩니다. 💀

2. service

다음으로는 pod보다 더 큰 단위인 service에 대해 소개하려고 합니다.

여러개의 pod들을 묶어서 하나의 서비스를 제공하는 것이 바로 service입니다. (하나의 pod를 가지고 서비스를 하는 경우는 드뭅니다.)

서비스는 (container가 모인) pod가 모여있는 집단이다!

clusterIP

service에도 pod에서와 같이 쿠버네티스 클러스터 내에서 접근할 수 있는 ip가 있습니다. 이 서비스 ip를 거쳐서, 서비스에 연결된 pod들에 접근할 수가 있습니다.

다음과 같이, 만약 서비스에 171.96.10.17 ip가 할당되어 있다면, 이와 연결된 pod들을 port(8080)를 통해서 접근 할 수 있습니다.

물론 이러한 방식이 아닌, pod의 ip(10.16.1.2) 에 직접 접근 할수도 있지만, 앞서 언급했듯이 pod는 장애가 생겨서 종료되고 재시작 된다면 ip가 변하기 때문에 고정된 엔드포인트로 호출이 어렵습니다. (해당 ip가 찾는 pod임을 보장하지 않습니다!)

그래서 pod ip는 신뢰성이 떨어집니다! 😓

이에 반해 서비스의 ip는 사용자가 직접 지우지 않는 이상, ip가 변하지 않는 특징이 있습니다. 그래서 pod에 접근할때는 pod의 ip가 아닌, 고정된 엔트포인트로 항상 호출 할 수 있어 신뢰성이 높은 service의 ip를 사용합니다.

이러한 안정성 있는 ip를 이용하면, 하나의 파드가 어떠한 노드에 있더라도, ip를 통해 pod끼리 통신을 할 수 있습니다.

nodePort

하지만 이러한 clusertIP를 활용한 통신은 쿠버네티스 클러스터 안에서만 통신이 가능하다는 단점이 있습니다.

만약 외부에서 이런 pod들에 접근하려면 어떻게 해야할까요?

바로 이 nodePort를 사용하면 클러스터 외부에서 접근할 수 있게 만들 수 있습니다.

nodePort service yaml

이렇게 yaml 파일에서의 내용처럼 서비스를 배포할 때 nodePort를 지정해주어 이 서비스를 클러스터에 올린다면, 외부에서도 30000 포트를 통해서 접근이 가능합니다.

load Balancer

nodePort 와 같은 성격을 갖지만, 로드 밸런서는 외부 플러그인을 활용하여 ip를 생성합니다.

GCP나 AWS 등 외부 플러그인을 통해서 서비스를 로드밸런서 타입으로 만들면, 플러그인 서비스들이 알아서 외부에서 접속할 수 있는 ip를 만들어줍니다.

클라우드(돈) 으로 해결!

이렇게 로드 밸런서를 활용하면 사용자들은 해당 ip를 통해서 외부에서 접근이 가능합니다. 이 ip는 실제 내부 아이피가 아닌, 특정 외부 플러그인을 통해 할당받은 아이피입니다.

그러면 이 세가지 service 타입들을 각각 어떠한 상황에서 사용해야 할까요?

우선 clusterIP의 경우에는 쿠버네티스 클러스터 안에서만 사용할 수 있고 외부에서는 접근할 수 없습니다. 그래서 클러스터 내부에서 접근하여 작업하는 (쿠버네티스 대쉬보드를 관리하고 파드 서비스를 디버깅하는 작업 등) 내부 관리자가 사용합니다. (로컬 환경)

두번째로 nodePort는 외부에서 접근할 수 있지만 내부 ip를 이용한다는 점에서, 내부관리자가 외부에서 접근 할 때 사용합니다. 아무래도 호스트 ip는 보안적으로 외부에서 함부로 접근하지 못하게 네트워크를 구성하기 때문에 내부인들만 쓰는것이 안전할 것입니다. 내부에서 개발하다가, 외부에서 테스트해볼 데모 버전을 위해 사용합니다. (테스트 환경)

세번째로 load Balancer실제적으로 외부에 서비스를 노출시킬때 사용합니다. 내부 ip가 노출되지 않고, 외부 아이피를 통해 안정적으로 서비스를 노출할 수 있도록 사용합니다. (상용, 배포 환경)

3. ingress

이처럼 nodePort와 load Balancer를 통해서 외부에서 쿠버네티스 클러스터 안으로 접근할 수 있지만,

service별로 일일이 nodePort / load Balancer를 세팅하기에는 개발/운영적 부담이 클 뿐 아니라, 이것만으로는 네트워크 요청에 대한 세부적인 처리를 하기에는 어려움이 있습니다.

이러한 어려움을 해결하기 위해서 Ingress가 있습니다.

Ingress는 외부에서 쿠버네티스 클러스터(서비스들)에 접근하기 전, 꼭 만나야 하는 문지기와 같은 역할을 합니다.

Ingress는 크게 두가지 기능이 있습니다.

  • 서비스 도메인으로 접근할때, 서비스 별로 도메인을 라우팅 해줌
  • 서비스를 접근할때, 연결될 서비스의 트래픽(%)을 분산시켜줌

Ingress Controller

우선 이러한 기능을 하는 ingress를 작동하게 해주는 ingress controller에 대해 이야기 하고자 합니다.

Ingress는 쿠버네티스를 설치했다면 원하는대로 세팅해서 생성할 수 있습니다. 다만 Ingress를 만들었다고해서 작동이 되는 것은 아닙니다. Ingress를 만드는 것은 Ingress가 어떻게 돌아갈지에 대해 규칙만 정하는 것일 뿐, 이를 직접 작동 시키는 것은 Ingress Controller입니다.

이렇게 ingress를 작동시키는 방법은 두가지가 있습니다.

  1. Ingress Controller를 직접 운영한다 (kong, Nginx)
  2. public Cloud Platform에 위임한다. (Google Kubernetes Engline, GKE)

물론 위임하는게 가장 편하지만, ( 💸으로 해결한다!)

여기서는 Nginx를 사용하여 ingress controller를 직접 운영한다고 가정하겠습니다.

service loadBalancing(라우팅)

만약 한 애플리케이션에서 운영하는 각각 다른 서비스(기능)들이 있다면 (예: 배송 서비스, 구매 서비스, 판매 서비스)

인그레스를 통해 라우팅을 하여, 외부에서 간편하게 해당 서비스들에 접근하게 할 수 있습니다.

Ingress 라우팅

인그레스를 통해 라우팅하는 step은 다음과 같습니다.

  1. Ingress를 정의합니다. 규칙에 서비스 이름(serviceName)과 그에 할당하고 싶은 경로(path)를 지정하여 정의합니다.
  2. 이를 Nginx ingress controller에 적용시켜서 라우팅 된 서비스들을 사용합니다.
  3. 소비자가 market 서비스에 접근하고 싶다면, 경로가 /market 으로 설정된ip: nodePort(192.168.0.30:30431)에 마켓 경로(/market)를 붙여주면 됩니다. (192.168.0.30:30431/market)

이렇게 ingress를 활용하면 외부에서 간단하게 여러 서비스에 url로 접근할 수 있습니다.

Canary Release(카나리 배포, 트래픽 분산)

앱을 업데이트 할 때, version A에서 version B로 확 바뀌어 버리면 리스크가 생길 수 있습니다. 예를 들어 업데이트를 하였는데 잘못된 코드가 있어서 앱이 먹통이 되는 문제가 발생할 수 있습니다.

100%로 업데이트를 해버리는 경우에는 모든 소비자들이 이러한 문제들에 영향을 받을 것입니다. 100만 유저가 있다면 100만 유저가 모두 안 좋은 이용 경험을 가져가는 것입니다 😵‍💫

하지만 본래버전인 version A를 99%, 업데이트할 버전인 version B를 1%의 소비자들이 경험하게 해준다면 1% 만이 테스트가 필요한 업데이트 버전을 경험하게 됩니다. 그러면 문제가 생기더라도 1%만이 문제를 겪어 문제상황을 최소화 시킬 수 있습니다.

만약 문제가 없어서 이제 점차 그 비율을 10%, 50%,..100%로 늘려나가며 업데이트를 한다면, 더 안정적이고 성공적으로 업데이트를 할 수 있을 것입니다.

이것이 카나리 배포이고, 하나의 SW배포 방법으로, 조금씩 사용자의 범위를 늘려가면서 배포하는 방식으로, 카나리란 과거 석탄 광산에서 유독가스를 미리 감지하고자 카나리아 새를 날려 보냈던 것에서 붙여진 이름입니다. 위에서 언급한것 처럼 소비자들에게 배포를 한 뒤 반응을 보고 사용자들을 늘려가면서 배포하는 방식입니다.

1%의 사용자들이 카나리아 새가 되겠고, 유독가스는 터질지도 모르는 새로운 버전의 업데이트가 되겠네요. 🐦

이러한 카나리 업그레이드를 ingress를 통해 쉽게 할 수 있습니다.

Ingress canary

예를 들어 www.app.com으로 사용자가 접근하면 service-v1 서비스로 연결되는 앱이 운영되고 있습니다.

이때 업데이트를 위해 업그레이드 된 버전을 테스트해보고 싶다면, 다음과 같은 절차를 통해 Ingress를 도입하고 카나리 업그레이드를 할 수 있습니다.

step

  1. 카나리 업그레이드를 테스트할 pod와 서비스(service-v2)를 띄웁니다.
  2. 새로운 ingress를 생성하고, host는 기존 ingress와 같은 이름으로 설정하고serviceName은 새로운 서비스 이름(service-v2)로 설정합니다.
  3. 그런 후에 weigth 10%를 주게되면 10%의 트래픽이 새로 붙인 서비스로 흘러갑니다.

이렇게 세팅을 하면 90%는 기존의 서비스로, 10%는 새로 붙인 서비스로 트래픽이 분산되어서 간단하게 카나리 업그레이드를 할 수 있습니다.

4. ECR & github Action

이제 이러한 k8s (ingress, pod, service)로 구축한 API 서비스를 자동으로 이미지 push 하기 위해 github action과 ECR을 사용하려고 합니다.

github Action? ECR?

github Action

github action이란, github repo를 기반으로 CI/CD를 구성할 수 있는 도구입니다. git에서 개발 Workflow 를 자동화 해줍니다.

github action에는 크게 4가지 개념이 있습니다.

1. workflows

workflows는 복수의 Jobs으로 구성되고 이벤트 기반으로 실행되며, 가장 최상위의 개념입니다. 의도한 동작을 정의해서 yaml을 작성하면 github action이 실행됩니다.

예를들면 코드 push나 pull request에 반응해서 실행되도록 하거나, 정해진 시간에 실행되도록 하는 scheduler를 만들 수 있습니다. (천하제일 깃허브 자랑대회에서도 사용하신것 같네요! (cron schedule))

2. Jobs

job은 복수의 steps로 구성되며, 이러한 여러개의 job은 독립으로 병렬 실행을 할 수도 있고, 직렬 실행을 할 수도 있습니다. 예를 들어 빌드-테스트를 하나로 묶어 실행 할 수도 있고, (빌드가 실패하면 테스트가 안됨) 빌드 따로, 테스트 따로를 실행 할 수도 있습니다.

3. Steps

job에서 커맨드를 실행하는 단위입니다. step은 순서대로 명령어를 실행합니다.

4. Actions

가장 작은 개념으로 action은 실행 명령어입니다. 단순 커맨드 실행이나 이미 정의된 Action을 가져와서 사용할 수도 있습니다.

( https://github.com/actions/)

이러한 개념들을 기반으로 github action을 작동시키는 yaml파일을 작성하면 workflow를 자동화 시킬수 있습니다.

ECR

ECR이란 개발자가 Docker 컨테이너 이미지를 손쉽게 저장, 관리 및 배포할 수 있게 해주는 완전관리형 Docker 컨테이너 레지스트리입니다. (AWS doc)

ECR을 통해 ECS에서 실행되는 애플리케이션에 대한 컨테이너 이미지를 손쉽게 저장, 실행 및 관리할 수 있습니다. 자체 컨테이너 리포지토리를 운영하거나 기본 인프라 확장에 대해 신경 쓸 필요성을 줄여준다는 장점이 있습니다.

EKS

ECR을 이용하여 컨테이너를 저장해 놓으면, 필요 시에 손쉽게 pull해와서 바로 사용할 수 있다는 장점이 있습니다!

마치는 말

이제 길었던 이론 부분을 마치고,

실제로 k8s로 구축한 API 서비스를 github action을 통해 ECR에 push를 하려고 합니다.

다음 2부에서는 해당 실습에 대해 소개하겠습니다! 😁

아직 배움이 부족해서, 혹시 부족한 부분이나 잘못된 부분이 있다면 말씀해주시면 감사하겠습니다!

감사합니다 🙏

--

--