블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 

'secret'에 해당되는 글 3

  1. 2018.07.07 쿠버네티스 #12 - Secret
  2. 2018.07.06  쿠버네티스 #11 - ConfigMap
  3. 2018.06.23 쿠버네티스 #8 - Ingress (3)
 

쿠버네티스 #12

Secret


조대협 (http://bcho.tistory.com)


Secret

configMap이 일반적인 환경 설정 정보나 CONFIG정보를 저장하도록 디자인 되었다면, 보안이 중요한 패스워드나, API 키, 인증서 파일들은 secret에 저장할 수 있다. Secret은 안에 저장된 내용을 지키기 위해서 추가적인 보안 기능을 제공한다. 예를 들어 secret의 값들은 etcd에 저장될때 암호화된 형태로 저장되고 API server나 node의 파일에는 저장되지 않고, 항상 메모리에 저장되어 있기 때문에 상대적으로 접근이 어렵다.

하나의 secret의 사이즈는 최대 1M까지 지원되는데, 메모리에 지원되는 특성 때문에, secret을 여러개 저장하게 되면 API Server나 노드에서 이를 저장하는 kubelet의 메모리 사용량이 늘어나서 Out Of Memory와 같은 이슈를 유발할 수 있기 때문에, 보안적으로 꼭 필요한 정보만 secret에 저장하도록 하는게 좋다.


사용 방법에 있어서는 secret와 configmap은 기본적으로 거의 유사하다. 기본적으로 키/밸류 형태의 저장구조를 가지고 있으며, 사용시 환경 변수를 통해서 Pod에 그 값을 전달하거나, 또는 디스크 볼륨으로 마운트가 가능한데, secret은 정의하는 방법이 다소 차이가 있다.

예를 들어 language라는 키로 java라는 값을 저장하고자 할때, configmap의 경우에는 이를 language:java 식으로 일반 문자열로 저장했지만 secret의 경우에는 값에 해당하는 부분을 base64 포맷으로 인코딩해야 한다.

즉 java라는 문자열을 base64로 인코딩을 하면, amF2YQo= 가 된다.

문자열을 base64포맷으로 인코딩 하려면 맥이나 리눅스에서 다음과 같은 명령을 이용하면 된다.

%echo java | base64

이렇게 인코딩 된 문자열을 이용해서 secret을 정의해보면 다음과 같다.


hello-secret.yaml 파일

apiVersion: v1
kind: Secret
metadata:
 name: hello-secret
data:
 language: amF2YQo=

base64로 인코딩이 되어 있지만 이를 환경변수로 넘길때나 디스크볼륨으로 마운트해서 읽을 경우에는 디코딩이되서 읽어진다. base64는 단순 인코딩이지 암호화가 아닌데, 왜 궂이 base64로 인코딩을 하는 것일까? secret에 저장되는 내용은 패스워드와 같은 단순 문자열의 경우에는 바로 저장이 가능 하지만, SSL 인증서와 같은 바이너리 파일의 경우에는 문자열로 저장이 불가능하다. 그래서 이러한 바이너리 파일 저장을 지원하기 위해서 secret의 경우에는 저장되는 값을 base64로 인코딩을 하여 저장하도록 되어 있다.


그러면 앞에서 작성한 secret을 테스트하기 위해서 node.js로 간단한 server.js 애플리케이션을 만들어보자.


var os = require('os');


var http = require('http');

var handleRequest = function(request, response) {

 response.writeHead(200);

 response.end(" my prefered secret language is "+process.env.LANGUAGE+ "\n");


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


이 코드는 LANGUAGE라는 환경 변수에서 값을 읽어서 출력하는 코드이다. (앞의 configmap 코드와 동일)

이 파일을 도커 컨테이너 이미지로 만든후에 gcr.io/terrycho-sandbox/hello-secret:v1 이름으로 등록한 후에, 아래와 같이 Deployment 코드를 작성해보자


hello-secret-literal-deployment.yaml 파일


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: hello-secret-deployment

spec:

 replicas: 3

 minReadySeconds: 5

 selector:

   matchLabels:

     app: hello-secret-literal

 template:

   metadata:

     name: hello-secret-literal-pod

     labels:

       app: hello-secret-literal

   spec:

     containers:

     - name: cm

       image: gcr.io/terrycho-sandbox/hello-secret:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

       env:

       - name: LANGUAGE

         valueFrom:

           secretKeyRef:

              name: hello-secret

              key: language


Deployment 파일은 configMap과 크게 다를 것이 없다. configMapKeyRef를 secrectKeyRef로 변경하였고, configMap과 마찬가지로 secret의 이름(hello-secret)을 정하고, 키 이름 (language)을 지정하였다. Deployment를 배포한후에, 서비스를 배포해서 웹으로 접속하면 아래와 같이 secret에 base64로 저장된 “java”라는 문자열이 디코딩되서 출력되는 것을 확인할 수 있다.



파일로 마운트 하기

secret도 configMap과 마찬가지로, 설정 값들을 환경변수 뿐만 아니라, 파일로도 넘길 수 있다. 환경변수로 넘기는 방법과 마찬가지로 파일을 base64로 인코딩해서 secret을 생성해야 하며, 인코딩된 secret을 Pod에 파일로 마운트될때는 디코딩된 상태로 마운트가 된다.


이번에는 secret을 파일에서 부터 만들어보자 사용자 ID를 저장한 user.property 파일과, 비밀 번호를 저장한 password.property 파일 두개가 있다고 하자.각 파일의 내용은 다음과 같다.



Filename : user.property

terry



Filename : password.property

mypassword


이 두개의 파일을 secret에 저장을 할것이다. 명령은 다음과 같다.

% kubectl create secret generic db-password --from-file=./user.property  --from-file=./password.property


db-password라는 secret을 생성하고, user.property, password.property에서 secret을 생성하게 된다. 생성된 secret은 user.property, password.property라는 파일명을 각각 키로하여 파일의 내용이 저장된다.

이때 파일을 통해서 secret을 만들경우에는 별도로 base64 인코딩을 하지 않더라도 자동으로 base64로 인코딩 되어 저장된다.


위의 명령을 보면 kubectl create secret 명령어 뒤에 generic 이라는 키워드를 붙였는데, 이는 secret을 generic이라는 타입으로 생성하기 위함이다. secret의 타입에 대해서는 뒤에서 설명하도록 한다.


이렇게 생성된 secret을 확인해보면 아래와 같이 user.property, password.property 두개의 키로 데이타를 저장하고 있는 것을 확인할 수 있다.




시크릿을 디스크로 마운트해서 읽는 것을 테스트해보기 위해서 간단하게 node.js로 server.js 라는 코드를 아래와 같이 작성한다. 아래 코드는 /tmp/db-password 디렉토리에서 user.property와 password.property 파일을 읽어서 화면에 출력하는 코드이다.


var os = require('os');

var fs = require('fs');

var http = require('http');


var handleRequest = function(request, response) {

 fs.readFile('/tmp/db-password/user.property',function(err,userid){

   response.writeHead(200);

   response.write("user id  is "+userid+" \n");

   fs.readFile('/tmp/db-password/password.property',function(err,password){

     response.end(" password is "+password+ "\n");

   })

 })


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


다음 이 코드에서 user.property와 password.property 를 /tmp/db-password 디렉토리에서 읽어올 수 있도록, 앞에서 만든 db-password 라는 시크릿을 /tmp/db-password  디렉토리에 마운트 하도록 deployment를 정의한다.

hello-secret-file-deployment.yaml


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: hello-serect-file-deployment

spec:

 replicas: 3

 minReadySeconds: 5

 selector:

   matchLabels:

     app: hello-secret-file

 template:

   metadata:

     name: hello-secret-file

     labels:

       app: hello-secret-file

   spec:

     containers:

     - name: hello-secret-file

       image: gcr.io/terrycho-sandbox/hello-secret-file:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

       volumeMounts:

         - name: db-password

           mountPath: "/tmp/db-password"

           readOnly: true

     volumes:

     - name: db-password

       secret:

         secretName: db-password

         defaultMode: 0600


configMap과 차이가 거의 없다.  configMap이 secret으로만 바뀐건데, 이번에는 마운트 되는 파일의 퍼미션을 지정하였다. (configMap도 지정이 가능하다.) defaultMode로 파일의 퍼미션을 정의해놓으면, 파일 생성시, 해당 퍼미션으로 파일이 생성된다. 여기서는 0600으로 정의했기 때문에, rw-------으로 파일이 생성될것이다. 만약에 퍼미션을 지정하지 않았을 경우에는 디폴트로 0644 퍼미션으로 파일이 생성된다.


위의 스크립트로 생성한 Pod에 SSH로 들어가 보면 아래와 같이 /tmp/db-password에 user.property파일과 password.property 파일이 생성된것을 확인할 수 있다.




그런데 파일 퍼미션을 보면 우리가 지정한 0600이 아닌데, 잘 보면 user.property와 password.property는 링크로 ..data/user.property 와  ..data/password.property 파일로 연결이 되어 있다.




Deployment 배포가 끝났으면, 서비스를 배포해서 웹으로 접속해보자


위와 같이 마운트된 시크릿 파일에서 데이타를 읽어와서 제대로 출력한것을 확인할 수 있다.

시크릿 타입

시크릿은 configMap과는 다르게 타입




쿠버네티스 #11

ConfigMap


조대협 (http://bcho.tistory.com)



애플리케이션을 배포하다 보면, 환경에 따라서 다른 설정값을 사용하는 경우가 있다. 예를 들어, 데이타베이스의 IP, API를 호출하기 위한 API KEY, 개발/운영에 따른 디버그 모드, 환경 설정 파일들이 있는데, 애플리케이션 이미지는 같지만, 이런 환경 변수가 차이가 나는 경우 매번 다른 컨테이너 이미지를 만드는 것은 관리상 불편할 수 밖에 없다.

이러한 환경 변수나 설정값들을 변수로 관리해서 Pod가 생성될때 이 값을 넣어줄 수 있는데, 이러한 기능을 제공하는 것이 바로 Configmap과 Secret이다.


아래 그림과 같이 설정 파일을 만들어놓고, Pod 를 배포할때 마다 다른 설정 정보를 반영하도록 할 수 있다.



Configmap이나 secret에 정의해놓고, 이 정의해놓은 값을 Pod로 넘기는 방법은 크게 두가지가 있다.

  • 정의해놓은 값을 Pod의 환경 변수 (Environment variable)로 넘기는 방법

  • 정의해놓은 값을 Pod의 디스크 볼륨으로 마운트 하는 방법

ConfigMap

configmap은 앞서 설명한것과 같이 설정 정보를 저장해놓는 일종의 저장소 역할을 한다.

configmap은 키/밸류 형식으로 저장이된다.

configmap을 생성하는 방법은 literal (문자)로 생성하는 방법과 파일로 생성하는 방법 두가지가 있다.

Literal

먼저 간단하게 문자로 생성하는 방법을 알아보자

키가 “language”로 하고 그 값이 “java”인 configMap을 생성해보자

Kubectl create configmap [configmap 이름] --from-literal=[키]=[값] 식으로 생성하면 된다.

아래 명령을 이용하면, hello-cm 이라는 이름의 configMap에 키는 language, 값은 java인 configMap이 생성된다.

% kubectl create configmap hello-cm --from-literal=language=java


또는 아래와 같이 YAML파일로도 configMap을 생성할 수 있다.

hello-cm.yaml

apiVersion: v1

kind: ConfigMap

metadata:

 name: hello-cm

data:

 language: java


데이타 항목에 [키]:[값] 형식으로 라인을 추가하면 여러개의 값을 하나의 configMap에 저장할 수 있다.

configmap이 생성되었으면 이 값을 Pod에서 환경 변수로 불러서 사용해보도록 하자.

node.js로 간단한 웹 애플리케이션을 만든후에 “LANGUAGE”라는 환경 변수의 값을 읽어서 출력하도록 할것이다.

아래와 같이 server.js node.js 애플리케이션을 만든다.


var os = require('os');


var http = require('http');

var handleRequest = function(request, response) {

 response.writeHead(200);

 response.end(" my prefered language is "+process.env.LANGUAGE+ "\n");


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


이 파일을 컨테이너로 패키징한 후에, 아래와 같이 Deployment를 정의한다

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: cm-deployment

spec:

 replicas: 3

 minReadySeconds: 5

 selector:

   matchLabels:

     app: cm-literal

 template:

   metadata:

     name: cm-literal-pod

     labels:

       app: cm-literal

   spec:

     containers:

     - name: cm

       image: gcr.io/terrycho-sandbox/cm:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

       env:

       - name: LANGUAGE

         valueFrom:

           configMapKeyRef:

              name: hello-cm

              key: language


configMap에서 데이타를 읽는 부분은 맨 아래에 env 부분인데, env 부분에 환경 변수를 정의하는데, name은 LANGUAGE라는 이름으로 정의하고 데이타는 valueFrom을 이용해서 configMap에서 읽어오도록 하였다. name에는 configMap의 이름인 hello-cm을, 그리고 읽어오고자 하는 데이타는 키 값이 “language”인 값을 읽어오도록 하였다. 이렇게 하면, LANGUAGE 환경 변수에, configMap에 “language” 로 저장된 “java”라는 문자열을 읽어오게 된다.


이 스크립트를 이용하여 Deployment를 생성한 후에, 이 Deployment 앞에 Service (Load balancer)를 붙여 보자.


apiVersion: v1

kind: Service

metadata:

 name: cm-literal-svc

spec:

 selector:

   app: cm-literal

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer


서비스가 생성이 되었으면 웹 브라우져에서 해당 Service의 URL을 접속해보자.



위와 같이 환경 변수에서 “java”라는 문자열을 읽어와서 출력한것을 확인할 수 있다.

File

위와 같이 개개별 값을 공유할 수 도 있지만, 설정을 파일 형태로 해서 Pod에 공유하는 방법도 있다.

예제를 보면서 이해하도록 하자.

profile.properties라는 파일이 있고 파일 내용이 아래와 같다고 하자

myname=terry

email=myemail@mycompany.com

address=seoul


파일을 이용해서 ConfigMap을 만들때는 아래와 같이 --from-file 을 이용해서 파일명을 넘겨주면 된다.

kubectl create configmap cm-file --from-file=./properties/profile.properties

이렇게 파일을 이용해서 configMap을 생성하면, 아래와 같이 키는 파일명이 되고, 값은 파일 내용이 된다.


환경변수로 값을 전달하기

생성된 configMap 내의 값을 Pod로 전달하는 방법은,앞에서 예를 든것과 같이 환경 변수로 넘길 수 있다.

아래 Deployment 예제를 보면


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: cm-file-deployment

spec:

 replicas: 3

 minReadySeconds: 5

 selector:

   matchLabels:

     app: cm-file

 template:

   metadata:

     name: cm-file-pod

     labels:

       app: cm-file

   spec:

     containers:

     - name: cm-file

       image: gcr.io/terrycho-sandbox/cm-file:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

       env:

       - name: PROFILE

         valueFrom:

           configMapKeyRef:

              name: cm-file

              key: profile.properties


cm-file configMap에서 키가 “profile.properties” (파일명)인 값을 읽어와서 환경 변수 PROFILE에 저장한다. 저장된 값은 파일의 내용인 아래 문자열이 된다.

myname=terry

email=myemail@mycompany.com

address=seoul


혼동하지 말아야 하는 점은, profile.properties 파일안에 문자열이 myname=terry 처럼 키/밸류 형식으로 되어 있다고 하더라도, myname 을 키로 해서 terry라는 값을 가지고 오는 것처럼 개개별 문자열을 키/밸류로 인식하는 것이 아니라 전체 파일 내용을 하나의 문자열로 처리한다는 점이다.


디스크 볼륨으로 마운트하기

configMap의 정보를 pod로 전달하는 방법은 앞에 처럼 환경 변수를 사용하는 방법도 있지만, Pod의 디스크 볼륨으로 마운트 시키는 방법도 있다.

앞의 cm-file configMap을 /tmp/config/에 마운트 해보도록 하자.

아래와 같이 Deployment 스크립트를 작성한다.


apiVersion: apps/v1beta2

kind: Deployment

metadata:

 name: cm-file-deployment-vol

spec:

 replicas: 3

 minReadySeconds: 5

 selector:

   matchLabels:

     app: cm-file-vol

 template:

   metadata:

     name: cm-file-vol-pod

     labels:

       app: cm-file-vol

   spec:

     containers:

     - name: cm-file-vol

       image: gcr.io/terrycho-sandbox/cm-file-volume:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080

       volumeMounts:

         - name: config-profile

           mountPath: /tmp/config

     volumes:

       - name: config-profile

         configMap:

           name: cm-file


configMap을 디스크 볼륨으로 마운트해서 사용하는 방법은 volumes 을 configMap으로 정의하면 된다. 위의 예제에서 처럼 volume을 정의할때, configMap으로 정의하고 configMap의 이름을 cm-file로 정의하여, cm-file configMap을 선택하였다. 이 볼륨을 volumeMounts를 이용해서 /tmp/config에 마운트 되도록 하였다.

이때 중요한점은 마운트 포인트에 마운트 될때, 파일명을 configMap내의 키가 파일명이 된다.


다음 테스트를 위해서 server.js 애플리케이션에 /tmp/config/profile.properties 파일을 읽어서 출력하도록 아래와 같이 코드를 작성한다.

var os = require('os');

var fs = require('fs');


var http = require('http');

var handleRequest = function(request, response) {

 fs.readFile('/tmp/config/profile.properties',function(err,data){

   response.writeHead(200);

   response.end("Read configMap from file  "+data+" \n");

 });


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


이 server.js를 도커로 패키징해서 배포한후, service를 붙여서 테스트해보면 다음과 같은 결과를 얻을 수 있다.



파일 내용이 출력되는 것을 확인할 수 있다

디스크에 마운트가 제대로 되었는지를 확인하기 위해서 Pod에 쉘로 로그인해서 확인해보자


그림과 같이 /tmp/config/profile.properties 파일이 생성된것을 확인할 수 있다.



쿠버네티스 #8

Ingress


조대협 (http://bcho.tistory.com)



쿠버네티스의 서비스는, L4 레이어로 TCP 단에서 Pod들을 밸런싱한다.

서비스의 경우에는 TLS (SSL)이나, VirtualHost와 같이 여러 호스트명을 사용하거나 호스트명에 대한 라우팅이 불가능하고, URL Path에 따른 서비스간 라우팅이 불가능하다.

또한 마이크로 서비스 아키텍쳐 (MSA)의 경우에는 쿠버네티스의 서비스 하나가 MSA의 서비스로 표현되는 경우가 많고 서비스는 하나의 URL로 대표 되는 경우가 많다. (/users, /products, …)

그래서 MSA 서비스간의 라우팅을 하기 위해서는 API 게이트웨이를 넣는 경우가 많은데, 이 경우에는 API 게이트웨이에 대한 관리포인트가 생기기 때문에, URL 기반의 라우팅 정도라면, API 게이트웨이 처럼 무거운 아키텍쳐 컴포넌트가 아니라, L7 로드밸런서 정도로 위의 기능을 모두 제공이 가능하다.


쿠버네티스에서 HTTP(S)기반의 L7 로드밸런싱 기능을 제공하는 컴포넌트를 Ingress라고 한다.

개념을 도식화 해보면 아래와 같은데, Ingress 가 서비스 앞에서 L7 로드밸런서 역할을 하고, URL에 따라서 라우팅을 하게 된다.


Ingress 가 서비스 앞에 붙어서, URL이 /users와 /products 인것을 각각 다른 서비스로 라우팅 해주는 구조가 된다.


Ingress 은 여러가지 구현체가 존재한다.

구글 클라우드의 경우에는 글로벌 로드 밸런서(https://github.com/kubernetes/ingress-gce/blob/master/README.md) 를 Ingress로 사용이 가능하며, 오픈소스 구현체로는 nginx (https://github.com/kubernetes/ingress-nginx/blob/master/README.md)  기반의 ingress 구현체가 있다.  상용 제품으로는 F5 BIG IP Controller (http://clouddocs.f5.com/products/connectors/k8s-bigip-ctlr/v1.5/) 가 현재 사용이 가능하고, 재미있는 제품으로는 오픈소스 API 게이트웨이 솔루션인 Kong (https://konghq.com/blog/kubernetes-ingress-controller-for-kong/)이 Ingress 컨트롤러의 기능을 지원한다.

각 구현체마다 설정 방법이 다소 차이가 있으며, 특히 Ingress 기능은 베타 상태이기 때문에, 향후 변경이 있을 수 있음을 감안하여 사용하자

URL Path 기반의 라우팅

이 글에서는 구글 클라우드 플랫폼의 로드밸런서를 Ingress로 사용하는 것을 예를 들어 설명한다.

위의 그림과 같이 users 와 products 서비스 두개를 구현하여 배포하고, 이를 ingress를 이용하여 URI가  /users/* 와 /products/* 를 각각의 서비스로 라우팅 하는 방법을 구현해보도록 하겠다.


node.js와 users와 products 서비스를 구현한다.

서비스는 앞에서 계속 사용해왔던 간단한 HelloWorld 서비스를 약간 변형해서 사용하였다.


아래는 users 서비스의 server.js 코드로 “Hello World! I’m User server ..”를 HTTP 응답으로 출력하도록 하였다.  Products 서비스는 User server를 product 서버로 문자열만 변경하였다.


var os = require('os');


var http = require('http');

var handleRequest = function(request, response) {

 response.writeHead(200);

 response.end("Hello World! I'm User server "+os.hostname() +" \n");


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


다음으로 서비스를 배포해야 하는데, Ingress를 사용하려면 서비스는 Load Balancer 타입이 아니라, NodePort 타입으로 배포해야 한다.  다음은 user 서비스를 nodeport 서비스로 배포하는 yaml 스크립트이다. (Pod를 컨트롤하는 Deployment 스크립트는 생략하였다.)


users-svc-nodeport.yaml

apiVersion: v1

kind: Service

metadata:

 name: users-node-svc

spec:

 selector:

   app: users

 type: NodePort

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 

같은 방식으로, Product 서비스도 아래와 같이 NodePort로 배포한다.

product-svc-nodeport.yaml

apiVersion: v1

kind: Service

metadata:

 name: products-node-svc

spec:

 selector:

   app: products

 type: NodePort

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

     

이때 별도로 nodeport를 지정해주지 않았는데, 자동으로 쿠버네티스 클러스터가 nodeport를 지정해준다.

아래와 같이 products-node-svc와 users-node-svc가 각각 배포된것을 확인할 수 있고, ClusterIP의 포트는 80, NodePort는 각각 31442, 32220으로 배포된것을 확인할 수 있다.




다음 Ingress를 생성해보자. 다음은 hello-ingress 라는 이름으로 위에서 만든 두개의 서비스를 라우팅해주는 서비스를 생성하기 위한 yaml  파일이다.

hello-ingress.yaml

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

 name: hello-ingress

spec:

 rules:

 - http:

     paths:

     - path: /users/*

       backend:

         serviceName: users-node-svc

         servicePort: 80

     - path: /products/*

       backend:

         serviceName: products-node-svc

         servicePort: 80


spec 부분에, rules.http.paths 부분에, 라우팅할 path와 서비스를 정의해준다.

User 서비스는 /users/* URI인 경우 라우팅하게 하고, 앞에서 만든 users-node-svc로 라우팅하도록 한다. 이때 servicePort는 ClusterIP의 service port를 지정한다. (Google Cloud HTTP Load balancer를 이용하는 Ingress의 경우에는  실질적으로는 nodeport로 통신을 하지만 별도로 지정하지 않고 ingress가 자동으로 해당 서비스의 nodeport를 찾아서 맵핑이 된다. )
(참고 : https://kubernetes.io/docs/concepts/services-networking/ingress/

Lines 12-14: A backend is a service:port combination as described in the services doc. Ingress traffic is typically sent directly to the endpoints matching a backend.)


%kubectl create -f hello-ingress.yaml

을 실행하면 ingress가 생성이 되고 kubectl get ing 명령어를 이용하면 생성된 ingress를 확인할 수 있다.



Ingress 가 생성된 후, 실제로 사용이 가능하기까지는 약 1~2분의 시간이 소요된다. 물리적으로 HTTP 로드밸런서를 생성하고, 이 로드밸런서가 서비스가 배포되어 있는 노드에 대한 HealthCheck를 완료하고 문제가 없으면 서비스를 제공하는데, HealthCheck 주기가 1분이기 때문에, 1~2분 정도를 기다려 주는게 좋다. 그전까지는 404 에러나 500 에러가 날것이다.


준비가 끝난후, curl 명령을 이용해서 ingress의 URL에 /users/ 와 /products를 각각 호출해보면 각각, users를 서비스 하는 서버와, products를 서비스 하는 서버로 라우팅이 되서 각각 다른 메세지가 출력되는 것을 확인할 수 있다.




그러면 내부적으로 클라우드 내에서 Ingress를 위한 인프라가 어떻게 생성되었는지 확인해보자

구글 클라우드 콘솔에서 아래와 같이 Network services > Load balancing 메뉴로 들어가보자



아래와 같이 HTTP 로드밸런서가 생성이 된것을 확인할 수 있다.


이름을 보면 k8s는 쿠버네티스용 로드밸런서임을 뜻하고, 중간에 default는 네임 스페이스를 의미한다. 그리고 ingress의 이름인 hello-ingress로 생성이 되어 있다.

로드밸런서를 클릭해서 디테일을 들어가 보면 아래와 같은 정보를 확인할 수 있다.




3개의 백엔드 (인스턴스 그룹)이 맵핑되었으며, /users/*용, /products/*용 그리고, 디폴트용이 생성되었다.

모든 트래픽이 쿠버네티스 클러스터 노드로 동일하게 들어가기 때문에, Instance group의 이름을 보면 모두 동일한것을 확인할 수 있다. 단, 중간에 Named Port 부분을 보면 포트가 다른것을 볼 수 있는데, 31442, 32220 포트를 사용하고 있고, 앞에서 users, produtcs 서비스를 nodeport로 생성하였을때, 자동으로 할당된 nodeport이다.


개념적으로 다음과 같은 구조가 된다.


(편의상 디폴트 백앤드의 라우팅은 표현에서 제외하였다.)


Ingress에 접속되는 서비스를 LoadBalancer나 ClusterIP타입이 아닌 NodePort 타입을 사용하는 이유는, Ingress로 사용되는 구글 클라우드 로드밸런서에서, 각 서비스에 대한 Hearbeat 체크를 하기 위해서인데, Ingress로 배포된 구글 클라우드 로드밸런서는 각 노드에 대해서 nodeport로 Heartbeat 체크를 해서 문제 있는 노드를 로드밸런서에서 자동으로 제거나 복구가되었을때는 자동으로 추가한다.

Static IP 지정하기

서비스와 마찬가지로 Ingress 역시 Static IP를 지정할 수 있다.

서비스와 마찬가지로, static IP를 gcloud 명령을 이용해서 생성한다. 이때 IP를 regional로 생성할 수 도 있지만, ingress의 경우에는 global IP를 사용할 수 있다. --global 옵션을 주면되는데, global IP의 경우에는 regional IP와는 다르게 구글 클라우드의 망 가속 기능을 이용하기 때문에, 구글 클라우드의 100+ 의 Pop (Point of Presence)를 이용하여 가속이 된다.


조금 더 깊게 설명을 하면, 일반적으로 한국에서 미국으로 트래픽을 보낼 경우 한국 → 인터넷 → 미국 식으로 트래픽이 가는데 반해 global IP를 이용하면, 한국에서 가장 가까운 Pop (일본)으로 접속되고, Pop으로 부터는 구글 클라우드의 전용 네트워크를 이용해서 구글 데이타 센터까지 연결 (한국 → 인터넷 → 일본 Pop → 미국 ) 이 되기 때문에 일반 인터넷으로 연결하는 것 대비에서 빠른 성능을 낼 수 있다.


아래와 같이 gcloud 명령을 이용하여, global IP를 생성한다.




구글 클라우드 콘솔에서, 정적 IP를 확인해보면 아래와 같이 hello-ingress-ip 와 같이 IP가 생성되어 등록되어 있는 것을 확인할 수 있다.



Static IP를 이용해서 hello-ingress-staticip 이름으로 ingress를 만들어보자

다음과 같이 hello-ingress-staticip.yaml 파일을 생성한다.


apiVersion: extensions/v1beta1

kind: Ingress

metadata:

 name: hello-ingress-staticip

 annotations:

   kubernetes.io/ingress.global-static-ip-name: "hello-ingress-ip"

spec:

 rules:

 - http:

     paths:

     - path: /users/*

       backend:

         serviceName: users-node-svc

         servicePort: 80

     - path: /products/*

       backend:

         serviceName: products-node-svc

         servicePort: 80


이 파일을 이용하여, ingress를 생성한 후에, ingress ip를 확인하고 curl 을 이용해서 결과를 확인하면 다음과 같다.


Ingress with TLS

이번에는 Ingress 로드밸런서를 HTTP가 아닌 HTTPS로 생성해보겠다.


SSL 인증서 생성

SSL을 사용하기 위해서는 SSL 인증서를 생성해야 한다. openssl (https://www.openssl.org/)툴을 이용하여 인증서를 생성해보도록 한다.


인증서 생성에 사용할 키를 생성한다.

%openssl genrsa -out hello-ingress.key 2048

명령으로 키를 생성하면 hello-ingress.key라는 이름으로 Private Key 파일이 생성된다.




다음 SSL 인증서를 생성하기 위해서, 인증서 신청서를 생성한다.인증서 신청서 생성시에는 앞에서 생성한 Private Key를 사용한다.

다음 명령어를 실행해서 인증서 신청서 생성을 한다.

%openssl req -new -key hello-ingress.key -out hello-ingress.csr

이때 인증서 내용에 들어갈 국가, 회사 정보, 연락처등을 아래와 같이 입력한다.


인증서 신청서가 hello-ingress.csr 파일로 생성이 되었다. 그러면 이 신청서를 이용하여, SSL 인증서를 생성하자. 테스트이기 때문에 공인 인증 기관에 신청하지 않고, 간단하게 사설 인증서를 생성하도록 하겠다.


다음 명령어를 이용하여 hello-ingress.crt라는 이름으로 SSL 인증서를 생성한다.

%openssl x509 -req -day 265 -in hello-ingress.csr -signkey hello-ingress.key -out hello-ingress.crt



설정하기

SSL 인증서 생성이 완료되었으면, 이 인증서를 이용하여 SSL을 지원하는 ingress를 생성해본다.

SSL 인증을 위해서는 앞서 생성한 인증서와 Private Key 파일이 필요한데, Ingress는 이 파일을 쿠버네티스의 secret 을 이용하여 읽어드린다.


Private Key와 SSL 인증서를 저장할 secret를 생성해보자 앞에서 생성한 hello-ingress.key와 hello-ingress.crt 파일이 ./ssl_cert 디렉토리에 있다고 하자


다음과 같이 kubectl create secret tls 명령을 이용해서 hello-ingress-secret 이란 이름의 secret을 생성한다.

%kubectl create secret tls hello-ingress-serect --key ./ssl_cert/hello-ingress.key --cert ./ssl_cert/hello-ingress.crt


명령을 이용하여 secret을 생성하면, key 이라는 이름으로 hello-ingress.key 파일이 바이너리 형태로 secret에 저장되고 마찬가지로 cert라는 이름으로 hello-ingress.crt 가 저장된다.


생성된 secret을 확인하기 위해서

%kubectl describe secret hello-ingress-secret

명령을 실행해보면 아래와 같이 tls.key 와 tls.crt 항목이 각각 생성된것을 확인할 수 있다.


다음 SSL을 지원하는 ingress를 생성해야 한다.

앞에서 생성한 HTTP ingress와 설정이 다르지 않으나 spec 부분에 tls라는 항목에 SSL 인증서와 Private Key를 저장한 secret 이름을 secretName이라는 항목으로 넘겨줘야 한다.


hello-ingress-tls.yaml


apiVersion: extensions/v1beta1

kind: Ingress

metadata:

 name: hello-ingress-tls

spec:

 tls:

 - secretName: hello-ingress-secret

 rules:

 - http:

     paths:

     - path: /users/*

       backend:

         serviceName: users-node-svc

         servicePort: 80

     - path: /products/*

       backend:

         serviceName: products-node-svc

         servicePort: 80


이 파일을 이용해서 TLS ingress를 생성한 후에, IP를 조회해보자

아래와 같이 35.241.6.159 IP에 hello-ingress-tls 이름으로 ingress가 된것을 확인할 수 있고 포트는 HTTP 포트인 80 포트 이외에, HTTPS포트인 443 포트를 사용하는 것을 볼 수 있다.



다음 HTTPS로 테스트를 해보면 다음과 같이 HTTPS로 접속이 되는 것을 확인할 수 있다.



사설 인증서이기 때문에 위처럼 Not Secure라는 메세지가 뜬다. 인증서 정보를 확인해보면 아래와 같이 앞서 생성한 인증서에 대한 정보가 들어가 있는 것을 확인할 수 있다.