Spring Application으로 Kubernetes에 명령 보내기

.·2022년 5월 17일
2

Intro

  • Kubernetes는 API 서버 입니다. 저희가 kubectl을 이용해 kubernetes로 명령을 보내는 것은 사실 kubernetes의 REST API 서버로 요청을 보내고 응답을 받아오는 것입니다.
  • 이번 글에서는 Kubernetes에 REST API 요청을 보내고 응답을 받아오는 방법과 Spring의 RestTemplate를 통해 이를 구현하는 방법을 살펴보겠습니다.
  • Kubectl get pod 명령을 Spring 어플리케이션에서 REST API로 구현하는 것을 목표로 합니다.
  • Kubernetes는 Minikube로 간단하게 구현했습니다. https://minikube.sigs.k8s.io/docs/start/를 참고부탁드립니다.

사전 작업

REST API 요청을 위한 JWT Token 발급

cat $HOME/.kube/config
apiVersion: v1
clusters:
- cluster:
    certificate-authority: /Users/gwon/.minikube/ca.crt
    extensions:
    - extension:
        last-update: Mon, 25 Apr 2022 23:04:12 KST
        provider: minikube.sigs.k8s.io
        version: v1.25.2
      name: cluster_info
    server: https://192.168.59.100:8443
  name: minikube
...
  • Kubernetes는 REST API 서버로 우리가 흔히 실행하는 명령어인 kubectl get pod, create pod, delete pod 등은 REST API로 구현 가능합니다.
  • $HOME/.kube/config에 존재하는 kubeconfig에는 API 서버, 인증 정보가 담겨져 있습니다.
  • 로컬에 구성한 쿠버네티스 클러스터의 kubeconfig를 확인해 쿠버네티스 클러스터의 주소 https://192.168.59.100:8443 을 확인했습니다.
kubectl get pods -v 8

I0425 23:06:59.868327   24208 loader.go:372] Config loaded from file:  /Users/gwon/.kube/config
I0425 23:06:59.871086   24208 cert_rotation.go:137] Starting client certificate rotation controller
I0425 23:06:59.877122   24208 round_trippers.go:463] GET https://192.168.59.100:8443/api/v1/namespaces/default/pods?limit=500
I0425 23:06:59.877137   24208 round_trippers.go:469] Request Headers:
I0425 23:06:59.877146   24208 round_trippers.go:473]     Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json
I0425 23:06:59.877152   24208 round_trippers.go:473]     User-Agent: kubectl/v1.23.6 (darwin/amd64) kubernetes/ad33385
I0425 23:06:59.887067   24208 round_trippers.go:574] Response Status: 200 OK in 9 milliseconds
I0425 23:06:59.887091   24208 round_trippers.go:577] Response Headers:
I0425 23:06:59.887102   24208 round_trippers.go:580]     Cache-Control: no-cache, private
I0425 23:06:59.887111   24208 round_trippers.go:580]     Content-Type: application/json
I0425 23:06:59.887117   24208 round_trippers.go:580]     X-Kubernetes-Pf-Flowschema-Uid: 7be26494-6c94-4cdc-ac87-3945e76e4328
I0425 23:06:59.887122   24208 round_trippers.go:580]     X-Kubernetes-Pf-Prioritylevel-Uid: fea539d0-00f1-44fe-bc79-5d749f427d70
I0425 23:06:59.887129   24208 round_trippers.go:580]     Content-Length: 2932
I0425 23:06:59.887134   24208 round_trippers.go:580]     Date: Mon, 25 Apr 2022 14:06:59 GMT
  • kubectl -v 8 태그를 통해 명령어의 REST API URL 정보를 확인할 수 있습니다.
  • kubectl get pods는 IP:PORT/api/v1/namespaces/default/pods URL로 GET Method 를 지닌 Request를 보낸 결과를 출력하는 명령어 입니다.
  • 리눅스의 curl 명령어를 통해 kubectl을 사용하지 않고 pod의 정보를 조회해보겠습니다.
