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


Archive»


 
 

로깅 시스템 #5 - Spring boot에서 JSON 포맷 로깅과 MDC 사용하기

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


실제로 백앤드 애플리케이션을 자바로 개발할때는 스프링 부트를 사용하는 경우가 대부분이기 때문에 앞에서 적용한 JSON 로그 포맷과 MDC 로깅을 스프링 부트에 적용해보자

스프링 부트라고 해도, 일반 자바 애플리케이션에 대비해서 로그 설정 부분에 다른점은 없다.

아래와 같이 pom.xml에 logback과 json 의존성을 추가한다.


<!-- slf4j & logback dependency -->

<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-classic</artifactId>

<version>1.2.3</version>

</dependency>


<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-json-classic</artifactId>

<version>0.1.5</version>

</dependency>


<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-jackson</artifactId>

<version>0.1.5</version>

</dependency>


<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

<version>2.9.3</version>

</dependency>


다음 로그 포맷팅을 JSON으로 하기 위해서 아래와 같이 logback.xml 파일을 작성하여 main/resources 디렉토리에 저장한다. 이번 예제에서는 스프링 부트로 기동할 경우 스프링 부트 자체에 대한 로그가 많기 때문에, JSON 으로 엘리먼트 단위로 출력하면 줄바꿈이 많아서, 로그를 보는데 어려움이 있으니 엘리먼트 단위로 줄을 바꾸지 않도록 <prettyPrint> 옵션을 false 로 처리하고, 대신 이벤트마다는 줄을 바꾸는게 좋으니, <appendLineSeperator>를 true로 설정하였다.


<?xml version="1.0" encoding="UTF-8"?>

