[글쓴이:] 라떼쉬폰

  • Terraform에서 MetalLB 설정 시 유의사항

    Terraform의 planapply 전략 상 k8s 클러스터에 설치되어 있지 않은 CRD (CustomResourceDefinitions)를 포함하는 kubernetes_manifest를 사용하는 경우 helm_release로 선행하여 helm chart를 설치하도록 하더라도 CRD를 인식하지 않아 에러가 발생한다.

    Terraform repo에 이미 이슈로 등록되어 있지만 해결해줄 의지가 없는 것으로 보인다.

    문제가 해결되기 전까지는 Helm Chart를 테라포밍하지 말고, k8s 클러스터에 수동으로 설치해주어야 문제가 발생하지 않는다.

    # resource "helm_release" "metallb" {
    #   name             = "metallb"
    #   namespace        = "metallb-system"
    #   create_namespace = true
    
    #   repository = "https://metallb.github.io/metallb"
    #   chart      = "metallb"
    #   version    = "0.14.9"
    
    #   skip_crds = false
    # }
    
    resource "kubernetes_manifest" "metallb_ip_pool" {
    #   depends_on = [helm_release.metallb]
    
      manifest = {
        apiVersion = "metallb.io/v1beta1"
        kind       = "IPAddressPool"
        metadata   = {
          name      = "default-address-pool"
          namespace = "metallb-system"
        }
        spec       = {
          addresses = var.ip_pool_addresses
        }
      }
    }
    
    resource "kubernetes_manifest" "metallb_advertisement" {
    #   depends_on = [helm_release.metallb]
    
      manifest = {
        apiVersion = "metallb.io/v1beta1"
        kind       = "L2Advertisement"
        metadata   = {
          name      = "l2"
          namespace = "metallb-system"
        }
      }
    }

    K8s 클러스터에 다음과 같이 직접 설치하거나 Helm Chart로 설치한다.

    kubectl create namespace metallb-system
    kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml

  • K8S 클러스터 구성

    설명은 추후 작성 예정.

    sudo systemctl enable ssh
    sudo systemctl start ssh
    
    sudo apt update && sudo apt upgrade
    
    sudo apt-get install -y ca-certificates curl
    
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    echo   "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
       $(. /etc/os-release && echo "$VERSION_CODENAME") stable" |   sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    
    sudo dphys-swapfile swapoff
    sudo systemctl disable dphys-swapfile
    
    sudo apt-get install -y apt-transport-https gpg
    
    curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
    echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
    sudo apt-get update
    sudo apt-get install -y kubelet kubeadm kubectl
    sudo apt-mark hold kubelet kubeadm kubectl
    
    sudo vi /boot/firmware/cmdline.txt
    # cgroup_enable=memory cgroup_memory=1 을 앞에 추가
    
    sudo containerd config default | sudo tee /etc/containerd/config.toml
    sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
    sudo systemctl restart containerd
    sudo systemctl restart kubelet
    
    sudo modprobe br_netfilter
    cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
    br_netfilter
    EOF
    
    cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
    net.bridge.bridge-nf-call-ip6tables = 1
    net.bridge.bridge-nf-call-iptables = 1
    EOF
    
    sudo sysctl --system
    
    sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --v=5
    kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

  • Jenkins에서 서브모듈이 포함된 git 저장소의 Github SSH 인증 방식 적용 시 유의사항

    git 저장소에 서브모듈이 포함된 경우 root 디렉토리에 .gitmodules 파일이 생성되어 있다. https 방식의 인증을 사용하는 머신에서 서브모듈을 설정하는 경우에 다음과 같이 서브모듈의 원격 저장소 주소도 https 프로토콜을 이용하도록 설정된다.

    .gitmodules 파일에 포함되어 있는 서브모듈 설정

    다만, 최근 github의 인증 보안이 강화되면서 https 방식의 credential을 이용해서는 jenkins에서 private 저장소에 대한 접근이 어렵다. 따라서 이 경우에는, jenkins에서 사용하던 https 방식의 github credential을 ssh 방식으로 전환해야 한다.

    Jenkins의 github credential 설정을 바꾸는 것은 어렵지 않은 일이지만, 서브모듈이 포함된 프로젝트가 배포 파이프라인에 포함된 경우에는 권한 오류 문제가 발생할 수 있음을 기억해야 한다.

    권한 문제로 서브모듈을 가져오지 못하는 경우에는 .gitmodules 파일에 기록된 서브모듈들의 원격 저장소 url을 ssh 프로토콜을 이용하도록 다음과 같이 수정해야 한다.

    [submodule "submodule_name"]
            path = submodule_path_in_root_directory
            url = git@github.com:username/repository-name.git

  • 석사 전문연구요원 편입 완료

    올해 초에 CS 분야 석사 학위를 받고, 졸업 후에 계약직 연구원으로 월급을 받으면서 연구 과제에 참여했다. 아무래도 전문연을 해야하는 입장에서 바로 회사로 가는 것이 베스트이지만, 그러기에는 내가 너무 지쳐있었다. 3~4월의 나는 정말 아무것도 하고 싶지 않은 번아웃 상태였다.

    연구원으로 일하면서 엄청난 워라밸을 방어하며 충분한 시간을 가지고 취업 준비를 했는데, 전문연을 해야 하는 사정을 고려해주신 지도교수님이 있었기에 가능했다. 지금도 정말 감사한 마음이다.

    역시 번아웃에는 쉬는 것이 최고다. 충분한 휴식 기간을 거치고 나는 6월 중순부터 두 페이지 가량의 이력서를 쓰고 벡엔드 개발 직무로 구직을 시작했다.

    알다시피 전문연 업체는 중소, 중견기업 위주이고, 괜찮은 업체보다는 SI, SM 등의 사업을 영위하는 소위 커리어 망칠 가능성이 높은 회사들이 많다. 특히, 메인 비즈니스 모델 없이 정부과제 사업만으로 굴러가는 곳은… 굳이 연구실 밖에서 그걸 경험하고 싶지는 않았다.

    요즘 CS 분야는 AI 전공이면 구인 공고가 꽤 많아서 좋은 회사를 갈 수 있는 스펙트럼이 비교적 넓어지지만, 그 외의 개발 직무(백엔드, 프론트엔드, AOS/iOS, 시스템 등)는 생각보다 그렇지 못하다. 처우가 열악한 회사들이 득실득실하다. 아무리 대체복무라지만 이상한 회사에 들어가서 커리어를 망치면 1년 6개월 현역으로 입대하느니만 못하다. 게다가 전문연이라고 연봉까지 후려치는 그런 곳이라면… 최악이다.

    혹시 AI 외의 개발 직무로 석사 전문연을 하실 계획이 있으신 분이라면 꼭 미리미리 구직 활동을 하시는 게 좋고, 웬만하면 SI, SM 위주의 비즈니스를 영위하는 회사는 본인의 커리어를 감안해서라도 다시 한 번 생각해보시는 게 좋을 것 같다. 정말 입대 연기가 불가능할 만큼 긴급한 상황이 아니라면…

    구직 활동 끝에 최근 내 기준에 부합하는 한 스타트업에 입사하였으며, 드디어 전문연으로의 편입이 완료되어 9월부터 3년간의 복무를 시작했다. 2024년이라니.. 까마득하긴 하다.

    병무청에서 날아온 반가운 문자

    입사 후 업무 적응하느라 자기계발을 하지 못하고 있었는데, github 잔디가 싱크홀이 뚫린 것 마냥 비워지는 걸 더 이상 두고봐서는 안될 것 같다. TIL 글이라도 써서 흔적을 남기려고 노력해야지. 블로그에 글도 열심히 남기고.

    계약 연봉이 요즘 핫한 IT 회사들에 비해서는 엄청 만족스러운 수준은 아니지만, 수평적인 문화를 가진 자유로운 분위기의 스타트업이어서 충분히 괜찮은 시작이라고 생각한다. 특히, 자사 솔루션 기술을 이용해 B2C 기반의 플랫폼으로 비즈니스 모델을 전환 중이어서 방향성도 마음에 든다. 무엇보다도 현역으로 복무하지 않고 내 분야의 커리어를 이어갈 수 있다는 것만으로도 이미 굉장한 혜택을 받았다고 느낀다.

    앞으로 꾸준히 자기계발을 이어나가면서 회사 생활도 열심히 해 볼 생각이다. 내년 이맘때의 내가 이 글을 보면서 후회하는 일이 없도록 매일매일 치열하게 살아야지.

  • gRPC 인터셉터 구현 – 클라이언트 (Java)

    지난 글에서 이어서 이번 글에서 클라이언트에서의 인터셉터 구현을 살펴보겠습니다.

    gRPC 인터셉터 – 클라이언트

    클라이언트에서의 gRPC 인터셉터도 이전 글에서 살펴본 서버와 동작 원리 및 구현 방법이 비슷합니다. 마찬가지로 라이브러리의 구조를 살펴보면서 gRPC 인터셉터를 구현해보도록 하겠습니다.

    (1) ClientCall.Listener<RespT> 정적 추상 클래스를 상속받는 클래스를 생성합니다.

    ClientCall.Listener<RespT>는 ClientCall 추상 클래스 안에 선언된 정적 추상 이너 클래스로, ServerCall.Listener와 유사합니다. 라이브러리에 선언된 ClientCall.Listener<RespT> 추상 클래스는 다음과 같으며, 모두 4개의 메서드가 정의되어 있습니다. onHeaders 메서드는 모든 RPC에 대해 서버로부터 응답 메시지가 처음 수신할 때 한 번만 호출되며, onMessage 메서드는 Stream RPC인 경우에도 서버로부터 응답 메시지를 수신받을 때마다 호출됩니다.

    public abstract class ClientCall<ReqT, RespT> {
    
      public abstract static class Listener<T> {
    
        public void onHeaders(Metadata headers) {}
        public void onMessage(T message) {}
        public void onClose(Status status, Metadata trailers) {}
        public void onReady() {}
      }
      ...
    }

    ClicentCall.Listener<RespT>는 이전에 살펴본 ServerCall.Listener<ReqT>와 같이 추상 클래스이므로 모든 메서드에 대한 오버라이딩이 필요합니다. 마찬가지로, 라이브러리에서는 delegate 패턴을 이용하여 다음과 같은 상속 관계를 가지는 클래스들을 제공합니다.

    • (abstract) ClientCall.Listener<RespT>
      • (abstract) PartialForwardingClientCallListener<RespT>
        • (abstract) ForwardingClientCallListener<RespT>
          • (abstract) ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>

    따라서 ClientCall.Listener<RespT> 추상 클래스 대신 ForwardingClientCallListener<RespT> 추상 클래스를 상속받아 구현하면 됩니다. 여기서는 ForwardingClientCallListener 클래스의 이너 클래스로 구현되어 있는 SimpleForwardingClientCallListener을 이용하여 간단하게 구현합니다.

    이제 ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT> 추상 클래스를 상속받는 UserClientCallListener을 다음과 같이 구현합니다. onHeaders 메서드에 대해서 오버라이딩하고, delegate 패턴에 대한 구현도 추가합니다. 서버로부터 응답 메시지가 처음 도달하면 표준 출력이 콘솔에 나타나도록 구현했습니다.

    // UserClientCallListener.java
    package com.lattechiffon.grpc;
    
    import io.grpc.ClientCall;
    import io.grpc.ForwardingClientCallListener;
    import io.grpc.Metadata;
    
    public class UserClientCallListener<RespT> extends ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT> {
        protected UserClientCallListener(ClientCall.Listener<RespT> delegate) {
            super(delegate);
        }
    
        @Override
        public void onHeaders(Metadata headers) {
            System.out.println("[Client Call Listener] client header : " + headers);
            super.onHeaders(headers);
        }
    }

    (2) ClientCall<ReqT, RespT> 추상 클래스를 상속받는 클래스를 생성합니다.

    ClientCall<ReqT, RespT> 추상 클래스에는 gRPC 클라이언트에서의 메시지 송수신을 처리하는 메서드들이 정의되어 있습니다. 과정 (1)에서 살펴본 ClientCall.Listener<RespT> 클래스도 ClientCall<ReqT, RespT> 추상 클래스의 이너 클래스입니다. ClientCall 클래스에 선언된 sendMessage 메서드를 이용하여 서버로 요청 메시지를 전송할 수 있으며, 이 메서드를 오버라이딩하여 요청 메시지를 서버로 전달하기 전에 수행해야 할 사전 작업을 정의할 수 있습니다.

    package io.grpc;
    
    import javax.annotation.Nullable;
    
    public abstract class ClientCall<ReqT, RespT> {
    
      public abstract static class Listener<T> { ... }
    
      public abstract void start(Listener<RespT> responseListener, Metadata headers);
      public abstract void request(int numMessages);
      public abstract void cancel(@Nullable String message, @Nullable Throwable cause);
      public abstract void halfClose();
      public abstract void sendMessage(ReqT message);
    
      public boolean isReady() {
        return true;
      }
      ...
    }

    ClientCall<ReqT, RespT>는 Listener와 마찬가지로 추상 클래스이므로, 모든 메서드에 대한 오버라이딩이 필요합니다. 라이브러리에서는 delegate 패턴을 이용하여 다음과 같은 상속 관계를 가지는 클래스들을 제공합니다.

    • (abstract) ClientCall<ReqT, RespT>
      • (abstract) PartialForwardingClientCall<ReqT, RespT>
        • (abstract) ForwardingClientCall<ReqT, RespT>
          • (abstract) ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>

    따라서 ClientCall<ReqT, RespT> 추상 클래스 대신 ForwardingClientCall<ReqT, RespT> 추상 클래스를 상속받아 구현하면 됩니다. 여기서는 ForwardingClientCall 클래스의 이너 클래스로 구현되어 있는 SimpleForwardingClientCall을 이용하여 간단하게 구현합니다.

    이제 ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT> 추상 클래스를 상속받는 UserClientCall을 다음과 같이 정의합니다. start와 sendMessage 메서드에 대해서 오버라이딩하고, delegate 패턴에 대한 구현도 추가합니다. 새로운 요청 메시지를 서버로 전달하기 전에 표준 출력이 콘솔에 나타나도록 구현했습니다.

    // UserClientCall.java
    package com.lattechiffon.grpc;
    
    import io.grpc.ClientCall;
    import io.grpc.ForwardingClientCall;
    import io.grpc.Metadata;
    
    public class UserClientCall<ReqT, RespT> extends ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT> {
        protected UserClientCall(ClientCall<ReqT, RespT> delegate) {
            super(delegate);
        }
    
        @Override
        public void start(Listener<RespT> responseListener, Metadata headers) {
            super.start(new UserClientCallListener<>(responseListener), headers);
        }
    
        @Override
        public void sendMessage(ReqT message) {
            System.out.println("[Client Call] Client Send Message : " + message);
            super.sendMessage(message);
        }
    }
    

    (3) ClientInterceptor 인터페이스의 구현 클래스를 생성합니다.

    마지막으로 ClientInterceptor 인터페이스를 살펴보겠습니다. ServerInterceptor 인터페이스와 마찬가지로 인터페이스의 구조는 단순합니다. 구현 클래스는 interceptCall이라는 하나의 메서드만 구현하면 됩니다. 세 개의 파라미터와 하나의 반환은 다음의 역할을 가집니다. 서버와는 그 구성이 다소 다릅니다.

    • MethodDescriptor<ReqT, RespT> method:
    • CallOptions callOptions:
    • Channel next:
    • ClientCall<ReqT, RespT> (return):

    package io.grpc;
    
    import javax.annotation.concurrent.ThreadSafe;
    
    @ThreadSafe
    public interface ClientInterceptor {
      <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
          MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next);
    }

    // UserClientInterceptor.java
    package com.lattechiffon.grpc;
    
    import io.grpc.*;
    
    public class UserClientInterceptor implements ClientInterceptor {
        @Override
        public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
            return new UserClientCall<>(next.newCall(method, callOptions));
        }
    }

    (4) gRPC 클라이언트 채널에 구현한 Interceptor 클래스를 인터셉터로 등록합니다.

    마지막으로 구현한 인터셉터가 정상적으로 동작하도록 다음과 같이 gRPC 클라이언트에 등록해줍니다. 이제 서버로부터 응답이 도달하면 인터셉터에 의해 앞서 구현한 일련의 과정들이 먼저 수행됩니다.

    // GrpcApplication.java
    ...
    public static void main(String[] args) {
        ...
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080).usePlaintext()
                .intercept(new UserClientInterceptor()).build();
        ...
    }

    지금까지 서버와 클라이언트에서의 gRPC 인터셉터 동작 원리와 Java를 이용한 구현 과정에 대해 살펴보았습니다. 다음 글에서는 gRPC에서 제공하는 데드라인 기능에 대해 살펴보고 마찬가지로 Java로 구현하는 내용을 다루도록 하겠습니다.

    • (참고) JDK 9부터는 JAVA EE 모듈이 제거되었기 때문에 gRPC 컴파일 코드를 포함한 프로젝트 전체를 빌드하기 위해서는 다음과 같이 javax.annotation 의존성을 추가해야 합니다.
    // build.gradle
    dependencies {
        ...
        implementation 'javax.annotation:javax.annotation-api:1.3.2'
        ...
    }