목차
이미지가 생겼고 어디서든 똑같이 돈다. 그런데 띄워놓은 뒤가 문제다.
새벽 세 시에 컨테이너가 죽으면
컨테이너 세 개를 띄워두고 퇴근했다. 새벽 세 시에 그중 하나가 메모리를 다 쓰고 죽는다.
남은 둘이 그 몫까지 받다가 하나가 더 죽는다. 마지막 하나에 모든 요청이 몰린다. 알림이 울리고, 누군가 일어나서 노트북을 열고, 서버에 접속해서 컨테이너를 다시 띄운다.
앞 글에서 “컨테이너는 죽이고 다시 띄우면 된다”고 했다. 맞는 말이다. 다시 띄우는 건 쉽다. 문제는 그걸 사람이 한다는 것이다.
그리고 사람은 자고 있다.
사람이 감시자가 되면 안 되는 이유
물론 자동화를 조금 해볼 수는 있다. 죽으면 다시 띄우는 옵션도 있고, 감시 스크립트를 하나 돌릴 수도 있다.
그런데 조금만 상황을 늘려보면 금방 막힌다.
| 상황 | 감시 스크립트로 되나 |
|---|---|
| 컨테이너가 죽었다 | 된다 |
| 컨테이너는 살아 있는데 응답을 안 한다 | 살았는지 죽었는지 판단 기준이 필요하다 |
| 서버 자체가 죽었다 | 그 서버에 있던 스크립트도 같이 죽었다 |
| 트래픽이 늘어 5개로 늘려야 한다 | 어느 서버에 여유가 있는지 누가 아나 |
| 새 버전을 올리는데 무중단이어야 한다 | 순서를 짜야 한다 |
특히 세 번째가 결정적이다. 서버 안에 있는 감시자는 그 서버가 죽으면 같이 죽는다. 그래서 감시자는 서버 바깥에, 여러 서버를 한꺼번에 보는 자리에 있어야 한다.
여러 서버에 걸쳐 컨테이너를 배치하고 돌보는 그 자리를 컨테이너 오케스트레이터라고 부르고, 사실상의 표준이 쿠버네티스다.
여기서 이 글의 범위를 정해둔다. 쿠버네티스는 크고 기능이 많지만, 그 많은 기능을 관통하는 발상은 하나다. 이 글은 그 하나만 본다.
”해라”가 아니라 “이래야 한다”를 적는다
지금까지 우리가 컴퓨터에 일을 시켜온 방식은 이랬다.
docker run -d myapp:1.4.2
docker run -d myapp:1.4.2
docker run -d myapp:1.4.2무엇을 하라고 시킨다. 세 번 실행하면 세 개가 뜬다. 이걸 **명령형(imperative)**이라고 한다.
쿠버네티스는 이렇게 말하지 않는다. 대신 이렇게 적는다.
# "myapp:1.4.2가 항상 3개 떠 있어야 한다"
replicas: 3
image: myapp:1.4.2무엇을 해야 하는지가 아니라, 어떤 상태여야 하는지를 적는다. 이걸 **선언형(declarative)**이라고 한다.
차이가 사소해 보이지만 결과는 완전히 다르다.
| 명령형 | 선언형 | |
|---|---|---|
| 적는 것 | 할 일 | 있어야 할 상태 |
| 두 번 실행하면 | 여섯 개가 뜬다 | 여전히 세 개다 |
| 하나가 죽으면 | 두 개인 채로 있다 | 세 개로 되돌려진다 |
| 지금 상태를 알려면 | 세어봐야 한다 | 적어둔 게 곧 정답이다 |
두 번째 줄이 특히 중요하다. 선언형은 몇 번 적용해도 결과가 같다. 앞 글에서 “같은 스크립트를 돌려도 서버 상태에 따라 결과가 달라진다”고 했던 문제가 여기서 사라진다. 시작 상태가 무엇이든, 끝나면 적어둔 상태가 된다.
원하는 상태와 실제 상태를 계속 비교한다
그러면 적어두기만 하면 어떻게 그 상태가 되나. 쿠버네티스 안에서 루프 하나가 쉬지 않고 돌기 때문이다.
이 루프를 **조정 루프(reconciliation loop)**라고 한다. 하는 일은 세 줄이 전부다.
- 원하는 상태를 읽는다
- 실제 상태를 관찰한다
- 다르면 같아지도록 움직인다
그리고 이걸 쉬지 않고 반복한다. 한 번 하고 끝나는 게 아니다. 뭔가 바뀌었다는 신호가 오면 즉시 돌고, 신호를 놓쳤을 때를 대비해 주기적으로도 한 번씩 다시 맞춰본다.
이 그림이 이 글에서 가져갈 단 하나다. 쿠버네티스가 하는 일 대부분이 저 루프의 모양이다.
이 발상은 쿠버네티스가 처음이 아니다. 온도조절기가 정확히 같은 일을 한다. “22도로 해라”가 아니라 “22도여야 한다”를 설정하면, 장치가 현재 온도를 계속 재서 낮으면 데우고 높으면 멈춘다. 사람은 목표만 정하고, 도달하는 과정은 장치가 계속 조정한다.
자가 치유는 기능이 아니라 루프의 부산물이다
쿠버네티스 소개에 늘 “자가 치유(self-healing)“라는 말이 나온다. 죽으면 알아서 살아난다는 것이다.
그런데 이건 별도로 만든 기능이 아니다. 조정 루프가 돌고 있으면 저절로 따라오는 성질이다.
새벽 세 시에 사람이 하던 일을 그대로 루프가 한다. “죽었으니 살려라”라는 규칙을 누가 적어둔 게 아니라, “3개여야 한다”에서 2개로 어긋났으니 메우는 것뿐이다.
서버가 통째로 죽어도 같다. 그 서버에 있던 컨테이너들이 실제 상태에서 사라지고, 루프는 부족한 만큼을 다른 살아 있는 서버에 다시 띄운다. 앞에서 “감시자가 서버 안에 있으면 같이 죽는다”고 했던 문제가 여기서 풀린다. 루프는 서버 밖, 클러스터 전체를 보는 자리에서 돌기 때문이다.
파드와 컨트롤러
개념 두 개만 이름을 붙이고 넘어간다. 이 글에서 필요한 건 이 둘뿐이다.
파드(Pod) - 쿠버네티스가 다루는 실행 단위다. 컨테이너 하나가 든 상자라고 봐도 대부분 맞다. 쿠버네티스는 컨테이너를 직접 다루지 않고 항상 파드 단위로 다룬다.
컨트롤러(Controller) - 조정 루프를 도는 쪽이다. “파드가 3개여야 한다”를 맡은 컨트롤러가 계속 세어보고 모자라면 채운다.
여기서 반드시 짚어야 할 게 하나 있다. 루프가 되살린 파드는 죽은 파드가 아니다. 고쳐진 게 아니라 같은 이미지로 새로 만들어진 다른 파드다. 이름도 다르고 IP도 다르다.
다만 되살리는 층이 둘이다. 파드 안의 컨테이너만 죽으면(메모리 초과 같은 경우) 그 자리에서 컨테이너만 다시 뜬다. 파드는 그대로라 이름도 IP도 안 바뀌고, 재시작 횟수만 올라간다. 파드 자체가 사라지거나 노드가 죽었을 때 비로소 컨트롤러가 새 파드를 만든다. 위 이야기는 그 두 번째 층이다.
앞 글의 “컨테이너는 버려도 되는 물건”이 여기서 전제 조건이 된다. 파드가 뭔가를 자기 안에 쥐고 있었다면 되살리는 순간 그게 사라진다. 버려도 되게 만들어놨기 때문에 루프가 마음대로 죽이고 만들 수 있는 것이다.
앞에서 “살았는지 죽었는지 판단 기준이 필요하다”고 했던 물음이 여기서 실제 설정이 된다. 무엇을 보고 살았다고 할지, 그 기준을 얕게 잡을 때와 깊게 잡을 때 무엇이 갈리는지는 로드 밸런서의 헬스 체크와 정확히 같은 문제다.
주소는 바뀌는데 부르는 쪽은 그대로여야 한다
파드가 죽고 다시 뜨면서 IP가 바뀐다면, 그 파드를 부르던 쪽은 어떻게 되나. 주소를 적어뒀으면 그 주소는 이제 없는 곳을 가리킨다.
그래서 **서비스(Service)**가 있다. 파드 앞에 서서 바뀌지 않는 이름과 주소를 제공하고, 뒤에 실제로 어떤 파드들이 있는지는 알아서 갱신한다.
부르는 쪽은 늘 myapp을 부른다. 뒤에서 파드 C가 죽고 D가 새로 떠도 부르는 쪽 코드는 아무것도 안 바뀐다.
이것 역시 선언의 결과다. “이 이름은 이런 파드들을 가리킨다”를 적어두면, 실제 목록은 루프가 계속 맞춘다.
같은 원리로 다른 일도 딸려온다
여기까지가 “죽으면 살아난다”였다. 그런데 조정 루프를 이해하면, 쿠버네티스의 다른 기능들이 전부 같은 일의 변형이라는 게 보인다.
| 하고 싶은 일 | 실제로 하는 것 |
|---|---|
| 3개를 10개로 늘린다 | 원하는 상태를 10으로 고친다. 루프가 7개를 만든다 |
| 새 버전으로 올린다 | 원하는 이미지를 1.4.3으로 고친다. 루프가 파드를 차례로 교체한다 |
| 롤백한다 | 원하는 이미지를 1.4.2로 되돌린다. 같은 일이 반대로 일어난다 |
| 서버를 점검한다 | ”이 서버는 쓰지 마라”를 선언한다. 루프가 파드를 다른 곳으로 옮긴다 |
전부 사람은 원하는 상태만 고치고, 거기 도달하는 과정은 루프가 한다. 배포도 스케일링도 별개의 절차가 아니라 “선언을 바꾸는 것” 하나로 통일된다.
이 성질에는 부수 효과가 하나 더 있다. 원하는 상태가 파일에 적혀 있으니 그 파일을 버전 관리할 수 있다. 지금 클러스터가 어떤 모양인지 알고 싶으면 서버에 들어가 세어볼 필요 없이 저장소를 보면 된다. 앞 글에서 “어제 서버가 어떤 상태였는지 아무도 모른다”고 했던 문제가 여기서 닫힌다.
저장소에 적힌 것만으로 바꿀 때 닫힌다. 급하다고 명령 한 줄로 클러스터를 직접 고치면, 그 순간 저장소의 선언과 실제가 갈라진다. 파일은 3개라고 하는데 실제로는 10개인 상태가 되고, 이건 앞 글에서 본 그 문제가 이름만 바꿔 돌아온 것이다. 선언을 진실로 삼으려면 선언만 고쳐야 한다.
그래서 값과 대가가 무엇인가
여기까지 좋은 이야기만 했으니 값을 치르는 쪽도 봐야 한다.
얻는 것. 사람이 새벽에 일어나지 않는다. 서버 하나가 죽어도 서비스가 이어진다. 배포·스케일·롤백이 같은 방식으로 처리된다. 클러스터의 모양이 코드로 남는다.
치르는 것. 배워야 할 개념이 많고, 클러스터 자체를 누군가 운영해야 하며, 문제가 생겼을 때 어디를 봐야 하는지가 어렵다. 앱이 안 뜨는 이유가 앱에 있는지, 이미지에 있는지, 선언에 있는지, 네트워크에 있는지를 가려내야 한다. 층이 하나 늘어난 값이다.
그래서 실무의 판단은 대략 이렇게 갈린다.
- 서버 한두 대에 앱 하나라면 굳이 필요 없다. 컨테이너를 띄우고 재시작 정책을 거는 것으로 충분하다.
- 여러 서버에 여러 앱을 올리고, 배포가 잦고, 죽었을 때 사람이 안 깨어나야 한다면 값을 치를 만하다.
- 클러스터 운영 자체가 부담이면 관리형 서비스를 쓴다.
기술을 고르는 기준은 “요즘 다 쓴다”가 아니라 **“내가 지금 사람 손으로 무엇을 메우고 있나”**다. 메우고 있는 게 없으면 아직 필요 없는 것이다.
선언적 조정이라는 발상 자체는 쿠버네티스 밖에서도 쓸모가 있다. 인프라를 코드로 선언하는 도구(Terraform 등)도 “원하는 상태를 적고 차이를 메운다”라는 같은 모양이다. 도구 이름이 아니라 이 모양을 알아두면 새 도구를 만나도 금방 읽힌다.
정리
| 문제 | 죽은 컨테이너를 사람이 살리면, 사람이 자는 동안은 아무도 안 살린다 |
| 명령형 | 할 일을 시킨다. 두 번 하면 두 배가 된다 |
| 선언형 | 있어야 할 상태를 적는다. 몇 번을 적용해도 결과가 같다 |
| 조정 루프 | 원하는 상태와 실제 상태를 계속 비교해서 차이를 메운다 |
| 자가 치유 | 별도 기능이 아니라 그 루프의 부산물 |
| 되살아난 파드 | 고쳐진 게 아니라 새로 만들어진 다른 파드다 |
| 서비스 | 파드가 바뀌어도 부르는 이름은 안 바뀌게 해준다 |
| 대가 | 층이 하나 늘어난다. 사람 손으로 메우는 게 없으면 아직 필요 없다 |
쿠버네티스에서 가져갈 한 문장은 이거다. 원하는 상태를 적어두면, 거기 도달하는 일은 계속 도는 루프가 한다.
그런데 이렇게 안에서 파드가 뜨고 죽고 옮겨 다니는 동안, 사용자는 브라우저에 주소 하나를 칠 뿐이다. 그 요청은 443 포트로 오는데 우리 앱은 8080에서 돈다. 그 사이를 누가 이어주나. 다음 글이 앞단 이야기다.