<configuration>

   <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">

       <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">

           <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">

               <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>

               <timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>

               <appendLineSeparator>true</appendLineSeparator>


               <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">

                   <!--

                   <prettyPrint>true</prettyPrint>

                    -->`

               </jsonFormatter>

           </layout>

       </encoder>

   </appender>


   <root level="debug">

       <appender-ref ref="stdout"/>

   </root>

</configuration>


다음으로 아래와 같이 간단한 Controller를 작성하였다. /orders/{id} 형태의 REST API로 사용자 이름을 userid라는 키로 HTTP Header를 통해서 받도록 하였다.


package com.terry.logging.controller;

import org.springframework.web.bind.annotation.PathVariable;


import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import com.terry.logging.model.*;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.MDC;


@RestController

@RequestMapping("/orders")


public class OrderController {

Logger log = LoggerFactory.getLogger("com.terry.logging.controller.OrderController");

@RequestMapping(value="/{id}",method=RequestMethod.GET)

public Order getOrder(@PathVariable int id,

@RequestHeader(value="userid") String userid) {

MDC.put("userId", userid);

MDC.put("ordierId",Integer.toString(id));

Order order = queryOrder(id,userid);

log.info("Get Order");

MDC.clear();

return order;

}

Order queryOrder(int id,String userid) {

String name = "laptop";

Order order = new Order(id,name);

order.setUser(userid);

order.setPricePerItem(100);

order.setQuantity(1);

order.setTotalPrice(100);


log.info("product name:"+name);

return order;

}

}


userid와 orderid를 MDC에 넣어서 매번 로그때 마다 출력하도록 하였다.

아래 코드는 위에서 사용된 Order Value Class 내용이다.


package com.terry.logging.model;


public class Order {

public Order(int id,String item) {

this.item=item;

this.id = id;

}

public String getItem() {

return item;

}

public void setItem(String item) {

this.item = item;

}

public int getPricePerItem() {

return pricePerItem;

}

public void setPricePerItem(int pricePerItem) {

this.pricePerItem = pricePerItem;

}

public int getQuantity() {

return quantity;

}

public void setQuantity(int quantity) {

this.quantity = quantity;

}

public int getTotalPrice() {

return totalPrice;

}

public void setTotalPrice(int totalPrice) {

this.totalPrice = totalPrice;

}

String item;

int pricePerItem;

int quantity;

int totalPrice;

int id;

String user;

public String getUser() {

return user;

}

public void setUser(String user) {

this.user = user;

}

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

}



코드를 실행한후 REST API 클라이언트 도구 (여기서는 Postman을 사용하였다.)를 호출하면 브라우져에는 다음과 같은 결과가 출력된다.

그리고 로그는 아래와 같이 출력된다.


MDC를 이용한 저장한 컨택스트는 아래와 같이 JSON의 mdc 컨택스에 출력되었고, log.info()로 출력한 로그는 message 엘리먼트에 출력된것을 확인할 수 있다.

{"timestamp":"2019-03-25T15:16:16.394Z","level":"DEBUG","thread":"http-nio-8080-exec-2","logger":"org.springframework.web.servlet.DispatcherServlet","message":"Last-Modified value for [/orders/1] is: -1","context":"default"}

{"timestamp":"2019-03-25T15:16:16.395Z","level":"INFO","thread":"http-nio-8080-exec-2","mdc":{"ordierId":"1","userId":"terry"},"logger":"com.terry.logging.controller.OrderController","message":"product name:laptop","context":"default"}

{"timestamp":"2019-03-25T15:16:16.395Z","level":"INFO","thread":"http-nio-8080-exec-2","mdc":{"ordierId":"1","userId":"terry"},"logger":"com.terry.logging.controller.OrderController","message":"Get Order","context":"default"}


전체 소스코드는 https://github.com/bwcho75/javalogging/tree/master/springbootmdc 에 저장되어 있다.


이렇게 하면, 스프링 부트를 이용한 REST API에서 어떤 요청이 들어오더라도, 각 요청에 대한 ID를 Controller에서 부여해서, MDC를 통하여 전달하여 리턴을 할때 까지 그 값을 유지하여 로그로 출력할 수 있다.


그러나 이 방법은 하나의 스프링 부트 애플리케이션에서만 가능하고, 여러개의 스프링 부트 서비스로 이루어진 마이크로 서비스에서는 서비스간의 호출이 있을 경우 이 서비스간 호출에 대한 로그를 묶을 수 없는 단점이 있다.

예를 들어 서비스 A → 서비스 B로 호출을 하였을 경우에는 서비스 A에서 요청에 부여한 ID와 서비스 B에서 요청에 부여한 ID가 다르기 때문에 이를 묶기가 어렵다. 물론 HTTP 헤더로 ID를 전달하는 등의 방법은 있지만, 그다지 구성이 깔끔 하지 않다. 이렇게 마이크로 서비스에서 서비스간의 ID를 추적할 수 있는 방법으로 분산 환경에서 서비스간의 지연 시간을 측정하는 프레임웍으로 Zipkin이라는 프레임웍이 있다. 다음 글에서는 이 Zipkin을 로그 프레임웍과 연결해서 마이크로 서비스 환경에서 스프링 부트 기반으로 서비스간의 로그 추적을 어떻게할 수 있는지에 대해서 알아보도록 한다.


로그 시스템 #2- 자바 로그 & JSON 로그 포맷

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


앞 글에서 간단하게 자바 로깅 프레임워크에 대해서 알아보았다. 그러면 앞에서 추천한 slf4j와 log4j2로 실제 로깅을 구현해보자

SLF4J + log4j2

메이븐 프로젝트를 열고 dependencies 부분에 아래 의존성을 추가한다. 버전은 최신 버전을 확인하도록 한다. artifactid가 log4j-slf4j-impl 이지만, log4j가 아니라 log4j2가 사용된다.


<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-slf4j-impl</artifactId>

<version>2.11.2</version>

</dependency>


다음 log4j2의 설정 정보 파일인 log4j2.properties 파일을 src/main/resources 디렉토리 아래에 다음과 같이 생성한다. Appender나, Layout등 다양한 정보 설정이 있지만 그 내용은 나중에 자세하게 설명하도록 한다.


appenders=xyz


appender.xyz.type = Console

appender.xyz.name = myOutput

appender.xyz.layout.type = PatternLayout

appender.xyz.layout.pattern = [MYLOG %d{yy-MMM-dd HH:mm:ss:SSS}] [%p] [%c{1}:%L] - %m%n


rootLogger.level = info


rootLogger.appenderRefs = abc


rootLogger.appenderRef.abc.ref = myOutput


그리고 아래와 같이 코드를 만든다.

LoggerFactory를 이용해서 Logger를 가지고 온다. 현재 클래스 명에 대한 Logger 를 가지고 오는데, 위의 설정 파일을 보면 rootLogger만 설정하였기 때문에, rootLogger가 사용된다.

package com.terry.logging.helloworld;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;



public class App

{

   private static Logger log = LoggerFactory.getLogger(App.class);

   public static void main( String[] args )

   {

       System.out.println( "Hello World!" );

       

       log.info("Hello slf4j");

   }

}



가저온 logger를 이용해서 log.info로 로그를 출력한다.

콘솔로 출력된 로그는 아래와 같다.

[MYLOG 19-Mar-18 23:07:01:373] [INFO] [App:71] - Hello slf4j


JSON 포맷으로 로그 출력

근래에는 시스템이 분산 구조를 가지고 있기 때문에 텍스트 파일로 남겨서는 여러 분산된 서비스의 로그를 모아서 보기가 어렵다. 그래서, 이런 로그를 중앙 집중화된 서버로 수집 및 분석하는 구조를 가지는데, 수집 서버에서는 이 로그들을 구조화된 포맷으로 저장하는 경우가 일반적이다. 각 로그의 내용을 저장 구조의 개별 자료 구조(예를 들어 테이블의 컬럼)에 맵핑해서 저장하는데, 이를 위해서는 로그가 JSON,XML 또는 CSV와 같은 형태로 구조화가 되어 있어야 한다.

이런 구조화된 로그를 structured logging 이라고 한다. 로그 엔트리 하나를 JSON에 포함해서 출력하는 방법에 대해서 알아본다.

slf4j + logback

SLF4 + logback을 이용하여 레이아웃을 JSON으로 출력하는 코드이다.


package com.terry.logging.logback;


import java.util.Map;

import java.util.TreeMap;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;


import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;


public class App

{

   private static Logger log = LoggerFactory.getLogger(App.class);

   public static void main( String[] args ) throws JsonProcessingException

   {


       log.info("hello log4j");

   }

}


pom.xml에 아래와 같이 logback과 json 관련 dependency를 추가한다.


<dependencies>

<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-classic</artifactId>

<version>1.1.7</version>

</dependency>


<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-json-classic</artifactId>

<version>0.1.5</version>

</dependency>


<dependency>

<groupId>ch.qos.logback.contrib</groupId>

<artifactId>logback-jackson</artifactId>

<version>0.1.5</version>

</dependency>


<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

<version>2.9.3</version>

</dependency>

</dependencies>



마지막으로 src/main/resources.xml 파일을 아래와 같이 작성한다.  

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

   <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">

       <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">

           <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">

               <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>

               <timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>


               <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">

                   <prettyPrint>true</prettyPrint>

               </jsonFormatter>

           </layout>

       </encoder>

   </appender>


   <root level="debug">

       <appender-ref ref="stdout"/>

   </root>

</configuration>


아래는 출력 결과이다. message 필드에 로그가 출력 된것을 볼 수 있다.


{

 "timestamp" : "2019-03-19T07:24:31.906Z",

 "level" : "INFO",

 "thread" : "main",

 "logger" : "com.terry.logging.logback.App",

 "message" : "hello log4j",

 "context" : "default"

}


slf4j + log4j2

다음은 slft4+log4j2 를 이용한 예제이다.  logback과 크게 다르지는 않다.

아래와 같이 pom.xml 의 dependencies에 아래 내용을 추가하자. json layout은 jackson을 사용하기 때문에 아래와 같이 jackson에 대한 의존성도 함께 추가한다.


<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-slf4j-impl</artifactId>

<version>2.11.2</version>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-core</artifactId>

<version>2.7.4</version>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

<version>2.7.4</version>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-annotations</artifactId>

<version>2.7.4</version>

</dependency>


다음 아래와 같이 log4j2.properties 파일을 src/main/resources 폴더에 저장한다.


status = info


appender.ana_whitespace.type = Console

appender.ana_whitespace.name = ana_whitespace

appender.ana_whitespace.layout.type = JsonLayout

appender.ana_whitespace.layout.propertiesAsList = false

appender.ana_whitespace.layout.compact = false

appender.ana_whitespace.layout.eventEol = true

appender.ana_whitespace.layout.objectMessageAsJsonObject = true

appender.ana_whitespace.layout.complete= true

appender.ana_whitespace.layout.properties= true


rootLogger.level = info

rootLogger.appenderRef.ana_whitespace.ref = ana_whitespace


위에 보면 layout.type을 JsonLayout으로 지정하였다. 기타 다른 필드에 대한 정보는

정보는 https://logging.apache.org/log4j/2.0/manual/layouts.html 를 참고하기 바란다.


그리고 아래와 같이 코드를 이용해서 info 레벨의 로그를 출력해보자

package com.terry.logging.jsonlog;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;



public class App

{

private static Logger log = LoggerFactory.getLogger(App.class);

   public static void main( String[] args )

   {

       

       log.info("Hello json log");

       log.error("This is error");

       log.warn("this is warn");

   }

}


코드를 컴파일 하고 실행하면 아래와 같은 형태로 로그가 출력된다. 로그 출력 형태는 logback과는 많이 차이가 있다.


[

{

 "thread" : "main",

 "level" : "INFO",

 "loggerName" : "com.terry.logging.jsonlog.App",

 "message" : "Hello json log",

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.slf4j.Log4jLogger",

 "instant" : {

   "epochSecond" : 1552923302,

   "nanoOfSecond" : 38337000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

}

, {

 "thread" : "main",

 "level" : "ERROR",

 "loggerName" : "com.terry.logging.jsonlog.App",

 "message" : "This is error",

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.slf4j.Log4jLogger",

 "instant" : {

   "epochSecond" : 1552923302,

   "nanoOfSecond" : 109170000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

}

, {

 "thread" : "main",

 "level" : "WARN",

 "loggerName" : "com.terry.logging.jsonlog.App",

 "message" : "this is warn",

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.slf4j.Log4jLogger",

 "instant" : {

   "epochSecond" : 1552923302,

   "nanoOfSecond" : 109618000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

}


]


json을 여러가지 포맷으로 출력할 수 있다. 위의 로그를  잘보면 로그 시작과 끝에 json 포맷을 맞추기 위해서 “[“와 “]”를 추가하고, 로그 레코드 집합당 “,”로 레코드를 구별한것을 볼 수 있다. 만약에 “[“,”]”를 로그 처음과 마지막에서 제거하고, 로그 레코드 집합동 “,”를 제거하고 newline으로만 분류하고 싶다면 log4j2.properties 파일에서 appender.ana_whitespace.layout.complete = false로 하면 된다.

아래는 layout.complete를 false로 하고 출력한 결과 이다.


{ ←  이부분에 “[” 없음

 "thread" : "main",

 "level" : "INFO",

 "loggerName" : "com.terry.logging.jsonlog.App",

 "message" : "Hello json log",

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.slf4j.Log4jLogger",

 "instant" : {

   "epochSecond" : 1552923722,

   "nanoOfSecond" : 98574000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

} ←  이부분에 콤마가 없음

{

 "thread" : "main",

 "level" : "ERROR",

 "loggerName" : "com.terry.logging.jsonlog.App",

 "message" : "This is error",

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.slf4j.Log4jLogger",

 "instant" : {

   "epochSecond" : 1552923722,

   "nanoOfSecond" : 167047000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

}

{

 "thread" : "main",

 "level" : "WARN",

 "loggerName" : "com.terry.logging.jsonlog.App",

 "message" : "this is warn",

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.slf4j.Log4jLogger",

 "instant" : {

   "epochSecond" : 1552923722,

   "nanoOfSecond" : 167351000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

} ←  이부분에 “]” 없음


그리고 로그파일을 보는데, JSON의 경우에는 위와 같이 각 element 마다 줄을 바꿔서 사람이 읽기 좋은 형태이기는 하지만, 대신 매번 줄을 바꾸기 때문에 검색이 어려운 경우가 있다. 그래서 로그 레코드 하나를 줄 바꿈 없이 한줄에 모두 출력할 수 있도록 할 수 있는데, appender.ana_whitespace.layout.compact = true로 주면 된다. 아래는 옵션을 적용한 결과 이다.

{"thread":"main","level":"INFO","loggerName":"com.terry.logging.jsonlog.App","message":"Hello json log","endOfBatch":false,"loggerFqcn":"org.apache.logging.slf4j.Log4jLogger","instant":{"epochSecond":1552923681,"nanoOfSecond":430798000},"contextMap":{},"threadId":1,"threadPriority":5}

{"thread":"main","level":"ERROR","loggerName":"com.terry.logging.jsonlog.App","message":"This is error","endOfBatch":false,"loggerFqcn":"org.apache.logging.slf4j.Log4jLogger","instant":{"epochSecond":1552923681,"nanoOfSecond":491757000},"contextMap":{},"threadId":1,"threadPriority":5}

{"thread":"main","level":"WARN","loggerName":"com.terry.logging.jsonlog.App","message":"this is warn","endOfBatch":false,"loggerFqcn":"org.apache.logging.slf4j.Log4jLogger","instant":{"epochSecond":1552923681,"nanoOfSecond":492095000},"contextMap":{},"threadId":1,"threadPriority":5}



[팁] 쿠버네티스 StatefulSet에서 Headless 서비스를 이용한 Pod discovery


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


statefulset에서 데이타베이스와 같이 master,slave 구조가 있는 서비스들의 경우에는 service를 통해서 로드밸런싱을 하지 않고, service 를 통해서 로드 밸런싱을 하는 것을 잘 사용하지 않고 개별 pod의 주소를 알고 접속해야 한다. 그래서 개별 Pod의 dns 이름이나 주소를 알아야 한다.


Pod들은 DNS이름을 가질 수 는 있으나, {pod name}.{service name}.{name space}.svc.cluster.local 식으로 이름을 가지기 때문에, pod 를 DNS를 이용해서 접근하려면 service name이 있어야 한다. 그러나 statefulset에 의한 서비스들은 앞에서 언급하였듯이 쿠버네티스 service를  이용해서 로드밸런싱을 하는 것이 아니기 때문에, 로드밸런서의 역할은 필요가 없고, 논리적으로, pod들을 묶어줄 수 있는 service만 있으면 되기 때문에 headless 서비스를 활용한다. Headless 서비스를 이용하면, service 가 로드 밸런서의 역할도 하지 않고, 단일 IP도 가지지 않지만, 아래 그림처럼 nslookup을 이용해서, headless 서비스에 의해서 묶여진 Pod들의 이름도 알 수 있고




{pod name}.{service name}.{name space}.svc.cluster.local  이름으로, 각 pod 에 대한 접근 주소 역시 얻을 수 있다.




minikube에서 서비스 테스트 하기

미니쿠베를 로컬환경에 설치하고 쿠버네티스 서비스를 로드 밸런서 타입으로 배포하면, External IP할당이 되지 않는다. 그래서 아래 그림과 같이 External-IP가 계속 <pending>으로 보이게 된다.


NAME         TYPE  CLUSTER-IP  EXTERNAL-IP PORT(S)          AGE

kubernetes   ClusterIP  10.96.0.1  <none> 443/TCP          7d2h

my-service   LoadBalancer  10.105.173.146  <pending> 8080:31203/TCP   4m10s


그러면 미니쿠베에서 서비스를 테스트하려면 어떻게 해야 할까? 미니쿠베는 서비스를 테스트하기 위해서 service라는 명령을 제공한다. 아래 그림과 같이 minikube service {쿠버네티스 서비스명} 을 입력하면, 로컬 브라우져에서 해당 서비스를 접속할 수 있도록 해준다.


% minikube service my-service

StatefulSet을 이용하여 상태가 유지되는 Pod 관리하기

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

ReplicaSet으로 Stateful Pod 관리하기

앞에서 쿠버네티스의 Pod를 관리하기 위한 여러가지 컨트롤러 (Replica Set, ReplicationController,Job 등)에 대해서 알아보았다.

이런 컨트롤러들은 상태가 유지되지 않는 애플리케이션(Stateless application)을 관리하기 위해 사용된다. Pod가 수시로 리스타트되어도 되고, Pod 내의 디스크 내용이 리스타트되어 유실되는 경우라도 문제가 없는 워크로드 형태이다. 웹서버나 웹애플리케이션 서버 (WAS)등이 그에 해당한다. 그러나 RDBMS나 NoSQL과 같은  분산 데이타 베이스등과 같이 디스크에 데이타가 유지 되어야 하는 상태가 유지되는 애플리케이션 (Stateful application)은 기존의 컨트롤러로 지원하기가 어렵다.

ReplicaSet (이하 RS)를 이용하여 데이타 베이스 Pod를 관리하게 되면 여러가지 문제가 발생한다.

Pod의 이름

RS 등, Stateless Pod를 관리하는 컨트롤러에 의해서 관리되는 Pod들의 이름은 아래 그림과 같이 그 이름이 불 규칙적으로 지정된다.



마스터/슬레이브 구조를 가지는 데이타 베이스등에서 마스터 서버의 이름을 특정 이름으로 지정할 수 가 없다.

Pod의 기동 순서

RS에 의해서 관리되는 Pod들은 기동이 될때 병렬로 동시에 기동이 된다. 그러나 데이타베이스의 경우에는 마스터 노드가 기동된 다음에, 슬레이브 노드가 순차적으로 기동되어야 하는 순차성을 가지고 있는 경우가 있다.

볼륨 마운트

Pod에 볼륨을 마운트 하려면, Pod는 PersistentVolume (이하 PV)를 PersistentVolumeClaim(이하 PVC)로 연결해서 정의해야 한다.

RS등의 컨트롤러를 사용해서 Pod를 정의하게 되면, Pod 템플릿에 의해서 PVC와 PV를 정의하게 되기 때문에, 여러개의 Pod들에 대해서 아래 그림과 같이 하나의 PVC와 PV만 정의가 된다. RS의 Pod 템플릿에 의해 정의된 Pod들은 하나의 PVC와 연결을 시도 하는데, 맨 처음 생성된 Pod가 이 PVC와 PV에 연결이 되기 때문에 뒤에 생성되는 Pod들은 PVC를 얻지 못해서 디스크를 사용할 수 없게 된다.   


아래 YAML 파일은 위의 내용을 테스트 하기 위해서 작성한 파일이다.


apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: helloweb-disk

spec:

accessModes:

  - ReadWriteOnce

resources:

  requests:

    storage: 30Gi

---

apiVersion: v1

kind: ReplicationController

metadata:

name: nginx

spec:

replicas: 3

selector:

  app: nginx

template:

  metadata:

    name: nginx

    labels:

      app: nginx

  spec:

    containers:

    - name: nginx

      image: nginx:1.7.9

      volumeMounts:

      - name: nginx-data

        mountPath: /data/redis

      ports:

      - containerPort: 8090

    volumes:

    - name: nginx-data

      persistentVolumeClaim:

        claimName: helloweb-disk



nginx Pod를 RC를 이용하여 3개를 만들도록 하고, nginx-data 라는 볼륨을 helloweb-disk라는 PVC를 이용해서 마운트 하는 YAML 설정이다. 이 설정을 실행해보면 아래 그림과 같이 nginx-2784n Pod 하나만 생성된다.




%kubectl describe pod nginx-6w9xf

명령을 이용해서 다른 Pod가 기동되지 않는 이유를 조회해보면 다음과 같은 결과를 얻을 수 있다.



내용중에 중요한 내용을 보면 다음과 같다.


“Multi-Attach error for volume "pvc-d930bfcb-2ec0-11e9-8d43-42010a920009" Volume is already used by pod(s) nginx-2784n”


앞에서 설명한 대로, 볼륨(PV)이 다른 Pod (nginx-2784n)에 의해 이미 사용되고 있기 때문에 볼륨을 사용할 수 없고, 이로 인해서, Pod 생성이 되지 않고 있는 상황이다.


RS로 이를 해결 하려면 아래 그림과 같이 Pod 마다 각각 RS을 정의하고, Pod마다 각기 다른 PVC와 PV를 바인딩하도록 설정해야 한다.



그러나 이렇게 Pod 마다 별도로 RS와 PVC,PV를 정의하는 것은 편의성 면에서 쉽지 않다.

StatefulSet

그래서 상태를 유지하는 데이타베이스와 같은 애플리케이션을 관리하기 위한 컨트롤러가 StatefulSet 컨트롤러이다. (StatefulSet은 쿠버네티스 1.9 버전 부터 정식 적용 되었다. )

StatefulSet은 앞에서 설명한 RS등의 Stateless 애플리케이션이 관리하는 컨트롤러로 할 수 없는 기능들을 제공한다. 대표적인 기능들은 다음과 같다.

Pod 이름에 대한 규칙성 부여

StatefulSet에 의해서 생성되는 Pod들의 이름은 규칙성을 띈다. 생성된 Pod들은 {Pod 이름}-{순번} 식으로 이름이 정해진다. 예를 들어 Pod 이름을 mysql 이라고 정의했으면, 이 StatefulSet에 의해 생성되는 Pod 명들은 mysql-0, mysql-1,mysql-2 … 가 된다.

배포시 순차적인 기동과 업데이트

또한 StatefulSet에 의해서 Pod가 생성될때, 동시에 모든 Pod를 생성하지 않고, 0,1,2,.. 순서대로 하나씩 Pod를 생성한다. 이러한 순차기동은 데이타베이스에서 마스터 노드가 기동된 후에, 슬레이브 노드가 기동되어야 하는 조건등에 유용하게 사용될 수 있다.

개별 Pod에 대한 디스크 볼륨 관리

RS 기반의 디스크 볼륨 관리의 문제는 하나의 컨트롤러로 여러개의 Pod에 대한 디스크를 각각 지정해서 관리할 수 없는 문제가 있었는데, StatefulSet의 경우 PVC (Persistent Volume Claim)을 템플릿 형태로 정의하여, Pod 마다 각각 PVC와 PV를 생성하여 관리할 수 있도록 한다.


그럼 StatefulSet 예제를 보자


apiVersion: apps/v1

kind: StatefulSet

metadata:

name: nginx

spec:

selector:

  matchLabels:

    app: nginx

serviceName: "nginx"

replicas: 3

template:

  metadata:

    labels:

      app: nginx

  spec:

    terminationGracePeriodSeconds: 10

    containers:

    - name: nginx

      image: k8s.gcr.io/nginx-slim:0.8

      ports:

      - containerPort: 80

        name: web

      volumeMounts:

      - name: www

        mountPath: /usr/share/nginx/html

volumeClaimTemplates:

- metadata:

    name: www

  spec:

    accessModes: [ "ReadWriteOnce" ]

    storageClassName: "standard"

    resources:

      requests:

        storage: 1Gi


RS나 RC와 크게 다른 부분은 없다. 차이점은 PVC를 volumeClaimTemplate에서 지정해서 Pod마다 PVC와 PV를 생성하도록 하는 부분이다. 위의 볼드처리한 부분


이 스크립트를 실행하면 아래와 같이 Pod가 배포 된다.



pod의 이름은 nginx-0,1,2,... 식으로 순차적으로 이름이 부여되고 부팅 순서도 0번 pod가 기동되고 나면 1번이 기동되고 다음 2번이 기동되는 식으로 순차적으로 기동된다.


template에 의해서 PVC가 생성되는데, 아래는 생성된 PVC 목록이다. 이름은 {StatefulSet}-{Pod명} 식으로 PVC가 생성이 된것을 확인할 수 있다.


그리고 마지막으로 아래는 PVC에 의해서 생성된 PV(디스크 볼륨)이다.

기동 순서의 조작

위의 예제에 보는것과 같이, StatefulSet은 Pod를 생성할때 순차적으로 기동되고, 삭제할때도 순차적으로 (2→ 1 → 0 생성과 역순으로) 삭제한다. 그런데 만약 그런 요건이 필요 없이 전체가 같이 기동되도 된다면 .spec.podManagementPolicy 를 통해서 설정할 수 있다.

.spec.podManagementPolicy 는 디폴트로 OrderedReady 설정이 되어 있고, Pod가 순차적으로 기동되도록 설정이 되어 있고, 병렬로 동시에 모든 Pod를 기동하고자 하면  Parallel 을 사용하면 된다.

아래는 위의 예제에서 podManagementPolicy를 Parallel로 바꾼 예제이다.

apiVersion: apps/v1

kind: StatefulSet

metadata:

name: nginx

spec:

selector:

  matchLabels:

    app: nginx

serviceName: "nginx"

podManagementPolicy: Parallel

replicas: 3

template:

  metadata:

    labels:

      app: nginx

  spec:

    terminationGracePeriodSeconds: 10

    containers:

:

Pod Scale out and in

지금까지 StatefulSet에 대한 개념과 간단한 사용방법에 대해서 알아보았다. 그러면, StatefulSet에 의해 관리되는 Pod가 장애로 인하거나 스케일링 (In/out)으로 인해서 Pod의 수가 늘거나 줄면 그에 연결되는 디스크 볼륨은 어떻게 될까?


예를 들어 아래 그림과 같이 Pod-1,2,3 이 기동되고 있고, 이 Pod들은 StatefulSet에 의해서 관리되고 있다고 가정하자. Pod들은 각각 디스크 볼륨 PV-1,2,3 을 마운트해서 사용하고 있다고 하자.



이때, Pod-3가 스케일인이 되서, 없어지게 되면, Pod는 없어지지면, 디스크 볼륨을 관리하기 위한 PVC-3는 유지 된다. 이는 Pod 가 비정상적으로 종료되었을때 디스크 볼륨의 내용을 유실 없이 유지할 수 있게 해주고, 오토 스케일링이나 메뉴얼로 Pod를 삭제했을때도 동일하게 디스크 볼륨의 내용을 유지하도록 해준다.



그러면 없앴던 Pod가 다시 생성되면 어떻게 될까? Pod가 다시 생성되면, Pod 순서에 맞는 번호로 다시 생성이 되고, 그 번호에 맞는 PVC 볼륨이 그대로 붙게 되서, 다시 Pod 가 생성되어도 기존의 디스크 볼륨을 그대로 유지할 수 있다.




PodDisruptionBudget

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


Pod의 개수는 컨트롤러가 붙어 있을 경우에는 컨트롤러 스펙에 정의된 replica 수 만큼을 항상 유지 하도록 되어 있다. Pod의 수가 replica의 수를 유지 하지 못하고 줄어드는 경우가 있는데, 애플리케이션이 크래쉬 나거나, VM이 다운되는 등의 예상하지 못한 사고로 인한 경우가 있고, 또는 시스템 관리자가 업그레이드등의 이슈로 노드를 인위적으로 다운 시키는 것과 같이 예상 가능한 상황이 있다.

예상 가능한 상황에서 Pod가 없어지는 것을 Voluntary disruptions 라고 하고, 커널 패닉이나 VM 크래쉬같은 예기치 못한 상황에서 Pod가 없어지는 것을 Involuntary disruptions 라고 한다.


노드를 인위적으로 줄일 경우(Voluntary disruptions) 그 노드에 Pod가 여러개 돌고 있을 경우 순간적으로 Pod의 수가 줄어들 수 있다. 예를 들어 웹 서버 Pod가 노드1에 5개, 노드 2에 8개가 돌고 있을 때 노드1을 다운 시키면 순간적으로 Pod의 총 수가 8개가 된다. replica수에 의해서 복귀는 되겠지만, 성능을 유지하기 위해서 일정 수의 Pod 수를 유지해야 하거나, NoSQL 처럼 데이타 저장에 대한 안정성을 확보하기 위해서 쿼럼값만큼  최소 Pod를 유지해야 하는 경우 , 이런 노드 다운은 문제가 될 수 있다.


그래서 인위적인 노드 다운등과 같이 volutary disruption 상황에도 항상 최소한의 Pod수를 유지하도록 해주는 것이 PodDistruptionBudget (이하 PDB)이라는 기능이다.  PDB를 설정하면 관리자가 노드 업그레이드를 위해서 노드를 다운 시키거나 또는 오토스케일러에 의해서 노드가 다운될 경우, Pod수를 일정 수 를 유지하지 못하면 노드 다운이나 오토스케일러에 의한 스케일 다운등을 막고, Pod 수를 일정 수준으로 유지할 수 있을때 다시 그 동작을 하도록 한다.


예를 들어 nginx-pod가 2개가 유지되도록 PDB를 설정했다고 가정하자


node-1

node-2

node-3

nginx-pod-1

nginx-pod-2

nginx-pod-3


위와 같은 상태에서 시스템 관리자가 node-1에 대한 패치를 위해서 node-1을 다운 시키려고 node draining 명령을 실행했다고 하자. 그럼 node-1에 있는 nginx-pod-1은 다른 node로 옮겨진다.(이를 eviction이라고 한다. ) 구체적으로는 nginx-pod-1 은 terminate 되고, node-2나 node-3에 새로운 pod가 생기게 된다. 여기서는 node-2에 nginx-pod-4라는 이름으로 새로운 pod가 생성된다고 가정하자


node-1 (draining)

node-2

node-3

nginx-pod-1(terminating)

nginx-pod-2

nginx-pod-4 (starting)

nginx-pod-3


이때 마음급한 관리자가 nginx-pod-4가 기동되는 것을 기다리지 않고, node-2를 패치하기 위해서 node-3 를 다운 시키려고 draining 명령을 내렸다고 하자.

이 경우 nginx-pod-4가 아직 기동중인 상태이기 때문에, 현재 사용 가능한 pod는 nginx-pod-2 와 nginx-pod-3 2개 뿐인데, node-3를 다운 시키면 nginx-pod-2 하나 밖에 남지 않기 때문에, PDB에서 정의한 pod의 개수 2개를 충족하지 못하기 때문에, node-3에 대한 다운 작업은 블록 된다.


PDB는 YAML 파일을 이용해서 리소스로 정의될 수 있다.

PDB에서 정의 하는 방법은 minAvailable 정의할 경우 앞의 예제에서 설명한 바와 같이 최소한 유지해야 하는 Pod의 수를 정의할 수 있고 또는 maxUnavailable 을 정의해서 최대로 허용할 수 있는 Unhealthy한 Pod 의 수를 정의할 수 있다. 이 두 값은 Pod의 숫자로 정의할 수 도 있고 또는 %를 사용할 수 있다.


예를 들어

  • minAvailable 가 5이면, 최소한 5개의 Pod 는 항상 정상 상태로 유지하도록 한다.

  • minAvailable 가 10%이면,  Pod의 replica 수를 기준으로 10%의 Pod 는 항상 정상 상태로 유지하도록 한다.

  • maxUnavailable 가 1이면, 최대 한개의 Pod만 비정상 상태를 허용하도록 한다.


그러면 이렇게 정의한 값들은 어떻게 Pod에 적용할까, PDB를 만든후에, 이 PDB안에 label selector를 정의해서 PDB를 적용할 Pod를 선택할 수 있다.


완성된 예제는 다음과 같다.

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
 name: zk-pdb
spec:
 minAvailable: 2
 selector:
   matchLabels:
     app: zookeeper


< 출처 : https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget >


이 예제는 app: zookeeper라고 되어 있는 Pod 들을 최소 2개는 항상 유지할 수 있도록 하는 설정이다.

지금까지 간단하게 PDB에 대한 개념을 설명하였다. 일반적인 Stateless 애플리케이션에는 크게 문제가 없겠지만, 일정 성능 이상을 요하는 애플리케이션이나, NoSQL과 같은 일정 수의 쿼럼 유지가 필요한 애플리케이션등의 경우에는 PDB를 정의하는 것이 좋다.


참고로 현재 PDB는 베타 상태이고 정식 릴리즈 상태는 아니기 때문에, 운영 환경에는 아직 사용하지 않는 것이 좋다.

Istio #4 - 설치 및 BookInfo 예제

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

Istio 설치

그러면 직접 Istio 를 설치해보자, 설치 환경은 구글 클라우드의 쿠버네티스 환경을 사용한다. (쿠버네티스는 오픈소스이고, 대부분의 클라우드에서 지원하기 때문에 설치 방법은 크게 다르지 않다.)

쿠버네티스 클러스터 생성

콘솔에서 아래 그림과 같이 istio 라는 이름으로 쿠버네티스 클러스터를 생성한다. 테스트용이기 때문에, 한존에 클러스터를 생성하고, 전체 노드는 3개 각 노드는 4 CPU/15G 메모리로 생성하였다.



다음 작업은 구글 클라우드 콘솔에서 Cloud Shell내에서 진행한다.

커맨드 라인에서 작업을 할것이기 때문에, gCloud SDK를 설치(https://cloud.google.com/sdk/gcloud/) 한후에,

%gcloud auth login

gcloud 명령어에 사용자 로그인을 한다.


그리고 작업을 편리하게 하기 위해서 아래와 같이 환경 변수를 설정한다. 쿠버네티스 클러스터를 생성한 리전과 존을 환경 변수에 아래와 같이 설정한다. 예제에서는 asia-southeast1 리전에 asia-southeast1-c 존에 생성하였다. 그리고 마지막으로 생성한 쿠버네티스 이름을 환경 변수로 설정한다. 예제에서 생성한 클러스터명은 istio이다.

export GCP_REGION=asia-southeast1
export GCP_ZONE=asia-southeast1-c
export GCP_PROJECT_ID=$(gcloud info --format='value(config.project)')
export K8S_CLUSTER_NAME=istio

다음 kubectl 명령어를 사용하기 위해서, 아래과 같이 gcloud 명령어를 이용하여 Credential을 세팅한다

% gcloud container clusters get-credentials $K8S_CLUSTER_NAME \
   --zone $GCP_ZONE \
   --project $GCP_PROJECT_ID

Credential 설정이 제대로 되었는지

% kubectl get pod -n kube-system

명령어를 실행하여, 쿠버네티스 시스템 관련 Pod 목록이 잘 나오는지 확인한다.

Istio 설치

쿠버네티스 클러스터가 준비되었으면, 이제 Istio를 설치한다.

Helm 설치

Istio는 Helm 패키지 매니져를 통해서 설치 한다.

% curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
% chmod 700 get_helm.sh
% ./get_helm.sh

Istio 다운로드

Istio 를 다운로드 받는다. 아래는 1.0.4 버전을 다운 받는 스크립트이다.

% cd ~

% curl -L https://git.io/getLatestIstio | sh -

% cd istio-1.0.4

% export PATH=$PWD/bin:$PATH

Helm 초기화

Istio를 설치하기 위해서 Helm용 서비스 어카운트를 생성하고, Helm을 초기화 한다.

% kubectl create -f install/kubernetes/helm/helm-service-account.yaml

% helm init --service-account tiller

Istio 설치

다음 명령어를 이용하여 Istio를 설치한다. 설치시 모니터링을 위해서 모니터링 도구인 kiali,servicegraph 그리고 grafana 설치 옵션을 설정하여 아래와 같이 추가 설치 한다.

% helm install install/kubernetes/helm/istio \

--name istio \

--namespace istio-system \

--set tracing.enabled=true \

--set global.mtls.enabled=true \

--set grafana.enabled=true \

--set kiali.enabled=true \

--set servicegraph.enabled=true


설치가 제대로 되었는지 kubectl get pod명령을 이용하여, istio 네임스페이스의 Pod 목록을 확인해보자

% kubectl get pod -n istio-system




BookInfo 샘플 애플리케이션 설치

Istio 설치가 끝났으면, 사용법을 알아보기 위해서 간단한 예제 애플리케이션을 설치해보자, Istio에는 BookInfo (https://istio.io/docs/examples/bookinfo/)  라는 샘플 애플리케이션이 있다.

BookInfo 애플리케이션의 구조

아래 그림과 같이 productpage 서비스 안에, 책의 상세 정보를 보여주는 details 서비스와 책에 대한 리뷰를 보여주는 reviews 서비스로 구성이 되어 있다.  


시스템의 구조는 아래와 같은데, 파이썬으로 개발된 productpage 서비스가, 자바로 개발된 review 서비스과 루비로 개발된 details 서비스를 호출하는 구조이며, review 서비스는 v1~v3 버전까지 배포가 되어 있다. Review 서비스 v2~v3는 책의 평가 (별점)를 보여주는  Rating 서비스를 호출하는 구조이다



< 그림 Book Info 마이크로 서비스 구조 >

출처 : https://istio.io/docs/examples/bookinfo/

BookInfo 서비스 설치

Istio의 sidecar injection 활성화

Bookinfo 서비스를 설치하기 전에, Istio의 sidecar injection 기능을 활성화 시켜야 한다.

앞에서도 설명하였듯이 Istio는 Pod에 envoy 를 sidecar 패턴으로 삽입하여, 트래픽을 컨트롤 하는 구조이 다. Istio는 이 sidecar를 Pod 생성시 자동으로 주입 (inject)하는 기능이 있는데, 이 기능을 활성화 하기 위해서는 쿠버네티스의 해당 네임스페이스에 istio-injection=enabled 라는 라벨을 추가해야 한다.

다음 명령어를 이용해서 default 네임 스페이스에 istio-injection=enabled 라벨을 추가 한다.

% kubectl label namespace default istio-injection=enabled


라벨이 추가되었으면

% kubectl get ns --show-labels

를 이용하여 라벨이 제대로 적용이 되었는지 확인한다.


Bookinfo 애플리케이션 배포

Bookinfo 애플리케이션의 쿠버네티스 배포 스크립트는 samples/bookinfo 디렉토리에 들어있다. 아래 명령어를 실행해서 Bookinfo 앺ㄹ리케이션을 배포하자.

% kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml


배포를 완료한 후 kubectl get pod 명령어를 실행해보면 다음과 같이 productpage, detail,rating 서비스가 배포되고, reviews 서비스는 v1~v3까지 배포된것을 확인할 수 있다.



Kubectl get svc 를 이용해서 배포되어 있는 서비스를 확인하자



Prodcutpcage,rating,reviews,details 서비스가 배포되어 있는데, 모두 ClusterIP 타입으로 배포가 되어 있기 때문에 외부에서는 접근이 불가능하다.


Istio gateway 설정

이 서비스를 외부로 노출 시키는데, 쿠버네티스의 Ingress나 Service는 사용하지 않고, Istio의 Gateway를 이용한다.

Istio의 Gateway는 쿠버네티스의 커스텀 리소스 타입으로, Istio로 들어오는 트래픽을 받아주는 엔드포인트 역할을 한다. 여러 방법으로 구현할 수 있으나, Istio에서는 디폴트로 배포되는 Gateway는 Pod 형식으로 배포되어 Load Balancer 타입의 서비스로 서비스 된다.


먼저 Istio Gateway를 등록한후에, Gateway를 통해 서비스할 호스트를 Virtual Service로 등록한다.


아래는 bookinfo에 대한 Gateway를 등록하는 Yaml 파일이다.


apiVersion: networking.istio.io/v1alpha3

kind: Gateway

metadata:

 name: bookinfo-gateway

spec:

 selector:

   istio: ingressgateway # use istio default controller

 servers:

 - port:

     number: 80

     name: http

     protocol: HTTP

   hosts:

   - "*"


selector를 이용해서 gateway 타입을 istio에서 디폴트로 제공하는 Gateway를 사용하였다. 그리고, HTTP프로토콜을 80 포트에서 받도록 하였다.

다음에는 이 Gateway를 통해서 트래픽을 받을 서비스를 Virtual Service로 등록해야 하는데, 그 구조는 다음과 같다.


apiVersion: networking.istio.io/v1alpha3

kind: VirtualService

metadata:

 name: bookinfo

spec:

 hosts:

 - "*"

 gateways:

 - bookinfo-gateway

 http:

 - match:

   - uri:

       exact: /productpage

   - uri:

       exact: /login

   - uri:

       exact: /logout

   - uri:

       prefix: /api/v1/products

   route:

   - destination:

       host: productpage

       port:

         number: 9080


spec에서 gateways 부분에 앞에서 정의한 bookinfo-gateway를 사용하도록 한다. 이렇게 하면 앞에서 만든 Gateway로 들어오는 트래픽은 이 Virtual Servivce로 들어와서 서비스 디는데, 여기서 라우팅 룰을 정의 한다 라우팅룰은 URL에 때해서 어느 서비스로 라우팅할 지를 정하는데 /productpage,/login,/lougout,/api/v1/products URL은 productpage:9080 으로 포워딩해서 서비스를 제공한다.


Gateway와 Virtual service 배포에 앞서서, Istio에 미리 설치되어 있는 gateway를 살펴보면, Istio default gateway는 pod로 배포되어 있는데, istio=ingressgateway 라는 라벨이 적용되어 있다. 확인을 위해서 kubectl get 명령을 이용해서 확인해보면 다음과 같다.

%kubectl get pod -n istio-system -l istio=ingressgateway



이 pod들은 istio-ingressgateway라는 이름으로 istio-system 네임스페이스에 배포되어 있다. kubectl get svc로 확인해보면 다음과 같다.

%kubectl get svc istio-ingressgateway -n istio-system --show-labels



그러면 bookinfo를 istio gateway에 등록해서 외부로 서비스를 제공해보자

% istioctl create -f samples/bookinfo/networking/bookinfo-gateway.yaml


게이트 웨이 배포가 끝나면, 앞에서 조회한 Istio gateway service의 IP (여기서는 35.197.159.13)에 접속해서 확인해보자

브라우져를 열고 http://35.197.159.13/productpage 로 접속해보면 아래와 같이 정상적으로 서비스에 접속할 수 있다.



모니터링 툴

서비스 설치가 끝났으면 간단한 테스트와 함께 모니터링 툴을 이용하여 서비스를 살펴보자

Istio를 설치하면 Prometheus, Grafana, Kiali,Jaeger 등의 모니터링 도구가 기본적으로 인스톨 되어 있다. 각각의 도구를 이용해서 지표들을 모니터링 해보자

Grafana를 이용한 서비스별 지표 모니터링

Grafana를 이용해서는 각 서비스들의 지표를 상세하게 모니터링할 수 있다.

먼저 아래 스크립트를 사용해서 간단하게 부하를 주자. 아래 스크립트는 curl 명령을 반복적으로 호출하여 http://35.197.159.13/productpage 페이지를 불러서 부하를 주는 스크립이다.


for i in {1..100}; do

curl -o /dev/null -s -w "%{http_code}" http://35.197.159.13/productpage

done


다음 Grafana 웹 콘솔에 접근해야 하는데, Grafana는 외부 서비스로 노출이 안되도록 설정이 되어 있기 때문에 kubectl을 이용해서 Grafana 콘솔에 트래픽을 포워딩 하도록 하자. Grafana는 3000번 포트에서 돌고 있기 때문에, localhost:3000 → Grafana Pod의 3000 번 포트로 트래픽을 포워딩 하도록 설정하자


kubectl -n istio-system port-forward $(kubectl -n istio-system get pod -l app=grafana -o jsonpath='{.items[0].metadata.name}') 3000:3000 &


다음 localhost:3000 번으로 접속해보면 다음과 같은 화면을 볼 수 있다.

각 서비스 productpage,review,rating,detail 페이지의 응답시간과 OPS (Operation Per Sec : 초당 처리량)을 볼 수 있다.




각 서비스를 눌러보면 다음과 같이 서비스별로 상세한 내용을 볼 수 있다. 응답 시간이나 처리량에 대한 트렌드나, Request의 사이즈등 다양한 정보를 볼 수 있다.



Jaeger를 이용한 분산 트렌젝션 모니터링

다음은 Jaeger 를 이용해 개별 분산 트렌젝션에 대해서 각 구간별 응답 시간을 모니터링 할 수 있다.

Istio는 각 서비스별로 소요 시간을 수집하는데, 이를 Jaeger 오픈소스를 쓰면 손쉽게 모니터링이 가능하다.

마찬가지로 Jaeger 역시 외부 서비스로 노출이 되지 않았기 때문에, kubectl 명령을 이용해서 로컬 PC에서 jaeger pod로 포트를 포워딩하도록 한다. Jaerger는 16686 포트에서 돌고 있기 localhost:16686 → Jaeger pod:16686으로 포워딩한다.


kubectl port-forward -n istio-system $(kubectl get pod -n istio-system -l app=jaeger -o jsonpath='{.items[0].metadata.name}') 16686:16686 &


Jaeger UI에 접속해서, 아래는 productpage의 호출 기록을 보는 화면이다. 화면 상단에는 각 호출별로 응답시간 분포가 나오고 아래는 개별 트렉젝션에 대한 히스토리가 나온다.



그중 하나를 선택해보면 다음과 같은 그림을 볼 수 있다.



호출이 istio-ingressgateway로 들어와서 Productpage를 호출하였다.

productpage는 순차적으로 productpage → detail 서비스를 호출하였고, 다음 productpage→ reviews → ratings 서비스를 호출한것을 볼 수 있고, 많은 시간이 reviews 호출에 소요된것을 확인할 수 있다.


Servicegraph를 이용한 서비스 토폴로지 모니터링

마이크로 서비스는 서비스간의 호출 관계가 복잡해서, 각 서비스의 관계를 시각화 해주는 툴이 있으면 유용한데, 대표적인 도구로는 service graph라는 툴과 kiali 라는 툴이 있다. BookInfo 예제를 위한 Istio 설정에는 servicegraph가 디폴트로 설치되어 있다.


마찬가지로 외부 서비스로 노출 되서 서비스 되지 않고 클러스터 주소의 8088 포트를 통해서 서비스 되고 있기 때문에, 아래와 같이 kubectl 명령을 이용해서 localhost:8088 → service graph pod의 8088포트로 포워딩하도록 한다.


kubectl -n istio-system port-forward $(kubectl -n istio-system get pod -l app=servicegraph -o jsonpath='{.items[0].metadata.name}') 8088:8088 &


그 후에, 웹 브루우져에서 http://localhost:8088/dotviz 를 접속해보면 서비스들의 관계를 볼 수 있다.



다음 글에서는 예제를 통해서 Istio에서 네트워크 경로 설정하는 부분에 대해서 더 자세히 알아보도록 하겠다.



Istio #2 - Envoy Proxy


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

그럼 앞에서 설명한 서비스 매쉬의 구조를 구현한 Istio를 살펴보기전에, Istio에 사용되는 envoy 프록시에 대해서 먼저 알아보자.
(이 글은 예전에 포스팅한 내용이지만, Istio 글의 흐름상 다시 포스팅 한다.)

Envoy Proxy

먼저 istio에 사용되는 envory proxy를 살펴보자. Envoy 프록시는 Lyft사에서 개발되었으면 오픈소스로 공개되었다.

기존 프록시 L4기능 뿐 아니라 L7 기능도 지원하면서 HTTP 뿐아니라 HTTP 2.0,TCP,gRPC까지 다양한 프로토콜을 지원한다.


성능 지표를 보면 아래 Twillo에서 2017년에 테스트 한 자료를 참고할만 한데, (원본 https://www.twilio.com/blog/2017/10/http2-issues.html) HAProxy 보다 약간 느린것을 확인할 수 있다. 아무래도 L4가 아닌 L7단의 로드밸런서이다 보니 다소 성능 감소는 부담해야 한다.




(참고. 위의 문서를 보면 Envoy HTTP2 의 성능이 낮게 나오는데, 이는 Envory 자체 문제라가 보다는 HTTP/2가 Connection을 reuse하는 특성에서 온다고 볼 수 있는데, 성능에 대한 이슈가 있는 만큼 HTTP/2를 사용할 경우에는 별도의 검증 등이 필요하리라 본다.)


주요 기능적인 특성을 보면 다음과 같다.


  • HTTP, TCP, gRPC 프로토콜을 지원

  • TLS client certification 지원

  • HTTP L7 라우팅 지원을 통한 URL 기반 라우팅, 버퍼링, 서버간 부하 분산량 조절등

  • HTTP2 지원

  • Auto retry, circuit breaker, 부하량 제한등 다양한 로드밸런싱 기능 제공

  • 다양한 통계 추적 기능 제공 및 Zipkin 통합을 통한 MSA 서비스간의 분산 트렌젝션 성능 측정 제공함으로써 서비스에 대한 다양한 가시성 (visibility)을 제공

  • Dynamic configuration 지원을 통해서, 중앙 레파지토리에 설정 정보를 동적으로 읽어와서 서버 재시작없이 라우팅 설정 변경이 가능함

  • MongoDB 및 AWS Dynamo 에 대한 L7 라우팅 기능 제공


등 매우 다양한 기능을 제공한다.

Envoy 배포 아키텍처

Envoy 프록시는 배포 위치에 따라서 다양한 기능을 수행할 수 있는데, 크게 다음과 같이 4가지 구조에 배포가 가능하다.


<그림. Envoy 배포 방식>

Front envoy proxy

특정 서비스가 아니라, 전체 시스템 앞의 위치하는 프록시로, 클라이언트에서 들어오는 호출을 받아서 각각의 서비스로 라우팅을 한다. URL 기반으로 라우팅을 하는 기능 이외에도, TLS(SSL) 처리를 하는 역할들을 할 수 있다. 통상적으로 nginx나 apache httpd가 리버스프록시로 이 용도로 많이 사용되었다.

Service to service ingress listener

특정 서비스 앞에 위치하는 배포 방식으로 서비스로 들어오는 트래픽에 대한 처리를 하는데, 트래픽에 대한 버퍼링이나 Circuit breaker 와 같은 역할을 수행한다.

Service to service egress listener

특정 서비스 뒤에서 서비스로부터 나가는 트래픽을 통제 하는데, 서비스로 부터 호출 대상이 되는 서비스에 대한 로드 밸런싱, 호출 횟수 통제 (Rate limiting)와 같은 기능을 수행한다.

External service egress listener

내부서비스에서 외부 서비스로 나가는 트래픽을 관리하는 역할인데, 외부 서비스에 대한 일종의 대행자(Delegator)와 같은 역할을 한다.


시스템 앞 부분이나 또는 시스템을 구성하는 서비스의 앞뒤에 배치할 수 있는 구조지만, 서비스 앞뒤로 붙는다고 실제로 배포를 할때 하나의 서비스 앞뒤로 두개의 envoy proxy를 배치하지는 않는다.

다음과 같이 하나의 서비스에 하나의 Envoy를 배치 한후, ingress/egress 두 가지 용도로 겸용해서 사용한다.



Envoy 설정 구조

다음은 Envoy 설정 파일을 살펴 보자

Envoy의 설정은 크게 아래 그림과 같이 크게 Listener, Filter, Cluster 세가지 파트로 구성된다.



  • Listener
    Listener는 클라이언트로 부터 프로토콜을 받는 부분으로, TCP Listener, HTTP Listener 등이 있다.

  • Filter
    Filter는 Listener 로 부터 많은 메시지를 중간 처리하는 부분으로, 압축이나 들어오는 Traffic 에 대한 제한 작업등을 한후, Router를 통해서 적절한 클러스터로 메시지를 라우팅 하는 역할을 한다.

  • Cluster
    Cluster는 실제로 라우팅이 될 대상 서버(서비스)를 지정한다.


이렇게 Listener를 통해서 메시지를 받고, Filter를 이용하여 받은 메시지를 처리한 후에, 라우팅 규칙에 따라서 적절한 Cluster로 라우팅을 해서 적절한 서비스로 메시지를 보내는 형식이다.


Envoy 설치

Envoyproxy를 빌드하고 설치하는 방법은 여러가지가 있다. 소스코드로 부터 빌드를 하는 방법이나 이미 빌드된 바이너리를 사용해서 설치하는 방법 그리고 이미 빌딩된 도커 이미지를 사용하는 방법이 있다.

소스코드로 빌드하는 방법의 경우에는 bazel (make와 같은 빌드 도구) 빌드를 이용해서 빌드해야 하고, 빌드된 바이너리는 특정 플랫폼에 대해서만 미리 빌드가 되어 있기 때문에, 모든 플랫폼에 사용하기가 어렵다.

마지막으로는 도커 이미지 방식이 있는데, 이 방식이 배포면에서 여러모로 편리하기 때문에 도커 이미지를 이용한 배포 방식을 설명하도록 하겠다.


다음 명령어 처럼

docker pull을 이용하여 envoyproxy 도커 이미지 최신 버전을 가지고 오고, 다음 docker run 명령을 이용하여, 해당 이미지  (envoyproxy/envoy:latest)를 기동한다. 이때 -p 10000:10000 포트를 도커의 10000번 포트를 VM의 10000포트로 포워딩하도록 설정한다.


$ docker pull envoyproxy/envoy:latest
$ docker run --rm -d -p 10000:10000 envoyproxy/envoy:latest
$ curl -v localhost:10000


배포가 끝났으면, curl을 이용하여 localhost:10000번에 호출 하는 테스트를 하도록 한다.

설정에는 디폴트로, 10000 번 포트로 들어오는 모든 트래픽을 *.google.com으로 라우팅 하도록 설정되어 있다.


원본 설정 파일은 https://github.com/envoyproxy/envoy/blob/master/configs/google_com_proxy.v2.yaml 에 있고,  상세 내용을 보면 아래와 같다.


  • admin:
    이 부분은 envoyproxy의 admin 서버를 기동하는 부분으로, envoy 서버의 각종 설정이나 상태 정보를 127.0.0.1:9901로 들어오는 요청은 admin 기능으로 라우팅하도록 한다.

  • static_resources:
    Listener와 Filter 설정에 해당하는 부분으로, 아래 부면, listeners로 정의가 되어 있고 socket_address 부분에 0.0.0.0에 포트 10000 으로 들어오는 요청을 처리하도록 하였다.

    다음 filter_chain 부분에 filter들을 연속해서 정의하는데, http_connection_manager를 이용하여 모든 트래픽을 service_google이라는 클러스터로 라우팅 하도록 설정하였다.

  • clusters:
    마지막으로 clusters 부분에는 “service_google”이라는 클러스터를 정의했으며, 이 호스트의 URL은 google.com 443 포트로 정의하였다.


admin:

access_log_path: /tmp/admin_access.log

address:

  socket_address: { address: 127.0.0.1, port_value: 9901 }


static_resources:

listeners:

- name: listener_0

  address:

    socket_address: { address: 0.0.0.0, port_value: 10000 }

  filter_chains:

  - filters:

    - name: envoy.http_connection_manager

      config:

        stat_prefix: ingress_http

        route_config:

          name: local_route

          virtual_hosts:

          - name: local_service

            domains: ["*"]

            routes:

            - match: { prefix: "/" }

              route: { host_rewrite: www.google.com, cluster: service_google }

        http_filters:

        - name: envoy.router

clusters:

- name: service_google

  connect_timeout: 0.25s

  type: LOGICAL_DNS

  # Comment out the following line to test on v6 networks

  dns_lookup_family: V4_ONLY

  lb_policy: ROUND_ROBIN

  hosts: [{ socket_address: { address: google.com, port_value: 443 }}]

  tls_context: { sni: www.google.com }


다음글에서는 Istio에 대해서 알아보도록 하겠다.


EAI, ESB, API 게이트 웨이,서비스 매쉬

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


서비스간의 연동은 작게 보면 마이크로 서비스 아키텍쳐로 인한 문제 같지만, 서비스간의 연동은 마이크로 서비스 아키텍쳐 이전에도 자주 있어왔던 전통적인 문제이다. 이러한 문제를 소프트웨어 개발 프레임웍이 아니라, 솔루션 차원에서 풀기 위한 여러가지 노력들이 있었다.


시스템 통합 문제

메인 프레임 시대에서 유닉스 시스템으로 내려오면서 부터 시스템들은 업무 단위로 분리가 되기 시작했다. ERP,CRM 등과 같은 시스템으로, 은행은 대내,대외,정보계와 같이 시스템으로 잘게 잘게 나눠지기 시작했는데, 당연히 이렇게 나눠진 시스템 사이에는 통신이 필요하게 되었고, 시스템이 거대화 되가면서, 시스템간에 직접 P2P로 통신하는 구조는 한계에 다다르기 시작하였다.  



시스템이 서로 얽히고 어느 시스템이 어떤 시스템과 통신하는지 통제가 어렵게 되는 상황이 되었다.

EAI (Enterprise Application Integration)

이러한 시스템간의 문제를 해결하기 위해서 등장한 솔루션이 EAI인데, 통합을 원하는 시스템을 기존에는 직접 1:1로 붙였다면, EAI는 EAI가 중앙에 허브 역할을 하면서, 모든 통신을 EAI를 거치도록 하였다.



EAI의 가장 큰 특징은 표준화 되지 않은 이기종 시스템간의 연동을 가능하게 해준다는 것이다. 메인프레임 → Unix ERP 시스템으로 데이타를 전송하게 한다던지 Oracle → CRM 시스템으로 데이타를 전송해주는 것과 같은 시스템 통합을 지원했는데, 이는 이기종간에 통신 프로토콜이나 통합 방식을 변경할 수 있는 아답터를 제공하기 때문이다. EAI는 복잡한 메세지 처리나 변동, 라우팅같은 다양한 기능을 가지고 있었지만, 주로 이 기종간의 메세지 변환이 가장 많이 사용되었다.

어쨌거나 이런 EAI는 중앙 통제를 통해서 1:1 / 다:다로 통신되는 복잡한 토폴로지를 통합하는 의미가 있다.


EAI 시스템이 점점 더 많아지자. 시스템 통합 아키텍쳐도 패턴화 되었다. EIP (Enterprise Integration Pattern)이라는 형태로 정리되었는데, 아직도 참고할만한 구조가 많으니 관심이 있으면 참고하기 바란다.

https://www.enterpriseintegrationpatterns.com/patterns/messaging/toc.html

SOA / ESB

이 기종간의 통합이 많아지고, 시스템이 점점 분리되다 보니, 아예 이를 표준화하고자 하는 작업이 진행되었는데, 이것이 바로 SOA (Service Oriented Archtiecture / 서비스 지향 아키텍쳐) 이다.

SOA 아키텍쳐의 컨셉 자체는 MSA와 유사하지만, XML 기반의 웹서비스와 맞춰져서 웹서비스를 대표하는 아키텍쳐가 되어버렸다.

(사실 SOA는 아키텍쳐 구현 컨셉이지 XML/HTTP를 대표하는 것이 아니지만, 시대적으로 벤더들에 의해 웹서비스로 포장되었다. SOA는 시스템을 서비스로 나눈 다음 표준화된 인터페이스로 통신한다는 컨셉으로, 요즘의 MSA도 이 SOA의 부분 집합이라고 할 수 있다. )


웹서비스 기반으로 통신이 표준화되었기 때문에 서비스간의 통신은 EAI처럼 별도의 아답터가 필요없어졌다. 대신 서비스간의 통신을 서비스 버스라는 통신 백본을 이용하여 통신을 하는 구조가 되었다.




이론적으로는 매우 좋은 구조지만 웹서비스 자체가 스펙이 너무 복잡했고, 그 당시의 컴퓨팅 파워가 복잡한 XML을 파싱하기에는 충분하지 않았기 때문에 그다지 성공하지는 못한 아키텍쳐가 되었다.

특히나 ESB내에서 서비스간의 통신시에, 복잡한 XML 변환등을 사용하다가 ESB에 부하가 걸리게 되고, 제대로된 성능을 내지 못하는 결과를 낳게 되었다.

API Gateway

엔터프라이즈에 의해서 IT가 주도되는 시대가 끝나고 웹과 서비스의 시대가 오면서 개방형 API 즉, 오픈 API가 각광 받는 시대가 오면서 시스템 내부간의 통합도 중요했지만 외부로 API를 서비스 하는 클라이언트 대 서버간의 통합 그리고 외부 서버와 내부 서버와의 통합이 중요한 요건으로 대두되었다.

웹 기반의 서비스들은 복잡한 형태의 메세징을 필요로하지 않았기 때문에 XML을 버리고 상대적으로 간단한 JSON이 메인으로 사용되었고, 이를 HTTP 통신에 사용하면서 HTTP/JSON 기반의 REST 아키텍쳐가 유행하기 시작했다.

그래서  API 게이트 웨이가 외부로 서비스를 제공하기 위한 솔루션으로 제공되고, 시스템 내부간의 통합도 HTTP REST 를 이용하여 마치 ESB처럼 내부 버스로써 내부 시스템 통합을 지원하였다.



그러나 API 게이트웨이의 주요 목적은 앞에서 언급한 시스템간의 통합보다는, 클라이언트와 서버 중간에서 API에 대한 라우팅이나 인증 처리와 같이 내부 API를 외부 서비스로 제공하는데 촛점이 맞추어졌다.

물론 API에 대한 중간 통로 역할을 한다는 의미에서 ESB의 대안 모델로 사용이 가능하기 때문에, API 게이트웨이를 내부 서비스간 통신용으로 위치시키는 아키텍쳐도 있지만,  원래 목적 자체가 API를 외부에 서비스 하기 위한 목적으로 디자인된 아키텍쳐이기 때문에 여러모로 맞지 않는 부분이 있다.

특히나 대부분의 API 게이트 웨이는 게이트 웨이 인스턴스(클러스터)를 포진 시키는 방식이라서 서비스간의 통합 포인트가 많아서 복잡도가 올라가는 경우 API 게이트 웨이에 많은 부하가 걸려서 성능 저하가 발생하고, API 게이트 웨이가 장애가 날 경우 전체 서비스에 영향을 주는 (SPOF : Single Point of Failure) 가 된다는 단점이 있다.  

Service Mesh

이런 단점을 보안한 아키텍쳐가 서비스 매쉬 아키텍쳐이다.

  • API 게이트웨이와는 달리 외부로 서비스를 노출하기 보다는 내부 서비스간의 통신을 조율에 중점을 둔다.

  • 구조적으로 중앙 집중화 구조를 벗어나서, 분산형 아키텍쳐를 취하는 구조이다. 그래서 SPOF를 생성하지 않고 스케일한 서비스를 지원하기 좋다.


그래서 근래의 마이크로 서비스 아키텍쳐에서 안정적인 구조는 외부 서비스는 API 게이트 웨이를 통해서 노출하고, 내부 서비스는 서비스 매쉬를 통해서 통제하는 구조가 된다.



서비스 매쉬의 개념은 다음과 같다.

서비스가 서비스를 호출 하는 구조가 있을때


위의 그림과 같이 서비스가 서비스를 바로 직접 호출 하는 것이 아니라, 아래 그림과 같이 프록시를 둬서, 프록시를 통해서 호출을 하게 하는 구조이다.


이러한 구조를 가지게 되면, 네트워크 트래픽을 컨트롤 하는 것을 서비스의 소스 코드 변경없이도 가능하다. 예를 들어 서비스가 다른 대상 서비스를 호출할때 아래 그림과 같이, HTTP 헤더에서, 클라이언트의 종류에 따라 트래픽을 안드로이드용 서비스나 IOS용 서비스로 라우팅을 할 수 있다.


이러한 구조의 장점은 서비스를 호출하는 쪽이나 호출 받는 쪽이나 소스 코드 변환이 필요 없이 가능하다는 것이다.

서비스 매쉬의 가장 대표적인 솔루션은 istio 라는 솔루션으로 다음 글에서는 Istio에 대해서 설명하도록 하겠다.




쿠버네티스 리소스(CPU/Memory)할당과 관리

조대협

리소스 관리


쿠버네티스에서 Pod를 어느 노드에 배포할지를 결정하는 것을 스케쥴링이라고 한다.

Pod에 대한 스케쥴링시에, Pod내의 애플리케이션이 동작할 수 있는 충분한 자원 (CPU,메모리 등)이 확보되어야 한다. 쿠버네티스 입장에서는 애플리케이션에서 필요한 자원의 양을 알아야, 그 만한 자원이 가용한 노드에 Pod를 배포할 수 있다.


쿠버네티스에서는 이런 컨셉을 지원하기 위해서 컨테이너에 필요한 리소스의 양을 명시할 수 있도록 지원하고 있다.  현재(1.9 버전) 지원되는 리소스 타입은 CPU와 메모리이며, 아직 까지는 네트워크 대역폭이나 다른 리소스 타입은 지원하고 있지 않다.

리소스 단위

리소스를 정의하는데 사용되는 단위는 CPU의 경우에는 ms(밀리 세컨드)를 사용한다. 해당 컨테이너에 얼마만큼의 CPU 자원을 할당할것인가인데, 대략 1000ms가 1 vCore (가상 CPU 코어) 정도가 된다. 클라우드 벤더에 따라 또는 쿠버네티스를 운영하는 인프라에 따라서 약간씩 차이가 있으니 참고하기 바란다.

메모리의 경우에는 Mb를 사용한다.

Request & Limit

컨테이너에 적용될 리소스의 양을 정의하는데 쿠버네티스에서는 request와 limit이라는 컨셉을 사용한다.

request는 컨테이너가 생성될때 요청하는 리소스 양이고, limit은 컨테이너가 생성된 후에 실행되다가 리소스가 더 필요한 경우 (CPU가 메모리가 더 필요한 경우) 추가로 더 사용할 수 있는 부분이다.


예를 들어 CPU request를 500ms로 하고, limit을 1000ms로 하면 해당 컨테이너는 처음에 생성될때 500ms를 사용할 수 있다. 그런데, 시스템 성능에 의해서 더 필요하다면 CPU가 추가로 더 할당되어 최대 1000ms 까지 할당될 수 있다.


리소스를 정의하는 방법은 아래와 같이 Pod spec 부분에서 개별 컨테이너 마다. Resources 파트에 request와 limit으로 필요한 리소스의 최소/최대양을 정의하면 된다.


apiVersion: v1

kind: Pod

metadata:

 name: frontend

spec:

 containers:

 - name: db

   image: mysql

   resources:

     requests:

       memory: "64Mi"

       cpu: "250m"

     limits:

       memory: "128Mi"

       cpu: "500m"

 - name: wp

   image: wordpress

   resources:

     requests:

       memory: "64Mi"

       cpu: "250m"

     limits:

       memory: "128Mi"

       cpu: "500m"


위의 예제에 따라서 정의된 Pod내의 컨테이너 CPU 리소스의 할당은 다음과 같이 된다.


db라는 이름과 wp라는 이름의 컨테이너는 생성시 250ms 만큼의 CPU 리소스를 사용할 수 있도록 생성이 되고, 필요시 최대 CPU를 500ms 까지 늘려서 사용할 수 있다.

모니터링 리소스

그러면 사용할 수 있는 리소스의 양과 현재 사용되고 있는 리소스의 양을 어떻게 모니터링할 수 있을까?

사용할 수 있는 리소스의 양은 쿠버네티스 클러스터를 생성하는데 사용된 node의 스펙을 보면 알 수 있다. 예를 들어 2 코어 VM 5대로 node를 만들었다면 그 총량은 10 코어 = 10,000ms가 된다.

그러나 이 자원을 모두 사용자 애플리케이션에 사용할 수 있는 것이 아니다. 쿠버네티스 클러스터를 유지하는 시스템 자원이나 또는 모니터링등에 자원이 소비되기 때문에 실제로 사용할 수 있는 자원의 양을 확인하는게 좋은데 “kubectl describe nodes” 명령을 이용하면 된다.

아래 예제는 kubectl describe nodes 명령으로 node들의 상세정보중에서 한 node의 자원 상태를 모니터링한 내용이다.



아래 붉은 박스를 보면 총 4 코어 머신으로 현재 request된 CPU는 1081m이고 limit으로 296m를 확보하고 있다. 메모리는 request 된것은 685M가 requested 되었고, 약 1G가 limit으로 확보되어 있다.

실제 사용량은 붉은 박스 위를 보면 되는데, default 네임 스페이스의 client-6bcxxx Pod는 현재 110m의 CPU를 request해서 사용중인것을 확인할 수 있다.


확보된 리소스와 현재 실제로 사용되는 리소스의 양은 다른데, “kubectl top nodes” 명령을 이용하면 실제로 사용되고 있는 리소스의 상태를 확인할 수 있다. 아래는 4개의 노드에서 실제로 사용되고 있는 리소스의 양이다. 붉은 색으로 표시된 노드가 위의 예제와 같은 노드인데, 위에서 requested 된 양은 1081m이었는데, 실제 사용된 cpu는 151m가 사용되고 있다.


Pod들의 리소스 사용량은 “kubectl top pods” 명령으로 확인이 가능하다.


ResourceQuota & LimitRange

이제까지 컨테이너 운영에 필요한 리소스의 양을 명시하여 요청하는 방법을 알아보았다.

만약에 어떤 개발자나 팀이 불필요하게 많은 리소스를 요청한다면, 쿠버네티스 클러스터를 운영하는 입장에서 자원이 낭비가 되고, 다른 팀이 피해를 볼 수 있는 상황이 될 수 있다. 그래서, 쿠버네티스에서는 네임스페이스별로 사용할 수 있는 리소스의 양을 정하고, 컨테이너마다 사용할 수 있는 리소스의 양을 지정할 수 있는 기능을 제공한다.

Resource Quota

Resource Quota는 네임스페이스별로 사용할 수 있는 리소스의 양을 정한다.

아래는 예는 demo 네임스페이스에, CPU 는 500m ~ 700m 까지, 메모리는 100M~500M까지 할당한 예제이다.



이 용량안에서 demo 네임스페이스내에 컨테이너를 자유롭게 만들어서 사용할 수 있다.

Limit Range

Resource Quota가 네임 스페이스 전체의 리소스양을 정의한다면, Limit Range는 컨테이너 개별 자원의 사용 가능 범위를 지정한다.

아래 예제를 보자.



  • default 로 정의된 부분은 컨테이너에 limit을 지정하지 않았을 경우 디폴트로 지정되는 limit이다. 여기서는 cpu 600m, 메모리 100m로 정의되었다.

  • defaultRequest 로 정의된 부분은 컨테이너의 request를 지정하지 않았을 경우 디폴트로 지정되는 request의 양이다.

  • max : 컨테이너에 limit을 지정할 경우, 지정할 수 있는 최대 크기이다.

  • min : 컨테이너에 limit을 지정할 경우, 지정할 수 있는 최소 크기이다.  

Overcommitted 상태

이  request와 limit의 개념이 있기 때문에 생기는 문제인데, request 된 양에 따라서 컨테이너를 만들었다고 하더라도, 컨테이너가 운영이되다가 자원이 모자르면 limit 에 정의된 양까지 계속해서 리소스를 요청하게 된다.

컨테이너의 총 Limit의 양이 실제 시스템이 가용한 resource의 양보다 많을 수 있는 경우가 발생한다. 이를 overcommitted 상태라고 한다.

Overcommitted 상태가 발생하면, CPU의 경우에는 실제 사용량을 requested 에 정의된 상태까지 낮춘다. 예를 들어 limit이 500, request가 100인 경우, 현재 500으로 가동되고 있는 컨테이너의 CPU할당량을 100으로 낮춘다. 그래도 Overcommitted 상태가 해결되지 않는 경우, 우선 순위에 따라서 운영중인 컨테이너를 강제 종료 시킨다.  

메모리의 경우에는 할당되어 사용중인 메모리의 크기를 줄일 수 는 없기 때문에, 우선 순위에 따라서 운영 중인 컨테이너를 강제 종료 시킨다.  Deployment,RS/RC에 의해 관리되고 있는 컨테이너는 다시 리스타트가 되고 초기 requested 상태의 만큼만 자원 (메모리/CPU)를 요청해서 사용하기 때문에, overcommitted  상태가 해제된다.

Best practice

구글 문서에 따르면 데이타 베이스등 아주 무거운 애플리케이션이 아니면, 일반적인 경우에는 CPU request를 100m 이하로 사용하기를 권장한다.

또한 세밀하게 클러스터를 운영하기 어려운 경우에는 request와 limit의 사이즈를 같게 하는 것을 권장한다. limit이 request보다 클 경우 overcommitted 상태가 발생할 수 있는데, 이때 CPU가 throttle down 되면, 실제 필요한 CPU양 보다 작은 CPU양으로 줄어들기 때문에 성능저하가 발생할 수 있다.  




쿠버네티스 보안 Best Practice


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


지금까지 여러가지 보안 기능에 대해서 알아보았다. 그러면 이러한 보안 기능을 어떻게 잘 사용할지 베스프 프렉틱스에 대해서 알아보자. 쿠버네티스 보안 베스트 프렉틱스는 쿠버네티스 공식 블로그 https://kubernetes.io/blog/2016/08/security-best-practices-kubernetes-deployment/ 에 2016년 8월에 포스팅과 https://kubernetes.io/docs/tasks/administer-cluster/securing-a-cluster/ 에 2018년 7월에 포스팅된 내용을 기반으로 한다. 쿠버네티스는 새버전 릴리즈가 빠른 편이고 버전마다 기능이나 아키텍쳐의 변화가 심하기 때문에, 항상 새로운 베스트 프렉틱스를 찾아서 참고하기 바란다.

이 글에서는 쉽지만 중요한 보안 정책을 위주로 설명한다.

Control plan security

TLS (SSL) 사용

쿠버네티스를 설치하면 디폴트로 API 통신은 TLS(SSL) 암호화를 이용하도록 되어 있으나, 일부 재배포판의 경우는 REST API통신을 HTTPS를 사용하지 않고, HTTP를 사용하는 경우가 있기 때문에, 이를 확인할 필요가 있다.


인증

쿠버네티스는 앞에서 설명한데로 여러가지 인증 방식을 제공하고 있는데, 그중에 BASIC_AUTH를 사용하는 방식등은, 비밀 번호가 그냥 네트워크를 통해서 전송되기 때문에, 중간에 패킷을 가로 채는 방식등으로 탈취가 가능하다.

쿠버네티스는 이외에도 Bootstrap token, static token, X509 인증서, Open ID 연동등 다양한 인증 방식이 있는데, 가급적이면 Open ID 인증 방식을 사용하는 것이 안전하다.

RBAC 사용

쿠버네티스의 기본 인증 방법은 ABAC(Attirbute-Based control) 이다.  사용자마다 기능에 대해서 권한을 배정하는 방법인데,

RBAC은 1.6에서 소개되었고, 1.8 부터는 디폴트이다. 1.6~1.8 버전은 RBAC 설정을 따로 하기 바라고, 1.6 이하 버전은 1.8 이상 버전으로 업그레이드 하는 것을 권장한다.

대쉬 보드 사용 금지

쿠버네티스 대쉬 보드는 편리하고 강력한 기능을 가지고 있지만, 별도의 접근 통제 기능이 디폴트로 탑재되어 있지 않다. 1.8 이전 버전에는 클러스터에 대한 모든 억세스가 가능한 서비스 어카운트가 바인딩되어 있기 때문에, 클러스터에 대한 모든 접근이나 보안 정보(토큰)등의 탈취가 가능하다.

실제로 테슬라의 경우에 쿠버네티스 대쉬보드 접근을 통해서, 해킹을 당한 사례가 유명하다. https://redlock.io/blog/cryptojacking-tesla

가급적이면 쿠버네티스 대쉬보드를 사용하지 않도록 하는 것이 좋고 (인스톨하지 않는다.), 사용한다고 해도, 계정 인증과, 내부 인터넷망을 통한 접근만을 허용하는등의 추가적인 보안 조치가 반드시 필요하다.

kubectl 억세스 통제 (마스터 노드 억세스 통제)

kubectl은 쿠버네티스를 통제할 수 있는 매우 강력한 툴이다. 일종의 어드민툴이기 때문에 접근 제어를 하는 것이 좋다. 쿠버네티스 방화벽 설정등을 해서 특정 머신에서만 오는 트래픽만 마스터 노드가 받아 드리도록 설정하는 방법이다.  태그 기반으로 k8s-controller로 가는 트래픽을 특정 머신에서 오는 트래픽만을 수용하게 하거나 또는 bastion을 놓고, bastion에서 들어온 API 호출만 수용하도록 하는 방법이 있다.

서비스 어카운트 토큰 마운트을 자동으로 마운트 하지 않게 한다.

Pod는 기본적으로 서비스 어카운트를 사용하게 되어 있다. 만약 서비스 어카운트를 지정하지 않으면 디폴트로 정의된 서비스 어카운트를 사용하게 되는데, 쿠버네티스에서는 디폴트로 서비스 어카운트를 사용하게 되면 서비스 어카운트의 API 토큰을 볼륨으로 마운트 한다.

서비스 어카운트 볼륨은 /var/run/secrets/kubernetes.io/serviceaccount 디렉토리에 마운트 되는데, 이 디렉토리 안에는 API인증을 위한 인증서와, 토큰이 들어 있다.

만약 이 토큰을 탈취당하게 되면, 토큰을 이용하여 쿠버네티스 API 접근이 가능하다.

일반적인 Pod의 경우에는 애플리케이션을 운영하기 위한 목적으로 사용될뿐, 쿠버네티스 API를 접근할 일이 없기 때문에 사용하지 않는 토큰을 마운트 하는 것은 위험하다. 해커가 컨테이너를 해킹해서  /var/run/secrets/kubernetes.io/serviceaccount 디렉토리를 접근한다면 토큰을 탈취할 수 있다.

이를 막기 위해서, 서비스 어카운트를 사용시 디폴트로 서비스 어카운트 토큰을 마운트하지 않게 하는 것이 좋다.


아래와 같이 서비스 어카운트를 생성할때, 자동으로 토큰을 마운트 하지 않는 옵션을 주거나,

apiVersion: v1

kind: ServiceAccount

metadata:

 name: nonroot-sa

automountServiceAccountToken: false


또는 아래와 같이 Pod 정의 부분에서 서비스 어카운트에 디폴트로 토큰을 마운트 하지 않게 정의하면 된다.


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

     automountServiceAccountToken: false

     securityContext:

       runAsUser: 1001

       fsGroup: 2001

     containers:

     - name: nonroot

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

       imagePullPolicy: Always

       ports:

       - containerPort: 8080


반대로 토큰을 사용하고 싶을때는 Pod 정의 부분에서 automountServiceAccountToken: true 옵션을 주면된다.


감사 로깅 (Audit)

쿠버네티스 클러스터에 대한 각종 명령어 (Pod 생성, Service 생성 삭제 등)에 대한 내용을 추적하기 위해서 모든 로그를 남겨서 정상적인 접근 여부를 판단하고, 비정상적인 접근이 발생하였을때, 이를 감지하고 추적할 수 있는 기반을 마련해야 한다. 모니터링/로깅 시스템 구축시 감사 로깅은 별도로 분리해서 감사에 대한 내용만을 따로 추적할 수 있도록 하는것이 좋다.

노드 시큐리티

컨테이너를 호스팅하는 노드에 대해서도 보안 조치가 필요하다. 다음 항목은 필수적으로 적용하기를 권장되는 항목이다.

노드에 Private IP 만 사용 (Public IP 사용 금지)

노드 서버의 IP를  Private IP만을 사용하여, 외부 인터넷으로 부터 노드 서버를 접속할 수 있는 경로를 원천적으로 차단한다.

Minimal OS 사용

노드 서버의 OS는 필요한 기능만을 가지고 있는 OS만을 설치하는 것이 좋다. OS에 따라서 디폴트로 메일 서버, FTP등 사용하지 않는 서비스가 디폴트로 제공됨으로써, 노드 서버로 접근할 수 있는 채널을 제공할 수 있다.

정기적인 패치

또한 노드 OS에 대해서 정기적인 보안 패치를 적용함으로써, 새롭게 발견되는 보안 위협에 대해 사전 봉쇄 조치를 취해야 한다

컨테이너 시큐리티

마지막으로 컨테이너에 대한 보안을 강화하는 방법이다.

컨테이너 이미지 관리

가장 중요한 것중의 하나가 컨테이너 이미지를 잘 관리하는 것인데, 일반적으로 도커로 이미지를 만들때, 베이스 이미지 (OS등이 깔려있는)를 외부 컨테이너 레파지토리에서 가져다가 사용하는 경우가 많다. 이 경우 공인되지 않는 이미지등을 사용해서, 해킹 프로그램이 깔려있는 이미지가 베이스 이미지로 사용되거나, 또는 최신 보안 패치가 되어 있지 않은 이미지로 전환하지 않고 계속 오래된 이미지를 사용해서, 보안에 헛점을 들어내는 경우가  많다.

베이스 이미지 사용은 반드시, 보안이 검증된 이미지를 사용하되, 지속적으로 최신 OS 패치를 적용한 이미지를 사용해야 한다.

베이스 이미지를 포함하여 실제 쿠버네티스에 배포되는 애플리케이션 컨테이너 이미지는 신뢰할 수 있는 이미지 저장 서비스를 이용하고, 해당 클러스터 및 허가된 IP나 사용자만 접근할 수 있도록 하는 것이 좋다.


또는 상용 서비스 중에는 컨테이너 저장소에 저장된 이미지를 스캔해서 보안에 위협이 되는 항목을 자동으로 검출하여 알려주는 서비스들이 있다.

아래 그림은 구글 클라우드의 컨테이너 저장소 서비스로, 컨테이너에 저장된 이미지에 대해서 보안 위협을 자동으로 스캔해서 리포팅 해주는 기능이다.




Security Context 사용

Pod를 정의할때, 불필요한 root나 커널 접근 권한을 최대한 제외하는 것이 좋다.

Security context를 이용해서 이런 권한을 통제할 수 있다.

  • 컨테이너는 꼭 필요하지 않는 이상 root 사용자 권한이 아니라 일반 사용자로 실행하도록 한다. securityContext에서 runAsUser와 fsGroup을 이용해서 사용자와 그룹을 지정할 수 있다.

  • Root 권한으로 실행할 수 없도록 securityContext에서 runAsNonRoot 를 true로 설정한다.

  • 꼭 필요한 경우가 아니라면 root 권한으로 생성된 파일이나 디렉토리에 대해서는 읽기만을 할 수 있도록 SecurityContext에서 readOnlyRootFilesystem 를 true로 설정한다.

  • 마지막으로 필요한 경우가 아니라면 호스트 커널에 대한 접근을 막기 위해서 securityContext에서 privileged를 false로 설정한다.

PodSecurityPolicy를 이용한 Pod Security Context 통제

SecurityContext를 위와 같이 설정하도록 권고하지만, 이를 빼먹을 수 있기 때문에,  PodSecurityPolicy (PSP)를 정의하여, Security context를 강제할 필요가 있다.

Network Policy를 이용한 트래픽 통제

마지막으로 Network policy를 정의해서, Pod로의 네트워크 접근을 통제하여 불필요한 접근을 막는다. 예를 들어 MySQL DB서버로의 접근은 label이 app=apiserver인 서버들만 3306으로 inbound 트래픽만 들어올 수 있도록 통제하는 등의 예를 들 수 있다.



아두이노 nodemcu로 온습도계를 만들어 보자


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


nodeMCU 개발환경 설정이 끝났으면 간단한 애플리케이션을 하나 만들어보자

온습도계를 만들어보도록 한다. 온습도를 측정하여 LED로 출력하는 모듈을 개발해보겠다.

개발이 끝나고 나서 아두이노 개발환경에 대한 결론 부터 이야기 하자면, 쉽다. 대부분의 파츠들에 대한 SDK가 제공되기 때문에 손쉽게 개발이 가능하다. 단 해당 파츠에 맞는 SDK를 찾는데 들억는 시간이 더 많다.

온습도계 센서 (DTH11)

개발에 사용할 온습도계 센서는 DTH11이라는 센서이다. 아래와 같이 생겼는데, 좌측이 데이타, 가운데가 3.3V, 가장 우측이 GND이다.



온도와 습도 두개를 측정하는데 데이타 단자가 하나이다. 아날로그 신호를 핀에서 직접 읽는 것이 아니라 SDK를 사용한다.  DTH11 라이브러리는 https://github.com/adafruit/DHT-sensor-library 에서 다운 받아서 사용하면 된다. Adafruit_sensor 라이브러리에 대한 의존성이 있기 때문에, 해당 라이브러리 https://github.com/adafruit/Adafruit_Sensor 도 같이 설치하도록 한다.


라이브러리 설치가 끝났으면, DTH11 센서를 브레드 보드에 설치한다 좌측을 GPIO G6 포트에, 가운데를 3.3V, 가장 우측은 GND에 연결한다.


I2C LCD

다음 습도와 온도를 출력하기 위해서 LCD를 사용한다. 여기서 사용한 LCD는 I2C LCD로 가로 16자로 2줄 (16x2) 를 출력할 수 있는 LCD이다.


앞판은 LCD가 있고




뒤에는 아래 그림과 같이 LCD 아답터가 붙어 있다.

  • VCC는 nodemcu의 Vin 핀에 연결한다. 이 핀은 5V의 전앞을 낸다.

  • GND는 nodemcu의 GND에

  • SCA는 nodemcu D2에

  • SCL은 nodemcu의 D1핀에 연결한다.



다음 이 LCD를 사용하기 위해서는 SDK를 설치해야 하는데, https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library 라이브러리를 다운 받아서 설치한다.


코드

이제 코드를 보자

#include <LiquidCrystal_I2C.h>

#include "DHT.h"


#define LED D5            // Led in NodeMCU at pin GPIO16 (D0).

#define DHTPIN D6

#define DHTTYPE DHT11   // DHT 11


LiquidCrystal_I2C lcd(0x27, 16, 2);

DHT dht(DHTPIN, DHTTYPE);

float h,t;

void setup() {

 pinMode(LED, OUTPUT);    // LED pin as output.

 dht.begin();


 // LCD setting

 lcd.begin();

 lcd.backlight();

 lcd.print("Hello, world!");

 

 // Serial communication setting

 Serial.begin(9600);   

 Serial.print("Hello nodemcu");

}

void loop() {

 digitalWrite(LED, HIGH);// turn the LED off.(Note that LOW is the voltage level but actually

                         //the LED is on; this is because it is acive low on the ESP8266.

 delay(200);            // wait for 1 second.

 digitalWrite(LED, LOW); // turn the LED on.

 delay(200); // wait for 1 second.

 h = dht.readHumidity();

 t = dht.readTemperature();

 lcd.setCursor(0,0);

 lcd.print("Humidity:");

 lcd.print(h);

 lcd.setCursor(0,1);

 lcd.print("Temp :");

 lcd.print(t);

 Serial.print("H:");

 Serial.print(h);

 Serial.print(" T:");

 Serial.println(t);

}


DHT11 관련 코드

DHT11을 사용하려면, 입력 포트를 정의해야 하고 센서의 종류를 정의해야 한다.

#define DHTPIN D6

#define DHTTYPE DHT11   // DHT 11


DHT dht(DHTPIN, DHTTYPE);


에서 핀은 D6로 지정하고 DHTTYPE은 DHT11로 정의하였다.

다음 센서를 가동 시키기 위해서 setup() 에서


 dht.begin();


와 같이 DHT 센서를 가동 시켰다. 다음은 센서에서 온도와 습도를 읽어와야 하는데, loop() 함수내에서


 h = dht.readHumidity();

 t = dht.readTemperature();


readHumidity()와 readTemperature() 함수를 이용하여, 습도와 온도를 읽어왔다.


LCD 관련 코드

LCD를 사용하려면 초기화를 해줘야 하는데


LiquidCrystal_I2C lcd(0x27, 16, 2);


로 초기화를 해준다. 0x27는 LCD의 주소로, D1,D2 핀을 사용할때 사용하는 주소이다.핀의 위치가 바뀌면 이 주소도 변경 되어야 한다. 핀에 위치에 따라 주소가 다르거나 또는 인식이 안되는 경우가 있는데, http://playground.arduino.cc/Main/I2cScanner 를 이용하면 LCD가 연결이 되어 있는지 아닌지, 그리고 LCD의 주소를 알려준다.

다음 인자인 16,2는 가로 16자에 세로 2자 LCD 임을 정의해준다.


다음 초기화를 해줘야 하는데, setup()함수에서

 lcd.begin();

 lcd.backlight();

 lcd.print("Hello, world!");


로 초기화를 해준다. begin()은 LCD를 시작하는 것이고 backlight()는 LCD의 백라이트를 키도록 하는것이다. 글자를 출력하고 싶으면 print(“문자열")을 이용하면된다.

초기화가 끝났으면, DTH11 센서에서 읽은 값을 출력해주면된다.

윗줄에 습도 아랫줄에 온도를 출력할것인데

 lcd.setCursor(0,0);

 lcd.print("Humidity:");

 lcd.print(h);

 lcd.setCursor(0,1);

 lcd.print("Temp :");

 lcd.print(t);


윗줄 첫번째 위치 부터 습도를 출력할것이기 때문에, 출력 위치를  setCursor(0,0)으로 해서 맨 앞칸 첫줄로 지정을 해서 습도를 출력하고, 다음 온도는 setCursor(0,1)로 해서 맨 앞칸 두번째 줄 부터 출력하도록 하였다.



<그림. 완성된 모습 >

다음글에서는 이렇게 수집한 정보를 HTTP 를 이용해서 서버로 전송하는 코드를 추가해보도록 한다.


참고 자료


맥 OSX에서 nodeMCU와 Wemos D1 환경 설정하기


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


아두이노 우노로 아두이노 개발을 시작하고 서버 통신을 위해서 ESP8266 계열인 ESP01 칩을 사용했는데,  ESP01은 연결도 까다롭고 소프트웨어 시리얼을 사용해서 SDK를 찾기 어려운점도 있었다. 개발하고자 하는 내용이 대부분 서버와 통신을 하는 부분이기 때문에, 보드를 우노에서 ESP8266 을 메인 MCU로 하는 보드로 변경하였다.


후보군으로 올른것이 nodeMCU v2와 Wemos D1 보드이다.


<그림 nodeMCU v2와 Wemos D1 호환 보드>


nodeMCU의 경우에는 크기가 작고 성능이 뛰어날뿐 아니라, 널리 사용되는 보드이기 때문에, SDK나 예제를 구하기 쉬울것이라고 생각하였고, Wemos D1은 ESP8266을 포함하고 있으면서도 아두이노 우노와 유사한 레이아웃과 GPIO 핀 배열을 가지고 있기 때문에, 일반적인 개발에 좀더 편리하지 않을까 싶었다.


맥을 사용하기 때문에, OSX에 맞춰서 개발환경을 설정해야 했다.

USB 드라이버 설치

nodeMCU를 맥에 연결해도 MAC에서는 USB 포트를 인식하지 못한다. 이유는 nodeMCU와 통신할 USB 드라이버가 없기 때문에, nodeMCU는 아래 그림과 같이 USB 통신을 위해서 CP2102라는 칩셋을 사용한다. 그래서 이 칩셋을 위한 드라이버를 설치해줘야 한다.




드라이버를 https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers 에서 받아서 설치하면 된다. 이 드라이버는 Kernel Extension 이라는 형태인데 커널을 확장하는 기능이기 때문에 보안적인 제약사항을 받는다. 설치를 하더라도 바로 반영이 안되는데,  이유는 커널 확장 기능을 설치하려면 보안 승인을 해야 한다. USB 드라이버를 설치하고 나면 System > Preference에서 Security & Privacy 부분을 보면 아래와 같이 Kernel extension이 loading 되는 것이 블록 되어 있는 것을 볼 수 있다. 오른쪽의 Allow 버튼을 누르면 승인이 되고, 정상적으로 Kernel extension이 설치 된다.


제대로 설정이 되었는지를 확인하려면 OSX에서 해당 포트를 인지했는지 보면 되는데,

%ls /dev/tty.*

를 실행하면 다음과 같이


tty.SLAB_USBtoUART 이름으로 포트가 인식된것을 확인할 수 있다.


보드 추가

다음으로는 아두이노 개발환경인 Sketch에서 nodeMCU 보드 타입을 등록해야 한다.

Sketch 툴에서 Arduino > Preference 를 선택한다.

다음 아래와 같이 화면이 나오면 “Additional Boards Managers URLs”에

http://arduino.esp8266.com/stable/package_esp8266com_index.json

주소를 넣는다.


이렇게 해주면 Sketch에서 사용할 수 있는 보드의 종류가 추가로 등록된다. 다음 nodemcu를 사용하도록 보드를 선택해야 하는데, Tools > Boards 메뉴로 가서 아래 그림과 같이 node MCU v1.0을 선택한다.




통신 포트를 선택하고 다음 통신 속도를 921600으로 선택한다. 다음 제품 스펙에 맞게 아래 그림에서 “CPU Frequency”를 160Mhz로 조정하여서 실행하였다.


이제 개발 준비가 끝나고 개발을 진행하면 된다.



Wemos D1 환경 설정하기

Wemos D1 환경 설정도 크게 다르지 않다. 다만 USB 칩을 CH341칩셋을 사용하기 때문에, 맞는 드라이버를 설치해야 한다. 설치 방법은 동일하고, 드라이버는 https://wiki.wemos.cc/downloads 에서 다운로드 받을 수 있다. 보드 매니져에 보드를 추가해야 하는데, esp8266 계열이기 때문에, 앞에 추가한 보드 메니져에 이미 wemos d1이 들어가 있기 때문에, 이를 선택해서 사용하면 된다.


참고


라즈베리 파이 호환 보드 조사


라즈베리 파이를 구입해놓고 보니 단순한 소형 리눅스 머신이나. IO를 위한 GPIO 포트가 있는것 빼고는 크게 다르지 않다. 아두이노 시리즈도 호환 보드가 있는것 처럼, 라즈베리파이도 호환 보드가 있을것으로 생각하고 찾아보니 당연히 있다.

Orange PI



우분투,Debian, 라즈베리 파이 이미지 실행 가능


이름

CPU/GPU

메모리

디스크

가격

오렌지 파이 PC+

1.6Ghz 4 core

600Mhz GPU (OPEN GL ES)

1G

8G Flash

37500

오렌지파이 PC2

64bit 4 core, GPU 내장

1G

2MB Flush

40000

오렌지파이 라이트

4 core, 600MHz GPU

512M


25000


Banana PI


국내에서는 판매하는 곳이 많지 않고, 알리바바나 아마존을 통해서 구입해야 하는데, 기존 호환 보드에 비해서 8Core CPU를 탑재한 모델(12만원)이 있는등, 성능이 뛰어나다. 가격은 고가인편. 성능이 좋은편인데, 국내에서 많이 보편화 되지 않은 것은 아무래도 가격 때문인듯

Asus Tinker Board


1.8 GHZ, 4 코어 CPU, 600 Mhz GPU, 2GB 메모리, 가격대는 15만원선 국내에서는 판매하는 곳이 그리 많지 않음


비글본 블랙



1GHz Arm CPU, GPU 지원, 512M, 4G Flash, WIFI 지원으로 약 13만원선


국내에서 구매가 편리하고 가격대가 좋은 보드로는 오렌지 보드가 가성비 측면에서 가장 좋지 않은가 싶다.

라즈베리파이 3B+가 64비트 쿼드 코어 + 1G 메모리에 43000원 정도의 가격을 형성하는데.

오렌지 보드 PC+의 경우 32비트 쿼드코어 + 1G 메모리에 8G 온보드 플래시를 지원하면서 37000원정도이다.  PC2 모델의 경우 64비트 쿼드코어에 1G메모리에 플래시는 탑재하지 않고 40000원 수준이다.

크게 가격적인 차이는 없지만, 세세한 스펙상의 차이가 있기 때문에, 호환 보드를 고민해도 좋지 않을까 싶은데, 가격적인 차이가 크지 않기 때문에, 개발을 시작하는 입장에 있어서는 코드나 장비 호환성 측면에서 라즈베리 파이로 시작하는게 좋을것 같고, 소형 장비를 개발할때는 오렌지 보드를 고민하는 것도 좋지 않을까 한다.


ESP01 (ESP8266)을 이용한 HTTP 통신


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


하드웨어 시리얼과 소프트웨어 시리얼

앞의 글에서 ESP01을 연결해봤는데, ESP01 연결시에 포트를 2,3번 포트를 사용하고, 코드는 SoftSerial 라이브러리를 이용하였다. 아두이노 우노의 0,1 번포트는 시리얼 통신을 위한 RX,TX 포트이다. 이를 Hardware Serial 이라고 한다. 그러면 하드웨어 시리얼 포트를 사용하지 않고 2,3번 포트를 사용한 후에 소프트웨어 시리얼 처리를 한 이유는 무엇을까?


하드웨어 시리얼 포트는 PC와 연결되어 있을때 PC와 통신을 목적으로 사용된다. 그래서 하드웨어 시리얼을 사용하지 않은것인데, ESP8266 관련 라이브러리들을 살펴보니, 대부분 하드웨어 시리얼을 사용하는 것으로 보인다. 그러면 이 문제를 어떻게 해결할것일까?


아두이오 우노 기판을 보면 TX와 RX라고 써있는 LED 가 있는데, 하드웨어 시리얼 통신을 할때만 깜빡 거린다.




PC에 연결이 되어 있을 때, AT 명령을 실행할때는 이 LED가 빛나지 않고, 코드를 업로드 할때만 빛이 난다. 즉 코드 업로드 시에만 하드웨어 시리얼을 PC가 사용한다는 것이기 때문에, 코드를 업로드한후에 ESP01 보드를 TX,RX 단자에 연결하면 하드웨어 시리얼을 사용할 수 있다.

아두이노를 이용한 HTTP 통신

아두이노에 ESP01 모듈을 연결했으면, HTTP 통신을 해보도록 한다. ESP01 (ESP8266 계열)은 TCP/UDP 스택이 이미 들어가 있기 때문에, TCP 통신은 가능하다. HTTP 통신을 하려면 TCP 프로토콜을 이용하여 HTTP 메세지를 보내주면 되는데, AT 명령어를 사용하면 된다.

AT 명령어 레퍼런스는 https://www.electrodragon.com/w/ESP8266_AT-Command_firmware 를 참고하기 바란다. AT명령을 이용하여 WIFI 네트워크에 연결하는 방법은 http://bcho.tistory.com/1283 에 설명되어 있다. WIFI 네트워크에 연결되어 있다고 가정하고 AT명령을 이용해서 HTTP 명령을 보내는 방법을 보자


HTTP  서버 연결

HTTP 요청을 보내기 위해서 먼저 웹서버에 TCP 커넥션을 연결해야 한다. TCP 커넥션 연결 명령어는 AT+CIPSTART로 다음과 같다.


AT+CIPSTART={TCP 연결 번호},”TCP”,”웹서버IP”,{포트번호}


이 명령은 해당 웹서버 IP의 포트로 연결을 하고 연결 번호(이름)를  “0”으로 지정한것이다.

210.118.92.89 서버의 80 포트로 연결하려면 Serial 콘솔에서 AT+CIPSTART=0,”TCP”,”210.118.92.89”,80 명령어를 실행하면 된다. 이 연결의 연결 번호는 0 번이 된다.

명령어 전달

서버로 연결이 되었으면, HTTP 명령어를 보내야 하는데, 명령어 전달은 AT+CIPSEND 명령어를 사용하면 된다. 명령어의 형식은 다음과 같다.


AT+CPISEND={TCP 연결번호},{보내고자 하는 메세지 길이}


예를 들어 “GET /index.html” 명령을 0번 TCP 연결로 보내고자 하면, 메세지의 길이가 15자이기 때문에,

AT+CPISEND=0,15 로 실행하고, 그 다음에

> 가 나오면 GET /index.html 을 전송하면 된다.

예제

실제 개발에서는 콘솔에서 AT 명령을 직접 입력해서 사용하는 것이 아니기 때문에, 이를 코드화 해야 하는데, 아래는 코드화 한 예제이다. (실제 개발에서는 사용하지 않을것이기 때문에 테스트는 하지 않았다)

#include <SoftwareSerial.h>


SoftwareSerial esp(2,3); //TX,RX

String SSID="{Wifi SSID}";

String PASSWORD="{Wifi password}";


void connectWifi(){

 String cmd = "AT+CWMODE=1";

 esp.println(cmd);

 cmd ="AT+CWJAP=\""+SSID+"\",\""+PASSWORD+"\"";

 esp.println(cmd);

 delay(1000);

 if(esp.find("OK")){

   Serial.println("Wifi connected");

 }else{

   Serial.println("Cannot connect to Wifi"+esp);

 }

}


void httpGet(String server,String uri){

 String connect_server_cmd = "AI+CIPSTART=4,\"TCP\",\""+server+"\",80";

 esp.println(connect_server_cmd);

 String httpCmd = "GET "+uri;

 String cmd = "AT+CIPSEND=4,"+httpCmd.length();

 esp.println(cmd);

 esp.println(httpCmd);


}

void setup() {

 // put your setup code here, to run once:

 esp.begin(9600);

 Serial.begin(9600);

 connectWifi();

 httpGet("210.192.111.122","/index.html");

}


void loop() {

 // put your main code here, to run repeatedly:


}


위의 붉은 색으로 표시된 부분을 보면 번호 4번으로 TCP연결을 열고, 4번 채널에 AT+CIPSEND 명령을 이용하여 GET /index.html 을 실행하였다.

SDK의 활용

HTTP 통신의 기본 원리를 이해하였으면, 이제 실제로 HTTP 호출을 해보자. AT 명령을 이용해도 되지만,  HTTP 명령어 종류가 많고, 연결, 메세지 전송/받기등 다양한 명령을 AT 명령으로 직접 코딩하기에는 코딩양이 많고 복잡해진다. 그래서 이런 명령을 잘 추상화하여 단순화 해놓은 SDK를 사용하는 것이 좋은데, 아두이노가 오픈 소스인 만큼, 오픈소스 기반의 SDK가 많다. 장점이기도 하지만 반대로 단점도 되는데, 아두이노나 ESP8266 펌웨어의 버전이 낮으면 동작하지 않는 경우도 있고, 또한 많은 라이브러리들이 아두이노 WIFI 실드를 기준으로 개발되었거나 또는 아두이노 → ESP8266으로 통신하는 용도가 아닌 ESP8266 기반의 아두이노 보드를 기준으로 개발된 라이브러리가 많다.

아두이노 라이브러리 추가하기

우리가 사용할 라이브러리는 WiFiESP라는 라이브러리 (https://github.com/bportaluri/WiFiEsp) 로 소프트웨어 시리얼과 하드웨어 시리얼 통신 양쪽을 모두 지원하며, HTTP 형태로 라이브러리가 잘 패키징 되어 있다. 또한 예제가 비교적 잘 정리되어 있어서 사용하기 좋다.

이런 오픈소스 라이브러리를 사용하려면 라이브러리를 설치해야 하는데, 설치 방법은 다음과 같다.

github에 배포된 코드의 경우, 해당 코드를 아래 그림과 같이 download 버튼을 이용하여 zip 파일로 다운로드 받는다.




다음 Sketch에서 Sketch > Include Library > Add .Zip Library 를 선택해서 앞에서 다운 받은 ZIP 파일을 선택하면 라이브러리로 등록되어 사용이 가능하다.




SDK를 이용하여 HTTP 호출


아래코드는 라이브러리를 추가한후에, 예제에 나온 코드를 그대로 사용한 예이다. 아두이노 2,3번 핀을 ESP01의 TX,RX 핀에 연결해서 소프트웨어 시리얼로 통신하였다. 그래서 코드 부분을 보면 SoftwareSerial Serial1(2,3)으로 소프트웨어 시리얼로 선언되어 있는 것을 확인할 수 있다.


#include "WiFiEsp.h"


// Emulate Serial1 on pins 6/7 if not present

#ifndef HAVE_HWSERIAL1

#endif

#include "SoftwareSerial.h"

SoftwareSerial Serial1(2, 3); // RX, TX


char ssid[] = "{WIFI SSID}";            // your network SSID (name)

char pass[] = "{WIFI Password}";        // your network password

int status = WL_IDLE_STATUS;     // the Wifi radio's status


char server[] = "arduino.cc";


// Initialize the Ethernet client object

WiFiEspClient client;


void setup()

{

 // initialize serial for debugging

 Serial.begin(9600);

 // initialize serial for ESP module

 Serial1.begin(9600);

 // initialize ESP module

 WiFi.init(&Serial1);


 // check for the presence of the shield

 if (WiFi.status() == WL_NO_SHIELD) {

   Serial.println("WiFi shield not present");

   // don't continue

   while (true);

 }


 // attempt to connect to WiFi network

 while ( status != WL_CONNECTED) {

   Serial.print("Attempting to connect to WPA SSID: ");

   Serial.println(ssid);

   // Connect to WPA/WPA2 network

   status = WiFi.begin(ssid, pass);

 }


 // you're connected now, so print out the data

 Serial.println("You're connected to the network");

 

 printWifiStatus();


 Serial.println();

 Serial.println("Starting connection to server...");

 // if you get a connection, report back via serial

 if (client.connect(server, 80)) {

   Serial.println("Connected to server");

   // Make a HTTP request

   client.println("GET /asciilogo.txt HTTP/1.1");

   client.println("Host: arduino.cc");

   client.println("Connection: close");

   client.println();

 }

}


void loop()

{

 // if there are incoming bytes available

 // from the server, read them and print them

 while (client.available()) {

   char c = client.read();

   Serial.write(c);

 }


 // if the server's disconnected, stop the client

 if (!client.connected()) {

   Serial.println();

   Serial.println("Disconnecting from server...");

   client.stop();


   // do nothing forevermore

   while (true);

 }

}



void printWifiStatus()

{

 // print the SSID of the network you're attached to

 Serial.print("SSID: ");

 Serial.println(WiFi.SSID());


 // print your WiFi shield's IP address

 IPAddress ip = WiFi.localIP();

 Serial.print("IP Address: ");

 Serial.println(ip);


 // print the received signal strength

 long rssi = WiFi.RSSI();

 Serial.print("Signal strength (RSSI):");

 Serial.print(rssi);

 Serial.println(" dBm");

}


접속하고자하는 서버의 주소는

char server[] = "arduino.cc";

에 arduino.cc 로 정의되어 있고


   client.println("GET /asciilogo.txt HTTP/1.1");

   client.println("Host: arduino.cc");

   client.println("Connection: close");

   client.println();


명령어를 이용해서 arduino.cc/asciilogo.txt 파일을 HTTP GET으로 읽어오게 되어있다.

읽어온 응답값을

 while (client.available()) {

   char c = client.read();

   Serial.write(c);

 }


코드를 이용해서 client.read를 통해서 한글자씩 읽어온후 화면에 출력하기 위해서 Seirial.write(c)를 이용하여 한글자씩 콘솔에 출력하였다.

실행 결과는 다음과 같다.



결론

테스트를 하면서 가장 어려웠던 점이 아두이노 우노에서 ESP01 로 소프트웨어 시리얼 통신을 하는 경우 HTTP나 WIFI SDK를 찾기가 어려웠다. 대부분 ESP8266을 MCU로 하는 보드 (예를 들어 nodeMCU, ESP12,Wemo D1/D1 mini)를 베이스로 하는 경우가 많았는데, 그도 그럴것이 개발이나 테스트 용도가 아니라 실제 디바이스로 만들어서 사용하려면 우노 + ESP01 조합은 크기도 크고 연결도 복잡해서 효용성이 없다. 네트워크 통신을 하는 시나리오등은 IOT와 같이 센서 데이타를 얻어서 서버에 전송하는 케이스이기 때문에,  하나의 소형 디바이스로 처리가 가능하다면 하나의 디바이스를 사용하는 것이 훨씬 효율적이다. (가격이나 배터리 용량면등).

다음 글에서는 실제로 센서에서 데이타를 읽어서 HTTP를 이용해서 서버에 전송하는 부분을 고민해보도록 하겠다.

참고자료



ESP01 (ESP8266) 사용하기

프로그래밍/아두이노 | 2018.09.30 00:00 | Posted by 조대협

아두이노 ESP01 모듈 사용하기

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

아두이노 WIFI 모듈

아두이노 WIFI 모듈은 여러가지가 있는데, 아두이노용으로 나온 와이파이 실드의 경우에는 가격이 비싸다. (아두이노에서 WIFI 연결하는 방법은 http://bcho.tistory.com/1280 자료 참고) 그래서 가장 범용적으로 사용되는 칩셋은 ESP8266 칩셋인데, ESP8266 칩셋으로 나온 보드는 여러가지가 있다. ESP01~ESP14 모듈등이 있고, 조금더 사용이 편한 모듈로는 nodemcu와 같은 모듈이 있다.




ESP8266 모듈 시리즈 https://en.wikipedia.org/wiki/ESP8266

오늘 다루는 모듈은 이중에서 가장 저렴한 ESP01 모듈이다.

아두이노에서 이모듈을 사용하기에는 몇가지 넘어야할 산이 있다.

전원

ESP 01은 3V로 동작을 하는데, 전류 소모량이 많다. 그래서 아두이노 보드의 3.3V 단자에 연결하게 되면 전력양이 낮아서 오작동하는 경우가 많고 GND와 전원으로 들어가는 선이 많아서, 아래와 같이 배선이 복잡해진다.


<출처 : https://m.blog.naver.com/roboholic84/221261124179>

통신속도와 펌웨어 업그레이드

그리고 가장 난관중 하나가, 아두이노의 시리얼 통신의 경우 9600bps를 사용하는데, 불행하게도 ESP01의 기본 통신속도는 115200bps로 설정되어 있기 때문에 펌웨어 업그레이드를 통해서 디폴트 통신 속도를 9600 bps로 변경해줘야 한다. 이 과정에 여러 배선 설계를 해야 하고 별도의 펌웨어 업그레이드 프로그램을 설치해야하기 때문에 그 과정이 까다롭다.



<그림 USB to ESP01 연결 아답터>

그래서 아두이노 보드를 거치지 않고 바로 PC USB에서 ESP01로 연결을해서 쉽게 펌웨어를 업데이트할 수 있게 해주는 USB to ESP01 아답터가 있기는 하지만 여전히 펌웨어 업데이트가 필요하다.


ESP01 아답터

그래서 검색을 하다 찾은 아답터가 ESP01 아답터 이다.


이렇게 아답터 위에 ESP01 보드를 설치하면 되는 형태이다. (구매 링크)

https://m.blog.naver.com/roboholic84/221261124179 글에 상세한 소개와 설명이 잘 나와 있는데, 따라해보니 ESP01의 펌웨어 자체가 업그레이드 되어 있어서, 명령어가 동작하지 않는 부분이 있어서 몇가지 수정을 하였다. 포스팅 내용을 참고하여 몇가지 부분을 수정하였다.

배선 작업은 아래와 같다.


대략 빵판에 연결하면 다음과 같은 모습이된다.



레큘레이터가 내장되어 있기 때문에 아두이노 5V에 VCC를 연결하면 되고, RX와 TX를 각각 3번과 2번 포트에 연결한다.

다음에 아래와 같은 코드를 작성하여 실행한다.

#include <SoftwareSerial.h>


SoftwareSerial mySerial(2, 3); // RX, TX


void setup() {

 Serial.begin(9600);

 mySerial.begin(115200);

}


void loop() {

 if (mySerial.available()) {

   Serial.write(mySerial.read());

 }

 if (Serial.available()) {

   mySerial.write(Serial.read());

 }

}


위의 예제는 아두이노 콘솔에서 받은 명령을 ESP01 시리얼 포트로 전송하고 ESP01 에서 나온 결과값을 아두이노 콘솔에 출력하도록 하는 코드이다. ESP01의 디폴트 통신속도가 115200이기 때문에 mySerial.begin에서 통신 속도를 115200으로 설정하였다. (추후 바꿀것이다.)


를 실행해서 AT 명령을 실행해보자. 연결이 제대로 되었으면 아래와 그림과 같이 OK 응답이 오는 것을 볼 수 있다.



다음 ESP01 보드의 통신 속도를 변경해보자,원본 문서에는 AT+CIOBAUD=9600 명령으로 변경하도록 되어 있는데, 이 명령은 최신 펌웨어에서는 동작하지 않고 AT+IPR이라는 명령을 사용해야 하는데, 이 명령은 연결되어 있는 동안만 통신 속도를 변경하고 다시 ESP01 디바이스를 뺐다가 다시 끼면 원래 통신속도로 돌아간다.

ESP01의 통신속도를 영구적으로 바꿔줄 수 있는 명령이 있는데, AT_UART_DEF라는 명령을 사용하면 된다. (참고 : https://www.esp8266.com/viewtopic.php?f=13&t=718)

사용법 : AT+ UART_DEF=<baudrate>,<databits>,<stopbits>,<parity>,<flow control>


통신속도를 9600으로 영구적으로 변경하기 위해서는 아래 명령을 수행한다.

AT+UART_DEF=9600,8,1,0,0



통신 속도를 9600으로 변경하였기 때문에 코드상에서도 ESP01로 통신하는 속도를 9600으로 변경해줘야 한다.

 mySerial.begin(115200);

부분을

 mySerial.begin(9600);

으로 변경하여 실행하자


WIFI 연결 테스트

네트워크 모드

ESP8266은 네트워크 연결에 대해 3가지 모드를 제공한다.

  • 1 : Stand alone

  • 2 : AP

  • 3 : AP + Standalone

Stand alone 모드는 클라이언트로 작동하는 모드로 AP에 붙어서 네트워크 통신을 할 수 있다.

AP 모드는 ESP8266이 서버가 되는 모드로 다른 단말이 ESP8266에 Wifi로 연결될 수 있게 한다. 그리고 AP+Stand alone 모드는 서버와 클라이언트를 동시에 지원하는 모드이다. 여기서는 1번 AP+Standalone 모드를 사용하도록 하겠다.

모드 전환은 AT+CWMODE={모드 번호}로 가능하다


WIFI 연결하기

클라이언트 모드로 연결을 하였으면 WIFI에 연결해보자.

WIFI 목록을 찾는 명령은 AT+CWLAP 이다. 명령을 실행하면 아래 그림과 같이 연결 가능한 WIFI 목록이 출력된다.


다음 AT+CWJAP=”SSID”,”비밀번호" 명령을 이용하여 연결하고자 하는 WIFI 네트워크에 연결이 가능하다. 여기서는 U+NetC070 네트워크에 연결해보도록 하겠다

AT+CWJAP="U+NetC070","WIFI비밀번호"


명령을 실행하면 아래와 같이 WIFI에 연결이 된것을 확인할 수 있다.



연결이 완료 되었으면

AT+CIFSR

명령을 이용하면 연결된 IP 주소를 읽어낼 수 있다.


이렇게 연결이 된 상태에서는 아두이노의 전원을 껐다가 켜도 다시 같은 네트워크로 자동으로 붙기 때문에, 연결을 명시적으로 끊으려면

AT+CWQAP

명령을 이용하여 명시적으로 연결을 끊어줘야 한다.

다음글에서는 ESP01 모듈을 이용하여 서버와 HTTP 통신을 하는 방법에 대해서 알아보도록 한다.



서보 모터 제어

프로그래밍/아두이노 | 2018.09.28 00:11 | Posted by 조대협


서보 모터 컨트롤 하기


아두이노에서 컨트롤 할 수 있는 모터중 한 종류인 서보 모터를 제어해봤다.

서보 모터는  (Servo Motor) DC 모터와는 다르게 정밀한 컨트롤이 가능한 모터이다.

정확한 각도나 속도로 회전을 할 수 있다. 


테스트에 사용한 모터는 저렴한 서보 모터를 사용했는데, 그래서 그런지 90도, 180도로 각도를 돌려봐도 정확하게 90도, 180도로 돌지는 않았다.


서보 모터는 종류에 따라서 180도만 회전하는 모터, 360 회전하는 모터등으로 최대 회전 각이 다르다.




이런 모양으로 생겼는데, 붉은 선은 5V, 갈색선은 GND, 오렌지 선이 컨트롤 선이다.

이 선을 아래 그림과 같이 9번 핀에 연결하였다.



그리고 아래와 같은 예제 코드를 사용하였다.


#include<Servo.h>


Servo servo;

int value = 0;

void setup() {

  // put your setup code here, to run once:

  servo.attach(9);

  servo.write(0);

  delay(3000);

  servo.write(90);

  delay(3000);

  servo.write(0);

}


void loop() {

  // put your main code here, to run repeatedly:


  servo.write(value);

  value+=10;

  delay(500);

  if(value > 180)

    value = 0;

    

}


Servo.h 라이브러리를 사용해야 하는데, 이클립스에서는 include 에러가 난다. C/C++ 컴파일러 경로 설정을 변경해야 하는데, 귀찮아서 패스. 그냥 아두이노 IDE를 사용하였다.


servo.write에 각도를 인자를 주면 그 각도로 회전이 된다.






구글 클라우드 해커톤 세션으로 진행한 강의 내용입니다

50분 정도 인데, 짧게 쿠버네티스에 대한 설명과, 마이크로서비스에 대한 설명

그리고 쿠버네티스 에코 시스템인 Spinnaker, Istio, KNative 등에 대해서 설명합니다



쿠버네티스 #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