# Default Service Account에 관리자 권한 부여
kubectl create clusterrolebinding default-cluster-admin --clusterrole cluster-admin --serviceaccount default:default

# Default Service Account JWT Token 추출
TOKEN=$(kubectl get secret $(kubectl get sa default \
    -ojsonpath="{.secrets[0].name}") \
    -ojsonpath="{.data.token}" | base64 -d)
echo $TOKEN

# Kubernetes로 REST API 요청
curl -k -H "Authorization: Bearer $TOKEN" [https://192.168.59.100:8443](https://192.168.59.100:8443)
  • Kubernetes에 REST API 요청을 보내기 위해서는 인증을 진행해야합니다. kubectl은 kubeconfig를 통해 인증을 진행했는데 curl 명령어는 kubeconfig를 사용할 수 없기 때문에 Bearer HTTP Authentiation을 통해 인증을 진행하겠습니다.
  • Bear HTTP Authentication : 서버로부터 token을 발급받고, 이후 요청 시 Authorization 헤더에 토큰을 실어 보냅니다.
  • kubernetes는 기본적으로 default라는 ServiceAccount를 제공하며 ServiceAccount에서 JWT TOKEN 추출 가능합니다.
  • default Service Account에 모든 권한을 부여하고 JWT TOKEN을 추출하겠습니다.
  • curl -H "Authorization: Bearer $TOKEN" <IP>:<PORT> 를 통해 인증을 한후 Kubernetes 서버로 명령을 내릴 수 있습니다.
# TOKEN 값 저장
export TOKEN= value

# kubectl get pods
curl -k -X GET -H "Authorization: Bearer $TOKEN" [https://192.168.59.100:8443](https://192.168.59.100:8443)/api/v1/namespaces/default/pods

{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "712"
  },
  "items": []
}%
  • 위 과정에서 추출한 jwt 토큰 값을 환경변수로 저장했습니다.
  • https 통신을 진행하기 위해서는 인증서를 검증하는 작업이 진행되야 하는데 로컬 클러스터에서는 서버 자체에서 발급한 인증서이기 때문에 검증이 불가능 합니다. curl의 -k 옵션을 통해 인증서 검증 작업을 생략하겠습니다.
  • -X 옵션을 통해 GET METHOD임을 지정했습니다.
  • curl의 -H 옵션을 통해 헤더에 추출한 토큰값을 넣어주었습니다.
  • IP:PORT/api/v1/namespaces/default/pods로 요청을 보내면 json 형태로 pod의 정보들이 반환됩니다.

Spring Application

  • Spring 에서는 RestTemplate를 사용해 REST API 요청을 보낼 수 있습니다.
  • RestTemplate를 사용해 kuberentes로 요청을 보내 pod 정보들을 조회해보겠습니다.

Configuration

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) throws NoSuchAlgorithmException, KeyManagementException {

        TrustManager[] trustAllCerts = new TrustManager[] {
                new X509TrustManager() {
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                    public void checkClientTrusted(
                            java.security.cert.X509Certificate[] certs, String authType) {
                    }
                    public void checkServerTrusted(
                            java.security.cert.X509Certificate[] certs, String authType) {
                    }
                }
        };
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
        CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLContext(sslContext)
                .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
                .build();
        HttpComponentsClientHttpRequestFactory customRequestFactory = new HttpComponentsClientHttpRequestFactory();
        customRequestFactory.setHttpClient(httpClient);
        return builder.requestFactory(() -> customRequestFactory).build();
    }
}

Util

@Component
@RequiredArgsConstructor
public class RestApiUtil {

    @Value("${config.kubernetes.url}")
    private String API_SERVER;

    @Value("${config.kubernetes.token}")
    private String API_TOKEN;

    private final RestTemplate restTemplate;

    private final String API_URL = "/api/v1/namespaces/default/";

    public static final String RESOURCE_TYPE_POD = "pods";

