3.5.1 선언적 API의 실제
우리가 쿠버네티스의 강력한 자동화와 자가 치유 능력의 비밀을 파헤치기 위한 여정의 첫 번째 관문은 바로 ‘선언적 API(Declarative API)’가 실제 쿠버네티스 환경에서 어떻게 구체적으로 구현되고 사용되는지를 이해하는 것입니다. 앞서 여러 차례 강조했듯이, 쿠버네티스는 우리가 시스템에게 “이렇게 해라, 저렇게 해라”라고 하나하나 절차를 지시하는 전통적인 ‘명령형(Imperative)’ 방식 대신, “나는 최종적으로 시스템이 이러한 모습이 되기를 원한다”라고 우리의 ‘의도(intent)’ 또는 ‘바람직한 최종 상태(Desired State)’를 명확하게 전달하는 ‘선언적(Declarative)’ 방식을 핵심 철학으로 채택하고 있습니다. 이는 마치 건축가에게 건물의 최종 설계도만을 전달하면, 시공 전문가들이 알아서 그 설계도대로 건물을 완성해 나가는 과정과도 유사합니다.
그렇다면, 우리는 쿠버네티스에게 이 ‘원하는 상태’를 과연 어떤 방식으로, 그리고 어떤 언어를 사용하여 전달할 수 있을까요? 바로 여기서 ‘쿠버네티스 오브젝트(Kubernetes Object)’ 또는 ‘쿠버네티스 리소스(Kubernetes Resource)’라는 핵심 개념과, 이를 기술하는 데 널리 사용되는 YAML(YAML Ain’t Markup Language) 형식의 매니페스트 파일, 그리고 이 파일들을 쿠버네티스 클러스터와 소통하는 데 사용되는 kubectl 커맨드 라인 도구가 등장합니다.
3.5.1.1.쿠버네티스 오브젝트(리소스)
쿠버네티스라는 거대한 시스템과 효과적으로 소통하고 우리가 원하는 바를 정확하게 전달하기 위해서는, 먼저 쿠버네티스가 이해할 수 있는 공통의 ‘언어’와 ‘개념’을 알아야 합니다. 쿠버네티스 세계에서는 우리가 관리하고 운영하고자 하는 모든 대상, 예를 들어 실행 중인 애플리케이션 인스턴스, 애플리케이션 그룹을 외부에 노출하는 네트워크 연결, 애플리케이션이 데이터를 영구적으로 저장하기 위한 스토리지 볼륨, 애플리케이션 실행에 필요한 환경 설정 정보, 심지어 클러스터 자체의 특정 정책이나 권한 설정에 이르기까지, 이 모든 것들을 ‘오브젝트(Object)’ 또는 때로는 동의어로 ‘리소스(Resource)’라는 잘 정의되고 추상화된 형태로 표현합니다.
이러한 쿠버네티스 오브젝트들은 단순히 개념적인 존재가 아니라, 쿠버네티스 시스템 내에서 실제로 지속적으로 유지되는 일종의 ‘기록(record)’ 또는 ‘엔티티(entity)’라고 생각할 수 있습니다. 이 기록들은 클러스터의 중앙 상태 저장소인 etcd에 저장되며, 이 기록들을 통해 우리는 클러스터의 현재 실제 상태(current state)를 정확하게 파악하고, 더 중요하게는 우리가 시스템에 바라는 ‘원하는 상태(desired state)’를 명시적으로 알릴 수 있는 매개체가 됩니다. 즉, 쿠버네티스 오브젝트는 사용자와 쿠버네티스 시스템 간의 핵심적인 의사소통 수단인 것입니다.
쿠버네티스에는 이미 다양한 운영 시나리오와 요구사항을 충족시키기 위해 수많은 종류의 내장(built-in) 오브젝트들이 미리 정의되어 있으며, 각 오브젝트는 특정 목적과 기능을 가지고 설계되었습니다. 마치 우리가 레고 블록을 조립할 때 다양한 모양과 크기, 그리고 용도를 가진 블록들을 사용하는 것처럼, 쿠버네티스에서도 이러한 다양한 오브젝트들을 조합하여 우리가 원하는 복잡한 애플리케이션 시스템을 구축하고 관리할 수 있습니다. 몇 가지 대표적이고 자주 사용되는 내장 오브젝트들의 예시와 그 핵심 역할은 다음과 같습니다.
- 파드(Pod): 쿠버네티스에서 배포 가능한 가장 작고 기본적인 단위입니다. 파드는 하나 이상의 밀접하게 연관된 컨테이너 그룹(예: 주 애플리케이션 컨테이너와 이를 보조하는 사이드카 컨테이너)과 이 컨테이너들이 공유하는 리소스(예: 고유한 네트워크 IP 주소, 포트 공간, 스토리지 볼륨)를 함께 캡슐화한 논리적인 호스트와 같습니다. 파드는 쿠버네티스가 관리하는 애플리케이션 인스턴스의 실행 환경 그 자체를 나타냅니다.
- 디플로이먼트(Deployment): 파드와 그 복제본 세트인 레플리카셋(ReplicaSet)의 선언적인 업데이트(stateless application update)를 관리하는 고수준 컨트롤러입니다. 사용자는 디플로이먼트를 통해 “내 애플리케이션의 특정 버전 컨테이너 이미지를 사용하여 항상 3개의 동일한 파드 복제본을 실행하고 싶다”와 같은 ‘원하는 상태’를 정의할 수 있습니다. 디플로이먼트는 롤링 업데이트(rolling update)나 롤백(rollback)과 같은 정교한 배포 전략을 자동화하여 서비스 중단 없이 안전하게 애플리케이션을 업데이트하는 데 핵심적인 역할을 합니다.
- 서비스(Service): 특정 레이블(label)을 가진 논리적인 파드 그룹에 대한 안정적이고 고정된 네트워크 접근점(endpoint)을 제공합니다. 파드의 IP 주소는 동적으로 변할 수 있지만, 서비스는 고유한 가상 IP 주소(클러스터 내부에서 유효한 ClusterIP)와 DNS 이름을 가지므로, 다른 애플리케이션이나 사용자는 이 서비스 주소를 통해 해당 파드 그룹에 항상 안정적으로 접근할 수 있습니다. 또한, 서비스는 여러 파드 인스턴스에 걸쳐 들어오는 네트워크 요청을 분산시켜 주는 기본적인 로드 밸런싱(load balancing) 기능도 수행합니다.
- 컨피그맵(ConfigMap): 애플리케이션 실행에 필요한 비민감성(non-confidential) 설정 정보(예: 환경 변수 값, 커맨드 라인 인수, 설정 파일의 내용 등)를 키-값(key-value) 쌍 형태로 저장하고 관리하는 데 사용됩니다. 이를 통해 애플리케이션 코드와 설정을 분리하여 관리의 유연성을 높일 수 있습니다.
- 시크릿(Secret): 데이터베이스 접속 비밀번호, API 키, TLS 인증서와 같이 민감한 정보(confidential data)를 안전하게 저장하고 관리하기 위한 오브젝트입니다. 컨피그맵과 유사한 방식으로 사용되지만, 데이터가 Base64로 인코딩되어 저장되며(이는 암호화는 아니지만, 일반 텍스트 노출을 최소화합니다), 쿠버네티스 클러스터 내에서 접근 제어에 더 많은 주의를 기울입니다.
- 네임스페이스(Namespace): 단일 물리적 쿠버네티스 클러스터 내에서 여러 사용자, 팀, 또는 프로젝트 간에 리소스를 논리적으로 분리하고 격리하는 가상 클러스터와 같은 역할을 합니다. 각 네임스페이스는 고유한 이름 공간을 가지므로, 서로 다른 네임스페이스에 동일한 이름의 리소스를 생성할 수 있습니다. 이는 멀티테넌시(multi-tenancy) 환경을 구축하거나, 개발, 테스트, 운영 환경을 분리하여 관리하는 데 유용합니다.
이 외에도, 상태 유지가 중요한 애플리케이션(예: 데이터베이스)을 위한 스테이트풀셋(StatefulSet), 클러스터의 모든 (또는 특정) 노드에 파드를 하나씩 실행시키는 데몬셋(DaemonSet), 일회성 배치 작업을 위한 잡(Job), 주기적인 스케줄링 작업을 위한 크론잡(CronJob), 클러스터 관리자가 프로비저닝한 영구 스토리지를 나타내는 퍼시스턴트볼륨(PersistentVolume), 그리고 사용자가 스토리지 자원을 요청하는 퍼시스턴트볼륨클레임(PersistentVolumeClaim) 등 실로 수많은 종류의 내장 오브젝트들이 존재하며, 각각의 오브젝트는 우리가 쿠버네티스에게 “무엇을 원하고, 시스템이 어떤 상태가 되기를 바라는지”를 구체적이고 명확하게 전달하는 데 사용되는 핵심적인 도구들입니다.
가장 중요한 점 중 하나는, 이렇게 다양한 종류의 오브젝트들이 존재함에도 불구하고, 모든 쿠버네티스 오브젝트들은 거의 동일하고 일관된 구조(예: apiVersion, kind, metadata, spec 필드)를 가지며, 표준화된 RESTful API를 통해 생성, 조회, 수정, 삭제(CRUD 연산)될 수 있다는 것입니다. 이는 쿠버네티스 시스템 전체의 복잡성을 관리하고, 사용자가 다양한 리소스를 일관된 방식으로 다룰 수 있도록 하는 데 매우 중요한 역할을 합니다. 마치 우리가 서로 다른 종류의 레고 블록이라도 동일한 방식으로 결합하고 분해할 수 있는 것과 같습니다. 이러한 일관성 덕분에 개발자와 운영자는 새로운 종류의 오브젝트를 배우고 활용하는 데 드는 학습 곡선을 크게 줄일 수 있으며, 다양한 도구들이 쿠버네티스 생태계와 쉽게 통합될 수 있는 기반이 마련됩니다.
3.5.1.2.YAML 매니페스트 파일
우리가 쿠버네티스 클러스터 내에서 특정 오브젝트, 예를 들어 새로운 애플리케이션 배포를 위한 ‘디플로이먼트(Deployment)’ 오브젝트를 생성하거나, 이미 실행 중인 오브젝트의 설정을 변경(업데이트)하고자 할 때, 우리는 해당 오브젝트가 최종적으로 가져야 할 ‘원하는 상태(Desired State)’를 매우 구체적이고 명확한 방식으로 기술하여 쿠버네티스 시스템에 전달해야 합니다. 이때 사용되는 가장 일반적이고 널리 권장되는 방법이 바로 YAML(YAML Ain’t Markup Language) 형식의 텍스트 파일을 작성하는 것입니다. 쿠버네티스는 JSON(JavaScript Object Notation) 형식도 완벽하게 지원하지만, YAML은 사람이 직접 읽고 작성하기에 더 간결하고 가독성이 뛰어나다는 장점 때문에 대부분의 경우 선호됩니다.
이 YAML 파일을 우리는 흔히 ‘매니페스트(Manifest) 파일’이라고 부릅니다. ‘매니페스트’라는 단어 자체가 ‘명백히 하다’, ‘분명히 나타내다’라는 의미를 가지고 있듯이, 이 파일은 우리가 쿠버네티스에게 “나는 시스템이 이러이러한 모습이 되기를 원한다”라는 우리의 의도를 명백하게 선언하는 문서입니다. 이는 마치 특정 건축물을 짓기 전에 건축가가 건물의 구조, 각 층의 용도, 사용될 자재, 창문의 크기와 위치 등 모든 세부 사항을 꼼꼼하게 기록한 상세한 ‘설계도’ 또는 ‘청사진’과도 같은 역할을 합니다. 쿠버네티스는 이 YAML 매니페스트 파일을 입력으로 받아, 그 안에 기술된 ‘원하는 상태’를 현실로 만들기 위해 필요한 모든 작업을 수행하게 됩니다.
놀랍게도, 쿠버네티스에서 다루는 수많은 종류의 서로 다른 오브젝트들(파드, 디플로이먼트, 서비스, 컨피그맵, 시크릿 등)은 그 목적과 기능은 각기 다르지만, 이들을 정의하는 YAML 매니페스트 파일은 일관되고 표준화된 기본 구조를 따르고 있습니다. 모든 쿠버네티스 오브젝트의 YAML 매니페스트 파일은 일반적으로 다음과 같은 네 가지 최상위 필수 필드를 공통적으로 가집니다. 이 네 가지 필드는 마치 모든 공식 문서의 표지와 제목처럼, 해당 문서가 무엇에 관한 것이고 어떤 규격을 따르는지를 명확히 알려주는 역할을 합니다.
- apiVersion (API 버전):이 필드는 해당 오브젝트를 생성하고 관리하는 데 사용할 쿠버네티스 API(Application Programming Interface)의 버전을 명시합니다. 쿠버네티스 API는 시간이 지남에 따라 새로운 기능이 추가되거나 기존 기능이 변경되면서 여러 버전으로 발전해 왔습니다. (예: v1, apps/v1, batch/v1, networking.k8s.io/v1 등). 각 API 버전은 특정 API 그룹은 핵심 API 그룹은 그룹 이름 없이 v1로 표시, 앱 관련 API는 apps 그룹, 배치 작업 관련 API는 batch 그룹등 에 속하며, 해당 버전에서 지원하는 오브젝트의 종류와 명세(spec) 형식을 정의합니다. apiVersion 필드를 통해 우리는 쿠버네티스에게 “나는 이 YAML 파일에 기술된 오브젝트를 이러이러한 API 규격에 맞춰 해석하고 처리해 주십시오”라고 알려주는 것입니다. kubectl api-versions 명령을 통해 현재 클러스터에서 사용 가능한 모든 API 버전 목록을 확인할 수 있습니다.
- kind (오브젝트 종류):이 필드는 우리가 YAML 파일을 통해 생성하거나 관리하고자 하는 쿠버네티스 오브젝트의 구체적인 ‘종류’를 명시합니다. 예를 들어, 파드를 만들고 싶다면 Pod, 디플로이먼트를 만들고 싶다면 Deployment, 서비스를 만들고 싶다면 Service, 설정을 관리하고 싶다면 ConfigMap과 같이 해당 오브젝트의 종류를 정확하게 지정해야 합니다. 쿠버네티스는 이 kind 필드의 값을 보고, 해당 종류의 오브젝트를 처리하는 적절한 컨트롤러에게 작업을 위임하게 됩니다. kubectl api-resources 명령을 통해 현재 클러스터에서 사용 가능한 모든 리소스 종류(kind)와 그 약어, API 그룹 등을 확인할 수 있습니다.
- metadata (메타데이터):이 필드는 해당 오브젝트 자체에 대한 부가 정보, 즉 ‘메타데이터(metadata)’를 정의하는 섹션입니다. 메타데이터는 오브젝트를 식별하고, 분류하며, 다른 도구나 시스템이 해당 오브젝트를 참조하거나 관리하는 데 필요한 다양한 정보를 포함합니다. metadata 섹션에는 주로 다음과 같은 하위 필드들이 포함됩니다.
- name: 오브젝트의 고유한 이름을 지정합니다. 이 이름은 해당 오브젝트가 속한 네임스페이스(namespace) 범위 내에서 유일해야 하며, kubectl 명령어로 특정 오브젝트를 지칭할 때 사용됩니다. (예: my-nginx-deployment)
- namespace (선택 사항): 오브젝트가 속할 네임스페이스를 지정합니다. 네임스페이스는 단일 클러스터 내에서 리소스를 논리적으로 분리하고 격리하는 데 사용됩니다. 만약 이 필드를 생략하면, 기본적으로 default 네임스페이스에 오브젝트가 생성됩니다.
- labels: 오브젝트를 식별하고 그룹화하며, 다른 오브젝트(예: 서비스가 파드를 선택할 때)와 연결하는 데 사용되는 키-값(key-value) 쌍의 레이블을 하나 이상 정의할 수 있습니다. 레이블은 쿠버네티스에서 매우 중요한 역할을 하며, 유연한 리소스 관리를 가능하게 합니다. (예: app: my-nginx, environment: production)
- annotations: 레이블과 유사하게 키-값 쌍으로 정의되지만, 주로 식별 목적이 아닌, 도구나 시스템이 사용할 수 있는 임의의 비식별 메타데이터(non-identifying metadata)를 추가하는 데 사용됩니다. 예를 들어, 빌드 버전 정보, 생성 도구 이름, 설명 문구 등을 어노테이션으로 추가할 수 있습니다.
- spec (명세 또는 스펙):이 필드는 해당 쿠버네티스 오브젝트의 ‘원하는 상태(Desired State)’를 구체적으로 기술하는 가장 중요한 부분입니다. 즉, “이 오브젝트가 앞으로 어떤 모습이어야 하고, 어떻게 작동해야 하는가”에 대한 모든 상세한 정의가 이 spec 섹션 안에 담기게 됩니다.매우 중요한 점은, 이 spec 필드의 내용과 구조는 바로 앞서 kind 필드에서 지정한 오브젝트의 종류에 따라 완전히 달라진다는 것입니다. 각 오브젝트 종류마다 자신이 관리해야 할 상태 정보와 설정 항목들이 고유하게 정의되어 있기 때문입니다.
- 예를 들어, kind: Deployment인 디플로이먼트 오브젝트의 spec에는 애플리케이션의 원하는 복제본 수(replicas), 어떤 컨테이너 이미지를 사용하여 파드를 만들 것인지에 대한 상세한 정의가 담긴 파드 템플릿(template), 그리고 애플리케이션을 업데이트할 때 사용할 전략(예: strategy: { type: RollingUpdate, rollingUpdate: { maxSurge: “25%”, maxUnavailable: “25%” } }) 등이 포함됩니다.
- 반면, kind: Service인 서비스 오브젝트의 spec에는 어떤 레이블을 가진 파드들로 트래픽을 전달할 것인지를 정의하는 셀렉터(selector), 서비스가 노출할 포트 정보(ports), 그리고 서비스를 외부로 어떻게 노출할 것인지를 결정하는 서비스 타입(type, 예: ClusterIP, NodePort, LoadBalancer) 등이 포함됩니다.이처럼, 각 오브젝트의 spec 필드를 정확하게 이해하고 작성하는 것이 쿠버네티스를 효과적으로 사용하는 핵심이라고 할 수 있습니다. (각 오브젝트별 spec 필드의 상세한 내용은 이후 각 오브젝트를 다루는 장에서 자세히 설명될 것입니다.)
예를 들어, 우리가 앞서 여러 번 예시로 들었던 “Nginx 웹 서버를 3개의 동일한 복제본으로 실행하는 디플로이먼트”를 정의하는 YAML 파일은 바로 이러한 네 가지 최상위 필드와 각 필드에 맞는 하위 구조를 정확하게 따르고 있습니다. 개발자나 운영자는 이 YAML 파일이라는 ‘코드’를 통해, “나는 nginx:1.25 컨테이너 이미지를 사용하는 파드를 항상 3개 실행하고 싶고, 각 파드는 이러이러한 자원을 사용하며, 특정 레이블을 가져야 한다”라는 자신의 ‘의도’ 또는 ‘원하는 상태’를 쿠버네티스 시스템에게 매우 명확하고 구조화된 방식으로 표현하는 것입니다.
이처럼 YAML 매니페스트 파일은 쿠버네티스와 소통하고 우리가 원하는 대로 시스템을 만들어가기 위한 가장 기본적인 약속이자, 우리가 그리는 미래 시스템의 모습을 담은 디지털 청사진이라고 할 수 있습니다. 이러한 선언적인 명세 파일을 작성하고 관리하는 기술은 쿠버네티스를 다루는 데 있어 필수적인 역량이며, 이를 통해 우리는 복잡한 분산 시스템을 마치 코드를 작성하듯이 체계적으로 정의하고 운영할 수 있게 됩니다. 다음으로는 이렇게 작성된 YAML 파일을 kubectl이라는 도구를 통해 어떻게 실제 쿠버네티스 클러스터에 적용하고 상호작용하는지 살펴보겠습니다.
3.5.1.3.kubectl 커맨드 라인 도구
우리가 정성스럽게 YAML 매니페스트 파일이라는 ‘설계도’에 쿠버네티스 클러스터가 최종적으로 도달해야 할 ‘원하는 상태(Desired State)’를 상세하고 명확하게 기술했다면, 이제 다음 단계는 이 우리의 의지를 실제 쿠버네티스 클러스터에 전달하고, 그 결과로 시스템이 변경되도록 반영시키는 작업입니다. 마치 잘 작성된 편지를 우체통에 넣어야 상대방에게 전달되듯이, 우리의 YAML 파일도 쿠버네티스 시스템에게 전달되어야 그 의미를 발휘할 수 있습니다.
이때 사용되는 가장 기본적이면서도 핵심적인 도구가 바로 kubectl(보통 ‘큐브컨트롤’, ‘큐브컷’, 또는 ‘큐브씨티엘’ 등으로 발음합니다)이라는 강력한 커맨드 라인 인터페이스(Command Line Interface, CLI) 도구입니다. kubectl은 쿠버네티스 클러스터의 중앙 관제탑이자 모든 통신의 관문 역할을 하는 ‘API 서버(API Server)’와 직접적으로 통신하여, 사용자가 정의한 다양한 쿠버네티스 리소스(오브젝트)를 생성(Create), 조회(Read), 업데이트(Update), 삭제(Delete)하는 등(이러한 기본 작업을 흔히 CRUD 연산이라고 합니다) 클러스터를 관리하고 운영하는 데 필요한 거의 모든 종류의 작업을 수행할 수 있는 그야말로 ‘만능 도구(Swiss Army knife)’와도 같습니다.
개발자나 운영자는 자신의 로컬 머신이나 관리 서버의 터미널에서 이 kubectl 명령어를 사용하여, 원격에 있는 쿠버네티스 클러스터를 마치 자신의 손안에 있는 것처럼 제어하고 상태를 확인할 수 있습니다. (물론, kubectl이 API 서버와 정상적으로 통신하기 위해서는 Kubeconfig 파일이라는 특별한 설정 파일이 올바르게 구성되어 있어, 클러스터의 위치, 사용자 인증 정보 등을 알고 있어야 합니다. 이는 이후 실습 과정에서 자세히 다루게 될 것입니다.)
그렇다면, 우리가 작성한 YAML 매니페스트 파일을 사용하여 쿠버네티스 클러스터에 ‘원하는 상태’를 전달하고 적용하는 가장 일반적이고 권장되는 kubectl 명령어는 무엇일까요? 그것은 바로 kubectl apply -f 입니다. 이 명령어의 각 부분을 좀 더 자세히 살펴보겠습니다.
kubectl apply -f <filename.yaml>
- kubectl apply:apply라는 하위 명령어는 ‘적용하다’라는 의미 그대로, 지정된 리소스 정의(YAML 파일에 기술된 내용)를 현재 클러스터의 상태에 ‘적용’하라는 지시입니다. 이 apply 명령어는 매우 지능적이고 선언적인 방식으로 작동합니다.
- 만약 YAML 파일에 기술된 이름과 종류의 오브젝트가 현재 클러스터에 존재하지 않는다면, apply는 해당 오브젝트를 새롭게 생성합니다.
- 만약 해당 오브젝트가 이미 클러스터에 존재한다면, apply는 제출된 YAML 파일의 내용과 현재 클러스터에 저장된 해당 오브젝트의 실제 상태를 비교합니다. 그리고 만약 차이점이 발견된다면, 변경된 부분만을 현재 클러스터의 상태에 업데이트하여 YAML 파일에 정의된 ‘원하는 상태’와 일치시키려고 시도합니다. 이 과정이 바로 ‘선언적 업데이트(declarative update)’의 핵심입니다. 이미 원하는 상태라면 불필요한 변경을 시도하지 않는다는 점(멱등성, Idempotence)도 중요한 특징입니다. 과거에는 kubectl create로 생성하고 kubectl replace나 kubectl patch로 수정하는 명령형 방식도 많이 사용되었지만, 최근에는 대부분의 경우 kubectl apply를 사용하는 것이 권장됩니다. apply는 생성과 업데이트를 모두 처리할 수 있어 훨씬 편리하고 일관성이 높기 때문입니다.
- f <filename.yaml> 또는 –filename <filename.yaml>:
- f (또는 더 명시적인 형태인 –filename) 옵션은 kubectl apply 명령어에게 리소스 정의를 어떤 소스로부터 읽어올 것인지를 알려주는 역할을 합니다. 이 경우 <filename.yaml>은 우리가 앞서 작성한 YAML 매니페스트 파일의 실제 경로와 이름을 의미합니다. kubectl은 이 파일의 내용을 읽어들여 파싱하고, 그 정보를 API 서버에 전달하게 됩니다. 이 -f 옵션은 단일 파일뿐만 아니라, 특정 디렉터리 내의 모든 YAML 파일(.yaml 또는 .yml 확장자)을 한 번에 적용하도록 지정할 수도 있습니다. (예: kubectl apply -f ./my-manifests-directory/)
예를 들어, 우리가 my-nginx-deployment.yaml이라는 파일에 앞서 예시로 들었던 Nginx 디플로이먼트와 관련 서비스의 정의를 모두 포함하여 작성했다고 가정해 봅시다. 이제 터미널에서 다음과 같이 명령을 실행하는 순간, 마법과 같은 일들이 벌어지기 시작합니다.
이 명령이 실행되면, kubectl은 먼저 my-nginx-deployment.yaml 파일의 내용을 읽어들여 유효한 YAML 형식인지, 그리고 쿠버네티스 오브젝트의 기본 구조(apiVersion, kind, metadata, spec)를 따르고 있는지 등을 내부적으로 검증합니다. 문제가 없다면, 이 YAML 파일의 내용을 직렬화하여(보통 JSON 형태로 변환됩니다) 쿠버네티스 API 서버에 HTTP(S) 요청 형태로 전달합니다.
API 서버는 이 요청을 수신하면, 먼저 요청한 사용자가 해당 작업을 수행할 권한이 있는지 확인하고(인증 및 인가 과정), 전달된 오브젝트 정의의 유효성을 다시 한번 검증합니다. 모든 검증 과정을 통과하면, API 서버는 이 새로운 ‘원하는 상태’ 정보를 클러스터의 신뢰할 수 있는 중앙 상태 저장소인 etcd에 안전하게 기록합니다.
그러면 잠시 후, 쿠버네티스 컨트롤 플레인 내부에서 각자의 임무를 수행하고 있던 다양한 컨트롤러들(예: 디플로이먼트 컨트롤러, 레플리카셋 컨트롤러, 서비스 컨트롤러 등)은 API 서버를 통해 이 새로운 ‘원하는 상태’가 등록되었거나 기존 상태가 변경되었다는 것을 감지하게 됩니다(보통 API 서버의 watch 기능을 통해 실시간으로 알림을 받습니다). 그리고 각 컨트롤러는 자신이 책임지고 있는 리소스에 대해 조정 루프(reconciliation loop)를 시작하여, 현재 클러스터의 실제 상태를 이 새로운 ‘원하는 상태’와 일치시키기 위한 필요한 작업들(예: 새로운 레플리카셋 생성, 스케줄러를 통한 파드 배치 요청, 서비스 엔드포인트 설정, 관련 이벤트 기록 등)을 자동으로, 그리고 지능적으로 수행하기 시작합니다. 이 모든 과정은 마치 잘 짜인 교향곡처럼 여러 컴포넌트들이 조화롭게 협력하며 진행됩니다.
kubectl 다양한 하위 명령(subcommand)
kubectl은 이처럼 apply 명령을 통해 우리의 ‘원하는 상태’를 클러스터에 전달하는 핵심적인 역할 외에도, 클러스터의 현재 상태를 확인하고, 실행 중인 애플리케이션과 상호작용하며, 문제를 해결하는 데 필요한 다양한 하위 명령(subcommand)들을 풍부하게 제공합니다. 몇 가지 자주 사용되는 예시는 다음과 같습니다.
- kubectl get [resource-name] [-n namespace] [-o wide|yaml|json]: 특정 종류의 리소스 목록이나 특정 리소스의 간략한 정보를 조회합니다. (예: kubectl get pods, kubectl get deployment my-nginx-app -n my-namespace -o yaml)
- kubectl describe [-n namespace]: 특정 리소스의 현재 상태, 관련 설정, 그리고 가장 중요한 최근 이벤트(Events) 목록 등 매우 상세한 정보를 보여줍니다. (클러스터나 애플리케이션에 문제가 발생했을 때 원인을 파악하는 데 가장 먼저 사용되는 매우 유용한 명령어입니다.)
- kubectl logs [-c container-name] [-n namespace] [-f] [–previous]: 특정 파드 내의 특정 컨테이너(또는 파드 내 컨테이너가 하나뿐이면 생략 가능)가 출력하는 로그를 확인합니다. -f 옵션은 실시간으로 로그를 스트리밍하고, –previous 옵션은 컨테이너가 재시작된 경우 이전 컨테이너 인스턴스의 로그를 보여줍니다.
- kubectl exec -it [-c container-name] [-n namespace] —
[args…]: 실행 중인 컨테이너 내부에서 지정된 명령을 실행합니다. -it 옵션은 대화형 터미널 세션을 열어 마치 SSH로 접속한 것처럼 컨테이너 내부를 탐색하고 디버깅할 수 있게 해줍니다. (예: kubectl exec -it my-nginx-pod — /bin/bash) - kubectl delete -f 또는 kubectl delete [-n namespace]: YAML 파일에 정의된 리소스 또는 이름으로 지정된 특정 리소스를 클러스터에서 삭제합니다.
이처럼, 쿠버네티스에서 선언적 API는 사용자가 YAML과 같은 표준화된 형식으로 ‘원하는 상태’를 명확하게 기술하고, kubectl과 같은 도구를 통해 이를 쿠버네티스 API 서버에 전달함으로써 시스템과 상호작용하는 핵심적인 방식입니다. 우리는 더 이상 시스템에게 “어떻게 하라”고 미시적인 절차를 지시하는 대신, “나는 이런 결과를 원한다”고 우리의 의도를 선언하기만 하면 됩니다. 그러면 쿠버네티스 내부의 지능적인 메커니즘이 알아서 그 의도를 현실로 만들어주는 것입니다. 이는 마치 우리가 자동차 운전석에 앉아 목적지를 내비게이션에 입력하면, 자동차가 스스로 경로를 계산하고 주행하는 자율 주행 기술과도 유사한 경험을 제공합니다. 이러한 선언적 접근 방식이야말로 쿠버네티스가 그토록 강력한 자동화와 안정성을 제공할 수 있는 근본적인 이유이며, 클라우드 네이티브 시대를 살아가는 우리에게 필수적으로 익혀야 할 핵심 개념이라고 할 수 있습니다. 다음으로는 이러한 선언된 ‘원하는 상태’를 실제 ‘현재 상태’와 일치시키기 위해 쿠버네티스 내부에서 끊임없이 작동하는 ‘조정 루프’의 비밀을 파헤쳐 보겠습니다.
