블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-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을 로그 프레임웍과 연결해서 마이크로 서비스 환경에서 스프링 부트 기반으로 서비스간의 로그 추적을 어떻게할 수 있는지에 대해서 알아보도록 한다.


로깅 시스템 #4 - Correlation id & MDC

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

Correlation id

하나의 프로그램은 여러개의 메서드들로 조합이 된다. 하나의 요청을 처리하기 위해서는 여러개의 메서드들이 순차적으로 실행이 되는데, 멀티 쓰레드 프로그램에서 여러개의 쓰레드 동시에 각각의 요청을 처리할때, 각 메서드에 로그를 남기게 되면, 멀티 쓰레드 프로그램에서는 쓰레드들이 서로 컨택스트를 바꿔가며 실행이 되기 때문에, 로그 메시지가 섞이게 된다

아래 그림을 보자.


요청 A와 B가 호출되어 각각 다른 쓰레드에서 실행이 되었을때, 위의 그림과 같이 로그 메시지가 섞이게 된다. 이런 경우 요청 A에 대한 처리 내용을 확인하기 위해서 요청 A에 대한 로그만을 보고 싶을때 로그가 섞여 버려서 요청 A에 대한 로그만을 분리해내는 것이 어렵다.


이런 문제를 해결하기 위해서는 로그를 기록할때, 요청 마다 고유의 ID를 부여해서 로그를 기록하게 되면, 그 ID를 이용해서 각 요청마다의 로그를 묶어서 볼 수 있다.


위의 그림을 보면 요청 A에 대해서는 “1”이라는 ID를 지정하고, 두번째로 들어온 요청 B에 대해서는 “A”라는 ID를 지정하면, 이 ID로 각 요청에 대한 로그들을 묶어서 조회할 수 있다. 이를 Correlation ID라고 한다.

ThreadLocal

그러면 이 Correlation ID를 어떻게 구현해야 할까?

요청을 받은 메서드에서 Correlation ID를 생성한 후, 다른 메서드를 호출할때 마다 이 ID를 인자로 넘기는 방법이 있지만, 이 방법은 현실성이 떨어진다. ID를 넘기기 위해서 모든 메서드에 공통적으로 ID 필드를 가져야 하기 때문이다.

이런 문제는 자바에서는 ThreadLocal이라는 변수를 통해서 해결할 수 있다.

ThreadLocal을 쓰레드의 로컬 컨텍스트 변수로 Thread 가 존재하는 한 계속해서 남아 있는 변수이다.

무슨 이야기인가 하면, 작업 요청이 들어왔을때, 하나의 쓰레드가 생성이 되고 작업이 끝나면 쓰레드가 없어진다고 하면, 쓰레드가 살아있을 동안에, 계속 유지되는 변수이다. 즉 쓰레드가 생성되어 있는 동안에, 쓰레드 안에서 계속 참고해서 쓸수 있는 쓰레드 범위의 글로벌 변수라고 생각하면 된다.

그래서 요청을 처음 받았을때 Correlation ID를 생성하고, 이를 ThreadLocal에 저장했다가 로그를 쓸때 매번 이 ID를 ThreadLocal에서 꺼내서 같이 출력하면 된다. 이 개념을 그림으로 표현해보면 다음과 같다.


ThreadLocal의 ID 변수에 Correlation ID를 저장해놓고, 각 메서드에서 이 값을 불러서 로그 출력시 함께 출력하는 방법이다.

MDC

그러나 이걸 일일이 구현하기에는 불편할 수 있기 때문에, slf4j,logback,log4j2등의 자바 로그 프레임워크에서는 이런 기능을 MDC(Mapped Diagnostic Context)로 제공한다.

단순하게 CorrelationID 뿐만 아니라 map 형식으로 여러 메타 데이타를 넣을 수 있다. Correlation ID는 여러 요청을  묶을 수 는 있지만 다양한 요청에 대한 컨텍스트는 알 수 없다. 실행되는 요청이 어떤 사용자로 부터 들어온것인지, 또는 상품 주문시 상품 주문 ID를 넣는다던지, 요청에 대한 다양한 컨텍스트 정보를 MDC에 저장하고 로그 출력시 함께 출력하면 더 의미 있는 로그를 출력할 수 있다.


MDC를 사용한 코드를 작성해보자

이 예제에서는 slf4j와 logback을 사용한다. 아래처럼 logback과 json 관련 의존성을 pom.xml 에 정의한다.


<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>



그리고 로그를 json 포맷으로 출력하기 위해서 아래와 같이 logback.xml 을 작성하여, main/resources/logback.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>



다음 아래와 같이 간단하게 MDC를 테스트 하는 코드를 작성한다.

package com.terry.logging.logbackMDC;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.MDC;



public class App

{

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

   public static void main( String[] args )

   {

    log.info("Hello logback");

   

    MDC.put("userid", "terrycho");

    MDC.put("event", "orderProduct");

    MDC.put("transactionId", "a123");

    log.info("mdc test");

   

    MDC.clear();

    log.info("after mdc.clear");

   }

}


MDC를 사용하는 방법은 MDC에 값을 넣을때는 mdc.put(key,value)를 이용하여, 값을 넣고 지울때는  mdc.remove(key)를 이용해서 특정 키를 삭제한다. 전체를 지울때는 mdc.clear()를 사용한다. mdc에 저장된 내용은 logger를 이용해서 로그를 출력할때 마다 mdc 라는 element로 자동으로 함께 출력된다.

위의 예제를 보면 log.info(“hello logback”)으로 로그를 출력하였고 그 다음 mdc에 userid,event,transactionid 라는 키들로 값을 채운 다음에 log.info(“mdc test”)라는 로그를 출력하였다. 그리고 마지막에는 mdc를 모두 지운 후에 “after mdc clear”라는 로그를 출력하는 코드이다. 결과는 다음과 같다.


{

 "timestamp" : "2019-03-23T05:42:19.102Z",

 "level" : "INFO",

 "thread" : "main",

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

 "message" : "Hello logback",

 "context" : "default"

}{

 "timestamp" : "2019-03-23T05:42:19.118Z",

 "level" : "INFO",

 "thread" : "main",

 "mdc" : {

   "event" : "orderProduct",

   "userid" : "terrycho",

   "transactionId" : "a123"

 },

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

 "message" : "mdc test",

 "context" : "default"

}{

 "timestamp" : "2019-03-23T05:42:19.119Z",

 "level" : "INFO",

 "thread" : "main",

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

 "message" : "after mdc.clear",

 "context" : "default"

}


첫번째 로그는 “Hello logback”이라는 메시지가 출력된 후에, 두번째 로그는 mdc 가 세팅되어 있기 때문에, mdc라는 element가 출력되는데, 그 안에 mdc에 저장한 event,userid,transactionid  값이 함께 출력되는 것을 볼 수 있다. 그리고 마지막에는 mdc를 clear 한후에, 로그를 출력하였기 때문에, 메시지만 출력이 되고 mdc 라는 element없이 출력된것을 확인할 수 있다.


slf4j+logback 을 사용할 경우에는 앞의 글에서 설명하였듯이 json으로 로그 출력시 message 문자열 하나면 json element로 출력할 수 있었다. 그래서 custom layout 등 다른 대체 기법을 사용하였는데, mdc를 사용하여 컨텍스트 정보를 넘기게 되면, slf4j와 logback의 제약 사항을 넘어서 json으로 여러 element를 로깅 할 수 있다.





로그 시스템 #3 - JSON 로그에 필드 추가하기

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

JSON 로그에 필드 추가