    public ResponseEntity execute(HttpMethod httpMethod, String resourceType) {

        String url = API_SERVER + API_URL + resourceType;

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(API_TOKEN);
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity httpEntity = new HttpEntity(headers);

        return restTemplate.exchange(url, httpMethod, httpEntity, Map.class);

    }

}
  • Kubernetes로 요청을 보내기 위해 RestTemplate에 적절한 값을 넣어주기 위해 RestApiUtil을 생성했습니다.
  • @Value를 통해 yaml 값에 서버의 url 주소와 Token 값을 넣어 타인에게 노출되지 않도록 안전하게 보호하겠습니다. - 해당 값은 /main/resources/application.yaml 에 넣어줍니다.
  • Configutation에서 만든 RestTemplate을 private final과 RequiredArgsConstructor를 통해 의존성을 주입 받았습니다.
  • HttpEntity는 Http Header, Http Body로 이루어져 있습ㄴ다. GET 요청에는 Body가 따로 필요 없기 때문에 HttpHeader를 통해 토큰값과 content-type 값을 지정해주고 HttpEntity를 생성했습니다.
  • restTemplate.exchange()는 요청을 보내고자 하는 url, httpMethod, httpEntity, 반환 타입을 인자로 받습니다.

Service

@Service
@Transactional
@RequiredArgsConstructor
public class ApiService {

    private final RestApiUtil restApiUtil;

    public List getResource(String resourceType) {

        Map response = (Map)restApiUtil.execute(HttpMethod.GET,resourceType).getBody();

        List<Map> items = (List) response.get("items");

        return items.stream()
                .map(item -> {

                    Map metadata = (Map)item.get("metadata");
                    String name = (String)metadata.get("name");
                    String namespace = (String)metadata.get("namespace");
                    Map status = (Map)item.get("status");
                    String phase = (String)status.get("phase");

                    return PodDto.builder()
                            .name(name)
                            .status(phase)
                            .namespace(namespace)
                            .build();
                })
                .collect(Collectors.toList());

    }
  • RestApiutil의 execute 함수에 GET Method, resourceType를 인자로 넘겨 해당 resource의 정보들을 가져오는 REST API를 호출합니다.
  • 가져온 데이터들은 responseEntity 형식으로 반환되는데 getBody()함수를 통해 body의 값을 Map 형태로 받아 옵니다.
  • response 데이터는 Map<String,LinkedHashMap> 형식으로 넘어오게 됩니다.

  • 인텔리제이의 디버깅 기능을 사용해 items의 값을 살펴본 결과 입니다.
  • java의 동적 캐스팅을 사용합니다.
  • item List에서 스트림을 사용해 각 아이템마다 필요한 정보를 추출한 후 Dto로 변환시켜 다시 리스트로 만듭니다.

Dto

@Getter
@Setter
@NoArgsConstructor
public class PodDto {

    private String name;
    private String status;
    private String namespace;

    @Builder
    public PodDto(String name, String status, String namespace) {
        this.name = name;
        this.status = status;
        this.namespace = namespace;
    }

}
  • Pod 정보를 담아 반환할 Dto를 정의했습니다.

Controller

@RestController
@RequiredArgsConstructor
public class ApiController {

    private final ApiService apiService;

    @GetMapping("/pods")
    public List<PodDto> getPods(){
        return apiService.getResource(RESOURCE_TYPE_POD);
    }

}
  • apiService를 의존성 주입 받습니다.
  • localhost:8080/pods 로 GET 요청이 들어오면 service의 getResource를 실행한 결과값을 반환합니다.
  • ResponseEntity를 이용해 Http 응답을 반환해줘야하지만 이번 글에서는 Kubernetes로 RestAPI를 보내는것을 설명하기 위해 간단하게 구현한 프로젝트이기 때문에 Dto를 바로 반환하게 만들어주었습니다.

실행


kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          27s

curl http://localhost:8080/pods
[{"name":"nginx","status":"Running","namespace":"default"}]
  • Spring Application을 통해 kubectl get pods 명령을 구현했습니다.

Reference

profile
지금부터 공부하고 개발한것들을 꾸준하게 기록하자.

0개의 댓글