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


Archive»


 
 

쿠버네티스 애플리케이션을 위한 개발환경

#1 IntelliJ
조대협 (http://bcho.tistory.com)
일반적인 경우에는 로컬 환경에서 개발하고, 로컬에서 톰캣등을 띄워서 테스트하면 되지만, 만약 부가적으로 레디스나, DB등 복잡한 개발/테스트 환경이 필요한 경우에는 결국에는 컴파일된 애플리케이션을 서버로 올려서 테스트해야 한다. 쿠버네티스를 이용해서 개발된 애플리케이션을 컨테이너로 패키징하는 개발 환경을 꾸미게 되면 개발을 조금 더 효율적으로 할 수 있다.


그렇지만 반대로, 컨테이너로 패키징해서 매번 쿠버네티스 개발용 클러스터에 배포해야 하기 때문에 복잡성이 증대하는 단점이 있다.  아래 그림은 쿠버네티스 기반의 애플리케이션을 개발할는 일반적인 개발 과정이다.


코드 작성이 끝나면, 컴파일을 해야 하고, 컴파일된 애플리케이션을 컨테이너로 패키징 해야 한다. 이때, 컨테이너의 태그 이름을 매번 바꿔줘야 하는데, 태그 이름을 변경하면 마찬가지로 YAML 파일내의 설정도 새로운 컨테이너로명으로 변경해줘야 한다. 


그리고, 변경된 내용을 쿠버네티스에 반영하면 테스트를 할 수 있는데, Cluster IP 서비스나 일반 Pod의 경우에는 외부 IP를 가지지 않기 때문에 바로 호출을 할 수 없다. 그래서 kubectl 을 이용해서 노트북의 로컬 주소를 해당 서비스나 Pod를 호출할 수 있도록 프록시 설정을 해줘야 한다.


마찬가지로 로깅 역시, 쿠버네티스 컨테이너로 돌고 있는 애플리케이션의 로그를 바로 볼 수 없기 때문에, kubectl logs 명령을 이용해서, 별도로 로그를 모니터링해야 하고, 런타임 디버깅은 지원조차 되지 않는다. 


개발된 코드가 테스트가 끝난 후에는 테스트를 위해서 기동했던 Pod 들을 삭제하는 것이 좋은데, 일반적으로, 하나의 애플레케이션은 여러가지의 리소스 (PSP, Deployment, Service, Configmap …)로 이루어지기 때문에, 일일이 삭제하려면 손이 많이 간다. 


이런 문제를 단순화 해주는 도구로 skaffold 가 로컬 환경을 지원하기 때문에, 많은 작업을 자동화할 수 있지만, 여전히 환경이 불편하다. 개발자들이 원하는 것은 쉘이나 기타 툴을 왔다갔다 하지 않고, 개발툴 (IDE)안에서 모든것을 해결하고 싶어한다.

이런 문제를 해결하기 위해서 구글에서 발표한것이 구글 클라우드 코드 (Google Cloud Code) 이다.

구글 클라우드 코드

구글 클라우드 코드는 개발도구에 확장 플러그인으로 제공되는 도구로, 위의 개발에 필요한 과정을 쿠버네티스와 연동하여  IDE안에서 모두 통합해서 지원할 수 있도록 한다. 내부 컨테이너 빌드와 배포는 Skaffold를 사용하고 있기 때문에, 운영 환경이 skaffold를 사용하고 있으면 그 설정을 공유해서 사용할 수 있다.

사실상 Skaffold를 IDE에 통합하고 몇가지 부가 기능이라고 보면 이해하기가 쉽고 사용방법도 Skaffold 설정을 그대로 사용한다. 


현재 클라우드 코드는 intelliJ와 Visual Studio Code (이하 VSCode)를 지원한다.

IntelliJ

먼저 IntelliJ에서 클라우드 코드를 사용하기 위해서는 다음 툴들이 로컬에 먼저 설치되어 있어야 한다. Docker client, kubectl, skaffold 그리고, 개발용 쿠버네티스 클러스터가 필요하다. 개발용 클러스터는 일반 쿠버네티스 환경을 지원하며, minikube도 지원한다.

설치

클라우드 코드 플러그인 설치는 File > Setting > Plugins 메뉴로 들어가서 "Cloud Code”를 검색하여 설치하면 된다.



테스트 애플리케이션 만들기

설치가 끝났으면 간단하게, Spring boot 애플리케이션을 만들어 보자

예제는 구글 클라우드에서 제공하는 Spring boot 예제 파일을 사용한다.

git clone https://github.com/GoogleCloudPlatform/cloud-code-samples.git

에서 코드를 가지고 온후에 java-hello-world project를 Maven 프로젝트로 Import 한다. 

디렉토리에는 쿠버네티스와 쿠버네티스 배포에 관련된 몇가지 파일들이 정의되어 있다


kubernetes-manifests

디렉토리에는 이 서비스를 배포하기 위한 deployment와, service YAML 파일이 정의되어 있다. 


Dockerfile

루트 디렉토리에는 애플리케이션을 패키징하기 위한 Dockerfile 이 들어있다.


skaffold.yaml

이 파일은 skaffold로 배포를 하기 위한 skaffold 설정이다. 이 파일에서는 컨테이너 이미지명과, kubernetes yaml 파일 경로등을 지정한다.

애플리케이션 배포하기




애플리케이션을 배포하기 위해서는 먼저 개발용 쿠버네티스 클러스터를 등록해야 한다.

Tools > Cloud Code > Kubernetes > Add Kubernetes Support

를 선택하면, 현재 Kubectl context에 연결된 쿠버네티스 클러스터를 자동으로 등록한다.

쿠버네티스 클러스터가 등록되면 아래 그림과 같이 상단에, 쿠버네티스 관련 메뉴가 나온다. 


여기서 Kubernetes Deploy를 선택한 후에, 오른쪽에 실행 버튼을 누르면 컴파일을 하고 쿠버네티스 클러스터로 배포를 하게 된다. 

아래 화면은 실제로 Kubernetes Continuous Deploy 모드로 배포한 결과이다. 

일반 Kubernetes Deploy 모드는, 버튼을 누를때만 배포가 되고, Continuous Deploy 모드는 코드 변화를 감지하여 코드가 변경되면 자동으로 재 배포를 한다. 


위의 그림과 같이 배포가 완료되면, 하단에 자동으로 로그 창이 떠서 컨테이너에서 실행되는 애플리케이션의 로그를 볼 수 있다. (Pod를 여러개 배포하면 여러개의 Pod 로그가 뒤섞여서 나오지만 앞에 pod 이름이 나오니까는 크게 걱정하지 않아도 된다) 

배포가 완료되면 kubectl get pod를 실행하보면 아래와 같이 java-hello-xxx pod가 배포된것을 확인할 수 있다.


테스트를 위해서, Cloud Code는 local port(PC의) 해당 서비스 포트로 자동 포워딩을 해준다. 배포가 끝난후에, 화면 하단을 보면 Port forwarding server port 라는 메세지와 함께, 로컬 포트를 쿠버네티스 Pod 포트로 어떻게 포워딩을 하는지 보여준다. 

아래 메세지는 localhost:8080 → 쿠버네티스 Pod:8080 으로 포워딩하는 것을 알리는 메시지 이다. 


단 여러 Pod 를 배포하게 되면 포트포워딩이 섞일 수 있으니, 개발시 환경에서는 하나의 Pod 만 배포하거나, Proxy를 kubectl 등으로 별도로 띄워서 서비스로 프록시를 하는것이 좋다. 

로컬에서 브라우져를 열어서 localhost:8080에 접속해보면 아래와 같이 서비스에 접속이 되서 결과가 나온다. 


디버깅

쿠버네티스 애플리케이션을 개발할 때 어려운 점이, 서버에 배포된 시스템을 디버깅 하기가 어렵다는 점인데, (쿠버네티스가 아니더라도 서버에 배포된 시스템을 리모트로 디버깅하는 것은 쉽지 않다)

클라우드 코드의 장점중의 하나가 쿠버네티스 컨테이너에서 돌고 있는 애플리케이션을 디버깅할 수 있는 기능이다.


디버깅을 위해서는 아래 그림과 같이 디버깅을 하고자하는 브레이크 포인트에, 20라인 처럼 붉은색으로 브레이크 포인트 처리를 하고 (붉은 점이 있는 부분을 클릭하면 브레이크 포인트 처리가 된다)



디버깅은 상단 메뉴에서 Kubernetes Continuos Deploy 모드로 맞춘후에, 아래 그림과 같이 오른쪽 녹색 디버깅 버튼을 누르면 된다. 



그후 애플리케이션을 실행하면 아래 그림과 같이 브레이킹 포인트에서 애플리케이션 실행이 멈추고 디버깅에 필요한 쓰레드 정보, 변수명등을 보면서 디버깅이 가능하다. 




본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

SRE #6-운영에서 반복적인 노가다 Toil

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

Toil

Toil의 사전적인 뜻은 “노역"이라는 뜻을 가지고 있는데, 비속어를 사용해서 표현하자면 운영 업무에서의 “노가다" 정도로 이해하면 된다.  Toil 에 대한 정의를 잘 이해해야 하는데, Toil은 일종의 반복적인 쓸모없는 작업 정도로 정의할 수 있다.

경비 처리나, 회의, 주간 업무 보고서 작성과 같은 어드민 작업은 Toil에 해당하지 않는다. Toil은 운영상에 발생하는 반복적인 메뉴얼 작업인데, 다음과 같은 몇가지 특징으로 정의할 수 있다.

메뉴얼 작업이고 반복적이어야함

Toil의 가장 큰 특징은 사람이 직접 수행하는 메뉴얼 작업이라는 것이다. 그리고 어쩌다 한번이 아니라 지속적으로 발생하는 반복적인 작업이다.

자동화 가능함

자동화가 가능하다는 것은 자동화가 가능한데, 시간이 없어서(?) 자동화를 못하고 사람이 작업을 하고 있다는 것이다. 즉 사람이 하지 않아도 되는 일을 시간을 낭비하면서 하고 있다는 것인데, 서버 배포를 테라폼등으로 자동화할 수 있는데, 자동화 하지 않고, 수동으로 작업하고 있는 경우 Toil에 해당한다.

밸류를 제공하지 않는 작업

Toil작업은 작업을 하고나도, 서비스나 비지니스가 개선되지 않는 작업이다. 작업 전/후의 상태가 같은 작업인데, 장애 처리와 같은 것이 대표적인 예에 속한다. 장애 처리는 시스템을 이전 상태로 돌리는 것 뿐일뿐 새로운 밸류를 제공하지 않는다.

서비스 성장에 따라서 선형적으로 증가하는 작업

Toil은 보통 서비스가 성장하고 시스템이 커지면 선형적으로 증가한다. 애플리케이션 배포나, 시스템 설정, 장애 처리도 시스템 인스턴스의 수가 늘어날 수 록 증가하게 된다.


정리해보자면, Toil이란 성장에 도움이 되지 않으면서 시간을 잡아먹는 메뉴얼한 작업이고, 서비스의 규모가 커지면 커질 수 록 늘어나는 자동화가 가능한 작업이다. 일종의 기술 부채의 개념과도 연결시켜 생각할 수 있다.

Toil을 왜? 그리고 어떻게 관리해야 하는가?

그러면 Toil을 어떻게 관리해야 할까? Toil은 의미없는 작업이 많지만, Toil을 무조건 없애는 것은 적절하지 않다. 예를 들어서 일년에 한두번 발생하는 작업을 자동화하려고 노력한다면, 오히려 자동화에 들어가는 노력이 더 많아서 투자 대비 효과가 떨어진다. 반대로 Toil이 많으면 의미있는 기능 개발작업이나 자동화 작업을할 수 없기 때문에 일의 가치가 떨어진다.

그래서 구글의 SRE 프랙티스 에서는 Toil을 30~50%로 유지하도록 권장하고 있다.  Toil을 줄이는 방법 중에 대표적인 방법은 자동화를 하는 방법이다. 자동화를 하면 Toil을 줄일 수 있는데, 그러면 남은 시간은 어떻게 활용하는가? 이 시간을 서비스 개발에 투자하거나 또는 다른 서비스를 운영하는데 사용한다.



위의 그림이 가장 잘 설명된 그림인데, 초반에는 Service A에 대해서 대부분의 Toil이 발생하는데, 자동화를 하게 되면, Toil 이 줄어든다.그러면 줄어든 Toil 시간을 다른 서비스를 운영하는데 사용을 하고 결과적으로는 여러 서비스를 동시에 적은 시간으로 운영할 수 있도록 된다.


이 개념을 확장해보면 Devops의 목표와도 부합이 되는데, Devops는 개발과 운영을 합쳐서 진행하는 모델인데, 개발이 직접 운영을 하기 위해서는 플랫폼이 필요하다. 즉 개발자가 직접 하드웨어 설치, 네트워크 구성등 로우 레벨한 작업을 하는것이 아니라, 자동화된 운영 플랫폼이 있으면, 개발팀이 직접 시스템을 배포 운영할 수 있게 된다.


<그림, Devops에서 개발자와 Devops 엔지니어의 역할>


그래서 개발팀이 이러한 플랫폼을 이용해서 Devops를 한다면, Devops엔지니어는 개발팀이 사용할 운영플랫폼을 개발하는데, 운영플랫폼이란 자동화된 플랫폼을 이야기 한다.

Toil을 측정해가면서 각 서비스별로 자동화 정도를 측정하고, 자동화가 될 수 록, 그 서비스에서 빠져가면서 새로운 서비스로 옮겨가는 모델이라고 볼 수 있다.


Toil를 어떻게 측정할것인가?

이제 Toil이 무엇인지, Toil을 줄여서 어떻게 활용하는지에 대해서 알아보았다.

그러면, 이 Toil을 어떻게 측정하는가?

Toil의 종류를 보면, 크게 배포나 장애처리 등으로 볼 수 있는데, 장애처리의 경우, 장애 발생시 장애 티켓을 버그 시스템에 등록한후에, 처리가 완료될때까지의 시간을 측정하면, 장애 처리에 대한 Toil을 측정할 수 있다.

메뉴얼 배포와 같은 경우에는 특별한 시스템 (Task management)을 사용하지 않는 이상 정확하게 측정하기가 어려운데 그래서 이런 경우에는 snippet (주간 업무보고)나 또는 주기적인 설문 조사를 통해서 측정하는 방법이 있다.



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

2019년 샌프란시스코 구글 NEXT 2019에서 발표한 로깅 시스템에 대한 강의 입니다.

구글 스택 드라이버 로깅에 대한 소개와, 삼성전자가 어떻게 스택 드라이버 툴을 이용해서 Devops로 적응했는지에 대한 사례입니다.

영어 컨텐츠 입니다.



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

Site Reliability Engineering(SRE)

#1 SRE/DEVOPS의 개념

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

배경

Devops는 운영팀과 개발팀을 하나의 팀으로 묶어놓고 전체적인 개발 사이클을 빠르게 하고자 하는 조직 구조이자 문화이다.


이 Devops라는 컨셉이 소개된지는 오래되었지만, Devops의 개념 자체는 명확하지만 이 Devops를 어떻게 실전에 적용할것인 가는 여전히 어려운 문제였다.(예전에 정리한 Devops에 대한 개념들 1 , 2)  예전 직장들에 있을때 Devops의 개념이 소개되었고 좋은 개념이라는 것은 이해하고 있었지만, 여전히 운영팀은 필요하였고, 그 역할이 크게 바뀌지 않았다. 심지어 Devops를 하는 기업들도 보면 기존 개발팀/운영팀이 있는데, 새롭게 Devops팀을 만들거나 또는 운영팀 간판을 Devops팀으로만 바꾸는 웃지 못할 결과들이 있었다.

나중에 위메프에서 CTO를 하셨던 김요섭님의 강의를 들을 수 있는 기회가 있었는데, 그때 구글이나 넷플릭스와 같은 사례에 대해 들을 수 있었지만, 그에 대한 디테일한 프렉틱스는 찾을 수 가 없었다.


여러 고민을 하고 있다가 구글에 입사한 후에, 구글의 Devops에 대해서 알게되었고, 여러 자료를 찾아서 공부하고 나니 어느정도 이해가 되서, 개념을 정리해놓는다.

Devops와 SRE

일반적으로 개발팀은 주어진 시간내에 새로운 기능을 내기 위해서 개발 속도에 무게를 두고, 운영팀의 경우에는 시스템 안정성에 무게를 둔다. 그래서 개발팀이 무리하게 기능을 배포하게 되면 장애로 이어지고, 이러한 장애로 인하여 서로를 욕하는 상황이 만들어져서 팀이 서로 멀어지게된다. 그래서 Devops는 이러한 두팀을 한팀에 묶어 놓고 운영하는 문화이자 일종의 운영 철학이다.

그런데 그러면 운영팀과 개발팀을 묶어놓으면 운영을 하던 사람들은 무엇을 하는가? 요즘은 클라우드가 발전해서 왠만한 부분은 개발자들이 직접 배포하고 운영도 할 수 있지만 시스템이 커지면 여전히 운영의 역할은 필요하다. 그렇다면 Devops 엔지니어라고 이름을 바꾼 Devops 엔지니어들이 하는 일은 무엇인가?


그 해답을 구글의 SRE(Site Reliability Engineering)에서 찾을 수 있었는데, 개발자가 셀프 서비스로 운영을 하려면 그 플랫폼이 자동화되어 있어야 한다. 애플리케이션을  빌드하고 유연하게 배포하고, 이를 모니터링할 수 있는 플랫폼이 필요한데, SRE의 역할은 이러한 플랫폼을 개발하고, 이 플랫폼 위에서 개발자들이 스스로 배포,운영을 하는 것이 목표이다. 물론 완벽한 셀프 서비스는 불가능하다. 여전히 큰 장애 처리나 배포등은 SRE 엔지니어가 관여하지만 많은 부분을 개발팀이 스스로 할 수 있도록 점점 그 비중을 줄여 나간다.


그러면 구글 버전의 Devops인 SRE는 서로 다른것인가? 그 관계는 어떻게 되는가? 이 질문에 대해서는 다음 하나의 문장으로 정리할 수 있다.

“ class SRE implements Devops

Devops가 개발과 운영의 사일로(분단) 현상을 해결하기 위한 방법론이자 하나의 조직문화에 대한 방향성이다. 그렇다면 SRE는 구글이 Devops에 적용하기 위한 구체적인 프렉틱스(실사례)와 가이드로 생각하면 된다. 구글도 다른 기업들과 마찬가지로  회사의 성장과 더블어 2000 년도 즈음에 개발자들이 속도에 무게를 두고 운영팀이 안정성에 무게를 둬서 발생하는 문제에 부딪혔고, 이 문제를 풀고자 하는 시도를 하였는데 이것이 바로 SRE (Site Reliability Engineering)이다. SRE는 크게 3가지 방향으로 이런 문제를 풀려고 했는데,

  • 첫번째는, 가용성에 대한 명확한 정의

  • 두번째는, 가용성 목표 정의

  • 세번째는, 장애 발생에 대한 계획

구글 팀은 이러한 원칙을 개발자/운영자뿐만 아니라 임원들까지 동의를 하였는데, 좀 더 구체적으로 이야기를 하면, 이러한 원칙에 따라 장애에 대한 책임을 모두 공유한다는 컨셉이다. 즉 장애가 나도 특정 사람이나 팀을 지칭해서 비난 하는게 아니라, 공동책임으로 규정하고 다시 장애가 나지 않을 수 있는 방법을 찾는 것이다.

위의 3가지 원칙에 따라서, 가용성을 측정을 위해서 어떤 지표를 사용할지를 명확히 정하고 두번째로는 그 지표에 어느 수준까지 허용을 할것인지를 정해서 그에 따른 의사결정은 하는 구조이다.

SRE는 단순히 구글의 운영팀을 지칭하는 것이 아니라, 문화와 운영 프로세스 팀 구조등 모든 개념을 포함한 포괄적인 개념이다.

What does an SRE Engineer do?

그러면 SRE에서 SRE엔지니어가 하는 일은 무엇일까? 아래 그림과 같이 크게 다섯까지 일을 한다.



<출처. 구글 넥스트 2018 발표 자료>

Metric & Monitoring

첫번째는 모니터링 지표를 정의하고, 이 지표를 모니터링 시스템을 올리는 일이다. 뒤에 설명하겠지만 구글에서는 서비스에 대한 지표를 SLI (Service Level Indictor)라는 것을 정하고, 각 지표에 대한 안정성 목표를 SLO (Service Level Objective)로 정해서 관리한다.

이러한 메트릭은 시스템을 운영하는 사람과 기타 여러 이해 당사자들에게 시스템의 상태를 보여줄 수 있도록 대쉬 보드 형태로 시각화 되어 제공된다.

그리고 마지막으로 할일은 이런 지표들을 분석해서 인사이트를 찾아내는 일이다. 시스템이 안정적인 상황과 또는 장애가 나는 지표는 무엇인지 왜인지? 그리고 이러한 지표를 어떻게 개선할 수 있는지를 고민한다. 기본적으로 SRE에서 가장 중요한점중 하나는 모든것을 데이타화하고, 의사결정을 데이타를 기반으로 한다.

Capacity Planning

두번째는 용량 계획인데, 시스템을 운영하는데 필요한 충분한 하드웨어 리소스(서버, CPU,메모리,디스크,네트워크 등)을 확보하는 작업이다. 비지니스 성장에 의한 일반적인 증설뿐만 아니라 이벤트나 마케팅 행사, 새로운 제품 출시등으로 인한 비정상적인 (스파이크성등) 리소스 요청에 대해서도 유연하게 대응할 수 있어야 한다.

시스템의 자원이란 시스템이 필요한 용량(LOAD), 확보된 리소스 용량 그리고 그 위에서 동작하는 소프트웨어의 최적화, 이 3가지에 대한 함수 관계이다.

즉 필요한 용량에 따라 적절하게 시스템 자원을 확보하는 것뿐만 아니라, 그 위에서 동작하는 소프트웨어 대한 성능 튜닝 역시 중요하다는 이야기다. 소프트웨어의 품질은 필요한 자원을 최소화하여 시스템 용량을 효율적으로 쓰게 해주기도 하지만 한편으로는 안정성을 제공해서 시스템 전체에 대한 안정성에 영향을 준다.

그래서 SRE 엔지니어는 자원 활용의 효율성 측면에서 소프트웨어의 성능을 그리고 안정성 측면에서 소프트웨어의 안정성을 함께 볼 수 있어야 한다.

Change Management

세번째는 한글로 해석하자면 변경 관리라고 해석할 수 있는데, 쉽게 이야기 하면 소프트웨어 배포/업데이트 영역이라고 보면 된다. (물론 설정 변경이나 인프라 구조 변경도 포함이 되지만)

시스템 장애의 원인은 대략 70%가 시스템에 변경을 주는 경우에 발생한다. 그만큼 시스템의 안정성에는 변경 관리가 중요하다는 이야기인데, 이러한 에러의 원인은 대부분 사람이 프로세스에 관여했을때 일어나기 때문에, 되도록이면 사람을 프로세스에서 제외하고 자동화하는 방향으로 개선 작업이 진행된다.

이러한 자동화의 베스트프래틱스는 다음과 같이 3가지 정도가 된다.

  • 점진적인 배포와 변경 (카날리 배포나 롤링 업데이트와 같은 방법)

  • 배포시 장애가 발생하였을 경우 빠르고 정확하게 해당 문제를 찾아낼 수 있도록 할것

  • 마지막으로 문제가 발생하였을때 빠르게 롤백할 수 잇을것

자동화는 전체 릴리스 프로세스 중에 일부분일 뿐이다. 잠재적인 장애를 막기 위해서는 코드 관리, 버전 컨트롤, 테스트 등 전체 릴리즈 프로세스를 제대로 정의 하는 것이 중요하다.

Emergency Response

네번째는 장애 처리이다. 시스템 안정성이란 MTTF(Mean Time to failure:장애가 발생하지 않고 얼마나 오랫동안 시스템이 정상 작동했는가? 일종의 건설현장의 "무사고 연속 몇일"과 같은 개념)와 MTTR(Mean time to recover:장애가 났을때 복구 시간)의 복합 함수와 같은 개념이다.

이 중에서 장애처리에 있어서 중요한 변수는 MTTR인데, 장애 시스템을 가급적 빠르게 정상화해서 MTTR을 줄이는게 목표중의 하나이다.

장애 복구 단계에서 사람이 직접 매뉴얼로 복구를 하게 되면 일반적으로 장애 복구 시간이 더 많이 걸린다. 사람이 컨트롤을 하되 가급적이면 각 단계는 자동화 되는게 좋으며, 사람이 해야 하는 일은 되도록이면 메뉴얼화 되어 있는 것이 좋다. 이것을 “Playbook”이라고 부르는데, 물론 수퍼엔지니어가 있는 경우에 수퍼엔지니어가 기가막히게 시스템 콘솔에 붙어서 장애를 해결할 수 있겠지만 대부분의 엔지니어가 수퍼엔지니어가 아니기 때문에, “Playbook” 기반으로 장애 처리를 할 경우 “Playbook”이 없는 경우에 비해 3배이상 MTTR이 낮다는 게 통계이다.

그리고 "Playbook”이 있다고 하더라도, 엔지니어들 마다 기술 수준이나 숙련도가 다르기 때문에, "Playbook”에 따른 장애 복구 모의 훈련을 지속적으로 해서 프로세스에 익숙해지도록 해야한다.

Culture

마지막으로 문화인데, SRE 엔지니어는 앞에서 설명한 운영에 필요한  작업뿐만 아니라 SRE 문화를 전반적으로 만들고 지켜나가는 작업을 해야 한다. 물론 혼자서는 아니라 전체 조직의 동의와 지원이 필요하고, 특히 경영진으로 부터의 동의와 신뢰가 없다면 절대로 성공할 수 없다.

나중에 설명하겠지만 SRE에는 Error budget 이라는 개념이 있는데, 모든 사람(경영층 포함)해서 이 Error budget에 대해서 동의를 하고 시작한다. Error budget은 특정 시스템이 일정 시간동안 허용되는 장애 시간이다. 예를 들어 일년에 1시간 장애가 허용 된다면 이 시스템의 Error budget는 1시간이고, 장애가 날때 마다 장애시간만큼 그 시간을 Error budget에서 차감한 후에, Error budget이 0이 되면 더 이상 신규 기능을 배포하지 않고 시스템 안정성을 올리는 데 개발의 초점을 맞춘다.

그런데 비지니스 조직에서 신규 기능 출시에 포커스하고 Error budget이 0이 되었는데도 신규 기능 릴리즈를 밀어붙이면 어떻게 될까? 아니면 시스템 운영 조직장이 Error budget이 10시간이나 남았는데도 불구하고 10분 장애가 났는데, 전체 기능 개발을 멈추고 시스템을 장애에 잘 견디게 고도화하라고 하면 어떻게 될까? 이러한 이유로 전체 조직이 SRE 원칙에 동의해야 하고,장애가 났을 때도 서로 욕하지 말고 책임을 나눠 가지는 문화가 필요하다.

이런 문화를 만들기 위해서는 크게 3가지 가이드가 있는데 다음과 같다.

  • 데이타에 기반한 합리적인 의사결정
    모든 의사결정은 데이타 기반으로 되어야 한다. 앞에서도 설명했듯이 이를 지키기 위해서는 임원이나 부서에 상관없이 이 원칙에 동의해야 하고, 이것이 실천되지 않는다면 사실상 SRE를 적용한다는 것은 의미가 없다. 많은 기업들이 모니터링 시스템을 올려서 대쉬 보드를 만드는 것을 봤지만 그건 운영팀만을 위한것이었고, SRE를 하겠다고 표방한 기업이나 팀들 역시 대표가 지시해서. 또는 임원이 지시해서 라는 말 한마디에 모든 의사결정이 무너지는 모습을 봤을 때, 이 원칙을 지키도록 고위 임원 부터 동의하지 않는 다면 SRE 도입 자체가 의미가 없다.

  • 서로 비난하지 않고, 장애 원인을 분석하고 이를 예방하는 포스트포턴 문화
    장애는 여러가지 원인에서 오지만, 그 장애 상황과 사람을 욕해봐야 의미가 없다. 장애는 이미 발생해버린 결과이고, 그 장애의 원인을 잘 분석해서 다음에 그 장애가 발생하지 않도록 하는  것이 중요하다. 보통 장애가 나고나서 회고를 하면 다음에는 프로세스를 개선한다던가. 주의하겠다는 식으로 마무리가 되는 경우가 많은데. 사람이 실수를 하도록 만든 프로세스와 시스템이 잘못된것이다. 사람은 고칠 수 없지만 시스템과 프로세스는 개선할 수 있다. 그리고 모든 개선은 문서화되어야 하고 가능한것들은 앞에서 언급한 Playbook에 반영되어야 한다.

  • 책임을 나눠가지는 문화
    그리고 장애에 대해 책임을 나눠 가지는 문화가 있어야 한다. 예를 들어 장애란 개발팀 입장에서 장애는 코드의 품질이 떨어져씩 때문에 장애가 일어난 것이고, 운영팀입장에서는 운영이 고도화 되지 않아씩 때문이며, 비지니스쪽에서는 무리하게 일정을 잡았기 때문이다.  책임을 나눠 가지는 문화는 누군가를 욕하지 않기 위해서라기 보다는 나의 책임으로 일어난 장애이기 때문에, 장애를 없애기 위한 노력도 나의 역할이 되고 동기가 된다.


지금까지 간단하게 나마 SRE의 개념과 SRE엔지니어가 무슨 일을 하는지에 대해서 설명하였다. 다음은 그러면 SRE 엔지니어들이 어떻게 이런일을 해나갈 수 있는지 How(방법)에 대해서 설명하도록 하겠다. 다음글 https://bcho.tistory.com/1325




Reference


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. scinix 2019.05.11 17:40  댓글주소  수정/삭제  댓글쓰기

    Change Management는 "변화관리"보다는 "변경관리"로 번역하시는 게 맞을 것 같네요. ("What does..." 챕터에 있는 대부분의 내용은 ITMS 운영에서 일반적인 내용인데, 해당 부분은 국내에서는 "변경관리"로 부릅니다. 그리고 "변화관리"는 조금 다른 의미로 쓰입니다.)
    글 뒤 쪽의 "포스트포던"은 아마 포스트모텀(Postmortem; 부검)의 오타가 아닌가 싶네요.

    참고로, 저는 이 주제와 관련해서 다음 팟케스트를 재밌게 들었... 아니, 읽었습니다.
    https://www.gcppodcast.com/post/episode-127-sre-vs-devops-with-liz-fong-jones-and-seth-vargo/

  2. sangikbae 2019.05.13 12:58  댓글주소  수정/삭제  댓글쓰기

    예를 들어 장애란 개발팀 입장에서 장애는 코드의 품질이 떨어져씩 때문에 장애가 일어난 것이고, 운영팀입장에서는 운영이 고도화 되지 않아씩 때문이며, 비지니스쪽에서는 무리하게 일정을 잡았기 때문이다. 책임을 나눠 가지는 문화는 누군가를 욕하지 않기 위해서라기 보다는 나의 책임으로 일어난 장애이기 때문에, 장애를 없애기 위한 노력도 나의 역할이 되고 동기가 된다.

    울림이있네요

  3. 제쉬 2019.05.14 08:04  댓글주소  수정/삭제  댓글쓰기

    관리자의 승인을 기다리고 있는 댓글입니다

SRE는 구글의 Devops의 프랙티스 로 구글의 서비스에 대한 배경과 철학을 읽을 수 있다.

SRE의 기본 사상중의 하나는 서비스의 안정성이 완벽할 수 없으며, (아니 완벽하지 않게 만들며) 장애를 허용하는 모델이다.

고 가용/고 성능 시스템을 만들기 위해서는 그만큼 많은 개발에 대한 노력이 소요되는데, 이로 인해서 기능 개발에 대한 속도가 느려지기 때문에, 사용자가 납득할만한 수준의 가용성을 제공하되 개발의 속도를 유지하는 철학이다.

배경을 살펴보면 구글은 모바일을 기반으로 한 B2C 서비스를 주력으로 하기 때문에, 서비스가 99.999%의 가용성을 제공하더라도, 스마트폰과 통신망 자체가 그정도의 안정성을 제공하지 않기 때문에, 백앤드 서비스가 높은 가용성을 제공하더라도 사용자가 느끼는 가용성은 그 정도 수준이 되지 않는다. 그렇기 때문에 장애를 허용하면서 적정 수준의 가용성을 가지는 구조로 Devops 운영 모델이 정립되었다고 생각한다. 이러한 내용은 구글 엔지니어들이 저술한 SRE 서적에서도 찾아볼 수 있다.


이러한 사상을 반영하여 실제 운영환경에 적용하기 위한 구체적인 가이드와 프랙틱스가 SRE이다. 예를 들어 운영에 측정 가능한 지표를 SLI (Service Level Indicator)로 정의하고, SLI에 대한 목표를 SLO로 정해서 수치화하고 서비스 운영의 지표로 삼는 모델등 다양한 프랙틱스가 있는데, 이 부분은 차차 살펴봐야 겠다.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

스택 드라이버 로그로 로그 백앤드 구축하기

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


앞의 글에서까지 로그를 남기는 방법에 대해서 알아보았다. 이번 글에서는 로컬에 남긴 로그를 중앙으로 수집하여 모니터링할 수 있는 도구에 대해서 알아보고자 한다.

보통 로그 시스템은 오픈소스 기반의 ELK (Elastic search + Logstash + Kibana)를 많이 사용한다. 좋은 시스템이기는 하지만 러닝 커브가 있고, 구축과 운영에 노력이 들어간다.

대안으로는 클라우드 기반의 매니지드 서비스를 사용하는 방안이 있는데, 구글 클라우드의 스택드라이버 로깅이 사용이 편리하기 때문에 스택드라이버를 소개하고자 한다.

구글 클라우드의 스택드라이버는 로깅뿐만 아니라 모니터링, 에러 리포팅등 다양한 기능을 제공하는 운영용 도구 이다. 그 중에서 이 글에서는 스택드라이버 로깅만을 설명한다.


스택드라이버 로깅이 로그를 수집하는 방법은 크게, SDK를 사용하는 방법과, 로그 에이전트를 사용하는 방법 두가지가 있다. SDK를 이용하는 방법의 경우에는 잘 알려진 로깅 프레임웍과 잘 통합이 되는 장점을 가지고 있으나, 애플리케이션이 아닌 데이타 베이스나 웹서버와 같은 솔루션 로그 수집은 SDK를 사용할 수 없으니 불가능하고, 경우에 따라서 로깅 프레임워크가 지원되지 않는 경우가 있기 때문에, 이 글에서는 에이전트를 이용하는 방식에 대해서 설명한다.


SDK를 이용하는 방법은 자바는 SLF4J+Logback을 이용하면 되는데, 이글을 참고하면 된다. node.js 예제는 이글을 참고하기 바란다. 로깅 시스템의 개념에서 부터, 시스템을 확장하는 방법까지 자세하게 설명해놓았기 때문에, 두 글을 모두 읽어보는것을 추천한다.

스택드라이버 로그 에이전트

스택드라이버 로그 에이전트는 오픈소스 fluentd 기반으로 개발되었다. 파일뿐만 아니라 여러 데이타 소스에서 로그를 읽어서 클라우드나 데이타베이스로 데이타가 전송이 가능하다.

설치 방법은 이 문서에 잘 정리되어 있기 때문에, 별도로 설명하지 않는다. 단 주의할점은 스택드라이버 로그 에이전트는 AWS와 구글 클라우드에서만 사용이 가능하다.

스택드라이버 로그 에이전트를 설치하면 syslog등 디폴트로 시스템 로그를 수집한다. 디폴트로 수집하는 로그 리스트와 로그 파일의 경로는 이 문서 를 참고하면 된다.

 

스택 드라이버 로그 에이전트의 설정 정보는 /etc/google-fluentd/config.d 디렉토리에 저장되어 있다. 에이전트의 상태는

$ sudo service google-fluentd status

명령을 이용하면 현재 에이전트가 제대로 작동하는지 확인이 가능하다.

에이전트 테스트

설치후 디폴트로 syslog 로그를 수집하기 때문에, 테스트를 위해서는 syslog에 로그를 남겨보면 된다. logger 라는 리눅스 명령어는 syslog에 로그를 남기는 명령어이다.

$ logger “테스트 메세지"

를 남기면, syslog 파일에 저장이 되고, 이 로그는 자동으로 스택드라이버 에이전트에 의해서 서버로 전송이 된다.  아래는 hello terry 등의 문자열을 테스트로 남긴 예제이다.


구글 스택드라이버 로그 웹 콘솔에서 GCE VM Instance 카테고리를 선택해서 보면 아래와 같이 logger에 의해서 보낸 로그가 전달된것을 확인할 수 있다.



에이전트 설정

이 예제에서는 Spring Boot 애플리케이션에서 로그를 파일로 남긴 후에, 이 파일을 스택드라이버 로그 에이전트를 통해서 수집하는 시나리오를 구현한다. 아래 예제에 사용한 Spring Boot 소스코드는 이 링크에 있다. 스택 드라이버 로그 에이전트에 대한 설정 방법은 이 문서를 참고하면 된다.


새로운 로그 파일을 정의하기 위해서는 스택드라이버 로그 에이전트의 설정 파일을 추가해야 한다.

/etc/google-fluentd/config.d 디렉토리 아래 springboot 파일에 설정 정보를 아래와 같이 기술하고 저장한다.


<source>

   @type tail

   # Format 'none' indicates the log is unstructured (text).

   format json

   # The path of the log file.

   path /home/terrycho/log.out

   # The path of the position file that records where in the log file

   # we have processed already. This is useful when the agent

   # restarts.

   pos_file /var/lib/google-fluentd/pos/springboot-log.pos

   read_from_head true

   # The log tag for this log input.

   tag springboot

</source>


path 부분에 로그 파일의 위치를 지정한다. 여기서는 Spring boot 애플리케이션의 로그를 /home/terrycho/log.out 파일에 남기도록 하였기 때문에, 이 파일 경로를 지정한다. 그리고 pos_file은 로그 파일을 어디까지 읽었는지를 표시하는 파일인데, 위의 경로에 지정한다.

마지막으로 tag는 로그를 구별하기 위해서 주는 값으로 여기서는 springboot 라는 태그를 부여하였다.

이 tag를 이용하여 로그 이름은 "projects/[Project-ID]/logs/[tag]” 이름으로 정의된다. 예를 들어 구글 클라우드 프로젝트 이름이 myproject이고, 태그를 springboot로 지정하였으면, 이 로그의 이름은 “projects/myproject/logs/springboot”로 지정된다.

설정이 끝났으면

%sudo service google-fluentd restart

명령어를 이용하여 스택드라이버 로그 에이전트를 재시작한다. 그리고 curl 명령어를 이용하여 Spring boot 애플리케이션에 트래픽을 줘서 로그를 남기게 되면 아래와 같이 로그가 스택드라이버 콘솔로 전송된것을 확인할 수 있다.

애플리케이션에서 JSON으로 저장한 로그는 스택드라이버 로그 엔트리에서 jsonPayload 아래에 json 형태로 저장된다.


<그림. 로그 예제>


그리고, 이 예제는 Zipkin과 MDC를 통합하여 traceId를 넘기는 형태로 아래 화면은 같은 Trace Id로 들어온 요청만 쿼리한 결과이다. trace Id를 통해서 하나의 리퀘스트로 들어온 모든 로그들을 모아볼 수 있다. 아래 두 로그를 보면 jsonPayload > mdc > traceId가 같다.


< 그림. 동일 트레이스 ID로 추적한 결과 >

스택드라이버 로그는 Export 기능을 이용하여 빅쿼리나 클라우드 스토리지로 export가 가능한데, 아래 화면은 테스트용 VM 인스턴스의 로그만 빅쿼리로 export 하도록 설정하는 화면이다.


<그림. Log Export 지정>


이렇게 빅쿼리로 로그가 Export 되면 아래 그림과 같이 SQL을 이용해서 로그를 분석할 수 있다.



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

Deep learning VM

아키텍쳐 /머신러닝 | 2018.12.05 05:36 | Posted by 조대협


클라우드에서 pre-built되서 제공되는 VM 이미지

GPU 드라이버, Tensorflow, Skitlearn,Pytorch들도 다 들어가 있고, 노트북이나 텐서보드도 들어가 있음 SSH Shell forwarding을 이용해서 쉽게 접속 가능함


https://cloud.google.com/deep-learning-vm/docs/concepts-images


gcloud compute ssh {VM name} -- -L 8888:localhost:8888 -L 6006:localhost:6006 -L 8080:localhost:8080

'아키텍쳐  > 머신러닝' 카테고리의 다른 글

피쳐 크로싱 (Feature crossing)  (0) 2019.05.21
Deep learning VM  (2) 2018.12.05
본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. 2018.12.10 18:02  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

  2. 손현술 2018.12.10 18:04  댓글주소  수정/삭제  댓글쓰기

    관리자의 승인을 기다리고 있는 댓글입니다

쿠버네티스 #19

보안 4/4 - Pod Security Policy

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



SecurityContext가 컨테이너나 Pod의 보안 기능을 정의 하는 것이라면, Pod Security Policy (이하 PSP)는 보안 기능에 대한 정책을 정의 하는 것이다.

예를 들어, 정책으로 Pod를 생성할때는 반드시 root 사용자를 사용하지 못하도록 강제한다던지, Privileged 모드를 사용못하도록 강제할 수 있다. 현재는 (2018년9월1일) 베타 상태이기 때문에 다소의 기능 변경이 있을 수 있음을 염두하고 사용하도록 하자.

개념

개념이 복잡하기 때문에 먼저 기본적인 개념을 이해한 후에, 각 상세를 살펴보도록 하자.

먼저 아래 그림을 보자 PSP는 생성후에, 사용자에게 지정이 된다.

그리고 Pod를 생성할때, Pod의 보안 요건을 SecurityContext를 이용해서 Pod 설정에 정의한다.

Pod를 생성하려고 할때, 생성자(사용자)의 PSP를 레퍼런스 하는데, Pod의 보안 요건이 사용자에게 정의되어 있는 PSP 요건을 만족하면, Pod가 생성된다.



반대로, Pod를 생성할때, Pod의 보안 요건 (SecurityContext)가 Pod를 생성하고자하는 사용자의 PSP요건을 만족하지 않으면, Pod 생성이 거부된다. 아래 그림은 사용자의 PSP에서 Privileged 모드를 사용할 수 없도록 설정하였으나, Pod를 생성할때 Privileged 모드를 Pod 가 사용할 수 있도록 설정하였기 때문에, Pod를 생성에 실패하는 흐름이다.




Pod Security Policy

Pod Security Policy는 Security Context와 달리 클러스터 리소스 (Cluster Resource)이다.

즉 적용하는 순간 클러스터 전체에 적용이 된다는 이야기이다.


정책 종류

Pod Security Policy를 통해서 통제할 수 있는 정책은 다음과 같다.

(출처 https://kubernetes.io/docs/concepts/policy/pod-security-policy/) 자세한 내용은 원본 출처를 참고하기 바란다.


Control Aspect

Field Names

Running of privileged containers

privileged

Usage of host namespaces

hostPID, hostIPC

Usage of host networking and ports

hostNetwork, hostPorts

Usage of volume types

volumes

Usage of the host filesystem

allowedHostPaths

White list of Flexvolume drivers

allowedFlexVolumes

Allocating an FSGroup that owns the pod’s volumes

fsGroup

Requiring the use of a read only root file system

readOnlyRootFilesystem

The user and group IDs of the container

runAsUser, supplementalGroups

Restricting escalation to root privileges

allowPrivilegeEscalation, defaultAllowPrivilegeEscalation

Linux capabilities

defaultAddCapabilities, requiredDropCapabilities, allowedCapabilities

The SELinux context of the container

seLinux

The AppArmor profile used by containers

annotations

The seccomp profile used by containers

annotations

The sysctl profile used by containers

annotations



포맷

PSP의 포맷을 이해하기 위해서 아래 예제를 보자

apiVersion: extensions/v1beta1

kind: PodSecurityPolicy

metadata:

 name: nonroot-psp

spec:

 seLinux:

   rule: RunAsAny

 supplementalGroups:

   rule: RunAsAny

 runAsUser:

   rule: MustRunAsNonRoot

 fsGroup:

   rule: RunAsAny

 volumes:

 - '*'


nonroot-psp 라는 이름으로 PSP를 정의하였고, seLinux,supplementalGroup,fsGroup과 volumes(디스크)에 대한 권한은 모두 허용하였다. runAsUser에 rule (규칙)을 MustRunAsNonRoot로 지정해서, 이 정책을 적용 받은 사용자는 Pod를 생성할때 Pod가 반드시 root 사용자가 아닌 다른 사용자를 지정하도록 정의했다.

PSP 사용자 적용

PSP 를 정의하고 실행한다고 해도, 실제로 적용되지 않는다. PSP를 적용하기 위해서는 생성한 PSP를 RBAC을 이용하여 ClusterRole을 만들고, 이 ClusterRole을 사용자에게 부여해야 실제로 정책이 적용되기 시작한다. 사용자에게 PSP를 적용하는 부분은 뒤의 예제에서 살펴보자

이때 주의할점은 사용자의 정의인데, 쉽게 생각하면 사용자를 사람으로만 생각할 수 있는데, 쿠버네티스의 사용자는 사람이 될 수 도 있지만 서비스 어카운트 (Service account)가 될 수 도 있다.

쿠버네티스에서 Pod를 생성하는 주체는 사용자가 kubectl 등으로 Pod를 직접생성할 경우, 사람이 사용자가 되지만, 대부분의 경우 Pod의 생성과 관리는 Deployment나 ReplicaSet과 같은 컨트롤러를 이용하기 때문에, 이 경우에는 컨트롤러들이 사용하는 서비스 어카운트가 사용자가 되는 경우가 많다.

그래서, PSP를 적용하는 대상은 일반 사용자가 될 수 도 있지만 서비스 어카운트에 PSP를 적용해야 하는 경우가 많다는 것을 반드시 기억해야 한다.

PSP 활성화

PSP는 쿠버네티스 클러스터에 디폴트로는 비활성화 되어 있다. PSP 기능을 사용하기 위해서는 이를 활성화 해야 하는데, PSP는 admission controller에 의해서 컨트롤 된다.

구글 클라우드

구글 클라우드에서 PSP를 활성화 하는 방법은 아래와 같이 gcloud 명령을 이용하면 된다.


%gcloud beta container clusters update {쿠버네티스 클러스터 이름} --enable-pod-security-policy --zone={클러스터가 생성된 구글 클라우드 존}


만약에 활성화된 PSP 기능을 비활성화 하고 싶으면 아래와 같이 gcloud 에서 --no-enable-pod-security-policy  옵션을 사용하면 된다.


gcloud beta container clusters update {쿠버네티스 클러스터 이름}  --no-enable-pod-security-policy --zone={클러스터가 생성된 구글 클라우드 존}

Minikube

minikube start --extra-config=apiserver.GenericServerRunOptions.AdmissionControl=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds,PodSecurityPolicy


주의할점은 PSP 기능이 활성화된후에, PSP가 적용되지 않은 사용자(사람과, 서비스어카운트 모두)의 경우에는 Pod를 생성할 수 없기 때문에, 기존에 잘 생성되던 Pod가 갑자기 생성되지 않는 경우가 많기 때문에, 반드시 기능을 활성화하기 전에 반드시, 사용자마다 적절한 PSP를 생성해서 적용하기 바란다. (PSP기능을 활성화하지 않더라도 기본적으로 PSP 정의및, PSP를 사용자에게 적용하는 것은 가능하다.)

예제

개념에 대한 이해가 끝났으면 이제 실제 예제를 통해서 어떻게 PSP를 생성 및 적용하는지를 알아보도록 하자. 예제는 다음 순서로 진행하도록 한다.

  1. PSP 정의 : Root 권한을 사용이 불가능한 PSP를 생성한다.

  2. 서비스 어카운트 생성 : PSP를 생성할 서비스 어카운트를 생성한다. Pod를 바로 생성하는 것이 아니라 Deployment를 통해서 생성할것이기 때문에 Deployment에서 이 서비스 어카운트를 사용할것이다.

  3. ClusterRole 생성 : 다음 1에서 만든 PSP를 2에서 만든 서비스 어카운트에 적용하기 위해서, PSP를 가지고 있는 ClusterRole을 생성한다.

  4. ClusterRoleBinding을 이용하여 서비스 어카운트에 PSP 적용 : 3에서 만든 ClusterRole을 2에서 만든 서비스 어카운트에 적용한다.

  5. Admission controller 활성화 : PSP를 사용하기 위해서 Admission controller를 활성화 한다.

  6. Pod 정의 및 생성 : 2에서 만든 서비스 어카운트를 이용하여 Deployment 를 정의한다.

  7. 테스트 : 테스트를 위해서, root user를 사용하는 deployment와, root user를 사용하지 않는 deployment 두개를 각각 생성해서 psp 가 제대로 적용되는지를 확인한다.

PSP 정의

PSP를 정의해보자. 아래와 같이 nonroot-psp.yaml 을 작성한다. 이 PSP는 runAsUser에서 MustRunAsNotRoot 규칙을 추가해서, Root 권한으로 컨테이너가 돌지 않도록 하는 정책이다.


# nonroot-psp.yaml

apiVersion: extensions/v1beta1

kind: PodSecurityPolicy

metadata:

 name: nonroot-psp

spec:

 seLinux:

   rule: RunAsAny

 supplementalGroups:

   rule: RunAsAny

 runAsUser:

   rule: MustRunAsNonRoot

 fsGroup:

   rule: RunAsAny

 volumes:

 - '*'


파일을 nonroot-psp.yaml 파일로 저장한후에,

%kubectl create -f nonroot-psp.yaml

명령어를 이용하여 PSP를 생성한후에,

%kubectl get psp

명령을 이용하여, PSP가 생성된것을 확인하자




서비스 어카운트 생성

서비스 어카운트 생성을 위해서 아래 yaml 파일을 작성하고, 서비스 어카운트를 생성하여 확인하자


#nonroot-sa.yaml

apiVersion: v1

kind: ServiceAccount

metadata:

 name: nonroot-sa



ClusterRole 생성 및 적용

서비스 어카운트를 생성하였으면, 앞에 만든 PSP nonroot-psp 를 사용하는 ClusterRole nonroot-clusterrole을 생성하고, 이 롤을 nonroot-clusterrole-bindings를 이용하여, 앞서 만든 서비스 어카운트 nonroot-sa 에 연결한다.


아래와 같이 ClusterRole을 생성하는데, resouces 타입을 podsecuritypolicies 로 정의하고, 리소스 이름은 앞에서 생성한 PSP인 nonroot-psp로 지정한다. 그리고, 이 psp를 사용하기 위해서 verb는 “use”로 지정한다

#nonroot-clusterbinding.yaml

apiVersion: rbac.authorization.k8s.io/v1

kind: ClusterRole

metadata:

 name: nonroot-clusterrole

rules:

- apiGroups:

 - policy

 resources:

 - podsecuritypolicies

 resourceNames:

 - nonroot-psp

 verbs:

 - use


%kubectl create -f nonroot-clusterrole.yaml

명령어를 이용하여 위의 ClusterRole을 생성한후에, 이 ClusterRole을 서비스 어카운트 nonroot-sa 에 적용하자.

아래와 같이 nonroot-clusterrolebinding.yaml 를 생성한후,


#nonroot-clusterrolebinding.yaml

apiVersion: rbac.authorization.k8s.io/v1

kind: ClusterRoleBinding

metadata:

 name: nonroot-clusterrole-bindings

subjects:

- kind: ServiceAccount

 name: sa-nonroot

 namespace: default

roleRef:

 apiGroup: rbac.authorization.k8s.io

 kind: ClusterRole

 name: nonroot-clusterrole


%kubectl create -f nonroot-clusterrolebinding.yaml

명령어를 이용하여 ClusterRole nonroot-clusterrole을 서비스 어카운트 sa-nonroot에 적용한다.

도커 컨테이너 생성

이제 PSP가 생성되었고, 이 PSP를 사용하는 서비스 어카운트 nonroot-sa 가 완성되었으면, 이를 실제로 배포에 적용해보자. 배포에 앞서서 컨테이너 이미지를 만든다.

아래는 Docker 파일인데, 앞의 보안 컨텍스트 설명때 사용한 컨테이너와 동일하다.


#Dockerfile

FROM node:carbon

EXPOSE 8080

RUN groupadd -r -g 2001 appuser && useradd -r -u 1001 -g appuser appuser

RUN mkdir /home/appuser && chown appuser /home/appuser

USER appuser

WORKDIR /home/appuser

COPY --chown=appuser:appuser server.js .

CMD node server.js > /home/appuser/log.out

생성된 도커이미지를 gcr.io/terrycho-sandbox/nonroot-containe:v1 이름으로 docker push 명령을 이용해서  컨테이너 레지스트리에 등록한다.

PSP 기능 활성화

이미지까지 준비가 되었으면, 이제 Pod를 생성할 모든 준비가 되었는데, PSP를 사용하려면, 쿠버네티스 클러스터에서 PSP 기능을 활성화 해야 한다.

다음 명령어를 이용해서 PSP를 활성화한다.

%gcloud beta container clusters update {쿠버네티스 클러스터 이름} --enable-pod-security-policy --zone={클러스터가 생성된 구글 클라우드 존}


아래 그림과 같이 PSP 기능이 활성화 되는 것을 확인한다.


Deployment 생성

기능 활성화가 끝났으면, 이제 Pod를 deploy해보자.

아래는 nonroot-deploy.yaml 파일이다.


#nonroot-deploy.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

 name: nonroot-deploy

spec:

 replicas: 3

 selector:

   matchLabels:

     app: nonroot

 template:

   metadata:

     name: nonroot-pod

     labels:

       app: nonroot

   spec:

     serviceAccountName: nonroot-sa

     securityContext:

       runAsUser: 1001

       fsGroup: 2001

     containers:

     - name: nonroot

       image: gcr.io/terrycho-sandbox/security-context:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


우리가 nonroot-psp를 사용하기 위해서, 이 psp가 정의된 서비스 어카운트 nonroot-sa를 사용하도록 하였다. 그래고 nonroot-psp에 정의한데로, 컨테이너가 root 권한으로 돌지 않도록 securityContext에 사용자 ID를 1001번으로 지정하였다.

%kubectl create -f nonroot-deploy.yaml

을 실행한후,

%kubectl get deploy 명령어를 실행해보면 아래와 같이 3개의 Pod가 생성된것을 확인할 수 있다.


보안 정책에 위배되는 Deployment 생성

이번에는 PSP 위반으로, Pod 가 생성되지 않는 테스트를 해보자.

아래와 같이 root-deploy.yaml 이라는 이름으로, Deployment 스크립트를 작성하자.


#root-deploy.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

 name: root-deploy

spec:

 replicas: 3

 selector:

   matchLabels:

     app: root

 template:

   metadata:

     name: root-pod

     labels:

       app: root

   spec:

     serviceAccountName: nonroot-sa

     containers:

     - name: root

       image: gcr.io/terrycho-sandbox/nonroot-containe:v1

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


이 스크립트는 앞에서 작성한 nonroot-deploy.yaml 과 거의 유사하지만 Security Context에서 사용자 ID를 지정하는 부분이 없기 때문에, 디폴트로 root로 컨테이너가 기동된다. 그래서 PSP에 위반되게된다.


%kubectl create -f root-deploy.yaml

을 실행하면 결과가 아래와 같다.



맨 아래 root-deploy-7895f57f4를 보면, Current 가 0으로 Pod가 하나도 기동되지 않았음을 확인할 수 있다.

원인을 파악하기 위해서 Pod를 만드는 ReplicaSet을 찾아보자

%kubectl get rs

명령을 아래와 같이 ReplicaSet 리스트를 얻을 수 있다.

%kubectl describe rs root-deploy-7895f57f4

명령을 실행해서 ReplicaSet의 디테일과 로그를 확인해보면 다음과 같다.



그림과 같이 Pod 생성이 정책 위반으로 인해서 실패한것을 확인할 수 있다.


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요


쿠버네티스 #18

보안 3/4 - Security Context

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

보안 컨택스트

보안 컨택스트 (Security context)는 쿠버네티스의 Pod나 컨테이너에 대한 접근 제어 설정(Access Control Setting)이나, 특수 권한 (Privilege)를 설정하는 기능을 제공한다. 단어가 추상적이기 때문에 바로 이해하기 약간 어려울 수 있는데, 몇가지 예를 들어보면, 컨테이너 내에서 동작하는 프로세스의 사용자 ID (UID)나, 그룹 ID (GID)를 설정하거나, 프로세스에 커널에 대한 접근 권한을 부여하는 것과 같은 기능을 수행할 수 있다.


구체적으로 보안 컨택스트가 지원하는 기능은 다음과 같다. 예제와 병행해서 살펴보도록 하자

예제에 사용된 코드는 https://github.com/bwcho75/kube101/tree/master/10.security/4.securityContext 에 있다.

프로세스 사용자 ID와 그룹 ID 지정

Pod나 컨테이너에서 구동되는 프로세스의 사용자 ID와 그룹 ID를 지정한다.

디폴트로 컨테이너에서 구동되는 모든 프로세스는 root 권한으로 실행이 된다. 이 경우 컨테이너 이미지가 오염되어, 악성적인 코드를 가지고 있을 경우에는 root 권한으로 컨테이너의 모든 기능을 장악할 수 있기 때문에, 이를 방지하기 위해서는 컨테이너 내에서 구동되는 사용자 애플리케이션 프로세스의 사용자 ID와 그룹 ID를 지정하여, 특정 자원 (파일이나 디렉토리)에 대한 액세스만을 허용하게 할 필요가 있다.

또한 프로세스의 사용자 ID와 그룹 ID를 지정하면, 생성되는 파일 역시 지정된 사용자 ID와 그룹 ID 를 통해서 생성된다.


간단한 예제를 하나 보자.  우리가 계속 사용해왔던 server.js 로 node.js 서버를 하나 올리는 예제이다.

이 예제를 변경하여, 사용자 ID를 1000으로, 그리고 그룹 ID를 1000으로 지정해서 Pod를 올려보도록 하자.


몇가지 수정이 필요한데, 먼저 기존에 아래와 같이 사용했던 Dockerfile을

FROM node:carbon

EXPOSE 8080

COPY server.js .

CMD node server.js > log.out


log.out 파일 경로를 /log.out에서 아래와 같이 /home/node/log.out 으로 변경한다.

기존의 예제들의 경우에는 컨테이너와 Pod를 root 권한으로 수행했지만 이 예제에서는 runAs를 이용하여 사용자 ID가 1000인 사용자로 돌리기 때문에, 루트 (“/”) 디렉토리에 파일을 생성하려면 권한 에러가 난다.

사용자 ID 1000은 node:carbon 이미지에서 정의되어 있는 node 라는 사용자로, 디폴트로 /home/node 라는 사용자 디렉토리를 가지고 있기 때문에, 이 디렉토리에 파일을 쓰도록 아래와 같이 변경한다.


FROM node:carbon

EXPOSE 8080

COPY server.js .

CMD node server.js > /home/node/log.out


다음 yaml 파일을 작성한다. (1.runas.yaml)

apiVersion: v1

kind: Pod

metadata:

 name: runas

spec:

 securityContext:

   runAsUser: 1000

   fsGroup: 1000

 volumes:

 - name: mydisk

   emptyDir: {}

 containers:

 - name: runas

   image: gcr.io/terrycho-sandbox/security-context:v1

   imagePullPolicy: Always

   volumeMounts:

   - name: mydisk

     mountPath: /mydisk

   ports:

   - containerPort: 8080


위와 같이 securityContext에 runAsUser에 사용자 ID 1000을 그리고 fsGroup에 그룹 ID 1000을 지정하여 Pod를 생성한다. 그리고, mydisk 디스크 볼륨을 생성하여, /mydisk 디렉토리에 마운트 하였다.

생성후 결과를 보자.


생성된 Pod에

%kubectl exec -it runas /bin/bash

명령을 이용하여 로그인 한후 다음 그림과 같이 권한을 체크해본다.




먼저 ps -ef로 생성된 컨테이너들의 사용자 ID를 보면 위와 같이 node (사용자 ID가 1000임)으로 생성되어 있는것을 볼 수 있다.

다음 ls -al /home/node 디렉토리를 보면 컨테이너 생성시 지정한 로그 파일이 생성이 되었고 마찬가지로 사용자 ID와 그룹 ID가 node로 지정된것을 확인할 수 있다.

다음 마운트된 디스크의 디렉토리인 /mydisk 에 myfile이란 파일을 생성해도 파일의 사용자 ID와 그룹 ID가 node로 설정되는것을 확인할 수 있다.


앞의 예제의 경우에는 사용자를 이미지에서 미리 정해진 사용자(node)를 사용하였는데, 만약에 미리 정해진 사용자가 없다면 어떻게 해야 할까?

여러가지 방법이 있겠지만, 도커 이미지를 생성하는 단계에서 사용자를 생성하면 된다. 아래는 사용자를 생성하는 도커 파일 예제이다.


FROM node:carbon

EXPOSE 8080

RUN groupadd -r -g 2001 appuser && useradd -r -u 1001 -g appuser appuser

RUN mkdir /home/appuser && chown appuser /home/appuser

USER appuser

WORKDIR /home/appuser

COPY --chown=appuser:appuser server.js .

CMD node server.js > /home/appuser/log.out


RUN 명령을 이용하여 useradd와 groupadd로 사용자를 생성하고, mkdir로 사용자 홈디렉토리 생성을 한후, 해당 디렉토리의 사용자를 생성한 사용자로 변경한다

그 후에 명령을 실행하기 위해서 명령어를 실행하는 사용자를 변경해야 하는데, USER 명령을 이용하면 사용자를 변경할 수 있다. 이후 부터 생성되는 사용자는 USER에 의해서 지정된 사용자로 실행이 된다.

그 다음은 디렉토리를 WORKDIR을 이용해서 홈디렉토리로 들어가서 COPY와 CMD 명령을 순차로 실행한다.


실행 결과 디렉토리를 확인해보면



와 같이 모든 파일이 앞에서 생성한 appuser 라는 사용자 ID로 생성이 되어 있고, 그룹 역시 appuser로 지정되어 있는 것을 확인할 수 있다.


프로세스를 확인해보면 아래와 같이 앞에서 생성한 appuser라는 사용자로 프로세스가 기동됨을 확인할 수 있다.




SecurityContext for Pod & Container

보안 컨택스트의 적용 범위는 Pod 에 적용해서 Pod 전체 컨테이너에 적용되게 할 수 도 있고, 개발 컨테이너만 적용하게 할 수 도 있다.


아래 예제의 경우에는 컨테이너에 보안 컨택스트를 적용한 예이고,

pods/security/security-context-4.yaml  

apiVersion: v1

kind: Pod

metadata:

 name: security-context-demo-4

spec:

 containers:

 - name: sec-ctx-4

   image: gcr.io/google-samples/node-hello:1.0

   securityContext:

     capabilities:

       add: ["NET_ADMIN", "SYS_TIME"]


아래 예제는 Pod 전체의 컨테이너에 적용한 예이다.

apiVersion: v1

kind: Pod

metadata:

 name: runas

spec:

 securityContext:

   runAsUser: 1000

   fsGroup: 1000

 volumes:

 - name: mydisk

   emptyDir: {}

 containers:

 - name: runas

   image: gcr.io/terrycho-sandbox/security-context:v1

   imagePullPolicy: Always

   volumeMounts:

   - name: mydisk

         : (중략)


노드 커널에 대한 억세스 권한을 제어

쿠버네티스에서 동작하는 컨테이너는 호스트 OS와 가상적으로 분리된 상태에서 기동된다. 그래서, 호스트 커널에 대한 접근이 제한이 된다. 예를 들어 물리머신에 붙어 있는 디바이스를 접근하거나 또는 네트워크 인터페이스에 대한 모든 권한을 원하거나 호스트 머신의 시스템 타임을 바꾸는 것과 같이 호스트 머신에 대한 직접 억세스가 필요한 경우가 있는데, 컨테이너는 이런 기능에 대한 접근을 막고 있다. 그래서 쿠버네티스는 이런 기능에 대한 접근을 허용하기 위해서 privilege 모드라는 것을 가지고 있다.

도커 컨테이너에 대한 privilege 모드 권한은 https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities 문서를 참고하기 바란다.

NFS 디스크 마운트나 FUSE를 이용한 디스크 마운트나, 로우 레벨 네트워크 모니터링이나 통제가 필요할때 사용할 수 있다.

privilege 모드를 true로 하면 커널의 모든 권한을 사용할 수 있는데,

아래 예제는 https://github.com/kubernetes/examples/blob/master/staging/volumes/nfs/nfs-server-rc.yaml 의 일부로 NFS 볼륨을 마운트 하는 리소스 컨트롤러 설정의 일부이다. NFS로 마운트를 하기 위해서 컨테이너의 privileged: true 로 설정하여 privileged 모드로 컨테이너가 실행하게 한것을 확인할 수 있다.


containers:

- name: nfs-server

 image: k8s.gcr.io/volume-nfs:0.8

 ports:

   - name: nfs

     containerPort: 2049

   - name: mountd

     containerPort: 20048

   - name: rpcbind

     containerPort: 111

 securityContext:

   privileged: true


Privileged 모드가 커널의 모든 기능을 부여한다면, 꼭 필요한 기능만 부여할 수 있게 세밀한 컨트롤이 가능하다. 이를 위해서 Linux capability 라는 기능이 있는데, 이 기능을 이용하면 커널의 기능을 선별적으로 허용할 수 있다. 자세한 설명은 https://linux-audit.com/linux-capabilities-hardening-linux-binaries-by-removing-setuid/ 문서를 참고하기 바란다.


아래 예제는 https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container 중의 일부로, 컨테이너 생성시에, NET_ADMIN과 SYS_TIME 권한 만을 컨테이너에 부여한 내용이다.


pods/security/security-context-4.yaml  

apiVersion: v1

kind: Pod

metadata:

 name: security-context-demo-4

spec:

 containers:

 - name: sec-ctx-4

   image: gcr.io/google-samples/node-hello:1.0

   securityContext:

     capabilities:

       add: ["NET_ADMIN", "SYS_TIME"]

기타

Security context는 이 이외에도 다양한 보안 관련 기능과 리소스에 대한 접근 제어가 가능하다.


  • AppAmor
    는 리눅스 커널의 기능중의 하나로 애플리케이션의 리소스에 대한 접근 권한을 프로필안에 정의하여 적용함으로써, 애플리케이션이 시스템에 접근할 수 있는 권한을 명시적으로 정의 및 제한 할 수 있다.
    (https://wiki.ubuntu.com/AppArmor)

  • Seccomp
    Security computing mode 의 약자로, 애플리케이션의 프로세스가 사용할 수 있는 시스템 콜의 종류를 제한할 수 있다. (https://en.wikipedia.org/wiki/Seccomp)

SELinux
보안 리눅스 기능으로 AppAmor와 유사한 기능을 제공하는데, 리소스에 대한 접근 권한을 정책으로 정의해서 제공한다.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요


쿠버네티스 #16


보안 1/4 - 사용자 계정 인증 및 권한 인가

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


이번글 부터는 몇회에 걸쳐 쿠버네티스 계쩡 인증,인가, 네트워크등 보안에 관련된 부분을 알아보도록 하겠다.

모든 시스템이 그렇듯이, 쿠버네티스 역시 보안이 매우 중요하다. 쿠버네티스는 보안에 관련된 여러가지 기능을 제공하는데, 각각에 대해서 살펴 보도록 하자

사용자 인증 및 권한 관리

인증과 인가 (Authentication & Authorization)

먼저 인증과 인가에 대한 개념에 대해서 이해 하자



인증(Authentication)은 사용자가 누구인지를 식별하는 것이다. 흔히 생각하는 사용자 로그인을 생각하면 된다.  인가는 인증된 사용자가 해당 기능을 실행할 수 있는 권한이 있는지를 체크하는 기능이다.

인증 (Authentication)

쿠버네티스는 계정 체계를 관리함에 있어서 사람이 사용하는 사용자 어카운트와, 시스템이 사용하는 서비스 어카운트 두가지 개념을 제공한다.

사용자 어카운트

사용자 어카운트는 우리가 일반적으로 생각하는 사용자 아이디의 개념이다.

쿠버네티스는 자체적으로 사용자 계정을 관리하고 이를 인증(Authenticate)하는 시스템을 가지고 있지 않다. 반드시 별도의 외부 계정 시스템을 사용해야 하며, 계정 시스템 연동을 위해서 OAuth나 Webhook가 같은 계정 연동 방식을 지원한다.

서비스 어카운트

서비스 어카운트가 다소 낮설 수 있는데, 예를 들어 클라이언트가 쿠버네티스 API를 호출하거나, 콘솔이나 기타 클라이언트가 쿠버네티스 API를 접근하고자 할때, 이는 실제 사람인 사용자가 아니라 시스템이 된다. 그래서, 쿠버네티스에서는 이를 일반 사용자와 분리해서 관리하는데 이를 서비스 어카운트 (service account)라고 한다.

서비스 어카운트를 생성하는 방법은 간단하다.

%kubectl create sa {서비스 어카운트명}

을 실행하면 된다. 아래는 foo 라는 이름으로 서비스 어카운트를 생성하는 예이다.



인증 방법

그러면 계정이 있을때, 이 계정을 이용해서 쿠버네티스의 API에 어떻게 접근을 할까? 쿠버네티스는 사용자 인증을 위해서 여러가지 메커니즘을 제공한다.

용도에 따라서 다양한 인증 방식을 제공한다.


  • Basic HTTP Auth

  • Access token via HTTP Header

  • Client cert

  • Custom made


Basic HTTP Auth는 HTTP 요청에 사용자 아이디와 비밀번호를 실어 보내서 인증하는 방식인데, 아이디와 비밀번호가 네트워크를 통해서 매번 전송되기 때문에 그다지 권장하지 않는 방법이다.

Access token via HTTP Header는 일반적인 REST API 인증에 많이 사용되는 방식인데, 사용자 인증 후에, 사용자에 부여된 API TOKEN을 HTTP Header에 실어서 보내는 방식이다.

Client cert는 클라이언트의 식별을 인증서 (Certification)을 이용해서 인증하는 방식이다. 한국으로 보자면 인터넷 뱅킹의 공인 인증서와 같은 방식으로 생각하면 된다. 보안성은 가장 높으나, 인증서 관리에 추가적인 노력이 필요하다.

그러면 쉽지만 유용하게 사용할 수 있는 Bearer token 방식의 인증 방식을 살펴보도록 하자

Bearer token을 사용하는 방법

인증 메커니즘 중에서 상대적으로 가장 간단한 방법은 API 토큰을 HTTP Header에 넣는 Bearer token 인증 방식이 있다.

서비스 어카운트의 경우에는 인증 토큰 정보를 secret에 저장을 한다. 이 토큰 문자열을 가지고, HTTP 헤더에 “Authorization: Bearer {토큰문자열}로 넣고 호출하면 이 토큰을 이용해서 쿠버네티스는 API 호출에 대한 인증을 수행한다.


서비스 어카운트에서 토큰 문자열을 가지고 오는 방법은

%kubectl describe sa {service account 이름}

을 실행하면 아래와 같이 Token 항목에 토큰을 저장하고 있는 secret 이름이 나온다.


위의 그림에서는 foo-token-zvnzz 이다. 이 이름으로 secret을 조회해보면,

%kubectl describe secret {시크릿명}

명령을 실행하면 아래와 같이 token이라는 항목에, 토큰이 문자열로 출력이 된다.



이 토큰을 HTTP Header 에 “Authorization: Bearer {토큰문자열}” 식으로 넣고 호출하면 된다.


간단한 스크립트를 통해서 API를 호출하는 것을 테스트 해보자

% APISERVER=$(kubectl config view | grep server | cut -f 2- -d ":" | tr -d " ")
명령을 수행하면 환경 변수 APISERVER에 현재 쿠버네티스 클러스터의 API SERVER IP가 저장된다.


다음 APISERVER의 주소를 알았으니

% curl $APISERVER/api

명령을 이용해서 HTTP GET의 /api를 호출해보자. 호출을 하면 SSL 인증서에 대한 인증 에러가 발생한다.


이는 API를 호출할때 인증에 필요한 정보를 기재하지 않았기 때문에, 디폴트로 Client cert를 이용한 인증을 시도하게 되고, 인증서를 지정하지 않았기 때문에 에러가 나게 되는것이다.


그러면 인증 정보를 제대로 지정하기 위해서 서비스 어카운트 default의 토큰을 얻어서 호출해보도록 하자.

다음 스크립트는 서비스 어카운트 default의 secret에서 토큰을 추출해서 저장하는 스크립트이다.

%TOKEN=$(kubectl describe secret $(kubectl get secrets | grep default | cut -f1 -d ' ') | grep -E '^token' | cut -f2 -d':' | tr -d '\t')

스크립트를 실행한후 TOKEN의 내용을 찍어 보면 아래와 같이 API TOKEN이 저장된것을 확인할 수 있다.




다음 이 토큰을 이용해서 API를 호출하면 된다.

%curl https://35.189.143.107/api --header "Authorization: Bearer $TOKEN" --insecure




Kubectl proxy를 이용한 API 호출

앞에서는 HTTP Header에 토큰을 직접 입력하는 방식을 사용했지만, 이렇게 사용하는 경우는 드물다. curl을 이용해서 호출할 경우에는 kubectl proxy 명령어를 이용해서 proxy를 설정하고 proxy로 API URL을 호출하면, 자동으로 이 Proxy가 현재 클라이언트의 kubeconfig file에 저장되어 있는 Credential (인증 정보)를 채워서 자동으로 보내준다.


%kubectl proxy --port=8080

을 실행하게 되면, localhost:8080을 프록시로 하여 쿠버네티스 API서버로 요청을 자동으로 포워딩 해준다.


그리고 curl localhost:8080/api 를 호출하면 {쿠버네티스 API Server}/api 를 호출해주게 된다.




SDK를 이용한 호출

일반적으로 간단한 테스트가 아닌 이상, curl 을 이용해서 직접 API를 호출하는 경우는 드물고, SDK를 사용하게 된다.  쿠버네티스에는 Go/Python/Java/Javascript 등 다양한 프로그래밍 언어를 지원하는 SDK가 있다.

https://kubernetes.io/docs/reference/using-api/client-libraries/#officially-supported-kubernetes-client-libraries


이들 SDK 역시, kubectl proxy 처럼, 로컬의 kubeconfig file의 Credential 정보를 이용해서 API를 인증하고 호출 한다.

권한 관리 (Authorization)

계정 체계와 인증에 대한 이해가 끝났으면, 이번에는 계정 권한에 대해서 알아보자. 쿠버네티스의 권한 처리 체계는 기본적으로 역할기반의 권한 인가 체계를 가지고 있다. 이를 RBAC (Role based access control)이라고 한다.


권한 구조를 도식화 해보면 다음과 같이 표현할 수 있다.


  • 사용자의 계정은 개개별 사용자인 user, 그리고 그 사용자들의 그룹은 user group, 마지막으로 시스템의 계정을 정의하는 service account로 정의된다.

  • 권한은 Role이라는 개념으로 정의가 되는데, 이 Role에는 각각의 리소스에 대한 권한이 정의된다. 예를 들어 pod 정보에대한 create/list/delete등을 정의할 수 있다. 이렇게

  • 이렇게 정의된 Role은 계정과 RoleBinding 이라는 정의를 통해서, 계정과 연결이 된다.


예제를 살펴보자, 아래는 Role을 정의한 yaml 파일이다.

pod-reader라는 Role을 정의하였고, pods에 대한 get/watch/list를 실행할 수 있는 권한을 정의하였다.



다음 이 Role을 사용자에게 부여하기 위해서 RoleBinding 설정을 아래와 같이 정의하자.

아래 Role-Binding은 read-pods라는 이름으로 jane이라는 user에서 Role을 연결하였고, 앞에서 정의한 pod-reader를 연결하도록 정의하였다.




이 예제를 그림으로 표현하면 다음과 같다.



Role vs ClusterRole

Role은 적용 범위에 따라 Cluster Role과 일반 Role로 분리 된다.

Role의 경우 특정 네임스페이스내의 리소스에 대한 권한을 정의할 수 있다.

반면 ClusterRole의 경우, Cluster 전체에 걸쳐서 권한을 정의할 수 있다는 차이가 있다.

또한 ClusterRole의 경우에는 여러 네임스페이스에 걸쳐 있는 nodes 와 같은 리소스스나 /heathz와 같이 리소스 타입이 아닌 자원에 대해서도 권한을 정의할 수 있다.

Role과 ClusterRole은 각각 RoleBinding과 ClusterRoleBinding 을 통해서 사용자에게 적용된다.

Predefined Role

쿠버네티스에는 편의를 위해서 미리 정해진 롤이 있다.


Default ClusterRole

Default ClusterRoleBinding

Description

cluster-admin

system:masters group

쿠버네티스 클러스터에 대해서 수퍼사용자 권한을 부여한다.
ClusterRoleBinding을 이용해서 롤을 연결할 경우에는 모든 네임스페이스와 모든 리소스에 대한 권한을 부여한다. RoleBinding을 이용하여 롤을 부여하는 경우에는 해당 네임 스페이스에 있는 리소스에 대한 모든 컨트롤 권한을 부여한다.

admin

None

관리자 권한의 억세스를제공한다. RoleBinding을 이용한 경우에는 해당 네임스페이스에 대한 대부분의 리소스에 대한 억세스를 제공한다.  새로운 롤을 정의하고 RoleBinding을 정의하는 권한을 포함하지만, resource quota에 대한 조정 기능은 가지지 않는다.

edit

None

네임스페이스내의 객체를 읽고 쓰는 기능은 가지지만, role이나 rolebinding을 쓰거나 수정하는 역할은 제외된다.

view

None

해당 네임스페이스내의 객체에 대한 읽기기능을 갔는다. role이나 rolebinding을 조회하는 권한은 가지고 있지 않다.


미리 정해진 롤에 대한 자세한 정보는  https://kubernetes.io/docs/reference/access-authn-authz/rbac/

를 참고하기 바란다.

권한 관리 예제

이해를 돕기 위해서 간단한 예제를 하나 테스트 해보자. 작성하는 예제는 Pod를 하나 생성해서 curl 명령으로 API를 호출하여, 해당 클러스터의 Pod 리스트를 출력하는 예제를 만들어보겠다.

Pod가 생성될때는 default 서비스 어카운트가 할당이 되는데, 이 서비스 어카운트는 클러스터의 정보를 호출할 수 있는 권한을 가지고 있지 않다. 쿠버네티스에 미리 정의된 ClusterRole중에 view 라는 롤은 클러스터의 대부분의 정보를 조회할 수 있는 권한을 가지고 있다.

이 롤을 sa-viewer 라는 서비스 어카운트를 생성한 후에, 이 서비스 어카운트에 ClusterRole view를 할당한후, 이 서비스 어카운트를 만들고자 하는 Pod에 적용하도록 하겠다.


apiVersion: v1

kind: ServiceAccount

metadata:

 name: sa-viewer

---

kind: ClusterRoleBinding

apiVersion: rbac.authorization.k8s.io/v1

metadata:

 name: default-view

subjects:

- kind: ServiceAccount

 name: sa-viewer

 namespace: default

roleRef:

 kind: ClusterRole

 name: view

 apiGroup: rbac.authorization.k8s.io


먼저 위와 같이 sa-viewer 라는 서비스 어카운트를 생성한후, ClusterRoleBiniding 을 이용하여, default-view라는 ClusterRolebinding 을 생성하고, sa-viewer 서비스 어카운트에, view 롤을 할당하였다.


다음 Pod를 생성하는데, 아래와 같이 앞에서 생성한 서비스 어카운트 sa-viewer를 할당한다.

apiVersion: v1

kind: Pod

metadata:

 name: pod-reader

spec:

 serviceAccountName: sa-viewer

 containers:

 - name: pod-reader

   image: gcr.io/terrycho-sandbox/pod-reader:v1

   ports:

   - containerPort: 8080


Pod 가 생성된 후에, kubectl exec 명령을 이용하여 해당 컨테이너에 로그인해보자

% kubectl exec -it pod-reader -- /bin/bash


로그인 후에 아래 명령어를 실행해보자


$ CA_CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

$ curl --cacert $CA_CERT -H "Authorization: Bearer $TOKEN" "https://35.200.91.132/api/v1/pods/"


CA_CERT는 API를 HTTPS로 호출하기 위해서 인증서를 저장한 파일의 위치를 지정하는 것이다. Pod의 경우에는 일반적으로 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt  디렉토리에 인증서가 자동으로 설치 된다. 다음은 API TOKEN을 얻기 위해서 TOKEN 값을 가지고 온다. TOKEN은 cat /var/run/secrets/kubernetes.io/serviceaccount/token 에 디폴트로 저장이 된다.

다음 curl 명령으로 https:{API SERVER}/api/v1/pods 를 호출하면 클러스터의 Pod 리스트를 다음 그림과 같이 리턴한다.


\



사용자 관리에 있어서, 계정에 대한 정의와 권한 정의 그리고 권한의 부여는 중요한 기능이기 때문에, 개념을 잘 잡아놓도록하자.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. zerobig 2018.08.22 08:41  댓글주소  수정/삭제  댓글쓰기

    정리 감사드립니다. 2편 기대 되네요~ 즐건 하루 되세요~~^

쿠버네티스 #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과는 다르게 타입



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. hyejong 2019.01.08 16:22  댓글주소  수정/삭제  댓글쓰기

    관리자의 승인을 기다리고 있는 댓글입니다


쿠버네티스 #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 파일이 생성된것을 확인할 수 있다.


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. 빌23 2019.03.19 14:52  댓글주소  수정/삭제  댓글쓰기

    블로그 감사히 잘읽었습니다. 많은 도움이 되고 있습니다.
    그런데 configMap 을 사용하더라도 dev, prod 같은 환경에 따라 다른 값이 적용되려면 결국 그때마다 key 값을 변경해주어야 하는건가요?

쿠버네티스 #7

서비스 (service)


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


Service

쿠버네티스 서비스에 대해서 자세하게 살펴보도록 한다.

Pod의 경우에 지정되는 Ip가 랜덤하게 지정이 되고 리스타트 때마다 변하기 때문에 고정된 엔드포인트로 호출이 어렵다, 또한 여러 Pod에 같은 애플리케이션을 운용할 경우 이 Pod 간의 로드밸런싱을 지원해줘야 하는데, 서비스가 이러한 역할을 한다.

서비스는 지정된 IP로 생성이 가능하고, 여러 Pod를 묶어서 로드 밸런싱이 가능하며, 고유한 DNS 이름을 가질 수 있다.


서비스는 다음과 같이 구성이 가능하며, 라벨 셀렉터 (label selector)를 이용하여, 관리하고자 하는 Pod 들을 정의할 수 있다.


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer

멀티 포트 지원

서비스는 동시에 하나의 포트 뿐 아니라 여러개의 포트를 동시에 지원할 수 있다. 예를 들어 웹서버의 HTTP와 HTTPS 포트가 대표적인 예인데,  아래와 같이 ports 부분에 두개의 포트 정보를 정의해주면 된다.

apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

   - name: https

     port: 443

     protocol: TCP

     targetPort: 8082

 type: LoadBalancer

로드 밸런싱 알고리즘

서비스가 Pod들에 부하를 분산할때 디폴트 알고리즘은 Pod 간에 랜덤으로 부하를 분산하도록 한다.

만약에 특정 클라이언트가 특정 Pod로 지속적으로 연결이 되게 하려면  Session Affinity를 사용하면 되는데, 서비스의 spec 부분에 sessionAffinity: ClientIP로 주면 된다.




웹에서 HTTP Session을 사용하는 경우와 같이 각 서버에 각 클라이언트의 상태정보가 저장되어 있는 경우에 유용하게 사용할 수 있다.

Service Type

서비스는 IP 주소 할당 방식과 연동 서비스등에 따라 크게 4가지로 구별할 수 있다.

  • Cluster IP

  • Load Balancer

  • Node IP

  • External name


ClusterIP

디폴트 설정으로, 서비스에 클러스터 IP (내부 IP)를 할당한다. 쿠버네티스 클러스터 내에서는 이 서비스에 접근이 가능하지만, 클러스터 외부에서는 외부 IP 를 할당  받지 못했기 때문에, 접근이 불가능하다.

Load Balancer

보통 클라우드 벤더에서 제공하는 설정 방식으로, 외부 IP 를 가지고 있는 로드밸런서를 할당한다. 외부 IP를 가지고 있기  때문에, 클러스터 외부에서 접근이 가능하다.

NodePort

클러스터 IP로만 접근이 가능한것이 아니라, 모든 노드의 IP와 포트를 통해서도 접근이 가능하게 된다. 예를 들어 아래와 같이 hello-node-svc 라는 서비스를 NodePort 타입으로 선언을 하고, nodePort를 30036으로 설정하면, 아래 설정에 따라 클러스터 IP의  80포트로도 접근이 가능하지만, 모든 노드의 30036 포트로도 서비스를 접근할 수 있다.


hello-node-svc-nodeport.yaml


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 type: NodePort

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

     nodePort: 30036


아래 그림과 같은 구조가 된다.




이를 간단하게 테스트 해보자.

아래는 구글 클라우드에서 쿠버네티스 테스트 환경에서 노드로 사용되고 있는 3개의 VM 목록과 IP 주소이다.


현재 노드는 아래와 같이 3개의 노드가 배포되어 있고 IP 는 10.146.0.8~10이다.

내부 IP이기 때문에, VPC 내의 내부 IP를 가지고 있는 서버에서 테스트를 해야 한다.


같은 내부 IP를 가지고 있는 envoy-ubuntu 라는 머신 (10.146.0.18)에서 각 노드의 30036 포트로 curl을 테스트해본 결과 아래와 같이 모든 노드의 IP를 통해서 서비스 접근이 가능한것을 확인할 수 있다.



ExternalName

ExternalName은 외부 서비스를 쿠버네티스 내부에서 호출하고자할때 사용할 수 있다.

쿠버네티스 클러스터내의 Pod들은 클러스터 IP를 가지고 있기 때문에 클러스터 IP 대역 밖의 서비스를 호출하고자 하면, NAT 설정등 복잡한 설정이 필요하다.

특히 AWS 나 GCP와 같은 클라우드 환경을 사용할 경우 데이타 베이스나, 또는 클라우드에서 제공되는 매지니드 서비스 (RDS, CloudSQL)등을 사용하고자할 경우에는 쿠버네티스 클러스터 밖이기 때문에, 호출이 어려운 경우가 있는데, 이를 쉽게 해결할 수 있는 방법이 ExternalName 타입이다.

아래와 같이 서비스를 ExternalName 타입으로 설정하고, 주소를 DNS로  my.database.example.com으로 설정해주면 이 my-service는 들어오는 모든 요청을 my.database.example.com 으로 포워딩 해준다. (일종의 프록시와 같은 역할)

kind: Service
apiVersion: v1
metadata:
 name: my-service
 namespace: prod
spec:
 type: ExternalName
 externalName: my.database.example.com

다음과 같은 구조로 서비스가 배포된다.



DNS가 아닌 직접 IP를 이용하는 방식

위의 경우 DNS를 이용하였는데, DNS가 아니라 직접 IP 주소를 이용하는 방법도 있다.

서비스 ClusterIP 서비스로 생성을 한 후에, 이 때 서비스에 속해있는 Pod를 지정하지 않는다.

apiVersion: v1

kind: Service

metadata:

 name: external-svc-nginx

spec:

 ports:

 - port: 80



다음으로, 아래와 같이 서비스의 EndPoint를 별도로 지정해주면 된다.

apiVersion: v1

kind: Endpoints

metadata:

 name: external-svc-nginx

subsets:

 - addresses:

   - ip: 35.225.75.124

   ports:

   - port: 80


이 때 서비스명과 서비스 EndPoints의 이름이 동일해야 한다. 위의 경우에는 external-svc-nginx로 같은 서비스명을 사용하였고 이 서비스는 35.225.75.124:80 서비스를 가르키도록 되어 있다.

그림으로 구조를 표현해보면 다음과 같다.




35.225.75.124:80 은 nginx 웹서버가 떠 있는 외부 서비스이고, 아래와 같이 간단한 문자열을 리턴하도록 되어 있다.



이를 쿠버네티스 내부 클러스터의 Pod 에서 curl 명령을 이용해서 호출해보면 다음과 같이 외부 서비스를 호출할 수 있음을 확인할 수 있다.

Headless Service

서비스는 접근을 위해서 Cluster IP 또는 External IP 를 지정받는다.

즉 서비스를 통해서 제공되는 기능들에 대한 엔드포인트를 쿠버네티스 서비스를 통해서 통제하는 개념인데, 마이크로 서비스 아키텍쳐에서는 기능 컴포넌트에 대한 엔드포인트 (IP 주소)를 찾는 기능을 서비스 디스커버리 (Service Discovery) 라고 하고, 서비스의 위치를 등록해놓는 서비스 디스커버리 솔루션을 제공한다. Etcd 나 hashcorp의 consul (https://www.consul.io/)과 같은 솔루션이 대표적인 사례인데, 이 경우 쿠버네티스 서비스를 통해서 마이크로 서비스 컴포넌트를 관리하는 것이 아니라, 서비스 디스커버리 솔루션을 이용하기 때문에, 서비스에 대한 IP 주소가 필요없다.

이런 시나리오를 지원하기 위한 쿠버네티스의 서비스를 헤드리스 서비스 (Headless service) 라고 하는데, 이러한 헤드리스 서비스는 Cluster IP등의 주소를 가지지 않는다. 단 DNS이름을 가지게 되는데, 이 DNS 이름을 lookup 해보면, 서비스 (로드밸런서)의 IP 를 리턴하지 않고, 이 서비스에 연결된 Pod 들의 IP 주소들을 리턴하게 된다.


간단한 테스트를 해보면


와 같이 기동중인 Pod들이 있을때, Pod의 IP를 조회해보면 다음과 같다.


10.20.0.25,10.20.0.22,10.20.0.29,10.20.0.26 4개가 되는데,

다음 스크립트를 이용해서 hello-node-svc-headless 라는 헤드리스 서비스를 만들어보자


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc-headless

spec:

 clusterIP: None

 selector:

   app: hello-node

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080


아래와 같이 ClusterIP가 할당되지 않음을 확인할 수 있다.



다음 쿠버네티스 클러스터내의 다른 Pod에서 nslookup으로 해당 서비스의 dns 이름을 조회해보면 다음과 같이 서비스에 의해 제공되는 pod 들의 IP 주소 목록이 나오는 것을 확인할 수 있다.




Service discovery

그러면 생성된 서비스의 IP를 어떻게 알 수 있을까? 서비스가 생성된 후 kubectl get svc를 이용하면 생성된 서비스와 IP를 받아올 수 있지만, 이는 서비스가 생성된 후이고, 계속해서 변경되는 임시 IP이다.

DNS를 이용하는 방법

가장 쉬운 방법으로는 DNS 이름을 사용하는 방법이 있다.

서비스는 생성되면 [서비스 명].[네임스페이스명].svc.cluster.local 이라는 DNS 명으로 쿠버네티스 내부 DNS에 등록이 된다. 쿠버네티스 클러스터 내부에서는 이 DNS 명으로 서비스에 접근이 가능한데, 이때 DNS에서 리턴해주는 IP는 외부 IP (External IP)가 아니라 Cluster IP (내부 IP)이다.


아래 간단한 테스트를 살펴보자. hello-node-svc 가 생성이 되었는데, 클러스터내의 pod 중 하나에서 ping으로 hello-node-svc.default.svc.cluster.local 을 테스트 하니, hello-node-svc의 클러스터 IP인 10.23.241.62가 리턴되는 것을 확인할 수 있다.



External IP (외부 IP)

다른 방식으로는 외부 IP를 명시적으로 지정하는 방식이 있다. 쿠버네티스 클러스터에서는 이 외부 IP를 별도로 관리하지 않기 때문에, 이 IP는 외부에서 명시적으로 관리되어야 한다.


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 externalIPs:

 - 80.11.12.11

 

외부 IP는 Service의 spec 부분에서 externalIPs 부분에 IP 주소를 지정해주면 된다.

구글 클라우드의 경우

퍼블릭 클라우드 (AWS, GCP 등)의 경우에는 이 방식 보다는 클라우드내의 로드밸런서를 붙이는 방법을 사용한다.


구글 클라우드의 경우를 살펴보자.서비스에 정적인 IP를 지정하기 위해서는 정적 IP를 생성해야 한다. 구글 클라우드 콘솔내의 VPC 메뉴의 External IP 메뉴에서 생성해도 되고, 아래와 같이 gcloud CLI 명령어를 이용해서 생성해도 된다.


IP를 생성하는 명령어는 gcloud compute addresses create [IP 리소스명] --region [리전]

을 사용하면 된다. 구글 클라우드의 경우에는 특정 리전만 사용할 수 있는 리저널 IP와, 글로벌에 모두 사용할 있는 IP가 있는데, 서비스에서는 리저널 IP만 사용이 가능하다. (글로벌 IP는 후에 설명하는 Ingress에서 사용이 가능하다.)

아래와 같이

%gcloud compute addresses create hello-node-ip-region  --region asia-northeast1

명령어를 이용해서 asia-northeast1 리전 (일본)에 hello-node-ip-region 이라는 이름으로 Ip를 생성하였다. 생성된 IP는 describe 명령을 이용해서 확인할 수 있으며, 아래 35.200.64.17 이 배정된것을 확인할 수 있다.



이 IP는 서비스가 삭제되더라도 계속 유지되고, 다시 재 사용이 가능하다.

그러면 생성된 IP를 service에 적용해보자

다음과 같이 hello-node-svc-lb-externalip.yaml  파일을 생성하자


apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer

 loadBalancerIP: 35.200.64.17


타입을 LoadBalancer로 하고, loadBalancerIP 부분에 앞에서 생성한 35.200.64.17 IP를 할당한다.

다음 이 파일을 kubectl create -f hello-node-svc-lb-externalip.yaml 명령을 이용해서 생성하면, hello-node-svc 가 생성이 되고, 아래와 같이 External IP가 우리가 앞에서 지정한 35.200.64.17 이 지정된것을 확인할 수 있다.




본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. js jang 2018.06.19 11:57  댓글주소  수정/삭제  댓글쓰기

    안그래도 service 개념 정리중이었는데 좋은 정리 공유 감사합니다! 한가지 궁금한점이 있는데요 NodePort 설명에 포함된 다이어그림을 보면 hello-node 가 위치한 pod 의 port 가 service.yaml 에서 정의한 targetport 8080 이 되는게 맞는건가요?

  2. js jang 2018.06.19 12:18  댓글주소  수정/삭제  댓글쓰기

    다시 한번 좋은 공유 감사합니다:)

  3. jtkim 2019.05.10 09:52  댓글주소  수정/삭제  댓글쓰기

    쿠버네티스 공부에 많은 도움을 받고 있어 감사드려요.
    내용 중에 node port 설명 부분에서 nodeport 지정하면 targetPort 는 없는게 맞는지요?

  4. pco 2019.06.06 19:16  댓글주소  수정/삭제  댓글쓰기

    정말 쿠버네티스 테스트해보고싶어서 해봤는데 너무 감사합니다.....

쿠버네티스 #6

Replication Controller를 이용하여 서비스 배포하기

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


1. 도커 파일 만들기

node.js로 간단한 웹서버를 만들어서 도커로 패키징 해보자.

실습을 진행하기 위해서 로컬 환경에 도커와, node.js 가 설치되어 있어야 한다. 이 두 부분은 생략하도록 한다.

여기서 사용한 실습 환경은 node.js carbon 버전 (8.11.3), 도커 맥용 18.05.0-ce, build f150324 을 사용하였다.

node.js 애플리케이션 준비하기

node.js로 간단한 웹 애플리케이션을 제작해보자 server.js라는 이름으로 아래 코드를 작성한다.

var os = require('os');

 

var http = require('http');

var handleRequest = function(request, response) {

 response.writeHead(200);

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

 

 //log

 console.log("["+

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

               "] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


이 코드는 8080 포트로 웹서버를 띄워서 접속하면 “Hello World!” 문자열과 함께, 서버의 호스트명을 출력해준다. 그리고 stdout에 로그로, 시간과 서버의 호스트명을 출력해준다.

코드 작성이 끝났으면, 서버를 실행해보자

%node server.js


다음 브라우저로 접속하면 다음과 같은 결과를 얻을 수 있다.


그리고 콘솔화면에는 아래와 같이 시간과 호스트명이 로그로 함께 출력된다.

도커로 패키징하기

그러면 이 node.js 애플리케이션을 도커 컨테이너로 패키징 해보자

Dockerfile 이라는 파일을 만들고 아래 코드를 작성한다.

FROM node:carbon

EXPOSE 8080

COPY server.js .

CMD node server.js > log.out


이 코드는 node.js carborn (8.11.3) 컨테이너 이미지를 베이스로 한후에,  앞서 작성한 server.js 코드를 복사한후에, node server.js > log.out 명령어를 실행하도록 하는 컨테이너를 만드는 설정파일이다.

설정 파일이 준비되었으면,  도커 컨테이너 파일을 만들어보자


% docker build -t gcr.io/terrycho-sandbox/hello-node:v1 .


docker build  명령은 컨테이너를 만드는 명령이고, -t는 빌드될 이미지에 대한 태그를 정하는 명령이다.

빌드된 컨테이너 이미지는 gcr.io/terrycho-sandbox/hello-node로  태깅되는데, 이는 향후에 구글 클라우드 컨테이너 레지스트리에 올리기 위해서 태그 명을 구글 클라우드 컨테이너 레지스트리의 포맷을 따른 것이다. (참고 https://cloud.google.com/container-registry/docs/pushing-and-pulling)

포맷은 [HOST_NAME]/[GOOGLE PROJECT-ID]/[IMAGE NAME]


gcr.io/terrycho-sandbox는 도커 이미지가 저장될 리파지토리의 경로를 위의 규칙에 따라 정의한 것인데,

  • gcr.io는 구글 클라우드 컨테이너 리파지토리 US 리전을 지칭하며,

  • terrycho-sandbox는 본인의 구글 프로젝트 ID를 나타낸다.

  • 이미지명을 hello-node 로 지정하였다.

  • 마지막으로 콜론(:) 으로 구별되어 정의한 부분은 태그 부분으로, 여기서는 “v1”으로 태깅을 하였다.


이미지는 위의 이름으로 지정하여 생성되어 로컬에 저장된다.




빌드를 실행하면 위와 같이 node:carbon 이미지를 읽어와서 필요한 server.js 파일을 복사하고 컨테이너 이미지를 생성한다.

컨테이너 이미지가 생성되었으면 로컬 환경에서 이미지를 기동 시켜보자


%docker run -d -p 8080:8080 gcr.io/terrycho-sandbox/hello-node:v1


명령어로 컨테이너를 실행할 수 있다.

  • -d 옵션은 컨테이너를 실행하되, 백그라운드 모드로 실행하도록 하였다.

  • -p는 포트 맵핑으로 뒤의 포트가 도커 컨테이너에서 돌고 있는 포트이고, 앞의 포트가 이를 밖으로 노출 시키는 포트이다 예를 들어 -p 9090:8080 이면 컨테이너의 8080포트를 9090으로 노출 시켜서 서비스 한다는 뜻이다. 여기서는 컨테이너 포트와 서비스로 노출 되는 포트를 동일하게 8080으로 사용하였다.


컨테이너를 실행한 후에, docker ps 명령어를 이용하여 확인해보면 아래와 같이 hello-node:v1 이미지로 컨테이너가 기동중인것을 확인할 수 있다.



다음 브라우져를 통해서 접속을 확인하기 위해서 localhost:8080으로 접속해보면 아래와 같이 Hello World 와 호스트명이 출력되는 것을 확인할 수 있다.


로그가 제대로 출력되는지 확인하기 위해서 컨테이너 이미지에 쉘로 접속해보자

접속하는 방법은


% docker exec -i -t [컨테이너 ID] /bin/bash

를 실행하면 된다. 컨테이너 ID 는 앞의 docker ps 명령을 이용하여 기동중인 컨테이너 명을 보면 처음 부분이 컨테이너 ID이다.

hostname 명령을 실행하여 호스트명을 확인해보면 위에 웹 브라우져에서 출력된 41a293ba79a7과 동일한것을 확인할 수 있다. 디렉토리에는 server.js 파일이 복사되어 있고, log.out 파일이 생성된것을 볼 수 있다.  

cat log.out을 이용해서 보면, 시간과 호스트명이 로그로 출력된것을 확인할 수 있다.



2. 쿠버네티스 클러스터 준비

구글 클라우드 계정 준비하기

구글 클라우드 계정 생성은 http://bcho.tistory.com/1107 문서를 참고하기 바란다.

쿠버네티스 클러스터 생성하기

쿠버네티스 클러스터를 생성해보자, 클러스터 생성은 구글 클라우드 콘솔의 Kubernetes Engine > Clusters 메뉴에서 Create 를 선택하면 클러스터 생성이 가능하다.



클러스터 이름을 넣어야 하는데, 여기서는 terry-gke-10 을 선택하였다. 구글 클라우드에서 쿠버네티스 클러스터는 싱글 존에만 사용가능한 Zonal 클러스터와 여러존에 노드를 분산 배포하는 Regional 클러스터 두 가지가 있는데, 여기서는 하나의 존만 사용하는 Zonal 클러스터를 설정한다. (Regional은 차후에 다루도록 하겠다.)

다음 클러스터를 배포한 존을 선택하는데, asia-northeast1-c (일본)을 선택하였다.

Cluster Version은 쿠버네티스 버전인데, 1.10.2 버전을 선택한다.

그리고 Machine type은 쿠버네티스 클러스터의 노드 머신 타입인데, 간단한 테스트 환경이기 때문에,  2 CPU에 7.5 메모리를 지정하였다.

다음으로 Node Image는 노드에 사용할 OS 이미지를 선택하는데, Container Optimized OS를 선택한다. 이 이미지는 컨테이너(도커)를 운영하기 위해 최적화된 이미지이다.

다음으로는 노드의 수를 Size에서 선택한다. 여기서는 3개의 노드를 운용하도록 설정하였다.


아래 부분에 보면  Automatic node upgrades 라는 기능이 있다.


구글 클라우드의 재미있는 기능중 하나인데, 쿠버네티스 버전이 올라가면 자동으로 버전을 업그레이드 해주는 기능으로, 이 업그레이드는 무정지로 진행 된다.


gcloud 와 kubectl 설치하기

클러스터 설정이 끝났으면 gloud (Google Cloud SDK 이하 gcloud)를 인스톨한다.

gcloud 명령어의 인스톨 방법은 OS마다 다른데, https://cloud.google.com/sdk/docs/quickstarts 문서를 참고하면 된다.

별다른 어려운 작업은 없고, 설치 파일을 다운 받아서 압축을 푼후에, 인스톨 스크립트를 실행하면 된다.


kubectl은 쿠버네티스의 CLI (Command Line Interface)로, gcloud를 인스톨한후에,

%gcloud components install kubectl

명령을 이용하면 인스톨할 수 있다.

쿠버네티스 클러스터 인증 정보 얻기

gcloud와 kubectl 명령을 설치하였으면, 이 명령어들을 사용할때 마다 쿠버네티스에 대한 인증이 필요한데, 인증에 필요한 인증 정보는 아래 명령어를 이용하면, 자동으로 사용이 된다.

gcloud container clusters get-credentials CLUSTER_NAME

여기서는 클러스터명이 terry-gke10이기 때문에,

%gcloud container clusters get-credentials terry-gke-10

을 실행한다.


명령어 설정이 끝났으면, gcloud 명령이 제대로 작동하는지를 확인하기 위해서, 현재 구글 클라우드내에 생성된 클러스터 목록을 읽어오는 gcloud container clusters list 명령어를 실행해보자



위와 같이 terry-gke-10 이름으로 asia-northeast1-c 존에 쿠버네티스 1.10.2-gke.3 버전으로 클러스터가 생성이 된것을 볼 수 있고, 노드는 총 3개의 실행중인것을 확인할 수 있다.

3. 쿠버네티스에 배포하기

이제 구글 클라우드에 쿠버네티스 클러스터를 생성하였고, 사용을 하기 위한 준비가 되었다.

앞에서 만든 도커 이미지를 패키징 하여, 이 쿠버네티스 클러스터에 배포해보도록 하자.

여기서는 도커 이미지를 구글 클라우드내의 도커 컨테이너 레지스트리에 등록한 후, 이 이미지를 이용하여 ReplicationController를 통해 총 3개의 Pod를 구성하고 서비스를 만들어서 이 Pod들을 외부 IP를 이용하여 서비스를 제공할 것이다.

도커 컨테이너 이미지 등록하기

먼저 앞에서 만든 도커 이미지를 구글 클라우드 컨테이너 레지스트리(Google Container Registry 이하 GCR) 에 등록해보자.

GCR은 구글 클라우드에서 제공하는 컨테이너 이미지 저장 서비스로, 저장 뿐만 아니라, CI/CD 도구와 연동하여, 자동으로 컨테이너 이미지를 빌드하는 기능, 그리고 등록되는 컨테이너 이미지에 대해서 보안적인 문제가 있는지 보안 결함을 스캔해주는 기능과 같은 다양한 기능을 제공한다.


컨테이너 이미지를 로컬환경에서 도커 컨테이너 저장소에 저장하려면 docker push라는 명령을 사용하는데, 여기서는 GCR을 컨테이너 이미지 저장소로 사용할 것이기 때문에, GCR에 대한 인증이 필요하다.

인증은 한번만 해놓으면 되는데

%gcloud auth configure-docker

명령을 이용하면, 인증 정보가 로컬 환경에 자동으로 저장된다.



인증이 완료되었으면, docker push 명령을 이용하여 이미지를 GCR에 저장한다.

%docker push gcr.io/terrycho-sandbox/hello-node:v1


명령어를 실행하면, GCR에 hello-node 이미지가 v1 태그로 저장된다.


이미지가 GCR에 잘 저장되었는지를 확인하기 위해서 구글 클라우드 콘솔에 Container Registry (GCR)메뉴에서 Images라는 메뉴를 들어가보자




아래와 같이 hello-node 폴더에 v1이라는 태그로 이미지가 등록된것을 확인할 수 있다.

ReplicationController 등록

컨테이너 이미지가 등록되었으면 이 이미지를 이용해서 Pod를 생성해보자,  Pod 생성은 Replication Controller (이하 rc)를 생성하여, rc가 Pod 생성 및 컨트롤을 하도록 한다.


다음은 rc 생성을 위한 hello-node-rc.yaml 파일이다.


apiVersion: v1

kind: ReplicationController

metadata:

 name: hello-node-rc

spec:

 replicas: 3

 selector:

   app: hello-node

 template:

   metadata:

     name: hello-node-pod

     labels:

       app: hello-node

   spec:

     containers:

     - name: hello-node

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

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


hello-node-rc 라는 이름으로 rc를 생성하는데, replica 를 3으로 하여, 총 3개의 pod를 생성하도록 한다.

템플릿 부분에 컨테이너 스팩에 컨테이너 이름은 hello-node로 하고 이미지는 앞서 업로드한 gcr.io/terrycho-sandbox/hello-node:v1 를 이용해서 컨테이너를 만들도록 한다. 컨테이너의 포트는 8080을 오픈한다. 템플릿 부분에서 app 이라는 이름의 라벨을 생성하고 그 값을 hello-node로 지정하였다. 이 라벨은 나중에 서비스 (service)에 의해 외부로 서비스될 pod들을 선택하는데 사용 된다.


여기서 imagePullPolicy:Always  라고 설정한 부분이 있는데, 이는 Pod를 만들때 마다 매번 컨테이너 이미지를 확인해서 새 이미지를 사용하도록 하는 설정이다.  컨테이너 이미지는 한번 다운로드가 되면 노드(Node) 에 저장이 되어 있게 되고, 사용이 되지 않는 이미지 중에 오래된 이미지는 Kublet이 가비지 컬렉션 (Garbage collection) 정책에 따라 이미지를 삭제하게 되는데, 문제는 노드에 이미 다운되어 있는 이미지가 있을 경우 컨테이너 생성시 노드에 이미 다운로드 되어 있는 이미지를 사용한다. 컨테이너 리파지토리에 같은 이름으로 이미지를 업데이트 하거나 심지어 그 이미지를 삭제하더라도 노드에 이미지가 이미 다운로드 되어 있으면 다운로드된 이미지를 사용하기 때문에, 업데이트 부분이 반영이 안된다.

이를 방지하기 위해서 imagePullPolicy:Always로 해주면 컨테이너 생성시마다 이미지 리파지토리를 검사해서 새 이미지를 가지고 오기 때문에, 업데이트된 내용을 제대로 반영할 수 있다.


%kubectl create -f hello-node-rc.yaml


명령어를 실행해서 rc와 pod를 생성한다.




위의 그림과 같이 3개의 Pod가 생성된것을 확인할 수 있는데, Pod가 제대로 생성되었는지 확인하기 위해서 hello-node-rc-rsdzl pod에서 hello-node-rc-2phgg pod의 node.js 웹서버에 접속을 해볼 것이다.

아직 서비스를 붙이지 않았기 때문에, 이 pod들은 외부 ip를 이용해서 서비스가 불가능하기 때문에, 쿠버네티스 클러스터 내부의 pod를 이용하여 내부 ip (private ip)간에 통신을 해보기 위해서 pod에서 pod를 호출 하는 것이다. kubectl describe pod  [pod 명] 명령을 이용하면, 해당 pod의 정보를 볼 수 있다. hello-node-rc-2hpgg pod의 cluster ip (내부 ip)를 확인해보면 10.20.1.27 인것을 확인할 수 있다.


kubectl exec 명령을 이용하면 쉘 명령어를 실행할 수 있는데, 다음과 같이 hello-node-rc-rsdzl pod에서 첫번째 pod인 hello-node-rc-2phgg의 ip인 10.20.1.27의 8080 포트로 curl 을 이용해 HTTP 요청을 보내보면 다음과 같이 정상적으로 응답이 오는 것을 볼 수 있다.


Service 등록

rc와 pod 생성이 끝났으면 이제 서비스를 생성해서 pod들을 외부 ip로 서비스 해보자

다음은 서비스를 정의한 hello-node-svc.yaml 파일이다.


hello-node-svc.yaml

apiVersion: v1

kind: Service

metadata:

 name: hello-node-svc

spec:

 selector:

   app: hello-node

 ports:

   - port: 80

     protocol: TCP

     targetPort: 8080

 type: LoadBalancer


Selector 부분에 app:hello-node 로 지정하여, pod들 중에 라벨의 키가 app이고 값이 hello-node인 pod 들만 서비스에 연결하도록 지정하였다. 다음 서비스의 포트는 80으로 지정하였고, pod의 port는 8080으로 지정하였다.


서비스가 배포되면 위와 같은 구조가 된다.

%kubectl create -f hello-node-svc.yaml

명령을 이용하면 서비스가 생성이 된다.


다음 생성된 서비스의 외부 ip를 얻기 위해서 kubectl get svc 명령을 실행해보자

아래 그림과 같이 35.200.40.161 IP가 할당된것을 확인할 수 있다.


이 IP로 접속을 해보면 아래와 같이 정상적으로 응답이 오는 것을 확인할 수 있다.


RC 테스트

rc는 pod의 상태를 체크하다가 문제가 있으면 다시, pod를 기동해주는 기능을 한다.

이를 테스트하기 위해서 강제적으로 모든 pod를 제거해보자. kubectl delete pod --all을 이용하면 모든 pod를 제거할 수 있는데, 아래 그림을 보면, 모든 pod를 제거했더니 3개의 pod가 제거되고 새롭게 3개의 pod가 기동되는 것을 확인할 수 있다.



운영중에 탄력적으로 pod의 개수를 조정할 수 있는데, kubectl scale 명령을 이용하면 된다.

kubectl scale --replicas=[pod의 수] rc/[rc 명] 식으로 사용하면 된다. 아래는 pod의 수를 4개로 재 조정한 내용이다.



자원 정리

테스트가 끝났으면 서비스, rc,pod를 삭제해보자.

  • 서비스 삭제는 kubectl delete svc --all 명령어를 이용한다.

  • rc 삭제는 kubectl delete rc --all

  • pod 삭제는 kubectl delete pod --all

을 사용한다.

삭제시 주의할점은 pod를 삭제하기 전에 먼저 rc를 삭제해야 한다. 아니면, pod가 삭제된 후 rc에 의해서 다시 새로운 pod가 생성될 수 있다.


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. Racoon 2018.07.04 17:22  댓글주소  수정/삭제  댓글쓰기

    쿠버네티스 참 막막했는데, 좋은글 덕에 정말 많이 이해가 됐습니다! 감사합니다ㅠㅠ 근데.. 한가지 궁금한 게 있는데요.. 현재 Minikube으로 테스트를 하고 있는데, Minikube의 경우에는 LoadBalancer를 적용할 수 없는건가요?

  2. jkh 2019.06.12 15:49  댓글주소  수정/삭제  댓글쓰기

    안녕하세요!! 올려주신 자료 덕분에 블로그를 통해 쿠버네티스를 공부하고 있는 초보개발자입니다. 여기서 컨테이너포트를 8080으로 설정한 것은 server.js에서 8080으로 설정해서 그런것인가요? 감사합니다

Stackdriver profiler

클라우드 컴퓨팅 & NoSQL/google cloud | 2018.04.08 21:44 | Posted by 조대협


Stack driver profiler


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


얼마전에 구글 클라우드의 모니터링 솔루션인 stack driver에서 profiler 기능이 발표되었다. (https://cloud.google.com/profiler) 

우리가 일반적으로 생각하는 성능 분석을 위한 profiling 도구로, 구글 클라우드 뿐만 아니라, 여러 서버에서 동작하는 Java/node.js/Go 애플리케이션의 성능을 모니터링할 수 있다.(파이썬은 곧 지원 예정)


장점은 코드 수정없이 간단하게 에이전트만 추가함으로써 프로파일러 사용이 가능하고, 프로파일링된 결과를 stackdriver 웹 콘솔에서 바로 확인이 가능하다는 것이다.


JDB등 전통적인 프로파일러가 있기는 하지만 보통 프로파일러가 적용되면, 애플리케이션의 성능이 극단적으로 느려지기 때문에, 운영환경에 적용이 불가능한데, Stack driver profiler의 경우에는 성능 저하가 미비하여 운영환경에도 적용이 가능하다.


"Stackdriver Profiler uses statistical techniques and extremely low-impact instrumentation that runs across all production application instances to provide a complete picture of an application’s performance without slowing it down."


아래는 자바 애플리케이션을 프로파일을 하기 위해서 프로파일러 바이너리를 agentPath에 추가한 형태이다


java \ -agentpath:/opt/cprof/profiler_java_agent.so=-cprof_service=user,-cprof_service_version=1.0.0 \ -jar ./User-0.0.1-SNAPSHOT.jar


아래는 자바 애플리케이션을 프로파일을 하기 위해서 프로파일러 바이너리를 agentPath에 추가한 형태이다

애플리케이션은 http://bcho.tistory.com/1247 에서 사용한 간단한 REST API를 사용하였다.

코드를 실행해서 프로파일링 데이타를 얻고 나면 아래와 같이 구글 클라우드 콘솔에서 프로파일링 결과를 확인할 수 있다.


위의 뷰는 WALL뷰로, 전체 프로그램이 수행되는 중에, 어느 코드가 시간을 얼마나 사용했는지를 프로파일링 해준결과이다.
이 외에도 CPU 시간으로 볼 수 도 있고, 메모리 사용률등 다양한 뷰
대규모 분산 서비스나 MSA 구조에 적합하도록 프로파일 결과를 볼 수 있는 범위를 선택이 가능한데, 상단의 메뉴를 보면 프로파일링 결과를 볼 서비스와, 프로파일 타입 (CPU,WALL:메서드별 실행시간, 메모리 사용률), 그리고 서비스가 배포된 클라우드 존, 서비스 버전 등에 따라서 선택이 가능하다. 아래는 언어별로 지원하는 프로파일 타입이다. 



Profiler의 뷰는 애플리케이션 타입에 상관이 없이 순수 프로그래밍 플랫폼에만 연관된 뷰로만 보여준다.
무슨이야기인가 하면, 보통 웹 애플리케이션은 멀티 쓰레드 타입으로 동작하고, REQUEST가 들어오면 쓰레드가 하나의 요청을 처리하고 빠지는 형태이기 때문에, 쓰레드별로 어떤 메서드가 순차적으로 실행되었는지등의 뷰를 선호하는데, JENNIFER나 오픈 소스 스카우터와 같은 APM (Application Peformance Monitoring)툴이 이러한 뷰를 제공한다. 

위의 샘플을 보더라도, 톰캣서버의 쓰레드들이 대부분 모니터링 될뿐 직접 코딩한 메서드들이 관측 되지는 않는다. (사용자 코드가 적고, 실행시 별로 크게 시간을 소요하지 않는 것도 원인이기는 하지만)

만약에 REQUEST에 대한 메서드별 소요 시간 모니터링 및 병목 구간 확인을 하려면, Stack driver profiler보다는 Stack driver trace를 사용하는 것이 적절하다. http://bcho.tistory.com/1245

그래서 Stack Driver는 성능 모니터링 (APM)제품군을 Trace, Profiler, Debugger 3가지로 묶고 있고, (Debugger는 나중에 시간이 되면 테스트하고 다루도록 하겠다.) 각기 다른 뷰로 상호 보완적인 관점에서 성능 모니터링이 가능하도록 하고 있다.



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

Zipkin을 이용한 MSA 환경에서 분산 트렌젝션의 추적 #3

Stackdriver를 zipkin으로 사용하기


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


앞의 예제에서는 간단하게 Zipkin 서버를 메모리 스토리지를 이용해서 올렸는데, 운영환경에서는 적절하지 않다. 실 운영환경에서는 대규모 트래픽 저장 및 쿼리를 위해서 Cassandra나 Elastic Search 등을 사용해야 하는데, 설정과 운영이 어렵다.

이에 대한 대안으로 구글 클라우드에는 분산 트렌젝션 추적을 위한 Stack driver trace (https://cloud.google.com/trace/) 라는 기능이 있다. 자체적인 SDK를 이용하여 트렌젝션을 추적하는 것도 가능하지만, Zipkin 클라이언트로 부터 로그를 수집할 수 있다.

즉 개발단은 Zipkin을 사용하고, 뒷단에는 복잡한 Zipkin 서버 대신 Stack driver trace를 사용하는 방법이다.


개념적으로 보면 다음과 같다. Zipkin 서버 대신 Zipkin/stack driver collector 라는 서버를 띄우면 이 서버가 Stackdriver 로 로그를 저장하고 시각화 해준다.



Zipkin/stack driver collector는 zipkin 서버를 대치하는 역할로, zipkin 클라이언트가 zipkin 서버 대신 이 zipkin/stack driver collector 를 바라보도록 주소와 포트만 변경해주면 된다.

흥미로운 점은 구글 클라우드 뿐 아니라, 로컬 환경, AWS,Azure,On Prem 등 다양한 환경에 설치가 가능하다. 그래서 모든 애플리케이션 서비스를 통합해서 Stack driver 로 trace가 가능하다.


Zipkin/stack driver collector를 설치하는 방법은 다음과 같다.

https://cloud.google.com/trace/docs/zipkin

Docker 이미지를 이용해도 되고 java jar 파일을 다운로드 받아서 사용해도 된다.

구글 클라우드 VM이나 도커로 실행할때는 상관이 없지만 구글 클라우드 인프라 밖에서 Zipkin Stackdriver collector를 실행할때는 추가적인 인증 정보를 설정해야 한다.


Stack driver collector가 Stackdriver 서버(클라우드)로 로그를 전달하기 위해서는 아무 로그나 받으면 안되고 인증된 로그만 받아야 하니 추가 인증 체계가 필요한데, 구글 클라우드에서는 애플리케이션 인증을 위해서 Service Account라는 JSON 파일을 사용한다.  Service Account 생성 방법은 https://medium.com/google-cloud/distributed-tracing-spring-boot-microservices-with-stackdriver-trace-7fe42c6de3f3 문서를 참고하기 바란다.


Service Account 파일이 생성되면, 아래와 같이 GOOGLE_APPLICATION_CREDENTAILS 환경 변수에 Service account 파일의 경로를 지정한다.

export GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json"
export PROJECT_ID="my_project_id"

다음 구글 클라우드의 어느 프로젝트에 있는 Stack Driver 와 연결할지를 지정해야 하는데, “PROJECT_ID” 환경 변수에 프로젝트 명을 지정해주면 된다.

환경 변수 설정이 끝나면 java -jar collector-0.6.0.jar 명령으로 collector를 실행한다.

아래는 환경 변수 설정과 collector 를 실행하는 스크립트 예제이다.


export GOOGLE_APPLICATION_CREDENTIALS="./terrycho-sandbox-zipkin-collector.json"

export PROJECT_ID="terrycho-sandbox"


echo $GOOGLE_APPLICATION_CREDENTIALS

echo $PROJECT_ID

java -jar collector-*.jar


포트는 디폴트로 9411을 사용하게 되어 있다. 이전 예제에서 zipkin 서버 대신 collector만 대신 띄운 후에 부하를 주면 로그를 수집할 수 있다.

아래는 로그를 수집한 후에, 분석화면의 일부분이다.


Zipkin UI와 동일하게 각 단일 트렌젝션에 대해서 Trace/Span 정보를 확인할 수 있고, Spot 그래프를 이용한 응답 시간 분포 확인이 가능하다.




아울러 각 서비스 별로 응답 시간에 대한 분포도를 아래와 같이 시각화 해준다.




참고

구글 클라우드내에서 Zipkin과 StackDriver 연결 방법 https://codelabs.developers.google.com/codelabs/cloud-spring-cloud-gcp-trace/index.html?index=..%2F..%2Findex#6





본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

Apache Beam (Dataflow)를 이용하여, 이미지 파일을 tfrecord로 컨버팅 하기


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



개요

텐서플로우 학습에 있어서 데이타 포맷은 학습의 성능을 결정 짓는 중요한 요인중의 하나이다. 특히 이미지 파일의 경우 이미지 목록과 이미지 파일이 분리되어 있어서 텐서플로우에서 학습시 이미지 목록을 읽으면서, 거기에 있는 이미지 파일을 매번 읽어야 하기 때문에, 코딩이 다소 지저분해지고,IO 성능이 떨어질 수 있다

텐서플로우에서는 이러한 학습 데이타를 쉽게 읽을 수 있도록 tfrecord (http://bcho.tistory.com/1190)라는 파일 포맷을 지원한다.


이 글에서는 이미지 데이타를 읽어서 tfrecord 로 컨버팅하는 방법을 설명하며, 분산 데이타 처리 프레임웍인 오픈소스 Apache Beam을 기준으로 설명하나, tfrecord 변환 부분은 Apache Beam과 의존성이 없이 사용이 가능하기 때문에, 필요한 부분만 참고해도 된다. 이 Apache Beam을 구글의 Apache Beam 런타임 (매니지드 서비스)인 구글 클라우드의 Dataflow를 이용하여, 클러스터를 이용하여 빠르게 데이타를 처리하는 방법에 대해서 알아보도록 한다.


전체 코드는 https://github.com/bwcho75/cifar-10/blob/master/pre-processing/4.%20Convert%20Pickle%20file%20to%20TFRecord%20by%20using%20Apache%20Beam.ipynb 에 있다.


이 코드는 CIFAR-10 이미지 데이타를 Apache Beam 오픈 소스를 이용하여, 텐서플로우 학습용 데이타 포맷인  tfrecord 형태로 변환 해주는 코드이다.


Apache Beam은 데이타 처리를 위한 프레임웍으로, 구글 클라우드 상에서 실행하거나 또는 개인 PC나 Spark 클러스터상 여러 환경에서 실행이 가능하며, 구글 클라우드 상에서 실행할 경우 오토스케일링이나 그래프 최적화 기능등으로 최적화된 성능을 낼 수 있다.


CIFAR-10 데이타 셋은 32x32 PNG 이미지 60,000개로 구성된 데이타 셋으로 해당 코드 실행시 최적화가 되지 않은 상태에서 약 16분 정도의 처리 시간이 소요된다. 이 중 6분 정도는 Apache Beam 코드를 구글 클라우드로 업로드 하는데 소요되는 시간이고 실제 처리시간은 10분정도가 소요된다. 전처리 과정에 Apache Beam을 사용하기 전에 고려해야 할 요소는 다음과 같다.

  • 데이타가 아주 많아서 전처리 시간이 수시간 이상 소요될 경우 Apache Beam + Google Cloud를 고려하여 여러 머신에서 동시에 처리하여 빠른 시간내에 수행되도록 할 수 있다.

  • 데이타가 그다지 많지 않고 싱글 머신에서 멀티 쓰레드로 처리를 원할 경우에는 Apache Beam으로 멀티 쓰레드 기반의 병렬 처리를 하는 방안을 고려할 수 있다. 이 경우 클라우드에 대한 의존성을 줄일 수 있다.

  • 다른 대안으로는 Spark/Hadoop 등의 오픈소스를 사용하여, On Prem에서 여러 머신을 이용하여 전처리 하는 방안을 고려할 수 있다.

여기서는 아주 많은 대량의 이미지 데이타에 대한 처리를 하는 것을 시나리오로 가정하였다.

전처리 파이프라인

Apache Beam을 이용한 데이타 전처리 파이프라인의 구조는 다음과 같다.

이미지 파일 준비

CIFAR-10 데이타셋 원본은 이미지 파일 형태가 아니라 PICKLE이라는 파일 포맷으로 되어 있기 때문에,  실제 개발 환경에서는 원본데이타가 이미지인것으로 가정하기 위해서 https://github.com/bwcho75/cifar-10/tree/master/pre-processing 의 1~2번 코드를 통해서 Pickle 파일을 이미지 파일로 변경하고, *.csv 파일에 {파일명},{레이블} 형태로 인덱스 데이타를 생성하였다.

생성된 이미지 파일과 *.csv 파일은 gsutil 명령어를 이용하여 Google Cloud Storage (aka GCS)에 업로드 하였다. 업로드 명령은 https://github.com/bwcho75/cifar-10/blob/master/pre-processing/2.%20Convert%20CIFAR-10%20Pickle%20files%20to%20image%20file.ipynb 에 설명되어 있다.


전처리 파이프라인의 구조

Apache Beam으로 구현된 파이프라인의 구조는 다음과 같다.


1. TextIO의 ReadFromText로 CSV 파일에서 한 라인 단위로 문자열을 읽는다.

2. parseLine에서 라인을 ,로 구분하여 filename과 label을 추출한다.

3. readImage 에서 filename을 가지고, 이미지 파일을 읽어서, binary array 형태로 변환한다.

4. TFExampleFromImageDoFn에서 이미지 바이너리와 label을 가지고 TFRecord 데이타형인 TFExample 형태로 변환한다.

5. 마지막으로 TFRecordIOWriter를 통해서 TFExample을 *.tfrecord 파일에 쓴다.

코드 주요 부분 설명

환경 설정 부분

이 코드는 구글 클라우드와 로컬 환경 양쪽에서 모두 실행이 가능하도록 구현되었다.

SRC_DIR_DEV는 로컬환경에서 이미지와 CSV 파일이 위치한 위치이고, DES_DIR_DEV는 로컬환경에서 tfrecord 파일이 써지는 위치이다.

구글 클라우드에서 실행할 경우 파일 저장소를  GCS (Google Cloud Storage)를 사용한다. DES_BUCKET은 GCS 버킷 이름이다. 코드 실행전에 반드시 구글 클라우드 콘솔에서 GCS 버킷을 생성하기 바란다.  SRC_DIR_PRD와 DES_DIR_PRD는 GCS 버킷내의 각각 image,csv 파일의 경로와 tfrecord 파일이 써질 경로 이다. 이 경로에 맞춰서 구글 클라우드 콘솔에서 디렉토리를 먼저 생성해 놓기를 바란다.




PROJECT는 구글 클라우드 프로젝트 명이고, 마지막으로 DEV_MODE가 True이면 로컬에서 수행이되고 False이면 구글 클라우드에서 실행하도록 하는 환경 변수이다.

의존성 설정 부분

로컬에서 실행할 경우필요한  파이썬 라이브러리가 이미 설치되어야 있어야 한다.

만약에 구글 클라우드에서 실행할 경우 이 Apache Beam 코드가 사용하는 파이썬 모듈을 명시적으로 정의해놔야 한다. 클라우드에서 실행시에는 Apache Beam 코드만 업로드가 되기 때문에(의존성 라이브러리를 같이 업로드 하는 방법도 있는데, 이는 추후에 설명한다.), 의존성 라이브는 구글 클라우드에서 Dataflow 실행시 자동으로 설치할 수 있도록 할 수 있는데, 이를 위해서는 requirements.txt 파일에 사용하는 파이썬 모듈들을 정의해줘야 한다. 다음은 requirements.txt에 의존성이 있는 파이썬 모듈등을 정의하고 저장하는 부분이다.


Apache Beam 코드

Apache Beam의 코드 부분은 크게 복잡하지 않기 때문에 주요 부분만 설명하도록 한다.

Service account 설정

Apache Beam 코드를 구글 클라우드에서 실행하기 위해서는 코드 실행에 대한 권한을 줘야 한다. 구글 클라우드에서는 사용자가 아니라 애플리케이션에 권한을 부여하는 방법이 있는데, Service account라는 것을 사용한다. Service account는 json 파일로 실행 가능한 권한을 정의하고 있다.

Service account 파일을 생성하는 방법은 http://bcho.tistory.com/1166 를 참고하기 바란다.

Service account 파일이 생성되었으면, 이 파일을 적용해야 하는데 GOOGLE_APPLICATION_CREDENTIALS 환경 변수에 Service account  파일의 경로를 정의해주면 된다. 파이썬 환경에서 환경 변수를 설정하는 방법은 os.envorin[‘환경변수명']에 환경 변수 값을 지정해주면 된다.

Jobname 설정

구글 클라우드에서 Apache Beam 코드를 실행하면, 하나의 실행이 하나의 Job으로 생성되는데, 이 Job을 구별하기 위해서 Job 마다 ID 를 설정할 수 있다. 아래는 Job ID를 ‘cifar-10’+시간 형태로 지정하는 부분이다


환경 설정

Apache Beam 코드를 구글 클라우드에서 실행하기 위해서는 몇가지 환경을 지정해줘야 한다.


  • staging_location은 클라우드 상에서 실행시 Apache Beam 코드등이 저장되는 위치이다. GCS 버킷 아래 /staging이라는 디렉토리로 지정했는데, 실행 전에 반드시 버킷아래 디렉토리를 생성하기 바란다.

  • temp_location은 기타 실행중 필요한 파일이 저장되는 위치이다. 실행 전에 반드시 버킷아래 디렉토리를 생성하기 바란다.

  • zone은 dataflow worker가 실행되는 존으로 여기서는 asia-northeast1-c  (일본 리전의 c 존)으로 지정하였다.


DEV_MODE 에 따른 환경 설정

로컬 환경이나 클라우드 환경에서 실행이냐에 따라서 환경 변수 설정이 다소 달라져야 한다.


디렉토리 경로를 바꿔서 지정해야 하고, 중요한것은 RUNNER인데, 로컬에서 실행하기 위해서는 DirectRunner를 구글 클라우드 DataFlow 서비스를 사용하기 위해서는 DataflowRunner를 사용하면 된다.


readImage 부분

Read Image는 이미지 파일을 읽어서 byte[] 로 리턴하는 부분인데, 로컬 환경이냐, 클라우드 환경이냐에 따라서 동작 방식이 다소 다르다.

클라우드 환경에서는 이미지 파일이 GCS에 저장되어 있기 때문에 파이썬의 일반 파일 open 명령등을 사용할 수 없다.

그래서 클라우드 환경에서 동작할 경우에는 GCS에서 파일을 읽어서 Worker의 로컬 디스크에 복사를 해놓고 이미지를 읽어서 byte[]로 변환한 후에, 해당 파일을 지우는 방식을 사용한다.


아래 코드에서 보면 DEV_MODE가 False 인경우 GCS에서 파일을 읽어서 로컬에 저장하는 코드가 있다.


storageClient는 GCS 클라이언트이고 bucket 을 얻어온후, bucket에서 파일을 get_blob 명령어를 이용하여 경로를 저장하여 blob.download_to_file을 이용하여 로컬 파일에 저장하였다.

실행

코드 작성이 끝났으면 실행을 한다. 실행 상태는 구글 클라우드 콘솔의 Dataflow  메뉴에서 확인이 가능하다.

아래와 같이 실행중인 그리고 실행이 끝난 Job 리스트들이 출력된다.




코드 실행중에, 파이프라인 실행 상황 디테일을 Job 을 선택하면 볼 수 있다.


여기서 주목할만한 점은 우측 그래프인데, 우측 그래프는 Worker의 수를 나타낸다. 초기에 1대로 시작했다가 오토 스케일링에 의해서 9대 까지 증가한것을 볼 수 있다.

처음 실행이었기 때문에 적정한 인스턴스수를 몰랐기 때문에 디폴트로 1로 시작하고 오토스케일링을 하도록 했지만, 어느정도 테스트를 한후에 적정 인스턴수를 알면 오토 스케일링을 기다릴 필요없이 디폴트 인스턴스 수를 알면 처음부터 그 수만큼 인스턴스 수로 시작하도록 하면 실행 시간을 줄일 수 있다.

만약에 파이프라인 실행시 에러가 나면 우측 상단에 LOGS 버튼을 누르면 상세 로그를 볼 수 있다.


아래 그림은 파이프라인 실행이 실패한 예에서 STACK TRACES를 통해서 에러 내용을 확인하는 화면이다.



해당 로그를 클릭하면 Stack Driver (구글의 모니터링 툴)의 Error Reporting 시스템 화면으로 이동하게 된다.

여기서 디테일한 로그를 볼 수 있다.

아래 화면을 보면 ReadImage 단계에서 file_path라는 변수명을 찾을 수 없어서 나는 에러를 확인할 수 있다.


TFRecord 파일 검증

파이프라인 실행이 끝나면, GCS 버킷에 tfrecord 파일이 생성된것을 확인할 수 있다.


해당 파일을 클릭하면 다운로드 받을 수 있다.

노트북 아래 코드 부분이 TFRecord를 읽어서 확인하는 부분이다. 노트북에서 tfrecord 파일의 경로를 다운로드 받은 경로로 변경하고 실행을 하면 파일이 제대로 읽히는 지 확인할 수 있다.


파일 경로 부분은 코드상에서 다음과 같다.



정상적으로 실행이 된 경우, 다음과 같이 tfrecord에서 읽은 이미지와 라벨값이 출력됨을 확인할 수 있다.


라벨 값은 Label 줄에 values 부분에 출력된다. 위의 그림에서는 순서대로 라벨 값이 4와 2가 된다.



본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

HBase와 구글의 빅테이블

#2 설치와 기본 사용 방법

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

HBase 설치

HBase를 개발 또는 운영환경에서 사용하기 위해서는 직접 HBase를 다운 받아서 설치하거나 또는 구글 클라우드의 빅테이블을 사용하면 된다 각각 설치 방법은 다음과 같다.


로컬 환경에 HBase 설치하기

설치 방법은 https://hbase.apache.org/book.html#quickstart 를 참고하도록 한다. 운영 환경용은 주키퍼등 여러 환경 설치가 뒤따라야 하기 때문에 여기서는 자세하게 설명하지 않는다.

$JAVA_HOME 환경 변수를 설정한 후에, HBase를 다운로드 받고, 압축을 푼다.

다음 ./bin/start-hbase.sh 을 수행하면 Hbase를 가동할 수 있다.

클라우드 빅테이블 이용하기

구글 클라우드에서 빅테이블을 사용하는 방법은 간단하다.

구글 클라우드 콘솔에서 빅테이블 메뉴로 이동한 다음




다음 메뉴에서 CREATE INSTANCE 버튼을 이용하여 빅테이블 클러스터를 생성한다.

이때 Production과 Development 모드를 설정할 수 있는데, Production 모드는 최소 3개의 노드를 사용해야 하며, Development 모드는 하나의 노드만 설정할 수 있기 때문에 개발시에는 Development 모드를, 운영시에는 Production 모드를 설정하기 바란다.


다음 데이타를 저장할 디스크 타입을 결정한다.




SSD의 경우 노드당 읽기,쓰기 각 10,000QPS (초당 쿼리 처리)를 보장하며, 쿼리 시간은 일반 put/get 쿼리의 경우에는 6ms 정도가 나온다. 테이블 스캔을 할경우, 노드당 220MB를 초당 스캔할 수 있다.


HDD의 경우에는 노드당 읽기 500 QPS, 읽기 속도는 쿼리당 200ms, 쓰기는 노드당 5000 QPS, 쓰기 속도는 50ms 가 나오며, 스캔은 초당 180MB를 스캔할 수 있다.


고속 데이타 조회가 필요한 경우에는 SSD를 사용하도록 하고 저속 (저속이라고는 하지만, 쓰기 50ms면 느린 속도가 아니다. )에 저비용을 선호하는 경우에는 HDD를 사용한다. 한번 디스크 타입을 정하면 클러스터를 새로 만들때 까지는 변경이 불가능하니 주의하기 바란다.

HBASE CLI 명령어 사용해보기

HBase는 Hbase CLI명령을 이용하여 데이타를 접근할 수 있으며, 빅테이블 역시 HBase CLI 명령을 이용하여 접근이 가능하다.

로컬 환경

로컬 환경에서 설치한 Hbase CLI명령을 사용하려면 $HBASE_HOME/bin/hbase shell 명령을 입력하면 CLI를 사용할 수 있다.

간단한 예제를 통하여 사용법을 알아보도록 하자

테이블 생성

테이블 생성은 create ‘테이블명’, ‘컬럼패밀리1’,’컬럼패밀리2’,... 형태로 지정하면된다.

아래는 컬럼패밀리 ‘cf1’만을 가지고 있는 ‘my-table’ 이라는 테이블 하나를 생성한 예이다.



데이타 입력

데이타 입력은 put 명령어를 사용하면 되며, put ‘테이블명',’로우키’,’컬럼패밀리명:컬럼명’,’값' 형태로 사용한다. 아래는 my-table이라는 테이블에 rowkey1이라는 키로 컬럼패밀리 cf1, 컬럼 c1에 “test-value”라는 값을 저장한 결과이다.


조회

조회는 get을 사용하면 되고 get ‘테이블명',’로우키' 로 조회를 하면 된다.


테이블 삭제

테이블 삭제는 drop ‘테이블명' 으로 하면 되고, 테이블 사용 정지는 disable ‘테이블명'을 하면 된다.




명령어들을 간단하게 설명하기는 했지만, get에서 특정 timestamp 범위만 쿼리한다던지, scan을 통해서 특정 범위의 특정 조건 데이타만 쿼리한다던지 다양한 기능이 가능하다.  자세한 내용은 HBase 메뉴얼을 참고하기 바랍니다.

구글 클라우드 환경

빅테이블은 HBase 호환이기 때문에, hbase shell 명령어를 그대로 사용할 수 있다. 단 접속 방식이 다르기 때문에, 별도로 빅테이블용 hbase shell 클라이언트를 다운 받아서 컴파일 하여 사용해야 한다.


구글 클라우드 환경에 접속하기 위해서는 gcloud CLI 명령어가 필요한데, 다운로드 및 인스톨 방법은 https://cloud.google.com/sdk/downloads 를 참고하기 바란다.

다음 빅테이블에 접근하기 위해서는 사용자 인증 과정이 필요한데,  다음 명령어를 실행하면, 웹브라우져