앞에 예제에서 로그를 Json 포맷으로 출력하였다. 그런데, 실제로 출력된 로그 메세지는 log.info(“문자열") 로 출력한 문자열 하나만 json log의 message 필드로 출력된것을 확인 할 수 있다.

그렇지만, 단순한 디버깅 용도의 로그가 아니라 데이터를 수집하는 용도등의 로깅의 message라는 하나의 필드만으로는 부족하다. 여러개의 필드를 추가하고자 할때는 어떻게 할까? Json Object를 log.info(jsonObject) 식으로 데이터 객체를 넘기면 좋겠지만 불행하게도 slf4j에서 logging에 남길 수 있는 인자는 String  타입만을 지원하고, 데이터 객체를 (json 객체나, Map 과 같은 데이타형) 넘길 수 가 없다.

slf4j + logback

slf4j + logback 의 경우에는 앞에서 언급한것과 같이 로그에 객체를 넘길 수 없고 문자열만 넘길 수 밖에 없기 때문에, json 로그에 여러개의 필드를 넘겨서 출력할 수 가 없다. 아래는 Map 객체를 만든 후에, Jackson json 라이브러리를 이용하여, Json 문자열로 변경하여 slf4j로 로깅한 코드이다.

package com.terry.logging.jsonlog;


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 Slf4j

{

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

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

   {

       Map<String,String> map = new TreeMap();

    map.put("name", "terry");

    map.put("email","terry@mycompany.com");

    String msg = new ObjectMapper().writeValueAsString(map);

    System.out.println("MSG:"+msg);

    log.info(msg);

   }

}

실행을 하면 message 엘리먼트 안에 json 문자열로 출력이 되는 것이 아니라 “ 등을 escape 처리하여 json 문자열이 아닌 형태로 출력이 된다.

{

 "thread" : "main",

 "level" : "INFO",

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

 "message" : "{\"email\":\"terry@mycompany.com\",\"name\":\"terry\"}",

 "endOfBatch" : false,

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

 "instant" : {

   "epochSecond" : 1553182988,

   "nanoOfSecond" : 747493000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

}


그래서 다른 방법을 사용해야 하는데, 로그를 남길때 문자열로 여러 필드를 넘기고 이를 로그로 출력할때 이를 파싱해서 json 형태로 출력하는 방법이 있다.

log.info("event:order,name:terry,address:terrycho@google.com");

와 같이 key:value, key:value, ..  식으로 로그를 남기고, Custom Layout에서 이를 파싱해서 json 으로

{

 “key”:”value”,

 “key”:”value”,

 “key”:”value”

}

형태로 출력하도록 하면 된다. 이렇게 하기 위해서는 log message로 들어온 문자열을 파싱해서 json으로 변환해서 출력할 용도로 Layout을 customization 하는 코드는 다음과 같다.


{package com.terry.logging.logbackCustom;


import ch.qos.logback.classic.spi.ILoggingEvent;

import ch.qos.logback.contrib.json.classic.JsonLayout;


import java.util.Map;

import java.util.StringTokenizer;

import java.util.TreeMap;


import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;


public class CustomLayout extends JsonLayout {

   @Override

   protected void addCustomDataToJsonMap(Map<String, Object> map, ILoggingEvent event) {

       long timestampMillis = event.getTimeStamp();

       map.put("timestampSeconds", timestampMillis / 1000);

       map.put("timestampNanos", (timestampMillis % 1000) * 1000000);

       map.put("severity", String.valueOf(event.getLevel()));

       map.put("original_message", event.getMessage());

       map.remove("message");

       

       StringTokenizer st = new StringTokenizer(event.getMessage(),",");

       Map<String,String> json = new TreeMap();


       while(st.hasMoreTokens()) {

       String elmStr = st.nextToken();

       StringTokenizer elmSt = new StringTokenizer(elmStr,":");

       String key = elmSt.nextToken();

       String value = elmSt.nextToken();

       json.put(key, value);

       }

       

    String msg;

try {

msg = new ObjectMapper().writeValueAsString(json);

} catch (JsonProcessingException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

    map.put("jsonpayload", json);

   

   }

}


먼저 JsonLayout을 상속받아서 CustomLayout 이라는 클래스를 만든다. 그리고 addCustomDataToJsonMap 이라는 메서드를 오버라이딩한다. 이 메서드는 로그로 출력할 메시지와 각종 메타 정보(쓰레드명, 시간등)을 로그로 최종 출력하기 전에, Map객체에 그 내용을 저장하여 넘기는 메서드이다.

이 메서드 안에서 앞에서 로그로 받은 문자열을 파싱해서 json 형태로 만든다. 아래 코드가 파싱을 해서 파싱된 내용을 Map에 key/value 형태로 저장하는 코드이고


       StringTokenizer st = new StringTokenizer(event.getMessage(),",");

       Map<String,String> json = new TreeMap();


       while(st.hasMoreTokens()) {

       String elmStr = st.nextToken();

       StringTokenizer elmSt = new StringTokenizer(elmStr,":");

       String key = elmSt.nextToken();

       String value = elmSt.nextToken();

       json.put(key, value);

       }

다음 코드는 이 Map을 json으로 변환한 후, 이를 다시 String으로 변환하는 코드이다.


msg = new ObjectMapper().writeValueAsString(json);


그 후에 이 json 문자열을 jsonpayload 라는 json element 이름으로 해서, json 내용을 json으로 집어 넣는 부분이다.


map.put("jsonpayload", json);

   

그리고, 이 CustomLayout을 사용하기 위해서 src/main/logback.xml에서 아래와 같이 CustomLayout 클래스의 경로를 지정한다.


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

<configuration>

   <appender name="CONSOLE-JSON" class="ch.qos.logback.core.ConsoleAppender">

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

           <layout class="com.terry.logging.logbackCustom.CustomLayout">

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

                   <prettyPrint>true</prettyPrint>

               </jsonFormatter>

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

               <appendLineSeparator>true</appendLineSeparator>

           </layout>

       </encoder>

   </appender>


   <root level="info">

       <appender-ref ref="CONSOLE-JSON"/>

   </root>

</configuration>



설정이 끝난 후에, 로그를 출력해보면 다음과 같이 jsonpayload element 부분에 아래와 같이 json 형태로 로그가 출력된다.


{

 "timestamp" : "2019-03-22T17:48:56.434+09:00",

 "level" : "INFO",

 "thread" : "main",

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

 "context" : "default",

 "timestampSeconds" : 1553244536,

 "timestampNanos" : 434000000,

 "severity" : "INFO",

 "original_message" : "event:order,name:terry,address:terrycho@google.com",

 "jsonpayload" : {

   "address" : "terrycho@google.com",

   "event" : "order",

   "name" : "terry"

 }

}


log4j2

log4j2의 경우 slf4+logback 조합보다 더 유연한데, log.info 와 같이 로깅 부분에 문자열뿐만 아니라 Object를 직접 넘길 수 있다. ( log4j2의 경우에는 2.11 버전부터 JSON 로깅을 지원 : https://issues.apache.org/jira/browse/log4j2-2190 )

즉 log.info 등에 json 을 직접 넘길 수 있다는 이야기다. 그렇지만 이 기능은 log4j2의 기능이지 slf4j의 인터페이스를 통해서 제공되는 기능이 아니기 때문에, slf4j + log4j2 조합으로는 사용이 불가능하고  log4j2만을 이용해야 한다.


log4j2를 이용해서 json 로그를 남기는 코드는 아래와 같다.


package com.terry.logging.jsonlog;


import java.util.Map;

import java.util.TreeMap;


import org.apache.logging.log4j.message.ObjectMessage;

import org.apache.logging.log4j.LogManager;

import org.apache.logging.log4j.Logger;


public class App

{

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


   public static void main( String[] args )

   {

    Map<String,String> map = new TreeMap();

    map.put("name", "terry");

    map.put("email","terry@mycompany.com");

    ObjectMessage msg = new ObjectMessage(map);

    log.info(msg);

   }

}



Map 객체를 만들어서 json 포맷처럼 key/value 식으로 데이타를 넣은 다음에 ObjectMessage 객체 타입으로 컨버트를 한다. 그리고 로깅에서 log.info(msg)로 ObjectMessage 객체를 넘기면 된다.

그리고 아래는 위의 코드를 실행하기 위한 pom.xml 에서 dependency 부분이다.


<dependency>

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

<artifactId>log4j-slf4j18-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>


실행을 해보면 아래와 같이 json 포맷으로 메세지가 출력된 결과이다. message element를 보면, 위에서 넣은 key/value 필드인 email과, name 항목이 출력된것을 확인할 수 있다.


{

 "thread" : "main",

 "level" : "INFO",

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

 "message" : {

   "email" : "terry@mycompany.com",

   "name" : "terry"

 },

 "endOfBatch" : false,

 "loggerFqcn" : "org.apache.logging.log4j.spi.AbstractLogger",

 "instant" : {

   "epochSecond" : 1553245991,

   "nanoOfSecond" : 414157000

 },

 "contextMap" : { },

 "threadId" : 1,

 "threadPriority" : 5

}



로그 시스템 #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}



로그 시스템 #1 - 자바 로그 프레임웍

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

로그 시스템

로그 시스템은 소프트웨어의 이벤트를 기록 함으로써, 소프트웨어 동작 상태를 파악하고 문제가 발생했을때 이 동작 파악을 통해서 소프트웨어의 문제를 찾아내고 해결하기 위해서 디자인 되었다.

주로 로그 파일이라는 형태로 하나의 파일에 이벤트들을 기록하였다.


그러나 소프트웨어 스택이 OS, 미들웨어, 사용자 애플리케이션 (자바나 파이썬등으로 구현된 애플리케이션)으로 점점 다중화되고 시스템이 대형화 되면서 한대가 아니라 여러대의 서버에 로그를 기록하고 또한 마이크로 서비스 아키텍처로 인하여 서버 컴포넌트가 분산됨에 따라서 로그를 수집해야할 포인트가 많아지게 되었다. 이로 인해서 로그 시스템이 분산 환경을 지원해야 할 필요가 되었고, 단순히 파일로 로그를 기록하는 것만으로는 이러한 여러 시스템과 다중 계층에 대한 모니터링이 불가능하게 되었다.


또한 데이터 분석의 중요성이 대두됨에 따라서, 에러등의 동작 파악성의 로그 뿐만 아니라 사용자의 액티버티를 수집하여 데이터 분석에 사용하기 위해서 데이터 수집 역시 로그 시스템을 통하기 시작하였다.


그래서 몇개의 글에 걸쳐서 좋은 로그 시스템을 개발하기 위한 아키텍처에 대해서 설명하고자 한다.

좋은 로그 시스템이란

먼저 좋은 로그 시스템의 기본 개념을 정의 해보면 다음과 같다.

  • 로그 메시지는 애플리케이션의 동작을 잘 이해할 수 있도록 충분히 구체적이어야 한다.

  • 로그 메시지를 기록하는데 성능 저하가 없어야 한다.

  • 어떤 배포 환경이라도 로그를 수집하고 저장할 수 있도록 충분히 유연해야 한다. (분산 환경 지원, 대용량 데이타 지원등)

자바 로깅 프레임워크

각 프로그래밍 언어마다 고유의 로깅 프레임워크을 지원하지만, 특히 자바의 경우에는 그 프레임웍 수가 많고 발전된 모델이 많아서 자바 프레임워크를 살펴보고 넘어가고자 한다.  

자바는 역사가 오래된 만큼 많은 로깅 프레임웍을 가지고 있다. log4j, logback, log4j2,apache common logging, SLF4J 등 다양한 프레임워크 들이 있는데, 그 개념과 장단점을 알아보도록 하자.

SLF4J

SLF4J는 (Simple Logging Facade for Java)의 약자로 이름이 뜻하는 것과 같이 로깅에 대한 Facade 패턴이다. SLF4J는 자체가 로깅 프레임웍이 아니라, 다양한 로깅 프레임웍을 같은 API를 사용해서 접근할 수 있도록 해주는 추상화 계층이다. 그래서 다른 로그프레임웍과 같이 사용해야 하는데, 보통 Log4J, Logback, Log4J2등이 많이 사용된다. 즉 애플리케이션은 SLF4J API 인터페이스를 통해서 호출하지만, 실제로 호출되는 로깅 프레임웍은 다른 프레임웍이 호출된다는 이야기이다. 이렇게 추상화를 통해서 용도와 목적에 맞게 다른 로깅 프레임워크 으로 쉽게 전환이 가능함은 물론이고, 로깅에 필요한 코드들을 추상화해주기 때문에, 훨씬 쉽고 간단하게 로깅이 가능하다. apache common logging 역시, SLF4J와 같이 다른 로깅 프레임워크 들을 추상화 해주는 기능을 제공한다.



<그림 : SLF4J 가 다른 로깅 프레임웍을 추상화 하는 개념도 >

출처 source


그러나 SLF4J 이전에 개발된 레거시 시스템들의 경우에는 이러한 추상화 계층이 없어서 로그 프레임웍을 변경하고 있기 때문에 로깅 프레임웍을 교체하기가 어렵다. 이런 상황을 해결하기 위해서 SLF4J는 기존 로그 프레임웍에 대한 브릿지를 제공한다. 예를 들어 log4J로 개발된 로깅을 브릿지를 이용해서 SLF4J를 사용하도록 전환할 수 있다. 이런 구조는 레거시 로깅 시스템을 사용해서 개발된 시스템에 대해서, 로그 프레임웍에 대한 코드를 변경하지 않고, 뒷단에 로그 프레임웍을 변경할 수 있게 해주기 때문에, 로깅 프레임웍에 대한 마이그레이션을 쉽게 해준다.



<그림 : SLF4J 브릿지를 이용해서, 기존 로그 시스템을 연동 하는 개념도 >


자바 로깅 프레임워크

자바 로그 프레임웍에는 여러가지 종류가 있지만 그중에서 대표적을 사용되는 로그 프레임웍은 log4j,logback,log4j2 세가지 이다.

Log4J

Log4J는 이 중에서 가장 오래된 로그프레임웍으로 로그 프레임웍에 대한 초반 개념을 설정했다고 볼 수 있다. 현재는 개발이 중지되고, Log4J2로 새로운 버전으로 변경되었다.

Logback

아마 현재 국내에서 가장 널리 많이 사용되고 있는 로그 프레임워크일것이다. Log4J 개발자가 개발한 로그 프레임워크로 주로 Log4J 성능 부분에 대한 개선 작업이 많이 이루어 졌다. SLF4J와 네이티브로 연동이 가능하다.

Log4J2

가장 근래에 나온 프레임워크로 Logback 보다 후에 나오고, 가장 빠른 성능을 제공한다. Logback과 SLF4J사이의 연동 문제를 해결하였으며 비동기 로깅 ( asynchronous logging ) 을 제공하여, 특히 멀티 쓰레드 환경에서 높은 성능을 제공한다.



(source : https://logging.apache.org/log4j/2.x/performance.html )


또한 근래의 로깅 시스템들은 로그를 파일로 기록하기 보다는 ELK(Elastic Search)나 Kafka 등 외부 시스템으로 로그를 전송하여 모으는 형태를 많이 취하기 때문에 이에 대한 연동을 Appender를 통해서 제공한다.


제공되는 Appender는 다음과 같다.

  • Console

  • File, RollingFile, MemoryMappedFile

  • Flume, Kafka, JDBC, JMS, Socket, ZeroMQ

  • SMTP (emails on errors, woo!)

  • … much more


만약에 새로운 시스템을 개발한다면, Logback 보다는 그 다음 세대인 격인 Lob4j2를 사용하는 것을 권장한다.

개발자용 노트북 구입기

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


집에서 개발 공부와, 테스트를 위해서 개인 노트북이 필요했는데, 아무래도 업무와 개인 공부를 분리하는것이 좋을것 같아서, 얼마전 부터 개인 노트북을 사용하기 시작했다.


여러가지 고민을 하다가, 딸에게 주었던 한성 U35S를 돌려받고, 딸에게는 ASUS 라이젠 노트북을 사주었다.



I7에 16G 메모리, 128G SSD로 나쁘지 않은 스펙이었지만, 대략 7년정도 된 노트북이라 우분투를 깔고 사용은 했지만, 일반적인 문서 작업이나 코딩들도 가능했지만, 아무래도 서버를 여러개 올리거나 컴파일을 많이 거니, 팬 소리가 너무 귀에 거슬리기 시작했고, 이참에 노트북을 하나 바꿔야 하겠다하고 개발용 노트북을 찾기 시작했다.


아무래도 개발용으로 사용할 노트북이기 때문에, 성능이 좋아야 하고, 쿠버네티스등 여러 서버를 띄울 것이기 때문에, 메모리가 충분해야 한다 (32G), 디스플레이는  개인적으로 IPS를 선호하고, IO는 nvme 디스크를 정해놓고 찾기 시작했다.


고 스펙으로 검색을 하다보니, 게이밍 노트북이 주로 올라왔는데, 이런 노트북은 I7-8550H (6코어)를 지원하는데다가, GTX-1050 TI나 1060 GPU를 지원하였다. 아무래도 GPU 지원만 잘된다면, 텐서플로우나 케라스같은 머신러닝 작업에 적합할것이라고 생각했다.


카페에서도 가끔 코딩을 하기 때문에 노트북이 너무 게이밍 스러우면 안되고, 그래서 좁혀진것이 레너보 Y530과 한성 TFG155 모델이었다.


예전과 다르게 요즘 랩탑들은 우분투 호환이 안되는 것들이 생각보다 많고 멀티 부트가 안되는 모델들이 많다. 특히 일반 노트북들은 크게 문제가 없지만 GTX GPU가 들어가는 경우에는 듀얼 부트가 문제가 많았다.



<그림. 레노버 Y530>


Y530은 다양한 모델 선택이 가능하고, 디자인도 괜찮은 편이고 냉각팬도 많이 달려있지만, 해외 커뮤니티를 살펴보니 우분투 설치가 쉽지는 않았지만, 여러 설정값을 조정하면 설치가 가능하다는 것을 찾았지만, 듀얼 부팅이 안되는 문제점이 있었다. 그리고 IPS 패널이기는 하지만  3 종류의 IPS 패널을 지원하는데, 60HZ, 250 nits 모델은 화면이 누렇다는 이야기가 많았다. 가격도 좋았지만, 결국 윈도우만 돌리고 있을거 같아서 아쉽지만 패스




<그림. 한성 TFG155>


다음 모델은 TFG155  모델인데, 알루미늄 상판에 디자인되 괜찮고, 스펙도 훌륭하다. 그러나 역시 누런 IPS 문제가 고질적인데, 이건 순전히 뽑기 운에 달려있고, 무엇보다 리눅스 설치에 성공했다는 사용자가 없었다. 특히 국내 내수용 모델이기 때문에 해외에서 몬가 관련된 문서도 없었다.  발열 문제도 꽤 심각한걸로 보이고, U35S를 기존에 사용해본 결과, 내구성에 문제가 있다고 판단하고 패스


6 코어 CPU를 가지고 싶었지만, 개발용 장비가 필요한거지 우분투 드라이버나 커널설정에 시간을 보내는게 목적이 아닌만큼, 6 코어를 포기하고 4 코어 제품을 살펴보기 시작하였다.

ASUS 노트북이 가격도 저렴하고 디자인도 깔끔해서 물망에 오르기는 했지만, nvme를 지원하지 않고, 16G 메모리까지 밖에 확장이 안되는 이유로 패스.


리누즈 토발즈가 DELL XPS 시리즈를 사용한다고 해서 살펴보았는데, 역시나이다.

디자인도 깔끔하고 9750 CPU를 지원하고, 우분투 설치도 보장이 된다.


결국은 가격이 문제 260만원이다.. 아.. 가지고 싶지만. 다른 랩탑보다 100~150만원은 비싼듯.. 이 돈이면 애들 학원을 하나 더 보내지..

그래서 하급 기종을 살펴보니 CPU를 8300까지 내려고 200만원..




아무리 스펙을 내려봐도 답이 안나온다. 그나마 미국에서는 가격이 저렴한 모델도 있고 Developer Edition이라고 따로도 있는데.. 한국에는 수입이 안된다. (DELL 관계자 여러분 Developer Edition좀 수입 해주시면 안되실까요? ) 개발자 전용 모델이라 스펙이 정말 개발용으로 딱이다. 미국 출장가서 사서 핸드캐리할까도 생각했지만, 글로벌 AS 가 안된다고 들어서.. 그냥 포기..


그래서 우분투에서 인증 받은 노트북을 찾아보니, 그나마 인스피리언 7580 시리즈가 눈에 들어왔다.

삼성, LG를 제외한 한성이나 다른 브렌드들은 AS가 안좋다는 말이 많은데, DELL은 AS가 좋기로 소문이 나있었고, 공식 우분투 지원이라니 괜히 모험을 할 필요가 없었다.


<그림. Ubuntu 18.04 LTS 지원 델 노트북 목록 >

https://certification.ubuntu.com/desktop/models/?query=&category=Laptop&level=Any&release=18.04+LTS&vendors=Dell

i7 코어에, 32G 메모리 확장 가능, NVME 탑재, IPS 디스플레이, 알루미늄 바디. 덤으로 MX150 GPU까지. 고성능 딥러닝 학습은 불가능하겠지만, 아쉬운데로 쓸 수 있을듯.

그리고 DELL 은 공식적으로 우분투를 지원하는지라, 윈도우와 듀얼 부트 가이드까지 공식적으로 지원한다. https://www.dell.com/support/article/us/en/04/sln301754/dell-pc%EC%97%90%EC%84%9C-ubuntu-%EB%B0%8F-windows-8-%EB%98%90%EB%8A%94-10%EC%9D%84-%EB%93%80%EC%96%BC-%EB%B6%80%ED%8C%85%EC%9C%BC%EB%A1%9C-%EC%84%A4%EC%B9%98-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95?lang=ko


설치 스트레스가 훨씬 덜할듯. 네이버에 후기를 찾아보니.. 헐.. 이 노트북 후기는 거의 없다. 110~120만원대에서는 약간 포지셔닝이 애매할듯. 보통 유사 스펙이면 이것보다 가격이 살짝 낮은데.. 일반적인 사용자들이 메모리 32G에 NVME를 꼽을일은 거의 없다. 그정도 스펙이면 게이밍인데, 대부분 게이밍 노트북으로 넘어가서 GTX GPU를 쓰겠지... (국내 노트북 시장은 개발자 마켓들을 따로 안보나보다..)


<그림 DELL Inspiron 7580 핑크 버전. 한국은 안판다.>


거기다 행사 기간이라고 무선 마우스까지 덤이다. 이것도 제대로 된거 사려면 3~4만원은 줘야 하는데, 윈도우 라이센스는 집에 정품이 있기에 필요는 없지만 그래도 윈도 프로가 들어있다.

그런데 판매하는 제품은 메모리 16 G 옵션밖에 없고 디스크도 128 SSD + 1TB HDD 또는 512 G 메모리만 지원이 된다. 250G 정도면 딱인데…

메모리 스펙을 찾아보니, 대략 16G 삼성 정품으로 10만원 정도면 구입이 가능한데, 인터넷에서는 8G 추가하는데 10만원(??) 호갱인가??? 그리고, 256 삼성 nvme도 10만원이면 따로 구입이 가능하다.

128SSD + 1TB → 512 모델 업그레이드가 10만원 차이인데… 디스크 모자르면 직접 사서 바꿀것으로 생각하고, 그냥 16GB 메모리 두개만 추가해서 구입.. (남는 8G는 당근 마켓에 싸게 팔아버릴 요량..)


발열로 인하 소음도 걱정되고, 모니터와 같이 병행 사용할것이라서 잘만 스탠드도 2만원주고 업글하고,

코딩할때 화면이 적어서 모니터도 하나 구입하기로 하고, 32인치를 사고 싶었지만.. 책상이 좁고 가격 때문에 27인치 결정. IPS에 무결점 그리고 책상 두께만 많이 차지 않으면 괜찮아서, 무난하게 알파 스캔 모니터 주문. 결정적으로 모니터암을 달 수 없는 구조지만.. 그냥 싸니까..



이렇게 해서,

DELL 7580 I7, 8G 메모리, 128 nvme + 1TB 디스크 + 마우스는 위메프에서 할인 쿠폰이 나와서 다나와 보다 싸게 115만원

삼성 DDR4 16G 메모리 2개 = 22만원

27인치 알파스캔 모니터 = 25만원

잘만 쿨러 24000원


이렇게 해서 대략 165만원 정도. 32 기가, 27인치 IPS 모니터, 알루미늄 바디 노트북 세트로 이정도 가격이면 대단히 선방한듯..

다음에는 더 많이 벌어서. 리누즈 토발즈 처럼 XPS15 풀스펙 모델을 쓸 수 있기를 바라며..

===

업데이트 노트북이 도착해서 인스톨 작업을 진행하였다.

128G SSD에 우분투를 인스톨하고 윈도우 듀얼 붓을 사용하려니 윈도우 파티션이 이것저것 나눠지고 깔리고 복잡해서 용량이 모잘라서 결국은 삼성 EVO 970 PRO를 추가 주문하였다. (+20만원). 돈 아끼려다가.. 그냥 초반에 512 NVME SSD (10만원추가)를 주문하는 것을 추천한다.


다음 인스톨 과정인데, 바이오스에서 RAID를 제외하고 ACHI모드인가로 인식하면 인식은 잘 된다. 다음 윈도우를 다시 인스톨 해야 하는데, 델 사이트에서 제공하는 리커버리 윈도우즈 이미지를 사용하면, 설치가 안된다. (디스크에 대한 드라이버를 찾지 못한다. ) 한참을 씨름하다가 마이크로 소프트의 윈도우즈 정식 이미지를 다운 받아서 사용하니 바로 인식도 되고 한번에 인스톨이 된다. (https://www.microsoft.com/ko-kr/software-download/windows10ISO)


트랙패드, 마우스, 디스플레이 모두 완벽하게 작동한다. 단 돌다가 가끔 커널 패닉에 빠지면서 멈추는데..

이건 NVIDIA GPU와 충돌 때문에 발생한다. 소프트웨어 업데이트 메뉴에 가서 드라이버 업데이트 메뉴를 보면  NVIDIA  드라이버 업데이트가 가능하다.

업데이트 후 문제없이 작동한다.





'사는 이야기' 카테고리의 다른 글

개발자 코딩 노트북 구입기 (DELL Inspiron 7580) 우분투  (2) 2019.03.10
블로그 600만 돌파  (1) 2018.12.10
블로그 500만 돌파  (0) 2018.05.10
지난 1년 회고  (0) 2017.09.20
블로그 400만 돌파  (2) 2017.07.17
2016년 업무 종료....  (3) 2016.12.29

[팁] 쿠버네티스 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 에 대한 접근 주소 역시 얻을 수 있다.




도커 컨테이너 보안 취약점 스캔 도구 Anchore

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


근래에 쿠버네티스를 로컬환경에서  이것저것 테스트하다보니, 클라우드에 있는 기능 보다 오픈 소스 기능을 많이 보게 되는데, 빌드 파이프라인을 보다가 재미있는 오픈소스를 하나 찾아서 정리해놓는다.


컨테이너 이미지에 대한 보안 문제

쿠버네티스와 같은 컨테이너 오케스트레이션 솔루션에서 가장 보안 취약이 있는 곳 중의 하나는 컨테이너 이미지 인데, 도커허브와 같이 널리 알려진 컨테이너 리파지토리에 저장되서 배포되는 이미지도 보안적으로 문제가 있는 이미지가 많다. 그래서, 가급적이면 벤더들에서 보안적으로 문제가 없도록 관리하는 베이스 이미지를 사용하는 것이 좋다. (구글에서 제공하는 도커 컨테이너 베이스 이미지 https://cloud.google.com/container-registry/docs/managed-base-images)

컨테이너 이미지 스캐닝 툴 (Anchore)

다음 방법으로는 컨테이너 이미지를 구울때마다 보안적인 문제가 없는지, 알려진 보안 취약점을 스캔해서 분석하는 방법인데, 이러한 도구를 vulnerability scaning 툴이라고 한다.

이런 툴중에 Anchore 라는 툴이 있는데, 상업 버전도 있지만 오픈 소스 버전이 있어서 손쉽게 사용이 가능하다. Anchore는 보안 CVE (Common Vulnerabilities and Exposures) 목록을 기반으로 해서 스캔하는데, CVE는 미정부에서 후원하는 National Cybersecurity FFRDC 에 의해서 관리된다.  

이외에도 몇개의 보안 취약점 정보를 몇군데서 가지고 오는데 (https://anchore.freshdesk.com/support/solutions/articles/36000020579-feeds)

재미있는 것은 NPM과 루비젬에 대한 보안 취약점 피드도 가지고 온다는 점이다.

  • Linux Distributions

    • Alpine Linux

    • CentOS

    • Debian

    • Oracle Linux

    • Red Hat Enterprise Linux

    • Ubuntu

  • Software Package Repositories

    • RubyGems.org

    • NPMJS.org

  • NIST National Vulnerability Database (NVD)


간단하게 들여다 보기

설치 및 사용 방법은 공식 홈페이지를 참고하면 되고, https://anchore.freshdesk.com/support/solutions/articles/36000020729-install-with-docker-compose 에는 docker-compose를 이용해서 간단하게 설치 및 사용하는 방법이 나와 있다. 아래는 공식 홈페이지의 내용을 참고해서 설명한 내용이다.


  1. 컨테이너 이미지를 anchore에 추가한다.
    다음 예제 명령은 dokcer
    % docker-compose exec anchore-engine anchore-cli --u admin --p foobar image add docker.io/library/debian:7

  2. anchore는 등록된 이미지를 자동으로 스캔한다.
    다음 명령어를 이용하면 스캔 상태를 모니터링 할 수 있다.
    % docker-compose exec anchore-engine anchore-cli --u admin --p foobar image get docker.io/library/debian:7 | grep 'Analysis Status'

  3. 스캔이 끝나면, 발견된 보안 취약점과 상세 내용을 확인할 수 있다.
    % docker-compose exec anchore-engine anchore-cli --u admin --p foobar image vuln docker.io/library/debian:7 all
    Vulnerability ID        Package                 Severity Fix Vulnerability URL                                                 
    CVE-2005-2541           tar-1.26+dfsg-0.1+deb7u1                 Negligible None https://security-tracker.debian.org/tracker/CVE-2005-2541         
    CVE-2007-5686           login-1:4.1.5.1-1+deb7u1                 Negligible None https://security-tracker.debian.org/tracker/CVE-2007-5686         
    CVE-2007-5686           passwd-1:4.1.5.1-1+deb7u1                Negligible None https://security-tracker.debian.org/tracker/CVE-2007-5686         
    CVE-2007-6755           libssl1.0.0-1.0.1t-1+deb7u4              Negligible None https://security-tracker.debian.org/tracker/CVE-2007-6755         
    ...
    ...
    ...

여기서 CVE-XXX 식으로 나오는 것이 보안 취약점이며, 자세한 내용은 뒤에 나오는 링크에서 확인이 가능하다.

CI/CD 파이프라인 통합

실제 개발/운영 환경에서 사용하려면, 커맨드 라인 뿐만 아니라 CI/CD 빌드 파이프라인에 통합을 해야 하는데, Anchore는 젠킨스 플러그인으로 제공되서 빌드 파이프라인에 쉽게 통합이 된다.

아래와 같은 개념으로 젠킨스에서 이미지를 빌드한 후에, 쿠버네티스나 기타 컨테이너 솔루션에 배포전에 보안 취약점을 스캔할 수 있다.


<그림. Anchore를 젠킨스 빌드 파이프라인에 추가 한 그림 >

출처 : https://anchore.com/opensource/


아래는 Anchore를 젠킨스 플러그인으로 설치한후에, 컨테이너 보안 취약점을 스캔한 결과를 출력해준 화면이다.




국내에는 아직 많이 알려지지 않은것 같은데, 젠킨스와 통합해서 사용한다면 꽤나 좋은 효과를 볼 수 있지 않을까 한다.

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

요즘 쿠버네티스를 로컬환경에서 이것 저것 테스트하고 있는데, 실행 방법은 다음과 같다.  

환경은 minikube를 인스톨하였다. (0.33.1 버전)


sudo -E minikube start --vm-driver=none --extra-config=kubelet.resolv-conf=/run/systemd/resolve/resolv.conf


쿠버네티스를 우분투에서 실행할때, 별도의 Virtual Machine 없이 실행이 가능하다. VM 없이 실행하려면 --vm-driver=none 옵션을 줘야 한다. 이때, Local DNS Pod 가 기동될때 문제가 생기는데, 이를 해결하기 위해서 --extra-config=kubelet.resolv-conf=/run/systemd/resolve/resolv.conf 옵션을 주면된다.


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는 베타 상태이고 정식 릴리즈 상태는 아니기 때문에, 운영 환경에는 아직 사용하지 않는 것이 좋다.

PodPreset


애플리케이션을 배포하는데 있어서, 애플리케이션 실행 파일과과 설정 정보를 분리해서 환경에 따라 설정 정보만 변경해서 애플리케이션을 재배포하는 방법이 가장 효율적이다.  


쿠버네티스에서는 ConfigMap을 이용하여 이러한 정보를 환경 변수로 넘기거나, 또는 파일이나 디렉토리로 마운트하는 방법을 제공해왔다. ConfigMap을 이용하는 경우 ConfigMap에서 여러 정보를 꺼내오도록 해야 한다. 또한 반복적으로 유사한 애플리케이션을 배포하고자 할때, 디스크가 필요한 경우에는 볼륨과 그 마운트에 대한 정의를 해야하기 때문에, 애플리케이션을 배포하는 Pod의 설정 파일이 다소 복잡해질 수 있다.


그래서 새롭게 소개되는 개념이 PodPreset 이다.

PodPreset에는 환경 변수 이외에도, 볼륨과 볼륨 마운트 정보를 정의할 수 있다.

이렇게 정의된 PodPreset은 label selector를 통해서 적용할 Pod를 지정할 수 있다.

label selector에 의해서 지정된 Pod는 Pod가 생성될때, PodPreset에 지정된 정보가 추가되서 Pod가 생성된다.


이해가 잘 안될 수 있는데, 예제를 살펴보는게 가장 빠르다. 예제는 쿠버네티스 공식 홈페이지에 있는 예제를 사용하였다.

https://kubernetes.io/docs/tasks/inject-data-application/podpreset/#simple-pod-spec-example



아래는 PodPreset  예제이다.

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
 name: allow-database
spec:
 selector:
   matchLabels:
     role: frontend
 env:
   - name: DB_PORT
     value: "6379"
 volumeMounts:
   - mountPath: /cache
     name: cache-volume
 volumes:
   - name: cache-volume
     emptyDir: {}


env 부분에서 DB_PORT 환경 변수에  데이타 베이스 접속 포트를 정해주었고, /cache 디록토리에 emptyDir을 마운트 하도록 하는 설정이다. 이 PodPreset을 role: frontend 인 Pod 들에 적용하도록 하였다.


아래는 Pod 정의 예제이다.


apiVersion: v1
kind: Pod
metadata:
 name: website
 labels:
   app: website
   role: frontend
spec:
 containers:
   - name: website
     image: nginx
     ports:
       - containerPort: 80

내용을 보면 알겠지만, 간단하게 nginx 웹서버를 실행하는 Pod 설정 파일이다.

그런데 이 Pod를 배포하게 되면 앞에 PodPreset에서 Selector로 role:frontend 를 적용하도록 하였기 때문에, 이 Pod에는 앞에서 정의한 PodPreset이 적용되서 실제 배포 모양은 아래와 같이 된다.


apiVersion: v1
kind: Pod
metadata:
 name: website
 labels:
   app: website
   role: frontend
 annotations:
   podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
 containers:
   - name: website
     image: nginx
     volumeMounts:
       - mountPath: /cache
         name: cache-volume
     ports:
       - containerPort: 80
     env:
       - name: DB_PORT
         value: "6379"
 volumes:
   - name: cache-volume
     emptyDir: {}


원래 Pod 설정 파일에 있는 내용에 추가해서 env, volumeMount, volumes 부분이 PodPreset에 정의된데로 추가 적용된것을 확인할 수 있다.


Configmap이 이미 있는데, 이런게 왜 필요할까 하고 잠깐 살펴봤는데, 편하다. 쿠버네티스에서 디스크 볼륨 설정이 그다지 간략하다고 말하기가 어렵고 그래서, 개발자 입장에서 애플리케이션을 간략하게 배포하고 싶은데, 디스크에 대한 설정을 해야 하는데, 이 부분을 모두 PodPreset에서 관리자가 해줄 수 있는 구조로 빠질 수 있다. 아직 알파단계 API이기 때문에 환경 정보와 볼륨 관련 정보만 지정하는데, 정식 버전에서는 좀 더 많은 설정 정보를 제공했으면 한다.



Kong API gateway

아키텍쳐 | 2019.02.07 23:11 | Posted by 조대협

Kong API gateway 간단 리뷰 노트 


요즘 MSA가 다시 올라오기 시작하고 있고, Kubernetes Istio 조합으로 좋은 아키텍쳐를 많이 그려낼 수 있게 되기 시작해서, API 게이트웨이를 다시 살펴보고 있는데, 시장에 API 게이트 웨이들은 대부분 인수가 된 상황이고, Kubernetes에서는 Ambassador가 올라오고 있는데, Istio 통합이나 Kubernetes 통합 기능은 강력하지만, 아직 신생 제품이라 기능이 그리 많이 않고, 보통 오픈 소스 대세는 Kong을 많이 이야기 하길래 쭈욱 살펴봤는데, 일단 CLI 명령이 거의 없고 제대로 이해한것이 맞다면, REST CALL로 설정을 잡아야 하고, Kubenetes 에 Ingress로 올라갈 수 도 있고, Istio 통합도 로드맵에는 있지만 아직은 완성전 단계..  설치 스크립트를 따라가 보니 Cassandra나 Postgres를 뒷단에 놓는것이 아마도 로그 수집 용도인것 같은데, 가볍게 사용하기에는 설정이 너무 무겁다. 

GUI 도구로는 지원 도구인 Konga가 있기는 하지만 UI를 봤을때 너무 단순하기도 하다. 

지켜보기는 하지만, 아직은 살짝 기다려야 하는 상태가 아닌가 싶다. 


참고 https://medium.com/@tselentispanagis/managing-microservices-and-apis-with-kong-and-konga-7d14568bb59d


Ksonnet

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


쿠버네티스의 리소스 배포는 YAML 스크립트를 기반으로 한다.

하나의 마이크로 서비스를 배포하기 위해서는 최소한 Service, Deployment 두개 이상의 배포 스크립트를 작성해야 하고, 만약에 디스크를 사용한다면 Persistent Volume (aka PV)와 Persistent Volume Claim (PVC)등 추가로 여러 파일을 작성해서 배포해야 한다.

그런데 이러한 배포 작업을 보면, 사실 비슷한 성격의 마이크로 서비스간에는 중복 되는 부분이 많다.


예를 들어 간단한 웹서비스 (node.js나 springboot)를 배포할때는 Service의 타입을 지정하고, Deployment에 의해서 관리되는 Pod의 수, 그리고 컨테이너 이미지 정도만 지정하면 기본적인 배포는 가능하다. 여기에 필요하다면, 요구되는 리소스 (CPU, 메모리) 정도만 지정하면 된다.

그러나 이를 위해서는 Service와 Deployment를 정의한 긴 YAML 파일을 작성해야 한다.

그렇다면 이러한 반복작업을 단순화하고, 환경(운영/개발, 또는 미국/유럽과 같은 다른 리전등)에도 반복적으로 재활용할 수 있는 방법은 없을까?


이런 목적에서 여러가지 솔루션이 소개되는데, 그중에 널리 사용되고 있는 것은 패키지 매니저인 Helm이 있다. Helm은 node.js 의 npm이나, 리눅스의 apt 유틸리티와 유사하게, 패키지 형태로 쿠버네티스 리소스를 배포할 수 있도록 해준다. 이외에도 여러가지 솔루션이 있는데, 오늘 이야기하는 솔루션은 ksonnet이라는 오픈소스로, 템플릿엔진 형태의 특징을 가지고 있는 솔루션이다.


Ksonnet

ksonnet은 JSON 기반의 템플릿 엔진인 jsonnet (https://jsonnet.org/)을 기반으로 한다. 기본적으로 템플릿 엔진의 특성을 가지고 있는데, 특이점이 인프라를 지원하는데 있어서 일종의 프로그래밍 언어적인 컨셉을 지원한다.

그러면 기본적인 개념을 이해 해보도록 하자


Prototype, Parameter & Component



먼저 Prototype 이라는 개념을 이해 해야하는데, prototype의 템플릿의 개념으로, 객체 지향형 프로그램에서 클래스의 개념으로 생각하면 된다. 이 prototype을 이용해서 component를 만들어 낸다. component는 객체 지향형에서 객체의 개념으로 생각하면 된다. 이렇게 만들어진 컴포넌트에 속성(변수)를 설정하면 되는데, Deployment 이름이나, 컨테이너 이미지등 실배포에서 애플리케이션 서비스별로 필요한 구체적인 값을 입력한다. 이 값을 입력하는 방법은 parameter 라는 리소스로 정의해서, 필요한 값을 전달한다.

이렇게 값이 채워진 component는 각 서비스 배포에 필요한 쿠버네티스 리소스들을 정의한 쿠버네티스 manifest 파일로 생성된다. (참고 : YAML이 아니라 JSON으로 생성되는데, 쿠버네티스의 manifest는 YAML뿐만 아니라 JSON으로도 설정이 가능하다. )


prototype에서 component를 생성하는 방식은 앞에서도 설명하였듯이, 하나의 prototype에서 parameter만 바꿔서 생성하면 다른 서비스에 대한 component를 반복적으로 생성할 수 있다. 아래 개념 그림은 하나의 prototype에서 도커 이미지와 서비스 타입만 변경해서 Guestbook과 BookStore 서비스에 대해서 각각의 컴포넌트를 생성하는 개념이다.



Environment

이렇게 생성된 component는 쿠버네티스에 적용해서, 실제 쿠버네티스 리소스를 만드는데, ksonnet은 동시에 여러개의 쿠버네티스 환경을 지원할 수 있다. 이를 ksonnet 안에서는 environment 라는 리소스로 정의하는데, 여러개의 쿠버네티스 클러스터를 하나의 ksonnet 프로젝트 안에 등록해놓고, 생성된 component를 등록된 쿠버네티스 환경중 하나에 배포한다. ksonnet의 environment는 쿠버네티스 클러스터에 대한 정보 (예를 들어 마스터 서버의 IP)를 가지고 있다.



Part

그런데 Prototype도 유사점이 있다. 예를 들어 redis 배포를 예를 들었을때, 어떤 redis 배포는 외장 디스크 (PV)를 붙여서 배포할 수 도 있고, 아니면 내장 디스크 (PV 없이) 배포할 수 있다. 비슷한 타입의 prototype의 반복적인 작업을 막기 위해서 prototype은 재사용이 가능한 자원으로 조합해서 정의 될 수 있다. 이를 ksonnet에서는 part라고 정의한다.




위의 그림을 보면 우측에 두가지 프로토 타입이 정의된것을 볼 수 있다. 하나는 PVC(Persistent Volume Claim)이 있는 것이고 하나는 없는 것이데, 이외에 차이가 없고 Service나 Deployment는 정의는 동일 하기 때문에 이를 part로 정의한 후에, 이 part를 조합하여 다른 prototype을 구성할 수 있다.

Application

하나의 애플리케이션은 여러개의 마이크로 서비스로 구성이 된다. 그래서 하나의 애플리케이션을 배포하기 위해서는 여러개의 Prototype과 component들이 필요하게 되는데, 하나의 쿠버네티스 클러스터에는 열개의 애플리케이션이 설치될 수 있기 때문에, 이 애플케이션 단위로, 리소스를 묶을 수 있는 바운더리 개념이 필요하다. ksonnet에서는 이 개념을 application이라는 단위로 정의해서, 이 애플리케이션 안에, 여러개의 prototype, part, component, environment를 묶을 수 있게 한다.

그래서 이 애플리케이션 단위로 설치도 가능하고 반대로 애플리케이션 단위로 설치된 애플리케이션을 삭제할 수 있다.

Package

Part와 Prototype은 다른 애플리케이션에서도 재 사용될 수 있다. 예를 들어 redis 설치 스크립트나, nginx 설치 스크립트 역시 어느 애플리케이션에서나 재 사용될 수 있고, 머신러닝 패키지인 kueflow와 같은 복잡한 애플리케이션의 경우에는 이 prototype과 part들만 있으면 손쉽게 설치될 수 있다. 이렇게 각 모듈들을 재사용 가능하도록 묶어놓은 것을 ksonnet에서는 package라고 한다. Package 안에는 일반적으로 아래와 같이 재 사용이 가능한 part와 prototype들이 들어가 있다.


Registry

그러면 이 패키지들 어디에 저장하고 어떻게 내 애플리케이션에 반영할까? ksonnet에서는 npm이나 docker repository처럼 이러한 패키지들을 저장할 수 있는 레파지토리 개념을 제공하는데, 이것이 바로 registry다.

Github repository나, 파일 시스템 Helm repository를 registry로 사용할 수 있다.


작성한 package는 이 registry에 저장할 수 있으며, 저장된 package들은 package install 명령어를 이용해서 설치한다.



Hello ksonnet

ksonnet의 개념을 이해했으면, 실제 테스트를 통해서 구체적은 사용법을 알아보도록 하자. 테스트를 위해서는 쿠버네티스 클러스터가 설치되어 있어야 하는데, 쿠버네티스 환경이 없는 경우에는 온라인에서 테스트할 수 있는 환경이 https://ksonnet.io/tour/welcome/ 에 있으니,이를 활용하는 것도 하나의 방법이다.

여기서 사용한 예제는 ksonnet 공식 홈페이지에 나와 있는 Guestbook 예제이다. https://ksonnet.io/docs/tutorial/guestbook/ 함축적으로 설명을 해놓은 간단한 예제라서 디테일한 내용을 이해하기는 어렵지만 대략적인 컨셉과 흐름을 이해하는데는 큰 무리가 없다.

애플리케이션 생성

먼저 애플리케이션을 생성한다. ks init 명령을 이용하면 애플리케이션 디렉토리가 생성되고, 필요한 초기 리소스들이 생성된다. 일종의 boiler plate와 같은 개념으로 보면 된다.

%ks init guest book

생성이 되면 아래와 같은 디렉토리 구조로 생성이 된다.

components에는 컴포넌트 파일과 parameter 등이 저장되는데, 정의된 컴포넌트가 없기 때문에, 컴포넌트 관련 파일은 없이 비어 있다.

Environment 디렉토리는 쿠버네티스 환경에 대한 정보가 들어있는데, 기본 환경값은 defaults라는 디렉토리에 저장된다.



Environment 생성

현재 환경을 cloud 라는 이름으로 등록해보자. 환경 등록은 ks add env라는 명령을 사용하면 된다. 등록해야하는 대상 쿠버네티스 클러스터가 많을 경우, 다른 환경을 추가로 등록하면 된다.

%ks add env cloud

Component 생성

다음으로 컴포넌트를 생성하는데, 등록된 prototype을 기반을 component를 생성해야 하는데, ks application을 생성하면 디폴트로 등록되어 있는 prototype이 있다. 등록되어 있는 prototype 목록은 ks prototype list 명령으로 조회가 가능하다.


%ks prototype list




여기서 사용할 prototype은 deployed-service를 사용하는데, image와 type 만을 지정한다.

이 prototype은 웹서비스를 위해서 쿠버네티스 리소스인 Service 와 Deployment를 생성해준다.

deployed-service prototype을 이용해서, 컴포넌트를 생성하기 위해서는 ks generate deployed-service {컴포넌트명} 으로 생성하고, 필요한 parameter는 --{parameter name}={parameter value) 식으로 지정한다.


% ks generate deployed-service guestbook-ui \

 --image gcr.io/heptio-images/ks-guestbook-demo:0.1 \

 --type ClusterIP


Component 생성이 끝나면 ~/components/ 디렉토리에 컴포넌트 파일이 생성된다.


생성된 컴포넌트 파일의 일부를 보면 다음과 같다.  컴포넌트들이 생성되고, parameter 처리되어 있는 부분은 아래 마크한바와 같이 parameter value를 각각 항목마다 지정되어 있다.


~/components/guestbook-ui.libsonnet

중략

:

 {

   "apiVersion": "apps/v1beta2",

   "kind": "Deployment",

   "metadata": {

     "name": params.name

   },

   "spec": {

     "replicas": params.replicas,

     "selector": {

       "matchLabels": {

         "app": params.name

       },

     },

     "template": {

       "metadata": {

         "labels": {

           "app": params.name

         }

       },

       "spec": {

         "containers": [

           {

             "image": params.image,

             "name": params.name,

             "ports": [

               {

                 "containerPort": params.containerPort

               }

             ]

           }

         ]

       }



     "name": params.name

   },

   "spec": {

     "ports": [

       {

         "port": params.servicePort,

         "targetPort": params.containerPort

       }

     ],

     "selector": {

       "app": params.name

     },

     "type": params.type

   }

 },


이하 생략

그리고 parameter로 지정한 내용들은 별도의 param.libsonnet 이라는 파일에 정의된다. 아래 그림과 같이 image명이나 type들이 앞에서 ks generate 단계에서 지정한 내용으로 정의된것을 확인할 수 있다. 다른 값들은 prototype에서 정의된 디폴트 값으로 채워진다.


~/components/param.libsonnet

local env = std.extVar("__ksonnet/environments");

local params = std.extVar("__ksonnet/params").components["guestbook-ui"];

[

중략

 components: {

   // Component-level parameters, defined initially from 'ks prototype use ...'

   // Each object below should correspond to a component in the components/ directory

   "guestbook-ui": {

     containerPort: 80,

     image: "gcr.io/heptio-images/ks-guestbook-demo:0.1",

     name: "guestbook-ui",

     replicas: 1,

     servicePort: 80,

     type: "ClusterIP",

   },

중략

생성된 Manifest 확인하기

생성된 컴포넌트가 각 쿠버네티스 환경에 어떻게 적용되는지를 살펴보기 위해서는  ks show {환경이름} 명령을 실행하면, 쿠버네티스에 적용된 YAML manifest 파일을 먼저확인해볼 수 있다.

%ks show cloud


다음은 “cloud” 라는 환경에 반영할 수 있는 YAML manifest 파일이다.

아래 그림은 YAML 파일중에서 Deployment 파트인데,  아래 그림고 같이 name이나 label등 구체적인 설정 값들이 앞에서 지정한 parameter 값으로 채워진것을 확인할 수 있다.

apiVersion: apps/v1beta2

kind: Deployment

metadata:

 labels:

   ksonnet.io/component: guestbook-ui

 name: guestbook-ui

spec:

 replicas: 1

 selector:

   matchLabels:

     app: guestbook-ui

 template:

   metadata:

     labels:

       app: guestbook-ui

   spec:

     containers:

     - image: gcr.io/heptio-images/ks-guestbook-demo:0.1

       name: guestbook-ui

       ports:

       - containerPort: 80



중략

Manifest 적용

이렇게 컴포넌트가 정의되었으면, 대상 쿠버네티스를 선택해서 ks apply 명령으로 적용 하면 된다. 아래 명령어는 “cloud” 라는 환경에 생성된 컴포넌트들을 적용하는 명령이다.

%ks apply cloud

적용된 서비스 확인 및 테스트

쿠버네티스에 적용된 리소스를 확인해보자. kubectl get svc와 kubectl get pod를 실행해보면 아래와 같이 Service와 Pod들이 생성된것을 확인할 수 있다.

%kubectl get svc




%kubectl get pod




생성된 서비스를 테스트해보자. 서비스를 ClusterIP로 생성하였기 때문에, 외부 IP를 통해서 접속은 불가능하고, kubectl proxy를 이용해서 프록시를 세팅해서 접속해야 한다. 아래는 kubectl proxy 명령어를 이용해서 proxy를 생성하고 접속을 하는 예제이다.

%kubectl proxy > /dev/null &

%KC_PROXY_PID=$!

%SERVICE_PREFIX=http://localhost:8001/api/v1

%GUESTBOOK_URL=$SERVICE_PREFIX/namespaces/default/services/guestbook-ui/proxy/


%open $GUESTBOOK_URL



프록시를 통해서 접속한 웹사이트에 대한 결과이다.


간단하게 컨셉과 예제를 통해서 실제 사용 방법을 확인해보았다.

ksonnet은 쿠버네티스의 다른 YAML 파일 배포 시스템과 다르게 prototype,component,application,environment 등과 같이 잘 추상화된 개념을 가지고 있으며, 특히 배포를 위해서 쿠버네티스에 무엇인가를 설치할 필요가 없이 클라이언트에 ks 툴만 설치하면 되기 때문에, 쿠버네티스 설치에 대한 의존성이 없이 깔끔하게 설치 및 운영이 가능하다.

또한 파일 형태로 모든 리소스를 관리할 수 있기 때문에 gitOps와 같은 형상 관리 시스템을 통해서 리소스를 관리할 수 있기 때문에, 변경 추적이나 코드 재 사용등에 많은 장점을 가질 수 있다.


End2End 머신러닝 플랫폼 Kubeflow 조대협 (http://bcho.tistory.com)

머신러닝 파이프라인

머신러닝에 대한 사람들의 선입견중의 하나는 머신러닝에서 수학의 비중이 높고, 이를 기반으로한 모델 개발이 전체 시스템의 대부분 일 것이라는 착각이다.

그러나 여러 연구와 경험을 참고해보면, 머신러닝 시스템에서 머신러닝 모델이 차지하는 비중은 전체의 5% 에 불과하다.


실제로 모델을 개발해서 시스템에 배포할때 까지는 모델 개발 시간보다 데이타 분석에 소요되는 시간 그리고 개발된 모델을 반복적으로 학습하면서 튜닝하는 시간이 훨씬 더 길다.

머신러닝 파이프라인은 데이타 탐색에서 부터, 모델 개발, 테스트 그리고 모델을 통한 서비스와 같이 훨씬 더 복잡한 과정을 거친다. 이를 머신러닝 End to End 파이프라인이라고 하는데, 자세하게 그 내용을 살펴보면 다음 그림과 같다.




  • Data ingestion : 머신러닝에 필요한 학습 데이타를 외부로 부터 받아서 저장하는 단계

  • Data analytics : 수집된 데이타를 분석하여, 의미를 찾아내고,필요한 피쳐(특징)을 찾아내는 단계로 주로 빅데이타 분석 시스템이 많이 활용된다. EDA (Exploratory Data Analytics) 방법을 많이 사용하는데, 저장된 데이타를 그래프로 시각화해서 각 값간의 관계나 데이타의 분포등을 분석한다.

  • Data Transformation : 수집된 데이타에서 학습에 필요한 데이타만 걸러내고, 학습에 적절하도록 컨버팅 하는 단계. 예를 들어 이미지 데이타의 크기를 정형화하고, 크롭핑 처리를 한후에, 행렬 데이타로 변환하는 과정등이 이에 해당한다.

  • Data Validation : 변환된 데이타가 문제는 없는지 데이타 포맷이나 범위등을 검증하는 단계

  • Data Splitting : 머신러닝 학습을 위해서 데이타를 학습용,테스트용,검증용으로 나눈다.

  • Build a Model : 머신러닝 모델을 만들고 학습하는 단계

  • Model Validation : 만들어진 모델을 검증하는 단계

  • Training at scale : 더 많은 데이타를 더 큰 인프라에서 학습 시켜서 정확도를 높이고, 하이퍼 패러미터 튜닝을 통해서 모델을 튜닝하는 단계로 주로 대규모 클러스터나 GPU 자원등을 활용한다.

  • Roll out : 학습된 모델을 운영환경에 배포하는 단계

  • Serving : 배포된 모델을 통해서 머신러닝 모델을 서비스로 제공하는 형태. 유스케이스에 따라서 배치 형태로 서빙을 하거나 실시간으로 서빙하는 방법이 있다.

  • Monitoring : 머신러닝 모델 서비스를 모니터링 해서 정확도등에 문제가 없는지 지속적으로 관찰하는 단계

  • Logging : 모델에 서비스에 대한 로그 모니터링


이 과정을 데이타의 변동이 있거나 모델을 향상시키고자 하거나 정확도가 떨어지는 경우 첫번째 과정부터 반복을 한다.


위에서 설명한 파이프라인 흐름을 시스템 아키텍쳐로 표현해보면 다음과 같다.




먼저 GPU를 지원하는 인프라 위에 머신러닝 플랫폼이 올라가게 되고, 빅데이타 분석 플랫폼이 같이 사용된다.

머신러닝 플랫폼은 데이타를 분석하는 EDA 단계의 데이타 분석 플랫폼 그리고, 분석된 데이타를 변환 및 검증하고 학습,테스트,검증 데이타로 나누는 Data Processing 시스템이 붙고, 이 데이타를 이용해서, 모델을 개발한후에, 이 모델을 학습 시키기 위한 학습 (Training) 플랫폼이 필요하다. 학습된 모델을 검증하고, 이 검증 결과에 따라서 하이퍼 패러미터를 튜닝한 후에, 이를 운영환경에 배포하여 서비스 한다. 데이타 분석 및 모델 개발 학습 단계는 주로 데이타 사이언티스트에 의해서 이루어지는데, 이러한 엔지니어들이 사용할 개발 환경이 필요한데, 주로 노트북 기반 (예. 파이썬 주피터 노트북)의 환경이 많이 사용된다.

학습이 완료된 모델을 서빙하는 Inference 엔진이 필요하고, 이를 외부 API로 노출하기 위해서 API 키 인증, 오토스케일링, 로깅 및 모니터링을 위한 API Serving 플랫폼이 필요하다.


컴포넌트가 많은 만큼 여기에 사용되는 프레임웍도 많다. 먼저 모델 개발 및 학습을 위해서는 머신러닝 프레임웍이 필요한데, Tensorflow, PyTorch, Sklearn, XGBoost등 목적에 따라서 서로 다른 프레임웍을 사용하게 되며, 완성된 모델을 서빙하는 경우에도 Tensorflow Serving, Uber에서 개발한 Horovod 등 다양한 플랫폼이 있다. 또한 모델을 서빙할때 REST API등으로 외부에 서비스 하려면 보안 요건에 대한 처리가 필요하기 때문에 별도의 API 인증 메커니즘등이 추가되어야 하고, 스케일링을 위한 오토 스케일링 지원 그리고 모델의 배포와 테스트를 위한 배포 프레임웍, A/B 테스트 환경등이 준비되어야 한다.

일부만 이야기한것이지만 실제 운영 환경에서 사용되는 머신러닝 시스템은 훨씬 더 복잡하고 많은 기술을 필요로 한다.

Kubeflow comes in

이러한 복잡성 때문에 머신러닝 플랫폼은 높은 난이도를 가지고 있고, 데이타 분석과 모델 개발에 집중해야 하는 머신러닝 엔지니어 입장에서는 큰 부담이 된다. (배보다 배꼽이 크다)

그래서 이러한 복잡성을 줄이고 머신러닝 엔지니어의 원래 업인 데이타 분석과 머신러닝 모델 개발에만 집중할 수 있도록 플랫폼을 추상화 해놓은 오픈 소스 프레임웍이 Kubeflow이다.

위에서 설명한 머신러닝 파이프라인의 End to End 전체를 커버할 수 있게 하고, 모든 단계의 컴포넌트를 패키지화 해놔서, 어려운 설치 없이 머신러닝 엔지니어는 머신러닝 모델 개발의 각 단계를 손쉽게 할 수 있도록 해준다.


Kuberflow는 Kubernetes(쿠버네티스) + ml flow 를 합한 의미로, 쿠버네티스 플랫폼 위에서 작동한다.

쿠버네티스는 도커 컨테이너 관리 플랫폼으로, 이 컨테이너 기술을 이용하여 머신러닝에 필요한 컴포넌트를 패키징하여 배포한다. 쿠버네티스에 대한 자세한 설명은 링크를 참고하기 바란다.

이로 인해서 가질 수 있는 장점은 다음과 같다.

  • 클라우드나 On-Prem (데이타 센터), 개인 개발 환경에 상관 없이 동일한 머신러닝 플랫폼을 손쉽게 만들 수 있기 때문에 특정 벤더나 플랫폼에 종속되지 않는다.

  • 컨테이너 기술을 이용해서 필요한 경우에만 컨테이너를 생성해서 사용하고, 사용이 끝나면 컨테이너를 삭제하는 방식이기 때문에 자원 활용율이 매우 높다. 특히 쿠버네티스의 경우에는 스케쥴링 기능을 이용해서 비어있는 하드웨어 자원에 컨테이너를 배포해서 (꾸겨넣는 방식으로) 사용하기 때문에 집적률이 매우 높다.

  • 컨테이너로 패키징이 되어있기 때문에 내부 구조를 알필요가 없이 단순하게 컨테이너만 배포하면 된다.

또한 쿠버네티스는 오픈소스 플랫폼이기 때문에 여러 종류의 머신러닝 관련 기술들이 손쉽게 합쳐지고 있다.

Kubeflow 컴포넌트 구성

그러면 간단하게 Kubeflow의 컴포넌트 구성을 살펴보자.

IDE 환경

IDE 개발환경으로는 JupyterLab을 지원한다. JupyterLab은 Jupyter 노트북의 확장 버전으로 코드 콘솔뿐 아니라 파일 브라우져나 시각화창등 확장된 UI를 지원한다.


<출처 : https://jupyterlab.readthedocs.io/en/stable/getting_started/overview.html>


개인적으로 기존 노트북 환경에 비해서 좋은 점은 주피터 노트북을 필요할때 마다 손쉽게 생성이 가능하며, 생성할때 마다 GPU 지원 여부나 텐서플로우 버전등을 손쉽게 선택이 가능하다.


<그림. 노트북 생성시 텐서플로우와 GPU 지원 여부를 선택하는 화면>


또한 아래 그림과 같이 노트북 인스턴스의 하드웨어 스펙 (CPU, Memory, GPU)를 정의할 수 있다.



GPU 드라이버

그리고 쿠버네티스상에서 GPU를 사용할 수 있도록 GPU 드라이버를 미리 패키징 해놓았다. 머신러닝 프레임웍을 사용하면 항상 까다로운 부분이 GPU 드라이버 설정이나 업그레이드인데, 이를 미리 해놓았기 때문에 머신러닝 엔지니어 입장에서 별도의 노력없이 손쉽게 GPU를 사용할 수 있다.

머신러닝 프레임웍

머신러닝 프레임웍으로는 현재 텐서플로우, 파이토치, MxNet등을 지원하는데, 플러그인 컴포넌트 형태이기 때문에 앞으로 더 많은 프레임웍을 지원할 것으로 기대된다.

데이타 프로세싱

데이타 프로세싱에서 데이타 변환 (Transformation)과 데이타 검증 (Validation)은 텐서플로우의 확장팩인 TFX에서 지원하는 TFDV (Tensorflow Data Validation)과 TFT (Tensorflow Transform)을 이용해서 지원한다.

학습 환경

개발된 모델을 학습할때 특히 분산학습의 경우에는 텐서플로우 클러스터와 우버에서 개발된 텐서플로우용 분산 학습 플랫폼인 Hornovod를 지원한다.  

모델 검증

학습된 모델 검증은 데이타 프로세싱과 마친가지로 텐서플로우 확장팩인 TFX의 TFMA (Tensorflow Model Analysis)를 지원한다.

하이퍼 패러미터 튜닝

학습된 모델에 대한 하이퍼 패레미터 튜닝은 katLib라는 컴포넌트를 이용해서 지원한다.

모델 서빙

학습이 완료된 모델은 TFX 패키지의 일부인 Tensorflow Serving 을 사용하거나 모델 서빙 전문 플랫폼인 SeldonIO를 사용한다. SeldonIO는 텐서플로우뿐만 아니라 Sklearn, Spark 모델, H2O 모델, R 모델등 좀 더 다양한 모델을 지원한다.

API 서비스

서비스된 모델에 대한 API 키 인증이나 라우팅등을 위해서 API 게이트 웨이가 필요한데, API 게이트 웨이로 Ambassador라는 오픈 소스를 이용한다. 이를 통해서 API 키등의 인증을 지원하고, 쿠버네티스 위에 네트워크 플랫폼인 ISTIO를 사용하여, API 서비스에 대한 모니터링 및 서비스 라우팅을 지원하는데, 서비스 라우팅 기능은 새 모델을 배포했을때 새모델로 트래픽을 10%만 보내고 기존 모델로 트래픽을 90% 보내서 새모델을 테스트하는 카날리 테스트나 API 통신에 대한 보안등 여러기능을 지원한다. Istio에 대한 자세한 설명은 링크를 참조하기 바란다.

워크플로우

이러한 컴포넌트를 매번 메뉴얼로 실행할 수 는 없고, 워크플로우 흐름에 따라서 자동으로 파이프라인을 관리할 수 있는 기능이 필요한데, 이를 워크플로우 엔진이라고 하고, Kubeflow에서는 argo라는 컨테이너 기반의 워크플로우 엔진을 사용한다. 자세한 내용은 링크 참조.

그런데 argo는 일반적인 워크플로우를 위해서 디자인된 플랫폼으로 머신러닝 파이프라인에 최적화되어 있지 않다. (예를 들어 학습 단계 종료후, 학습 결과/accuracy등을 모니터링 한다던지, Tensorflow Dashboard와 통합된다던지.) 그래서 argo위해 머신러닝 기능을 확장하여 개발중인 오픈소스가 Kubeflow pipeline이 있다. Kubeflow pipeline에 대해서는 나중에 더 자세히 설명하도록 한다.

컴포넌트에 대한 정의

Kubeflow에서 사용되는 거의 모든 컴포넌트에 대해서 설명하였다. 그러면 이런 컴포넌트를 어떻게 쿠버네티스에 배포하고, 어떻게 실행을 할것인가? 매번 쿠버네티스의 설정 파일을 만들어서 하기에는 파일의 수도 많고 반복작업이면서 또한 쿠버네티스에 대한 높은 전문성을 필요로하기 때문에 어렵다.

그래서 이러한 반복작업을 줄여주고, 템플릿화하여 실행하도록 해주는 엔진이 ksonnet 이라는 오픈소스를 사용한다. ksonnet은 jsonnet 템플릿 엔진 기반으로, 위에서 나열한 컴포넌트들을 쿠버네티스에 설치할 수 있도록 해주고, 각 단계별 컴포넌트를 손쉽게 실행할 수 있도록 해준다.


이 솔루션들을 앞에서 설명한 머신러닝 플랫폼 아키텍쳐에 맵핑 시켜보면 다음과 같은 그림이 된다.




Kubeflow는 현재 개발중인 버전으로 이글을 쓰는 현재 0.4 버전이 개발중이다.

컨셉적으로 매우 훌륭하고 0.4 버전인것에 비해서는 매우 완성도가 높지만 1.0 릴리즈 전이기 때문에 다소 변화가 심하기 때문에 버전간 호환이 안될 수 있다. 이점을 염두하고 사용하기 바란다.


Kubeflow를 이해하기 위해서는 먼저 Kubeflow의 컴포넌트를 배포하고 실행하게 해주는 ksonnet에 대한 이해가 먼저 필요하다. 다음 글에서는 이 ksonnet에 대해서 알아보도록 하겠다.


컨테이너 기반의 워크플로우 솔루션 argo

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


argo는 컨테이너 워크플로우 솔루션이다.

컨테이너 기반으로 빅데이타 분석, CI/CD, 머신러닝 파이프라인을 만들때 유용하게 사용할 수 있는 오픈 소스 솔루션으로 개념은 다음과 같다.


워크플로우를 정의하되 워크플로우의 각각의 스텝을 컨테이너로 정의한다.

워크플로우 스펙은 YAML로 정의하면, 실행할때 마다 컨테이너를 생성해서, 작업을 수행하는 개념이다.


기존에 아파치 에어플로우 (https://airflow.apache.org/)등 많은 워크 플로우 솔루션이 있지만, 이러한 솔루션은 컴포넌트가 VM/컨테이너에서 이미 준비되서 돌고 있음을 전제로 하고, 각각의 컴포넌트를 흐름에 따라서 호출하는데 목적이 맞춰서 있다면, argo 의 경우는 워크플로우를 시작하면서 컨테이너를 배포하고, 워크플로우 작업이 끝나면 컨테이너가 종료되기 때문에, 실행할때만 컨테이너를 통해서 컴퓨팅 자원을 점유하기 때문에 자원 활용면에서 장점이 있다고 볼 수 있다.


argo 설치는 쿠버네티스 클러스터가 있는 상태라면 https://argoproj.github.io/docs/argo/demo.html 를 통해서 간단하게 설치가 가능하다. 설치와 사용법은 위의 문서링크를 활용하기 바란다.

HelloWorld

간단한 워크플로우 예제를 살펴보자. 워크 플로우를 실행하기 위해서는 워크플로우 스펙을 yaml 파일로 정의해야 한다. 아래는 helloworld 의 간단한 예제이다.


apiVersion: argoproj.io/v1alpha1

kind: Workflow

metadata:

 generateName: hello-world-

spec:

 entrypoint: whalesay

 templates:

 - name: whalesay

   container:

     image: docker/whalesay:latest

     command: [cowsay]

     args: ["hello world"]


워크플로우의 이름은 metadata 부분에 generateName에서 워크플로우 JOB의 이름을 정의할 수 있다. 여기서는 hello-world-로 정의했는데, 작업이 생성될때 마다 hello-world-xxx 이라는 이름으로 작업이 생성된다.

Templates 부분에 사용하고자 하는 컨테이너를 정의한다. 위의 예제에서는 docker/whalesay:latest 이미지로 컨테이너를 생성하도록 하였고, 생성후에는 “cosway”라는 명령어를 “hello world” 라는 인자를 줘서 실행하도록 하였다.

Template 부분에는 여러개의 컨테이너를 조합하여, 어떤 순서로 실행할지를 정의한다. 이 예제에서는 하나의 컨테이너만 실행하도록 정의하였다.

다음에 어느 컨테이너 부터 시작하게 할것인지는 sepc 부분에 정의하고, 시작 부분은 entrypoint라는 구문을 이용해서 정의할 수 있다.이 예제에서는 template에 정의한 whalesay라는 컨테이너부터 실행하도록 한다.

이렇게 생성된 워크플로우 스펙은 “argo submit”이라는 CLI 명령을 이용해서 실행한다.


%argo submit --watch {yaml filename}


워크플로우가 실행되면 각 단계별로 쿠버네티스 Pod 가 생성되고, 생성 결과는 argo logs {pod name}으로 확인할 수 있다.


%argo list

명령을 이용하면 argo 워크플로우의 상태를 확인할 수 있다.



위의 그림과 같이 hello-world는 hello-world-smjxq 라는 작업으로 생성되었다.

Pod 명은 이 {argo 작업이름}-xxx 식으로 명명이 된다.

%kubectl get pod

명령으로 확인해보면 아래 그림과 같이 hello-world-smjxq 라는 이름으로 pod가 생성된것을 확인할 수 있다.


이 pod의 실행 결과를 보기 위해서

%argo logs hello-world-smjxq

명령을 실행하면 된다.


위의 그림과 같이 고래 그림을 결과로 출력한것을 확인할 수 있다.

ArgoUI

워크플로우의 목록과 실행결과는 CLI뿐 아니라 웹 기반의 GUI에서도 확인이 가능하다.

argo ui는 argo라는 이름의 deployment에 생성이 되어 있는데, clusterIP (쿠버네티스 내부 IP)로 생성이 되어 있기 때문에 외부에서 접근이 불가능하다. 포트포워딩 기능을 이용해서 argo deployment의 8001 포트를 로컬 PC로 포워딩해서 접속할 수 있다.


% kubectl -n argo port-forward deployment/argo-ui 8001:8001


다음에 http://localhost:8001을 이용해서 접속해보면 다음과 같이 현재 등록되어 있는 워크플로우 목록을 확인할 수 있다.




이 목록에서 아까 수행한 hello-world-xxx 워크플로우를 확인해보자. 아래 그림과 같이 워크플로우의 구조를 보여준다.



hello-world-xxx 노드를 클릭하면 각 노드의 상세 내용을 볼 수 있다.


그림에서 Summary > Logs 부분을 선택하면 아래 그림과 같이 각 단계별로 실행한 결과 로그를 볼 수 있다.


연속된 작업의 실행

앞에서 간단한 설치 및 사용법에 대해서 알아봤는데, 앞에서 살펴본 예제는 하나의 태스크로 된 워크플로우이다. 워크플로우는 좀더 복잡하게 여러개의 태스크를 순차적으로 실행하거나 또는 병렬로 실행이 된다.


예제 원본 https://argoproj.github.io/docs/argo/examples/README.html


다음 워크플로우 정의를 보자

apiVersion: argoproj.io/v1alpha1

kind: Workflow

metadata:

 generateName: steps-

spec:

 entrypoint: hello-hello-hello


 # This spec contains two templates: hello-hello-hello and whalesay

 templates:

 - name: hello-hello-hello

   # Instead of just running a container

   # This template has a sequence of steps

   steps:

   - - name: hello1            #hello1 is run before the following steps

       template: whalesay

       arguments:

         parameters:

         - name: message

           value: "hello1"

   - - name: hello2a           #double dash => run after previous step

       template: whalesay

       arguments:

         parameters:

         - name: message

           value: "hello2a"

     - name: hello2b           #single dash => run in parallel with previous step

       template: whalesay

       arguments:

         parameters:

         - name: message

           value: "hello2b"


 # This is the same template as from the previous example

 - name: whalesay

   inputs:

     parameters:

     - name: message

   container:

     image: docker/whalesay

     command: [cowsay]

     args: ["{{inputs.parameters.message}}"]


구조를 살펴보면 다음과 같다.