<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>MSFL{ccambo}</title>
    <link>https://ccambo.tistory.com/</link>
    <description>개발과 관련된 것들, 일상 생활에 대한 것들을 정리해 보는 곳!!!</description>
    <language>ko</language>
    <pubDate>Mon, 13 Apr 2026 22:25:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ccambo</managingEditor>
    <image>
      <title>MSFL{ccambo}</title>
      <url>https://t1.daumcdn.net/cfile/tistory/2270DB455829AB2518</url>
      <link>https://ccambo.tistory.com</link>
    </image>
    <item>
      <title>Kubernetes 확장인 CRD와 CR 에 대한 개념 정리</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-%ED%99%95%EC%9E%A5%EC%9D%B8-CRD%EC%99%80-CR-%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;h1&gt;Kubernetes의 확장인 CRD Custom Resource Definition 와 CR Custom Resource 에 대한 개념 정리&lt;/h1&gt;
&lt;p&gt;기존에 &lt;a href=&quot;https://ccambo.tistory.com/53#comment8843344&quot;&gt;Kubernetes 상의 Operator 나름대로 정리&lt;/a&gt; 라는 게시글을 작성했다.&lt;/p&gt;
&lt;p&gt;게시글 자체는 Operator를 설명하기 위한 내용이었는데, 댓글로 문의를 주신 분들의 의문점들을 살펴보니 Operator의 관점에서 바라봤을 때 CRD와 CR에 대한 실체들에 대한 궁금점들이었기 때문에 근본적인 내용을 설명하는 것이 좋을 것 같아서 이 게시글을 작성한다.&lt;/p&gt;
&lt;h2&gt;Kubernetes의 기본적 동작 원리&lt;/h2&gt;
&lt;p&gt;Kubernetes 구성에 대한 자세한 내용은 &lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/overview/components/&quot;&gt;Kubernetes Components&lt;/a&gt; 참고&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kubernetes는 상태관리 시스템&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이다.예를 들어 Replica를 3이라고 지정하면 Kubernetes가 Deployment 요청을 받았을 때 Replica 개수를 확인하고 Pod를 3개 실행시키려고 한다. 즉, &lt;code&gt;처음 배포되면 사용자가 정의한 Replica 값이 Pod 실행 개수라는 상태 값&lt;/code&gt;이 된다는 것이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;운영 중에 3개의 Pod 중에 1개 Pod가 오류로 중지되면 Kubernetes 입장에서는 3개가 유지 (Desired State) 되어야 하는 상태인데, 현재 상태는 2개 (Current State) 가 되므로 이를 다시 3개로 만들려고 액션을 취하게 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;아주 간단한 예는 실행될 Pod의 개수를 의미하는 Replicas 설정이라고 생각하면 된다. 보통 애플리케이션을 작성하고 컨테이너 이미지를 만들고 Kubernetes에 실행을 맡기기 위해 Deployment Spec에 애플리케이션 정보 뿐만 아니라 Replica 개수를 지정하고 kubectl 명령을 통해 실행하게 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;상태를 관리하는 것은 Controller의 역할&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이다.간단하게 정리하면 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Kubernetes는 Resource들의 변경을 감시하고 있다.&lt;/li&gt;
&lt;li&gt;변경이 감지되면 Kubernetes는 관련된 이벤트를 발생시킨다.&lt;/li&gt;
&lt;li&gt;발생된 이벤트는 Controller의 Reconcile 함수로 전달된다.&lt;/li&gt;
&lt;li&gt;Reconcile 함수에서 전달된 이벤트 데이터에 따라 Current State를 Desired State로 맞추기 위한 작업을 진행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;위의 예에서 2개만 존재하는 Pod를 3개로 만들려고 액션을 취한다고 언급했는데, Kubernetes에서 상태가 변경된 것을 감지하고 상태를 맞추기 위해 실제 동작하는 것이 Controller 의 역할이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;모든 리소스는 GVR로 식별&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;된다.G Group, V Version, R Resource 의 조합으로 모든 리소스를 식별한다. 아래와 같이 단순한 Deployment Spec를 보면 알 수 있다.위와 같이 Kubernetes의 모든 리소스들은 GVR 식별 구조를 가진다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;apiVersion: apps/v1 &amp;lt;&amp;lt;- Group이 apps, Version이 V1 kind: Deployment &amp;lt;&amp;lt;- Resource가 Deployment ...&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pod, Deployment, .. 모두 Kubernetes의 리소스로 Controller의 관리 대상들이다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kubernetes 확장을 위한 CRD 와 CR, 그리고 Operator&lt;/h2&gt;
&lt;p&gt;Kubernetes에서는 실제 애플리케이션 동작 및 운영에 필요한 Pod, Deployment, Service 등과 같이 기본적인 리소스 오브젝트만 제공하고 있다. 물론 이 정도만 있어도 사용하는데 문제가 없다.&lt;/p&gt;
&lt;p&gt;요즘 많이 사용하고 있는 프로메테우스를 기준으로 확장을 검토해 보자.&lt;/p&gt;
&lt;p&gt;Kubernetes 기본 오브젝트만을 기준으로 운영한다면 수집해야할 정보와 규칙들이 많아질 수록 프로메테우스의 설정이 복잡해지고, 대상 시스템에 따라서 하나 이상의 프로메테우스를 실행해서 연계 (Federation) 하거나 각각 메트릭 수집을 분리해서 운영하는 등의 작업이 필요한 상황들이 발생하게 된다.&lt;/p&gt;
&lt;p&gt;운영자의 입장에서는 이런 작업들이 자동화되어 관리만 할 수 있는 것을 원할것이다. 그러나 Kubernetes에서는 이런 작업을 할 수 있는 방법이 없다. 이유는 Kubernetes는 상태관리 시스템이기 때문에 자동화를 위한 상태정보를 관리할 대상이 없기 때문이다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;실제 CRD, CR 구성을 위해서는 Operator Framework, KubeBuilder 등의 툴을 사용해서 개발해야 한다.&lt;/p&gt;
&lt;p&gt;아래의 내용은 그냥 개념적인 이해를 위해 서술한 것이므로 주의해야 한다.&lt;/p&gt;
&lt;p&gt;Kubernetes의 관점에서는 사용자정의 리소스를 의미하지만 아래의 설명에서는 개발적인 관점에서 이해하기 쉽도록 오브젝트라고 명기 또는 혼용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;개발 언어 (여기서는 Go) 관점에서 풀어보면 구조체 Structure 를 오브젝트 데이터 관리용으로 사용한다. 구조체에서 관리할 데이터 항목과 형식을 지정하며, 관리할 데이터를 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type struct HelloSpec {
  Message string    `json:&amp;quot;message&amp;quot;`
  ...
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;우리가 오브젝트를 만들었다고 해도 Kubernetes는 이 오브젝트를 인식하지 못하기 때문에 GVR 형식으로 Kubernetes가 인식할 수 있도록 등록해 줘야 한다. 즉,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes에 사용자가 정의한 오브젝트(Kubernetes에서는 리소스)에 대한 이름과 형식과 사이즈등의 데이터 관리 정보를 GVR 기준으로 정의한 것이 CRD&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;다.&lt;/p&gt;
&lt;p&gt;아래의 내용은 간단하게 CRD를 구성한 매니페스트 파일이다. (hello_crd.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apiextensions.k8s.io/v1        # Kubernetes에서 제공하는 CRD용 Group과 Version
kind: CustomResourceDefinition                # Kubernetes에서 제공하는 Resource
metadata:
  name: hellos.examples.com              # CRD 식별명
spec:
  group: examples.com                # Group
  versions: 
    - name: v1alpha1        # Version
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                message:        # 필드명
                  type: string  # 필드 형식
                replicas:
                  type: integer
                  minimum: 1
                  maximum: 10
  names:
    kind: Hello                            # Resource
    plural: hellos                    # List 등으로 표현할 Resource의 복수형
  scope: Namespaced                    # Namespace 범위로 한정&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 CRD는 아래의 명령으로 실행하고 생성된 내역을 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; kubectl apply -f hello_crd.yaml
customresourcedefinition.apiextensions.k8s.io/hellos.examples.com created

&amp;gt; kubectl get crd
NAME                                                  CREATED AT
...
hellos.examples.com                                   2021-11-19T04:30:20Z
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;단지 Kubernetes에 사용할 오브젝트에 대한 정보만 등록을 한 것 뿐이기 때문에 현재는 할 수 있는 것이 아무것도 없다.&lt;/p&gt;
&lt;p&gt;Kubernetes가 상태관리 시스템이라고 했기 때문에&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CRD로 Kubernetes가 인식할 수 있는 오브젝트의 정의를 등록했다면, 실제 상태 정보를 관리할 수 있는 오브젝트가 CR&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이다. 개발 언어로 이해한다면 클래스가 CR 이라고 생각하면 된다. CR로 생성한 실체들이 인스턴스가 된다.&lt;/p&gt;
&lt;p&gt;아래의 내용은 간단하게 CR을 구성한 매니페스트 파일이다. (hello_cr.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: examples.com/v1alpha1        # CRD에서 지정한 Group/Version
kind: Hello                                                    # CRD에서 지정한 Resource Kind
metadata:
  name: my-new-hello-object                    # CR 식별명 (오브젝트 인스턴스 식별)
spec:
  message: &amp;quot;hi crd!!&amp;quot;                                # 필드에 저장할 값&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 CR은 아래의 명령으로 실행하고 생성된 내역을 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; kubectl apply -f hello_cr.yaml
hello.examples.com/my-new-hello-object created

&amp;gt; kubectl get Hello        # Hello Resource Kind에 대해 생성된 인스턴스들 조회
NAME                  AGE
my-new-hello-object   33m

&amp;gt; kubectl describe Hello my-new-hello-object  # 인스턴스 정보 상세 조회
Name:         my-new-hello-object
Namespace:    koreobs-system
Labels:       &amp;lt;none&amp;gt;
Annotations:  &amp;lt;none&amp;gt;
API Version:  examples.com/v1alpha1
Kind:         Hello
Metadata:
  Creation Timestamp:  2021-11-19T05:00:07Z
  Generation:          1
  Managed Fields:
    API Version:  examples.com/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:message:
    Manager:         kubectl-client-side-apply
    Operation:       Update
    Time:            2021-11-19T05:00:07Z
  Resource Version:  47889293
  Self Link:         /apis/examples.com/v1alpha1/namespaces/koreobs-system/hellos/my-new-hello-object
  UID:               89f3c291-64d7-4e77-aacb-fec1106813f8
Spec:
  Message:  hi crd!!        # 필드 값을 가지고 있다.
Events:     &amp;lt;none&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 같이 생성된 CR의 인스턴스가 상태정보를 가지는 실체가 된다. 위의 예에서 Hello은 CRD로 정의한 오브젝트고, my-new-hello-object는 생성되어 실체를 가지는 인스턴스가 된다.&lt;/p&gt;
&lt;p&gt;단, 지금까지 작업한 것은 단지 사용자가 운영할 새로운 리소스를 생성하고 etcd에 정보를 저장하는 것 뿐이다. 아직 Kubernetes의 기본 리소스인 Pod, Deployment, Service와 같이 실행 가능한 리소스들과 연계해서 움직이는 것은 아니다.&lt;/p&gt;
&lt;p&gt;위에서 Kubernetes의 기본 리소스들의 상태가 변경되면 이를 처리해주는 것이 Controller라고 언급했다. 따라서 여기서 생성한 Hello 의 인스턴스인 my-new-hello-object 의 상태 값이 변경되면 (예를 들어 “hi crd!!” 값을 “hi controller” 라고 변경했을 경우 등) 연결될 Controller가 없기 때문에 동작하지 않는 상태다.&lt;/p&gt;
&lt;p&gt;따라서 사용자 리소스의 인스턴스들에 변경이 발생했을 때 Kubernetes를 통해 변경 이벤트를 받아서 처리할 사용자 정의 Controller가 필요하다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;이 부분은 Operator에 대한 샘플들을 찾아보면 된다. 여기서는 개념적인 이해를 위한 것이기 때문에 따로 설정하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;상태 값을 출력하는 Pod를 운영한다는 가정을 하고 어떻게 운영되는지를 정리해 보도록 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hello CRD 를 구성해서 Kubernetes에 등록 : Kubernetes에서는 CRD를 인식할 수 있고, CR 인스턴스가 생성되면 상태를 저장할 수 있다.&lt;/li&gt;
&lt;li&gt;Custom Controller 작성 및 Kubernetest에 연결 : Operator 작성 및 배포 방식에 따라서 Kubernetes에 등록, Custom Controller는 CR 인스턴스에서 변경이 발생하면 정보를 받아서 처리할 Reconcile 함수를 가지고 있다. 여기서는 전달된 CR 인스턴스를 통해 단순히 Eco 기능만 제공하는 Pod를 동작 시키는 Deployment를 구성하고 실행하는 것으로 가정한다.&lt;/li&gt;
&lt;li&gt;사용자가 Hello 인스턴스 생성 : 생성도 상태 변경에 해당한다.&lt;/li&gt;
&lt;li&gt;Kubernetes는 이미 등록된 CRD와 Custom Controller를 인지하고 있기 때문에 다음과 같이 처리한다.&lt;br&gt;4.1. Hello 인스턴스 생성에 따른 이벤트를 Custom Controller의 Reconcile 함수 호출 (이 부분의 연결고리는 Operator에 대해 검토해 보면 된다)&lt;br&gt;4.2. Custom Controller에서 전달된 이벤트의 Hello 인스턴스 정보를 기준으로 Echo Deployment 구성 후 반환&lt;br&gt;4.3. Kubernetes에서 반환된 Deployment를 실행해서 Pod 실행&lt;/li&gt;
&lt;li&gt;사용자가 Hello 인스턴스의 Spec 값 변경 : 여기서는 Message ()&lt;/li&gt;
&lt;li&gt;&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;작업 재 실행.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위의 같이 단순한 작업의 흐름으로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;지금까지 설명한&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CRD 구성 및 배포, Custom Controller 연계 및 실행등을 모두 합쳐 놓은 것이 Operator&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;다.&lt;/p&gt;
&lt;h2&gt;실제 사례로 바라보기&lt;/h2&gt;
&lt;p&gt;이전 게시글에서 가장 문의가 많았던 프로메테우스를 기준으로 다시 한번 정리해 보도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/991743415C259BAF33&quot; alt=&quot;Prometheus Operator&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그림에 점선 박스는 CRD를 의미하는 내용이다. 정확하게는 “ServiceMonitor” 와 “Prometheus” 라는 사용자 정의 리소스를 의미하고 있다.&lt;ul&gt;
&lt;li&gt;“ServiceMonitor 1”, “ServiceMonitor 2” 는 “ServiceMonitor” 의 CR 인스턴스를 나타낸다.&lt;ul&gt;
&lt;li&gt;ServiceMonitor 1 인스턴스는 Metric을 제공하는 Service 1, 2, 3 를 연계할 수 있는 Endpoint 정보를 상태 정보로 갖도록 정의되어 생성된 것이다.&lt;/li&gt;
&lt;li&gt;ServiceMonitor 2 인스턴스는 Metric을 제공하는 Service 4, 5 를 연계할 수 있는 Endpoint 정보를 상태 정보로 갖도록 정의되어 생성된 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Prometheus 라는 CR 인스턴스는 ServiceMonitor CR 인스턴스와 관련된 정보 뿐만 아니라 Prometheus Server 구동에 필요한 정보들을 상태 정보로 갖도록 정의되어 생성된 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Operator는 ServiceMonitor와 Prometheus 라는 CR의 변경에 대한 처리를 담당한 Custom Controller를 구동하는 실행 모듈이다.&lt;/li&gt;
&lt;li&gt;Prometheus Server는 실제 동작하는 Prometheus Server다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 상황에서 사용자의 액션에 따라 다음과 같이 동작한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자가 ServiceMonitor CR 인스턴스를 추가로 생성 (ServiceMonitor 3) 한 경우&lt;ul&gt;
&lt;li&gt;Kubernetes에서 동작하는 Operator로 변경이 감지된다.&lt;/li&gt;
&lt;li&gt;ServiceMonitor CR 정보를 Controller의 Reconcile 함수로 전달&lt;/li&gt;
&lt;li&gt;Reconcile에서 ServiceMonitor CR 정보를 기준으로 Prometheus Server에서 인식할 수 있도록 Configmap 정보 재구성&lt;/li&gt;
&lt;li&gt;동작하고 있는 Prometheus 설정 (Configmap)을 재 로드하도록 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자가 Prometheus CR 인스턴스의 값을 변경한 경우&lt;ul&gt;
&lt;li&gt;Kubernetes에서 동작하는 Operator로 변경이 감지된다.&lt;/li&gt;
&lt;li&gt;Prometheus CR 정보를 Controller의 Reconcile 함수로 전달&lt;/li&gt;
&lt;li&gt;Reconcile에서 Prometheus CR 정보를 기준으로 Prometheus Server를 재 시동하도록 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정확하고 상세한 내용은 아니지만, 기존 게시글에 문의 되었던 것들이 이해될 수 있기를 바랍니다.&lt;/p&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>CD</category>
      <category>CRD</category>
      <category>custom resource</category>
      <category>Kubernetes</category>
      <category>operator</category>
      <category>쿠버네티스</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/74</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-%ED%99%95%EC%9E%A5%EC%9D%B8-CRD%EC%99%80-CR-%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC#entry74comment</comments>
      <pubDate>Fri, 19 Nov 2021 17:13:06 +0900</pubDate>
    </item>
    <item>
      <title>[kubernetes-troubleshooting] Namespace 삭제 명령에도 삭제되지 않는 Pod 삭제하기</title>
      <link>https://ccambo.tistory.com/entry/kubernetes-troubleshooting-Namespace-%EC%82%AD%EC%A0%9C-%EB%AA%85%EB%A0%B9%EC%97%90%EB%8F%84-%EC%82%AD%EC%A0%9C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-Pod-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to force deletion of pods on namespace&lt;/h1&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;이전 게시글인 &lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Troubleshooting-%EC%82%AD%EC%A0%9C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-Namespace-%EA%B0%95%EC%A0%9C%EB%A1%9C-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0&quot;&gt;삭제되지 않는 네임스페이스 &lt;sup&gt;Namespace&lt;/sup&gt; 강제로 삭제하기&lt;/a&gt; 를 통해 네임스페이를 삭제하는 방법을 알아 보았다. &lt;/p&gt;
&lt;p&gt;이번 경우는 확실하게 눈으로 확인할 수 있는 파드 &lt;sup&gt;Pod&lt;/sup&gt; 들이 남아 있고 &lt;code&gt;Terminating&lt;/code&gt; 상태로 삭제되지 않는 문제가 발생했다. 테스트 했던 M3 오퍼레이터 &lt;sup&gt;Operator&lt;/sup&gt; 를 장시간 유지하면서 etcd 클러스터가 응답하지 않는 상태가 발생했고, 삭제를 했지만 삭제되지 않는 문제인 상태다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl -n m3cluster get pods
NAME                READY   STATUS            RESTARTS   AGE
...
etcd-0            1/1     Terminating   2          16d
etcd-1            1/1     Terminating   2          16d
etcd-2            1/1     Terminating   2          16d
...&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;문제 원인 확인과 처리 방법&lt;/h2&gt;
&lt;p&gt;정상적으로 삭제될 수 있는 시간을 지나서도 &lt;code&gt;Terminating&lt;/code&gt; 상태로 남아있는 상태는 대부분은 아래와 같은 원인으로 발생한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파드에 처리되지 않는 파이널라이저 &lt;sup&gt;Finalizer&lt;/sup&gt; 가 연결된 경우&lt;/li&gt;
&lt;li&gt;파드가 종료 시그널에 응답하지 않는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 상황에서 정보를 확인해야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;정보 출력&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl -n &amp;lt;namespace_name&amp;gt; get pod -p &amp;lt;pod_name&amp;gt; -o yml [&amp;gt; undeleted_pod.yaml]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;화면에 출력을 사용하던지 아니면 화일로 출력해서 내용 중에서 &lt;code&gt;status&lt;/code&gt; 부분과 &lt;code&gt;metadata.finalizer&lt;/code&gt; 내용을 검토한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;파이널라이저 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;status 상에 별다른 문제가 없다면 출력된 정보에서 파이널라이저가 존재하는지 확인하고 아래의 명령을 사용해서 해당 파이널라이저를 제거한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl -n &amp;lt;namepsace_name&amp;gt; patch pod &amp;lt;pod_name&amp;gt; -p &amp;#39;{&amp;quot;metadata&amp;quot;:{&amp;quot;finalizers&amp;quot;:null}}&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;노드 상태 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;파드가 정상적인 상태로 판단이 되면 노드를 확인해 봐야 한다. 출력물에서 &lt;code&gt;spec.nodeName&lt;/code&gt; 을 통해서 배정된 노드 명을 확인하고 쿠버네티스 대시보드 등을 통해서 노드에 배정된 모든 파드가 &lt;code&gt;Terminating&lt;/code&gt; 상태인지를 확인해야 한다.&lt;/p&gt;
&lt;p&gt;이 상황이라면 쿠버네티스 마스터를 통해서 삭제 요청된 내용이 해당 노드가 응답할 수 있는 상태가 되어야 처리될 수 있으므로 노드를 재 부팅하는 등으로 처리하면 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;포드 강제 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;위의 모든 상황이 정상적이라면 종료 시그널에 응답하지 않는 상태인 것으로 일반적으로는 다음과 같은 상황일 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;인터럽트 신호를 허용하지 않는 사용자 공간의 루프 실행 중&lt;/li&gt;
&lt;li&gt;애플리케이션 런타임의 유지 관리 프로세스 진행 중 (예를 들면 가비지 수집 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 경우라면 강제로 종료 시키는 방법을 사용할 수 있다. 그러나 이런 처리는 향후 예상치 않은 문제가 발생할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl -n &amp;lt;namespace_name&amp;gt; delete pod &amp;lt;pod_name&amp;gt; --grace-period=0 --force&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하나의 파드라면 위의 명령을 사용하면 되지만 여러 개의 파드가 대상이라면 아래와 같이 일괄처리도 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;for p in $(kubectl -n &amp;lt;namespace_name&amp;gt; get pods | grep Terminating | awk &amp;#39;{print $1}&amp;#39;); do kubectl -n &amp;lt;namespace_name&amp;gt; delete pod $p --grace-period=0 --force; done&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;kubelet 재 시작&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;마지막으로 해 볼 수 있는 것은 종료 시그널이 실제 배정된 노드의 &lt;code&gt;kubelet&lt;/code&gt; 과 연결되지 못해서 처리가 안되는 경우로 SSH를 통해 해당 노드로 접근해서 kubelet을 재 시작하는 것이다.&lt;/p&gt;
&lt;p&gt;당연히 이를 처리하기 위한 권한이 존재해야 하고, 처리하기 전에 kubelet 로그를 확인해서 의심되는 상황이 존재하는지를 우선 검토하도록 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>Delete</category>
      <category>Force</category>
      <category>Kubernetes</category>
      <category>namespace</category>
      <category>pod</category>
      <category>terrminating</category>
      <category>강제</category>
      <category>네임스페이스</category>
      <category>쿠버네티스</category>
      <category>파드</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/73</guid>
      <comments>https://ccambo.tistory.com/entry/kubernetes-troubleshooting-Namespace-%EC%82%AD%EC%A0%9C-%EB%AA%85%EB%A0%B9%EC%97%90%EB%8F%84-%EC%82%AD%EC%A0%9C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-Pod-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0#entry73comment</comments>
      <pubDate>Tue, 15 Jun 2021 19:02:25 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes-Storage] 정상적으로 동작하던 Dynamic NFS Provisioner 오류 해결하기 (SelfLink 관련)</title>
      <link>https://ccambo.tistory.com/entry/%EC%A0%95%EC%83%81%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8D%98-Dynamic-NFS-Provisioner-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-SelfLink-%EA%B4%80%EB%A0%A8</link>
      <description>&lt;h1&gt;정상적으로 동작하던 Dynamic NFS Provisioning에서 오류가 발생하는 문제 해결&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 &lt;sup&gt;Kubernetes&lt;/sup&gt; 클러스터에 동적으로 NFS &lt;sup&gt;Network File System&lt;/sup&gt; Provisioning 에 대한 글은 아래 게시글 참고&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/CentOS-NFS-CentOS-8%EC%97%90-NFS-%EC%84%A4%EC%A0%95-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot;&gt;[CentOS - NFS] CentOS 8에 NFS 설정 및 테스트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Storage-CentOS-8%EC%97%90-Dynamic-NFS-Client-Provisioner-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0&quot;&gt;[Kubernetes-Storage] CentOS 8에 Dynamic NFS Client Provisioner 구성하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 게시글을 기준으로 NFS Provisioning 을 잘 사용하고 있었다. 추가로 쿠버네티스 클러스터를 구성해서 테스트를 해야하는 작업이 생겼고, 최신 버전의 쿠버네티스로 설치를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일하게 NFS Provisioning 설정을 했고, 애플리케이션을 배포했을 때 계속 &lt;code&gt;Pending&lt;/code&gt; 상태로 진행되지 않는 상황이 발생해서 로그 정보들을 통해서 검증하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 아래와 같이 &lt;code&gt;nfs-client-provisioner&lt;/code&gt; 파드에서 오류가 발생하는 것으로 확인할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;2021-06-09T06:31:34.335294986Z  E0609 06:31:34.335012       1 controller.go:682] Error watching for provisioning success, can't provision for claim &quot;default/test-web-0&quot;: events is forbidden: User &quot;system:serviceaccount:default:nfs-provisioner&quot; cannot list resource &quot;events&quot; in API group &quot;&quot; in the namespace &quot;default&quot;
2021-06-09T06:31:34.335430080Z  I0609 06:31:34.335031       1 leaderelection.go:156] attempting to acquire leader lease...
2021-06-09T06:31:34.362925534Z  I0609 06:31:34.362178       1 leaderelection.go:178] successfully acquired lease to provision for pvc default/test-web-0
2021-06-09T06:31:34.363014753Z  I0609 06:31:34.362277       1 controller.go:1068] scheduleOperation[provision-default/test-web-0[6ebb2da5-4c46-451a-ad82-f60c8e524bf8]]
2021-06-09T06:31:34.369124223Z  E0609 06:31:34.368990       1 controller.go:766] Unexpected error getting claim reference to claim &quot;default/test-web-0&quot;: selfLink was empty, can't make reference&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;code&gt;... cannot list resource &quot;events&quot; in API group &quot;&quot; in the namespace &quot;default&amp;rdquo;&lt;/code&gt; 오류는 권한과 관련된 부분이니 설정을 조정하면 되지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #090;&quot;&gt;&lt;b&gt;selfLink was empty, can't make reference&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류는 기존에 보지 못했던 것이라 이 부분을 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련된 검색을 진행한 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #090;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/kubernetes/enhancements/issues/1164&quot;&gt;KEP-1164: Deprecate and Remove SelfLink&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 다음과 같은 내용이 존재한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In v1.16, we will deprecate the SelfLink field in both ObjectMeta and ListMeta objects by: documenting in field definition that it is deprecated and is going to be removed adding a release-note about field deprecation We will also introduce a feature gate to allow disabling setting SelfLink fields and opaque the logic setting it behind this feature gate.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;In v1.20 (12 months and 4 release from v1.16) we will switch off the feature gate&lt;/b&gt; &lt;b&gt;which will automatically disable setting SelfLinks. However it will&lt;/b&gt; &lt;b&gt;still be possible to revert the behavior by changing value of a&lt;/b&gt; &lt;b&gt;feature gate&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In v1.21, we will get rid of the whole code propagating those fields and fields themselves. In the meantime, we will go over places referencing that field (see below) and get rid of those too.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, v1.16 부터 ObjectMeta와 ListMeta 객체 모두에서 SelfLink 필드를 더 이상 사용하지 않기 때문에 삭제될 기능이며, v1.20 부터는 SelfLink 값을 자동으로 해제하는 기능이 동작한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제시하고 있는 것과 같이 SelfLink를 사용하지 않는 것이 정상이지만, 현재 구성한 NFS Provisioner에서 SelfLink를 참조해서 처리하고 있기 때문에 다음과 같이 처리해서 정상적으로 동작할 수 있도록 해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FeatureGate 우회하는 방법 (권장하지 않음)&lt;/b&gt;쿠버네티스 클러스터의 마스터 노드에서 &lt;code&gt;/etc/kubernetes/manifests/kube-apiserver.yaml&lt;/code&gt; 파일을 열고 아래와 같이 &lt;code&gt;--feature-gates=RemoveSelfLink=false&lt;/code&gt; Command Option을 설정하고 재 시작하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-yaml&quot;&gt;...
spec:
  containers:
  - command:
    - kube-apiserver
    - --feature-gates=RemoveSelfLink=false
...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;실제 SelfLink를 자동으로 해제하는 것은 &lt;a href=&quot;https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/&quot;&gt;FeaturGate - RemoveSelfLink&lt;/a&gt; 와 관련된 부분이므로 이를 해제하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Provisioner를 교체하는 방법 (권장)&lt;/b&gt;아래와 같이 NFS Provisioning 관련 매니페스트 파일을 조정하고 배포하면 된다. (nfs_setup.yaml)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-yaml&quot;&gt;# ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: default
---
# ClusterRole
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [&quot;&quot;]
    resources: [&quot;nodes&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
  - apiGroups: [&quot;&quot;]
    resources: [&quot;persistentvolumes&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;delete&quot;]
  - apiGroups: [&quot;&quot;]
    resources: [&quot;persistentvolumeclaims&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;update&quot;]
  - apiGroups: [&quot;storage.k8s.io&quot;]
    resources: [&quot;storageclasses&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
  - apiGroups: [&quot;&quot;]
    resources: [&quot;events&quot;]
    verbs: [&quot;create&quot;, &quot;update&quot;, &quot;patch&quot;]
---
# ClusterRoleBinding
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
---
# leader locking Role
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
rules:
  - apiGroups: [&quot;&quot;]
    resources: [&quot;endpoints&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;]
---
# leader locking RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: leader-locking-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default # provisioner가 배포될 네임스페이스
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
---
# Deployment
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  namespace: default
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner    
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccount: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: k8s.gcr.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2    # 이미지 교체
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner        # Provisioner Env 변경
            - name: NFS_SERVER
              value: &amp;lt;nfs server ip&amp;gt;
            - name: NFS_PATH
              value: &amp;lt;nfs server volume path&amp;gt;
      volumes:
        - name: nfs-client-root
          nfs:
            server: &amp;lt;nfs server ip&amp;gt;
            path: &amp;lt;nfs server volume path&amp;gt;
---
# StorageClass
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: &quot;true&quot;
  name: managed-nfs-storage
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner    # Provisioner 변경
parameters:
    archiveOnDelete: &quot;false&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;span style=&quot;color: #090;&quot;&gt;&lt;b&gt;v1.21 버전에서는 완전히 SelfLink가 삭제&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
된다고 하니 이 방법을 사용하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kubernetes/enhancements/issues/1164&quot;&gt;https://github.com/kubernetes/enhancements/issues/1164&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/&quot;&gt;https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner/issues/25&quot;&gt;https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner/issues/25&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner&quot;&gt;https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>dynamic provisioning</category>
      <category>featuregate</category>
      <category>k8s</category>
      <category>KEP-1164</category>
      <category>Kubernetes</category>
      <category>NFS</category>
      <category>selflink emtpy</category>
      <category>Storage</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/72</guid>
      <comments>https://ccambo.tistory.com/entry/%EC%A0%95%EC%83%81%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8D%98-Dynamic-NFS-Provisioner-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-SelfLink-%EA%B4%80%EB%A0%A8#entry72comment</comments>
      <pubDate>Wed, 9 Jun 2021 17:07:34 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes] kubeadm 으로 ContainerD 기반 K8S Cluster를 CentOS 8 설치하기 (Docker 사용하지 않음)</title>
      <link>https://ccambo.tistory.com/entry/kubeadm-%EC%9C%BC%EB%A1%9C-ContainerD-%EA%B8%B0%EB%B0%98-K8S-Cluster%EB%A5%BC-CentOS-8-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-Docker-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%8C</link>
      <description>&lt;h1&gt;Docker를 사용하지 않고 Containerd 기반의 Kubernetes Cluster를 CentOS 8 에 kubeadm으로 설치하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CentOS 8 버전부터 도커 &lt;sup&gt;Docker&lt;/sup&gt; 는 레드햇의 도구인 &lt;code&gt;podman, buildah&lt;/code&gt; 로 대체된 상태로 기본 패키지 저장소에서 제거되었고, API를 사용해서 처리되는 것이기 때문에 특정 툴에 한정될 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 제공되고 있는 컨테이너 런타임&lt;sup&gt;Container Runtime&lt;/sup&gt; 은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/container-runtimes/#docker&quot;&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/container-runtimes/#cri-o&quot;&gt;CRI-O&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/container-runtimes/#containerd&quot;&gt;Containerd&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문서는 containerd를 사용해서 클러스터를 구성한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1개의 마스터 노드와 3개의 워커 노드 모두 CentOS 8 설치&lt;/li&gt;
&lt;li&gt;각 노드는 2G RAM, 2 CPU 를 최소 사양으로 한다.&lt;/li&gt;
&lt;li&gt;모든 노드는 저장소에서 쿠버네티스 &lt;sup&gt;Kubernetes&lt;/sup&gt; 및 기타 필수 패키지를 설치할 수 있도록 인터넷에 연결이 가능해야 하고, dnf 패키지 관리자를 사용할 수 있으며 원격으로 패키지를 가져올 수 있는지 검증되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설치환경 요약&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스의 &lt;code&gt;최소 요구 사항은 2G RAM, 2 CPU&lt;/code&gt; 를 기준으로 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Master
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CentOS 8.2.2004&lt;/li&gt;
&lt;li&gt;monitor.master&lt;/li&gt;
&lt;li&gt;centos&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Worker
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CentOS 8.2.2004&lt;/li&gt;
&lt;li&gt;monitor.worker1, 2, 3&lt;/li&gt;
&lt;li&gt;centos&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CNI : Calico&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CentOS 의 버전을 확인하는 방법은 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;$ cat /etc/redhat-release
CentOS Linux release 8.2.2004 (Core) 

$ cat /etc/*release*
CentOS Linux release 8.2.2004 (Core) 
Derived from Red Hat Enterprise Linux 8.2 (Source)
NAME=&quot;CentOS Linux&quot;
VERSION=&quot;8 (Core)&quot;
ID=&quot;centos&quot;
ID_LIKE=&quot;rhel fedora&quot;
VERSION_ID=&quot;8&quot;
PLATFORM_ID=&quot;platform:el8&quot;
PRETTY_NAME=&quot;CentOS Linux 8 (Core)&quot;
ANSI_COLOR=&quot;0;31&quot;
CPE_NAME=&quot;cpe:/o:centos:centos:8&quot;
HOME_URL=&quot;https://www.centos.org/&quot;
BUG_REPORT_URL=&quot;https://bugs.centos.org/&quot;

CENTOS_MANTISBT_PROJECT=&quot;CentOS-8&quot;
CENTOS_MANTISBT_PROJECT_VERSION=&quot;8&quot;
REDHAT_SUPPORT_PRODUCT=&quot;centos&quot;
REDHAT_SUPPORT_PRODUCT_VERSION=&quot;8&quot;

CentOS Linux release 8.2.2004 (Core) 
CentOS Linux release 8.2.2004 (Core) 
cpe:/o:centos:centos:8&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CentOS 8.2의 경우 &lt;code&gt;일반 계정 생성 후에 sudo 권한&lt;/code&gt;을 줄 경우는 아래의 명령을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;$ usermode -aG wheel &amp;lt;user&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CentOS 8.2에는 &lt;code&gt;wheel&lt;/code&gt; 이라는 특별한 그룹이 존재하는데 sudo 설정에는 이 그룹에 한해 sudo 권한을 부여하고 있기 때문에 이 그룹에 사용자를 포함시키면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공통 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터와 워커 노드 모두 Static IP를 가지고 있어야 하며, 일반 계정이 존재해야 하며 &lt;code&gt;sudo&lt;/code&gt; 권한을 가져야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;관리자 권한으로 설정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패키지 관리자 갱신&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;dnf -y upgrade&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hosts 정보 설정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;# 각 노드에서 각자에 맞도록 호스트 명 설정
hostnamectl set-hostname &amp;lt;host name&amp;gt;

# /etc/hosts 에 각 구성 노드들의 IP와 HostName 설정
vi /etc/hosts
...
&amp;lt;node ip&amp;gt;    &amp;lt;hostname&amp;gt;
&amp;lt;node ip&amp;gt;    &amp;lt;hostname&amp;gt;
...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방화벽 설정&lt;/b&gt;CentOS 8에는 기본적인 방화벽이 설치되어 있기 않기 때문에 설치를 수행한다.&lt;b&gt;정상적인 방화벽 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방화벽 규칙 설정&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;# 공통으로 설정할 경우
$ sudo firewall-cmd --permanent --add-port=22/tcp        # SSH
$ sudo firewall-cmd --permanent --add-port=80/tcp        # Web
$ sudo firewall-cmd --permanent --add-port=443/tcp        # API Requests and End-User, used by Workers
$ sudo firewall-cmd --permanent --add-port=2376/tcp        # Docker daemon TLS, used by all
$ sudo firewall-cmd --permanent --add-port=2379-2380/tcp    # ETCD Server client API, used by kueb-apiserver, etcd
$ sudo firewall-cmd --permanent --add-port=6443/tcp        # Kubernetes API Server, used by all
$ sudo firewall-cmd --permanent --add-port=8285/udp        # Flannel overlay network (UDP Backend), used by Master, Worker (if use Flannel)
$ sudo firewall-cmd --permanent --add-port=8472/udp        # Flannel overlay network (VxLan), used by Master, Worker (if use Flannel)
$ sudo firewall-cmd --permanent --add-port=9099/tcp        # Liveness/Readiness kubelet, API, used by Master, Worker
$ sudo firewall-cmd --permanent --add-port=10250/tcp        # kubelet API, used by self and control plane
$ sudo firewall-cmd --permanent --add-port=10251/tcp        # kube-scheduler, used by self
$ sudo firewall-cmd --permanent --add-port=10252/tcp        # kube-controller-manager, used by self
$ sudo firewall-cmd --permanent --add-port=10254/tcp        # Ingress
$ sudo firewall-cmd --permanent --add-port=10255/tcp        # Heapster, used by Worker
$ sudo firewall-cmd --permanent --add-port=30000-32767/tcp    # NodePort services, used by all
$ sudo firewall-cmd --permanent --add-port=30000-32767/udp    # NodePort services, used by all

# 역할별 - ETCD 운영용 노드인 경우
$ sudo firewall-cmd --permanent --add-port=2376/tcp        # Docker daemon TLS, used by all
$ sudo firewall-cmd --permanent --add-port=2379-2380/tcp    # ETCD Server client API, used by kueb-apiserver, etcd
$ sudo firewall-cmd --permanent --add-port=8472/udp        # Flannel overlay network (VxLan), used by Master, Worker (if use Flannel)
$ sudo firewall-cmd --permanent --add-port=9099/tcp        # Liveness/Readiness kubelet, API, used by Master, Worker
$ sudo firewall-cmd --permanent --add-port=10250/tcp        # kubelet API, used by self and control plane

# 역할별 - 마스터 Control Plane 노드인 경우
$ sudo firewall-cmd --permanent --add-port=80/tcp        # Web
$ sudo firewall-cmd --permanent --add-port=443/tcp        # API Requests and End-User, used by Workers
$ sudo firewall-cmd --permanent --add-port=2376/tcp        # Docker daemon TLS, used by all
$ sudo firewall-cmd --permanent --add-port=6443/tcp        # Kubernetes API Server, used by all
$ sudo firewall-cmd --permanent --add-port=8285/udp        # Flannel overlay network (UDP Backend), used by Master, Worker (if use Flannel)
$ sudo firewall-cmd --permanent --add-port=8472/udp        # Flannel overlay network (VxLan), used by Master, Worker (if use Flannel)
$ sudo firewall-cmd --permanent --add-port=9099/tcp        # Liveness/Readiness kubelet, API, used by Master, Worker
$ sudo firewall-cmd --permanent --add-port=10250/tcp        # kubelet API, used by self and control plane
$ sudo firewall-cmd --permanent --add-port=10254/tcp        # Ingress
$ sudo firewall-cmd --permanent --add-port=30000-32767/tcp    # NodePort services, used by all
$ sudo firewall-cmd --permanent --add-port=30000-32767/udp    # NodePort services, used by all

# 역할별 - 워커 노드인 경우
$ sudo firewall-cmd --permanent --add-port=22/tcp        # SSH
$ sudo firewall-cmd --permanent --add-port=80/tcp        # Web
$ sudo firewall-cmd --permanent --add-port=443/tcp        # API Requests and End-User, used by Workers
$ sudo firewall-cmd --permanent --add-port=2376/tcp        # Docker daemon TLS, used by all
$ sudo firewall-cmd --permanent --add-port=8472/udp        # Flannel overlay network (VxLan), used by Master, Worker (if use Flannel)
$ sudo firewall-cmd --permanent --add-port=9099/tcp        # Liveness/Readiness kubelet, API, used by Master, Worker
$ sudo firewall-cmd --permanent --add-port=10250/tcp        # kubelet API, used by self and control plane
$ sudo firewall-cmd --permanent --add-port=10254/tcp        # Ingress
$ sudo firewall-cmd --permanent --add-port=30000-32767/tcp    # NodePort services, used by all
$ sudo firewall-cmd --permanent --add-port=30000-32767/udp    # NodePort services, used by all

# Calico Pod Network (마스터, 워커)
$ sudo firewall-cmd --permanent --zone=public --add-port=179/tcp    # Calico Networking (BGP), used by all (if use calico)

$ sudo firewall-cmd --reload&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;# 방화벽 설치
dnf -y install firewalld

# 활성화
systemctl enable firewalld
systemctl start firewalld

# 방화벽에 IP Masquerade 활성화
firewall-cmd --add-masquerade --permanent
firewall-cmd --reload

# 상태 확인
firewall-cmd --state
firewall-cmd --list-all

# 방화벽 끄기
systemctl stop firewalld
systemctl disable firewalld&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;방화벽은 쿠버네티스에서 제시하는 포트들을 열어주면 사용이 가능하지만 실제 사용할 때는 쿠버네티스 공식문서에 제시되지 않은 포트들도 존재하고, 이상 동작하는 상황들도 발생하기 때문에 일반적인 테스트를 위한 실행기준으로는 방화벽을 끄고 운영하는 것을 권장한다. (당연히 프로덕션 환경이라면 필요한 포트들을 방화벽에 등록하고 정상적으로 사용해야 한다)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Container Runtime 설치&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사전 설정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &amp;gt; /etc/modules-load.d/containerd.conf &amp;lt;&amp;lt;EOF
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

# Setup required sysctl params, these persist across reboots.
cat &amp;gt; /etc/sysctl.d/99-kubernetes-cri.conf &amp;lt;&amp;lt;EOF
net.bridge.bridge-nf-call-iptables  = 1
net.ipv4.ip_forward                 = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF

sysctl --system&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;containerd 설치&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install required packages
dnf install -y yum-utils device-mapper-persistent-data lvm2 iproute-tc

# Add docker repository
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo

# dnf update and Install containerd
dnf update -y &amp;amp;&amp;amp; dnf install -y containerd.io

# Configure containerd
mkdir -p /etc/containerd
containerd config default &amp;gt; /etc/containerd/config.toml

# Restart containerd
systemctl restart containerd

# Enable containerd on boot
systemctl enable containerd&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CRI &lt;sup&gt;Container Runetime Interface&lt;/sup&gt; 런타임으로 사용할 &lt;code&gt;containerd&lt;/code&gt; 를 설치한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kubernetes 설치 (kubeadm, kubelet, kubectl)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사전설정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kubernetes Repository 추가
cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설치&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install Kubernetes (kubeadm, kubelet and kubectl) - 특정 버전을 지정할 수 있다.
dnf install -y kubeadm kubelet kubectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 활성화&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;# Enable kubelet service on boot
systemctl enable kubelet

# Configure kubelet
echo 'KUBELET_EXTRA_ARGS=&quot;--fail-swap-on=false&quot;' &amp;gt; /etc/sysconfig/kubelet

# Start kubelet service
systemctl start kubelet&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CentOS 8에 기본적으로 쿠버네티스 리포지토리가 설치되어 있지 않기 때문에 수동으로 추가해야 한다. &lt;code&gt;kubeadm&lt;/code&gt; 은 단일 쿠버네티스 컨트롤 플레인을 생성 및 활성화하고, 업그레이드/다운그레이드 및 부트스트랩 토큰 관리와 같은 다른 클러스터 수명주기 기능도 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마스터 노드 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 통신 및 액세스를 위해 다양한 포트를 사용하기 때문에 방화벽 &lt;sup&gt;Firewall&lt;/sup&gt; 에 제한되지 않고 쿠버네티스에 액세스할 수 있는 상태여야 한다. 아래의 방화벽 규칙을 참고하면 된다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;Protocol&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Direction&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Port Range&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Purpose&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Used By&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;TCP&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;inbound&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;6443&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;k8s API Server&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;TCP&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;inbound&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;2379 - 2380&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;ETCD server client API&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;kube-apiserver, etcd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;TCP&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;inbound&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;10250&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;kubelet API&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;self, control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;TCP&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;inbound&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;10251&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;kube-scheduler&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;self&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;TCP&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;inbound&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;10252&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;kube-controller-manager&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;self&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;kubeadm Images Pull&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;kubeadm config images pull&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Create Control Plane with kubeadm&lt;/b&gt;&lt;code&gt;--apiserver-cert-extra-sans&lt;/code&gt; 는 외부에서 접근할 수 있는 IP를 지정해서 내부와 외부에서 모두 API Server로 접근할 수 있도록 지정하고 인증서에 관련된 IP 정보를 포함시킨키는 것이다.
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;kubeadm join xxx.xxx.xxx.xxx:6443 --token ur9jpx... \        
        --discovery-token-ca-cert-hash sha256:48e4d3cca... &lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;정상적으로 실행되면 아래와 같이 워커 노드들을 클러스터에 참여시킬 수 있는 구문이 출력된다. 이 명령을 복사해 놓고 이후 워커 노드 작업에서 사용하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;kubeadm init --pod-network-cidr=10.15.0.0/16 --apiserver-cert-extra-sans=&amp;lt;master node ip&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;kube-api 통신을 위한 환경 설정&lt;/b&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;쿠버네티스 마스터 명령은 모두 일반 계정으로 처리된다. 따라서 일반 계정이 kube-api 와 통신을 위해서는 아래와 같이 처리해 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Pod Network 구성&lt;/b&gt;Calico는 쿠버네티스가 운영되는 환경에 따라 설치 과정에 차이가 있고, 같은 환경이라도 노드의 수에 따라서 설치하는 것도 다를 수 있다. 따라서 매뉴얼을 확인하고 작업할 것을 권장한다.
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml&lt;/code&gt;&lt;/pre&gt;
설치된 Calico 는 워커 노드가 클러스터에 참여하면 설치되어 동작하게 된다.&lt;/li&gt;
&lt;li&gt;여기서는 On-Premise 환경이고 노드가 50개 미만이므로 아주 간단하게 적용하도록 한다.&lt;/li&gt;
&lt;li&gt;쿠버네티스 클러스터를 구성할 때 &lt;code&gt;--pod-network-cidr&lt;/code&gt; 옵션으로 지정한 CNI &lt;sup&gt;Container Network Interface&lt;/sup&gt; 로 어떤 것을 사용할지에 따라 달라진다. 여기서는 Calico를 사용하도록 한다. 다른 CNI 들을 &lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#pod-network&quot;&gt;Installing a pod network add-on&lt;/a&gt; 을 참고하도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;워커 노드 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터 노드에 Control Plane이 설치되었으므로 이제 구성된 쿠버네티스 클러스터에 참여할 워커 노드들을 구성하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kubernetes Cluster 참여&lt;/b&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;kubeadm token create --print-join-command&lt;/code&gt;&lt;/pre&gt;
위의 쿠버네티스 클러스터를 구성할 때 출력된 명령을 사용하거나 토큰이 만료된 상태라면 바로 위의 신규 토큰 생성 방법을 통해서 아래와 같이 클러스터에 참여하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;kubeadm join 10.0.1.186:6443 --token tofo13.... --discovery-token-ca-cert-hash sha256:e39c05bb474f8e6726ef...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;기본적으로 마스터 노드에 쿠버네티스 클러스터를 구성하는 작업을 통해서 발행된 토큰은 24시간 존재하도록 되어 있기 때문에 나중에 워커 노드를 추가할 경우는 토큰이 없는 상태가 발생하므로 새롭게 토큰을 생성해서 참여하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dashboard 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로 설치하면 Dashboard에 접근하기 위해서는 &lt;code&gt;kubectl proxy&lt;/code&gt; 명령을 사용하고 &lt;code&gt;http://localhost:8001/...&lt;/code&gt; 로 접근해야 한다. 그러나 실제로는 외부에서도 대시보드를 접근해서 확인하는 경우가 많기 때문에 NodePort 방식을 활용하도록 한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# wget이 없는 경우는 설치
dnf install -y wget

# Dashboard Manifest 파일 다운로드 (현재 버전 v2.2.0)
wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.2.0/aio/deploy/recommended.yaml

vi recommended.yaml
...  
  ports:  
  - nodePort: 32444     # 이 부분 추가 (생략하면 Kubernetes에서 임의로 지정하므로 중복되지 않는 값으로 설정)    
    port: 443    
    targetPort: 8443  
  selector:    
    k8s-app: kubernetes-dashboard  
  type: NodePort    # 이 부분 추가
...

# 직접 설치 
kubectl apply -f recommended.yaml

# 설치 검증
kubectl get services --all-namespaces # 또는 kubectl get services -n kubernetes-dashboard&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 버전에서는 기존의 &lt;code&gt;kube-system&lt;/code&gt; 네임스페이스에서 &lt;code&gt;kubernetes-dashboard&lt;/code&gt; 네임스페이스로 이전되었으며, dashboard와 scraper 파드가 설치된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Dashboard를 사용하기 위해서는 사용자 정보와 클러스터 역할 및 바인딩 정보를 구성해야 한다. (파일로 구성해도 되고 직접 kubectl 명령으로 생성해도 된다)&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 서비스 계정 생성
cat &amp;lt;&amp;lt;EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:  
  name: admin-user  
  namespace: kubernetes-dashboard
EOF

# 클러스터 역할 바인딩 생성
cat &amp;lt;&amp;lt;EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:  
  name: admin-user
roleRef:  
  apiGroup: rbac.authorization.k8s.io  
  kind: ClusterRole  
  name: cluster-admin
subjects:
- kind: ServiceAccount  
  name: admin-user  
  namespace: kubernetes-dashboard
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Dashboard에 접속하기 위해서는 &lt;code&gt;Bearer Token&lt;/code&gt;을 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl -n kubernetes-dashboard get secret $(kubectl -n kubernetes-dashboard get sa/admin-user -o jsonpath=&quot;{.secrets[0].name}&quot;) -o go-template=&quot;{{.data.token | base64decode}}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과로 추출된 Bearer Token을 &lt;code&gt;https://&amp;lt;cluster mastaer or worker node ip&amp;gt;:32444&lt;/code&gt; 로 접근해서 &lt;code&gt;Token&lt;/code&gt; 입력에 넣고 연결하면 대시보드를 확인할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Helm 3 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Helm은 쿠버네티스에서 동작하는 많은 애플리케이션들을 손쉽게 설치하도록 지원하는 프로그램으로 Ubuntu의 apt 나 CentOS 의 dnf 와 같은 패키지 관리자와 유사하다고 이해하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Helm 2 에서는 Tiler라는 Helm 서버가 필요했지만 Helm 3 에서는 좀 더 단순한 구조로 변경이 되었기 때문에 쉽게 설치 및 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 &lt;code&gt;kubectl&lt;/code&gt;이 지원되는 모든 노드에서 사용 가능하다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 설치 스크립트를 다운로드해서 설치
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 &amp;gt; get_helm.sh
chmod 700 get_helm.sh
./get_helm.sh

# 설치 확인 (현재 버전 v3.5.4)
helm version

# 설치는 /usr/local/bin 경로에 처리되므로 PATH 환경변수에 해당 경로가 설정되어 있어야 한다.
export PATH=/usr/local/bin:$PATH&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 저장소를 가지고 있지 않기 때문에 설치를 진행할 수 없으므로 기본 저장소를 추가해 준다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Repo 검증
helm search repo

# 기본 Repo 추가 (기존의 https://kubernetes-charts.storage.googleapis.com/ 는 더이상 지원되지 않는다)
helm repo add stable https://charts.helm.sh/stable

# Repo 챠트 리스트 확인
helm search repo stable

# Repo 정보 갱신
helm repo update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 및 저장소 설정이 되었으므로 필요한 애플리케이션들을 저장소를 통해서 쿠버네티스 클러스터에 설치가 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Metrics Server 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 클러스터는 기본적으로 모니터링 정보를 알 수 있는 기능이 제공되지 않기 때문에 추가로 설치해야 한다. 기존에는 &lt;code&gt;Heapster&lt;/code&gt; 기반으로 구성되었지만 더 이상 개발되지 않고 &lt;code&gt;Metrics Server&lt;/code&gt; 로 대체되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 모니터링 기능 (실 시간 정보, 수집된 정보를 메모리에서만 관리하는)을 추가 설치해야 쿠버네티스 컴포넌트들에 대한 자원 모니터링도 가능하고 Autoscaling 에도 사용 가능해진다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 최신 버전 다운로드 (현재 v0.4.2)
wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메트릭스 서버가 API Server와 통신할 때 TLS 문제가 존재한다. API Server는 자체 TLS를 사용하지만 메트릭스 서버는 Public TLS를 사용하기 때문에 그냥 설치하면 문제가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래와 같이 다운로드한 파일에서 TLS와 hostNetwork 사용 관련된 부분을 수정해 줘야 한다. Tab 키가 아닌 Space 키로 여백을 처리해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;vi components.yaml
...
      - args:
        - --cert-dir=/tmp        
        - --secure-port=4443        
        - --kubelet-insecure-tls=true     # 이 부분 추가 (TLS 관련)        
        - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
...      
      hostNetwork: true              # 이 부분 추가 (Host Network 사용 관련)      
      nodeSelector:
        kubernetes.io/os: linux
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경한 &lt;code&gt;components.yaml&lt;/code&gt; 파일을 쿠버네티스 클러스터에 배포한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Deploy
kubectl apply -f components.yaml 

# Pods 확인
kubectl get pods -n kube-system

# Deploy 확인
kubectl get deploy -n kube-system

# Node 리소스 사용량 조회
kubectl top node

# Pod 리소스 샤용량 조회
kubectl top pod --all-namespaces&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metrics Server 가 배포되어 정상적으로 동작하고 있다고 해도 즉시 데이터를 가져오는 것은 아니다. 따라서 설치 직후에는 다음과 같이 오류 메시지가 나올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Error from server (ServiceUnavailable): the server is currently unable to handle the request (get nodes.metrics.k8s.io)
Error from server (ServiceUnavailable): the server is currently unable to handle the request (get pods.metrics.k8s.io)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 수집되지 않아서 처리할 수 없다는 것을 의미한다. 대략 1 ~ 3분 정보 지나서 데이터가 수집되기 시작하면 정상적으로 출력이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 위의 TLS 및 hostNetwork 관련 수정과 다른 문제가 존재한다면 설정 부분을 다시 확인해야 한다. 이미 배포된 후에 Deployment 옵션을 변경할 경우는 아래의 명령을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;kubectl edit deployments.apps -n kube-system metrics-server&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/&quot;&gt;Kubernetes Official Site&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>Calico</category>
      <category>containerd</category>
      <category>Dashboard</category>
      <category>helm3</category>
      <category>install</category>
      <category>k8s</category>
      <category>Kubernetes</category>
      <category>metrics server</category>
      <category>without docker</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/71</guid>
      <comments>https://ccambo.tistory.com/entry/kubeadm-%EC%9C%BC%EB%A1%9C-ContainerD-%EA%B8%B0%EB%B0%98-K8S-Cluster%EB%A5%BC-CentOS-8-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-Docker-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%8C#entry71comment</comments>
      <pubDate>Tue, 8 Jun 2021 14:04:17 +0900</pubDate>
    </item>
    <item>
      <title>[MacOS] 특정 경로 밑의 디렉터리 일괄 삭제하기</title>
      <link>https://ccambo.tistory.com/entry/MacOS-%ED%8A%B9%EC%A0%95-%EA%B2%BD%EB%A1%9C-%EB%B0%91%EC%9D%98-%EB%94%94%EB%A0%89%ED%84%B0%EB%A6%AC-%EC%9D%BC%EA%B4%84-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to remove directories from Mac&lt;/h1&gt;
&lt;p&gt;Mac의 임시 파일들을 삭제하는 방법을 게시했던 &lt;a href=&quot;https://ccambo.tistory.com/entry/MacOSGit-DSStore-%EB%93%B1-%EC%88%A8%EA%B9%80%ED%8C%8C%EC%9D%BC-%EC%A0%95%EB%A6%AC-%EB%B0%8F-gitignore-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot;&gt;._xxx, .DS_Store 등 숨김파일 정리 및 .gitignore 처리하기&lt;/a&gt; 에서 언급했던 방식을 응용해서 Node 기반의 개발에서 사용했던 &lt;code&gt;node_modules&lt;/code&gt; 디렉터리를 일괄 삭제하는 방법을 정리해 본다.&lt;/p&gt;
&lt;p&gt;개발하면서 참조한 소스들, 개발한 소스들이 많기 때문에 &lt;code&gt;특정 경로 이하의 모든 node_modules 들을 찾아서 삭제하는 것보다는 일괄적으로 삭제&lt;/code&gt;하기 위해서 &lt;code&gt;find&lt;/code&gt; 명령을 사용하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ find . -name &amp;quot;node_modules&amp;quot; -type d -prune -print -exec rm -rf &amp;#39;{}&amp;#39; +&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;find .&lt;/strong&gt;: 현재 경로를 루트 경로로 설정하고  검색&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-name &amp;quot;node_modules&amp;quot;&lt;/strong&gt;: &amp;quot;node_modules&amp;quot;라는 이름을 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-type d&lt;/strong&gt;: 지정한 이름의 디렉터리만 찾도록 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-prune&lt;/strong&gt;: &amp;quot;node_modules&amp;quot;가 발견되면 그 경로 하위로 내려가지 않도록 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-print&lt;/strong&gt;: 검색된 대상의 경로 출력하도록 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-exec rm -rf &amp;#39;{}&amp;#39; +&lt;/strong&gt;: 일치된 결과에 대한 삭제 처리를 수행하는데, &lt;code&gt;{} +&lt;/code&gt; 는 마지막에 선택한 이름을 추가해서 명령줄을 빌드하도록 하는 것으로 전체 발견된 &amp;quot;node_modules&amp;quot;의 수보다 적은 횟수로 호출할 수 있도록 조정하는 것으로 성능 향상을 위한 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;무조건 실행되면 다시 복구할 수 없는 상태&lt;/code&gt;가 되므로 아래의 명령으로 실제 대상들이 제대로 검색되는지를 먼저 확인하고 진행하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ find . -name &amp;quot;node_modules&amp;quot; -type d -prune -print | xargs du -chs
254M    ./K3Lab/RNDWorks/apigw/samples/etri/web/node_modules
4.0K    ./Blogs/hugo/hugoblog/node_modules
...
 14G    total&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;xargs&lt;/strong&gt;: 앞의 명령에 대한 출력을 다음 명령의 입력으로 사용할 수 있도록 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;du&lt;/strong&gt;: &amp;quot;Disk Usage&amp;quot;를 나타내는 명령으로 디렉터리의 디스크 사용량 출력&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-chs&lt;/strong&gt;: 대상 디렉터리의 사용량 출력과 전체 총 합계 사용량을 보기 쉬운 단위 (KB, MB, GB) 로 표시하도록 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;상기와 같이 검색된 &amp;quot;node_modules&amp;quot;의 개별 사용량과 최종 전체 사용량을 확인할 수 있다. 결과에 별다른 문제가 없다면 삭제 명령을 진행하면 된다.&lt;/p&gt;</description>
      <category>개발/기타공통</category>
      <category>du</category>
      <category>Find</category>
      <category>MAC</category>
      <category>node_modules</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/69</guid>
      <comments>https://ccambo.tistory.com/entry/MacOS-%ED%8A%B9%EC%A0%95-%EA%B2%BD%EB%A1%9C-%EB%B0%91%EC%9D%98-%EB%94%94%EB%A0%89%ED%84%B0%EB%A6%AC-%EC%9D%BC%EA%B4%84-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0#entry69comment</comments>
      <pubDate>Thu, 28 Jan 2021 13:49:17 +0900</pubDate>
    </item>
    <item>
      <title>[MacOS] Terminal 에서 zsh compinit: insecure directories 문구가 발생하는 경우 대처하기</title>
      <link>https://ccambo.tistory.com/entry/MacOS-Terminal-%EC%97%90%EC%84%9C-zsh-compinit-insecure-directories-%EB%AC%B8%EA%B5%AC%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EA%B2%BD%EC%9A%B0-%EB%8C%80%EC%B2%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to set compinit mode on zsh&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;KinD (Kubernetes in Docker)로 MacOS 상에 Multi-Nodes Kubernetes Cluster를 구성&lt;/code&gt;하면서 kubectl autocomplete를 설정했는데 아래와 같은 메시지가 터미널 실행 시에 계속 나타나는 문제가 발생했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;zsh compinit: insecure directories, run compaudit for list.
Ignore insecure directories and continue [y] or abort compinit [n]?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정보를 찾아보니 대 부분 zsh와 관련된 소유권 문제를 많이 이야기하고 있었다. 즉, &lt;font color=&quot;royalblue&quot; size=&quot;3&quot;&gt;외부의 패키지나 라이브러리 등을 설치할 떄 외부의 스크립트를 사용할 경우에 &lt;code&gt;/usr/local/share&lt;/code&gt; 폴더의 소유권과 권한이 변경&lt;/font&gt;되는 문제로 발생한다는 것이다.&lt;/p&gt;
&lt;p&gt;상황을 확인해 보기 위해서 아래의 명령을 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ compaudit

There are insecure directories:
/usr/local/share

또는

There are insecure directories:
/usr/local/share/zsh/.site-functions
/usr/local/share/zsh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 같은 결과를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;결과를 바로 소유권 및 권한 처리를 하는 방법은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 검증 및 변경 처리
$ compaudit | xargs chmod g-w    

# 재 검증
$ compaudit

There are insecure directories:&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 방법이 아니라 개별적으로 처리하는 경우는 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;/usr/local/share/zsh/.site-functions 가 나온 경우&lt;/strong&gt; &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ cd /usr/local/share/zsh
$ sudo chown -R root:root ./site_functions&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;/usr/local/share 가 나온 경우&lt;/strong&gt; &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ cd /usr/local/share/
$ sudo chmod -R 755 zsh
$ sudo chown -R root:staff zsh&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;원인과 해결 방식에 대한 것이 궁금한 경우는 &lt;a href=&quot;https://www.wezm.net/technical/2008/09/zsh-cygwin-and-insecure-directories&quot;&gt;zsh, Cygwin and Insecure Direcotries&lt;/a&gt;를 참고하고, 좀 더 상세한 내용은 &lt;a href=&quot;http://zsh.sourceforge.net/Doc/Release/Completion-System.html&quot;&gt;zsh: 20 Completion System&lt;/a&gt;를 참고하면 된다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.zsh.org/mla/users/2012/msg00062.html&quot;&gt;Zsh.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Homebrew/homebrew/issues/7801#issuecomment-2187273&quot;&gt;Homebrew issues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>compaudit</category>
      <category>compinit</category>
      <category>insecure directories</category>
      <category>macos</category>
      <category>zsh</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/68</guid>
      <comments>https://ccambo.tistory.com/entry/MacOS-Terminal-%EC%97%90%EC%84%9C-zsh-compinit-insecure-directories-%EB%AC%B8%EA%B5%AC%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EA%B2%BD%EC%9A%B0-%EB%8C%80%EC%B2%98%ED%95%98%EA%B8%B0#entry68comment</comments>
      <pubDate>Thu, 21 Jan 2021 20:21:58 +0900</pubDate>
    </item>
    <item>
      <title>[MacOS,Git] ._, .DS_Store 등 숨김파일 정리 및 .gitignore 처리하기</title>
      <link>https://ccambo.tistory.com/entry/MacOSGit-DSStore-%EB%93%B1-%EC%88%A8%EA%B9%80%ED%8C%8C%EC%9D%BC-%EC%A0%95%EB%A6%AC-%EB%B0%8F-gitignore-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to remove temporary hidden files like .DS_Store / ._xxxx&lt;/h1&gt;
&lt;h2&gt;Remove &lt;code&gt;._xxxx&lt;/code&gt; files&lt;/h2&gt;
&lt;p&gt;MacOS에서 여러 작업을 하다보면 &lt;code&gt;._&lt;/code&gt; 으로 시작하는 파일들이 생성된 것을 확인할 수 있다. (일반적으로 숨김 파일들이라 보이지 않는다) 이런 파일은 다양한 작업 중에 생성되는데 일반적인 상황은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MacOS HDD/SDD에서 외장 HDD/SDD로 복사했을 때&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MacOS에서 압축했을 때&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;더 많은 상황이 있을 수 있지만 결국은 다른 플랫폼 (또는 다른 FileSystem)과 혼용할 경우에 아이콘을 생성하기 위해 파일을 가져오는 과정에서 메타정보 저장용으로 자동 생성된다.&lt;/p&gt;
&lt;p&gt;이런 파일의 생성과 제거는 다음과 같은 방법들이 존재한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;압축하는 경우라면 다른 플랫폼에서 이런 파일들이 유지되지 않도록 &lt;code&gt;COPYFILE_DISABLE&lt;/code&gt; 설정을 하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 압축 명령에서 사용해서 tar가 메타 정보를 추가하지 않도록 설정
$ COPYFILE_DISABLE=1 tar -cf xxxx.tar file*&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;매번 이렇게 처리하는 것이 귀찮다면 아예 shell에 설정하는 것도 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.zshrc 또는 ~/.bash_profile, ....
...
COPYFILE_DISABLE=1; export COPYFILE_DISABLE
...&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;이미 파일이 존재하는 경우라면 일괄 삭제할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ find ./ -name ._\* -delete&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Git에서 MacOS의 불필요 파일들 제외하기&lt;/h2&gt;
&lt;p&gt;위에서 설명한 것과 같이 자동 생성되는 파일들을 git 에서 제외하려면 &lt;code&gt;.gitignore&lt;/code&gt; 파일을 사용하면 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;git에 포함되지 않도록 제외하는 경우 &lt;pre&gt;&lt;code&gt;# .gitignore 파일
...
# OS Generated Files #
######################
**/.DS_Store
**/._*
...&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;이미 git에 포함된 경우&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# git 정보 검색 및 삭제
$ find . -name .DS_Store -print0 | xargs -0 git rm -f --cached --ignore-unmatch
# 변경 commit
$ git commit -m &amp;quot;&amp;lt;commit message&amp;gt;&amp;quot;
# Push to remote repository
$ git push orgin master&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;많은 프로젝트를 운영하는 경우라면 매번 gitignore를 처리하기 힘들 수 있으므로 이런 공통된 규칙을 전역으로 처리할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 전역으로 사용할 .gitignore 파일 생성
$ echo .DS_Store &amp;gt;&amp;gt; ~/.gitignore_global

# git에서 사용할 수 있도록 설정
$ git config --global core.excludesfile ~/.gitignore_global&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;단, 이 방법이 올바른 방법인지를 검증을 해 본 후에 판단해야 할 것 같다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://superuser.com/questions/61185/why-do-i-get-files-like-foo-in-my-tarball-on-os-x&quot;&gt;Why do i get files like foo in my tarball on OSX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apple.stackexchange.com/questions/14980/why-are-dot-underscore-files-created-and-how-can-i-avoid-them&quot;&gt;Why are dot underscore files created and how can i avoid them&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>.DS_Store</category>
      <category>.gitignore</category>
      <category>._</category>
      <category>dot underscore files</category>
      <category>GIT</category>
      <category>macos</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/67</guid>
      <comments>https://ccambo.tistory.com/entry/MacOSGit-DSStore-%EB%93%B1-%EC%88%A8%EA%B9%80%ED%8C%8C%EC%9D%BC-%EC%A0%95%EB%A6%AC-%EB%B0%8F-gitignore-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0#entry67comment</comments>
      <pubDate>Thu, 21 Jan 2021 19:52:03 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes-Troubleshooting] 삭제되지 않는 Namespace 강제로 삭제하기</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Troubleshooting-%EC%82%AD%EC%A0%9C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-Namespace-%EA%B0%95%EC%A0%9C%EB%A1%9C-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to force deletion of a namespace&lt;/h1&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;Argo Project의 Argo Events를 테스트해 보기 위해서 여러 가지 작업을 하던 중 제대로 처리가 되지 않아서 다시 시작할 겸 Namespace를 삭제해서 소속된 리소스들을 모두 삭제했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://drive.google.com/uc?export=view&amp;id=1xpTaSRfI7ycKqYmU17PPS3H5RTidL9Z_&quot; alt=&quot;Kubernetes에서 Namespace가 삭제되지 않는 문제&quot;&gt;&lt;/p&gt;
&lt;p&gt;그런데 위의 그림처럼 Argo Events Namespace가 삭제되지 않고 &lt;code&gt;Terminating&lt;/code&gt; 상태로 계속 유지되는 문제가 발생했다.&lt;/p&gt;
&lt;h2&gt;문제 원인&lt;/h2&gt;
&lt;p&gt;정상적으로 삭제될 수 있는 시간을 지나서도 &lt;code&gt;Terminating&lt;/code&gt; 상태로 남아있어서 원인에 대한 부분을 찾다가 Namespace의 다른 모든 Resource들은 삭제되었는데 (정확하게는 Dashboard에도 조회가 되지 않고, kubectl get 명령으로도 보이지 않는) Namespace만 저런 상태라서 Namespace에 대한 정보를 출력해 보았다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Resource 정보 출력
$ kubectl get namespace argo-events -o yaml

apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: &amp;quot;2021-01-13T10:40:07Z&amp;quot;
  deletionTimestamp: &amp;quot;2021-01-15T09:31:30Z&amp;quot;
...
spec:
  finalizers:
  - kubernetes              # Namespace에 대한 Finalizers
status:
  conditions:
  - lastTransitionTime: &amp;quot;2021-01-15T09:31:36Z&amp;quot;
    message: All resources successfully discovered
    reason: ResourcesDiscovered
    status: &amp;quot;False&amp;quot;
    type: NamespaceDeletionDiscoveryFailure
  - lastTransitionTime: &amp;quot;2021-01-15T09:31:36Z&amp;quot;
    message: All legacy kube types successfully parsed
    reason: ParsedGroupVersions
    status: &amp;quot;False&amp;quot;
    type: NamespaceDeletionGroupVersionParsingFailure
  - lastTransitionTime: &amp;quot;2021-01-15T09:31:36Z&amp;quot;
    message: All content successfully deleted, may be waiting on finalization
    reason: ContentDeleted
    status: &amp;quot;False&amp;quot;
    type: NamespaceDeletionContentFailure
  - lastTransitionTime: &amp;quot;2021-01-15T09:31:36Z&amp;quot;
    message: &amp;#39;Some resources are remaining: eventbus.argoproj.io has 1 resource instances&amp;#39;    # 일부 리소스가 남아 있다는 상태
    reason: SomeResourcesRemain
    status: &amp;quot;True&amp;quot;
    type: NamespaceContentRemaining
  - lastTransitionTime: &amp;quot;2021-01-15T09:31:36Z&amp;quot;
    message: &amp;#39;Some content in the namespace has finalizers remaining: eventbus-controller
      in 1 resource instances&amp;#39;                                                                # 일부 리소스의 Finalizer가 남아 있다는 상태
    reason: SomeFinalizersRemain
    status: &amp;quot;True&amp;quot;
    type: NamespaceFinalizersRemaining
  phase: Terminating&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 결과에서 &lt;code&gt;status&lt;/code&gt; 부분을 보면 2 가지 문제가 존재하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eventbus.argoproj.io&lt;/code&gt; 에 하나의 리소스 인스턴스가 남아 있는 상태&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eventbus-controller&lt;/code&gt; 의 &lt;code&gt;finalizer&lt;/code&gt;가 남아 있는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;유추해 보면 리소스 자체는 삭제가 되었지만 Finalizer가 제대로 처리되지 못해서 발생하는 상태로 보인다. 주로 이와 같은 상황은 아래와 같이 두 가지로 판단할 수 있을 것 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom Finalizer가 정리되지 않는 경우&lt;/strong&gt;이런 경우라면 finalizer와 연관된 Controller가 무엇인지를 확인하고 수정해야 한다.&lt;/li&gt;
&lt;li&gt;Namespace의 Spec (&lt;code&gt;.spec.finalizers&lt;/code&gt;) 내용에 &lt;code&gt;kubernetes&lt;/code&gt; 이외의 다른 요소가 존재하는 경우는 주로 CRD (Custom Resource Definition)에 따른 Extension Cotroller가 정리되지 않는 상태로 볼 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes Finalizer가 정리되지 않는 경우&lt;/strong&gt;확인 결과 모든 리소스가 삭제된 상태라면 &lt;code&gt;admission webhook&lt;/code&gt;이나 &lt;code&gt;extension api server&lt;/code&gt;에서 오류가 발생했을 수 있음을 의미한다. Namespace가 삭제될 때 각 리소스를 삭제하기 위해서 보내는 요청은 &lt;code&gt;delete&lt;/code&gt;가 아니라 &lt;code&gt;delete-collection&lt;/code&gt; 이므로 이 요청이 제대로 처리되었는지를 확인해야 한다.&lt;/li&gt;
&lt;li&gt;Namespace의 Spec (&lt;code&gt;.spec.finalizers&lt;/code&gt;) 내용에 &lt;code&gt;kubernetes&lt;/code&gt;만 존재하는 경우로 기본 Finalizer이므로 Namespace내의 모든 리소스가 삭제되면 마지막으로 처리가 되는 것인데 진행되지 않고 있다는 것은 실제 모든 리소스가 삭제되었는지 확인해 봐야하는 것을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Namespace의 &lt;code&gt;.spec.finalizers&lt;/code&gt; 는 Sub resource기 때문에 &lt;code&gt;kubectl edit&lt;/code&gt;, &lt;code&gt;kubectl update&lt;/code&gt; 등의 명령으로 수정되지 않는다. 따라서 curl 또는 Postman 등으로 직접 API를 호출해서 처리해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;해당 오류를 찾아서 해결하면 되지만 Namespace를 삭제하는 과정에서 이런 문제가 발생된 것이기 때문에 이미 Namespace의 리소스들은 삭제 처리가 되어 정상적인 방법으로 처리할 수 없다. 따라서 이 상태에는 Namespace를 기준으로 강제 삭제를 해야 한다.&lt;/p&gt;
&lt;h2&gt;해결 방법&lt;/h2&gt;
&lt;p&gt;지금 발생한 상황은 위에서 설명한 두 가지 경우중 &lt;code&gt;Kubernetes Finalizer가 정리되지 않는 경우&lt;/code&gt;에 해당하기 때문에 모든 리소스가 삭제되었는지를 먼저 확인해 보도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get all&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 명령은 &lt;code&gt;Category All&lt;/code&gt;의 의미기 때문에 실제 모든 리소스를 보여주는 것이 아니다. 따라서 아래의 명령으로 실제 &lt;code&gt;api-resourrce&lt;/code&gt;에 namespace로 한정된 리소스들의 이름을 조회해 보아야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl api-resources --namespaced=true -o name
...
eventbus.argoproj.io           # argo-events namespace 내의 리소스
eventsources.argoproj.io       # argo-events namespace 내의 리소스
sensors.argoproj.io            # argo-events namespace 내의 리소스
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 명령으로 모든 리소스들의 이름을 확인해 본 결과 위의 결과와 같이 이미 삭제되었어야 할 리소스 이름이 남아 있었다. 이들 리소슥가 삭제되지 못한 이유를 확인해야 한다. 대부분은 &lt;code&gt;metadata.finalizer&lt;/code&gt;에 문제의 원인이 있을 가능성이 높다.&lt;/p&gt;
&lt;p&gt;지금까지의 판단으로는 CRD를 사용했을 때 이런 문제가 많이 발생했던 것으로 유추되므로 처리하기 이전에 아래와 같이 CRD가 존재한다면 Finalizer를 미리 제거해도 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# CRD 확인
$ kubectl get crd -A

# CRD의 Finalizer 제거
$ kubectl patch crd &amp;lt;crd name&amp;gt; -p &amp;#39;{&amp;quot;metadata&amp;quot;:{&amp;quot;finalizers&amp;quot;: []}}&amp;#39; --type=merge&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위에서 설명한 것과 같이 현재는 개별 문제가 발생된 리소스에 접근할 수 있는 방법이 없기 때문에 (방법을 못 찾았을 수도 있다) Namespace를 강제 삭제하도록 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;삭제되지 않는 Namespace 정보를 JSON 형식으로 저장한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$ kubectl get namespace argo-events -o json &amp;gt; argo-events-namespace-for-delete.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;저장된 Namespace 정보에서 &lt;code&gt;Finalizers&lt;/code&gt; 부분의 &lt;code&gt;- kuberntes&lt;/code&gt;를 제거한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;# 저장된 내용 ... spec: finalizers: - kubernetes ... # 위의 내용에서 Finalizers로 지정된 kubernetes 삭제 ... spec: finalizers: ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes API를 직접 호출해서 Finalize 처리 진행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;# kubectl proxy를 이용해서 저장된 인증 토큰을 사용 $ kubectl proxy # 다른 터미널을 열어서 API 호출 $ curl --insecure -k -H &amp;quot;Content-Type: application/json&amp;quot; -X PUT --data-binary @argo-events-namespace-for-delete.json http://localhost:8001/api/v1/namespaces/argo-events/finalize&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Namespace가 삭제되지 못하고 있는 원인인 Finalize를 직접 API 서버를 호출해서 처리하면 Namespace를 삭제할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이 방법으로 삭제되지 않고 남아있는 Namespace는 삭제를 할 수 있지만 정말 해당 리소스들이 깨긋하게 삭제되었는지에 대한 검증을 할 수는 없었다. 이에 대한 검증이나 추가적인 문제들이 있는지는 향후 검토가 필요하다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Update at :  2021-01-19&lt;br&gt;Namespace가 삭제된 후에 다시 한번 확인해 보면 몇 가지를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl api-resource --namespaced
NAME                        SHORTNAMES   APIGROUP                    NAMESPACED   KIND
...
cronworkflows               cwf,cronwf   argoproj.io                 true         CronWorkflow
sensors                     sn           argoproj.io                 true         Sensor
workfloweventbindings       wfeb         argoproj.io                 true         WorkflowEventBinding
workflows                   wf           argoproj.io                 true         Workflow
workflowtemplates           wftmpl       argoproj.io                 true         WorkflowTemplate
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 Namespace가 삭제되었지만 남아있는 리소스들을 확인할 수 있다. 따라서 이 리소스들을 삭제 해 줘야 한다. 그렇지 않으면 동일한 Namespace를 생성했을 때 오 동작의 원인이 될 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>Delete</category>
      <category>Force</category>
      <category>Kubernetes</category>
      <category>namespace</category>
      <category>terrminating</category>
      <category>강제 삭제</category>
      <category>네임스페이스</category>
      <category>쿠버네티스</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/66</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Troubleshooting-%EC%82%AD%EC%A0%9C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-Namespace-%EA%B0%95%EC%A0%9C%EB%A1%9C-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0#entry66comment</comments>
      <pubDate>Sun, 17 Jan 2021 02:04:25 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - Operator] KUDO를 활용한 Galera Operator 단계별로 적용하기 - PART 3: Bootstrap 삭제 및 서비스 중단 없이 Scale Up/Down 처리</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-3-Bootstrap-%EC%82%AD%EC%A0%9C-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A4%91%EB%8B%A8-%EC%97%86%EC%9D%B4-Scale-UpDown-%EC%B2%98%EB%A6%AC</link>
      <description>&lt;h1&gt;How to building real world sample step by step - Part 3&lt;/h1&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;게시글 참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이 게시글은 KUDO Blog 샘플을 기준으로 테스트하면서 발생한 문제점들을 처리하고 동작을 검증하면서 정리한 내용입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&quot;&gt;PART 1&lt;/a&gt;에서는 부트스트랩 노드 구성&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-2-Nodes-%EC%B0%B8%EC%97%AC%EC%99%80-%EC%99%B8%EB%B6%80-%EC%A0%91%EC%86%8D%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%83%9D%EC%84%B1-%EB%B0%8F-StatefulSet-%EA%B5%AC%EC%84%B1&quot;&gt;PART 2&lt;/a&gt;에서는 클러스터에 노드들이 참여할 떄 사용할 서비스와 설정등을 구성하고 StatefulSet을 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 문서는 KUDO Blog 샘플을 기준으로 테스트하면서 발생한 문제점들을 해결하고 동작을 검증하면서 정리한 내용으로 구성된 Galera Cluster의 사용하지 않는 bootstrap 정보를 제거하고, 외부 연결을 위한 서비스 생성 및 서비스의 중단없이 Scale Up/Down 할 수 있도록 나머지 부분을 적용해 본다. 이 과정까지 완료되면 프로덕션 환경에 적용할 수 있는 정도가 된다.&lt;/p&gt;
&lt;h2&gt;Cleanup bootstrap node&lt;/h2&gt;
&lt;p&gt;PART 2 에서 모든 노드들을 클러스터에 참여시켰기 때문에 더 이상은 부트스트랩 노드가 필요하지 않다.&lt;/p&gt;
&lt;p&gt;따라서 &lt;code&gt;operator.yaml&lt;/code&gt;에 부트스트랩 노드와 관련된 리소스를 제거하는 Step과 Task를 추가하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;...
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          ...
          - name: cleanup    # 추가
            tasks:
              - bootstrap_cleanup
tasks:
  ...
  - name: bootstrap_cleanup      # 추가
    kind: Delete
    spec:
      resources:
        - bootstrap_deploy.yaml
        - bootstrap_service.yaml
        - bootstrap_config.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;추가된 &lt;code&gt;bootstrap_cleanup&lt;/code&gt;은 삭제 작업으로 PART 1에서 만들었던 bootstrap 관련 리소스들이 대상이며 이 작업이 실행되면 resources로 지정된 *.yaml 파일에 정의된 부트스트랩 리소스들 (ConfigMap, Service, Deploy)이 모두 삭제된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그러나 실제 Galera 노드들에서 사용하는 ConfigMap에 부트스트랩 노드 정보 (wsrep_cluster_address = gcomm://{{ .Name }}-bootstrap-svc)&lt;/code&gt;가 남아 있기 떄문에 Galera 인스턴스가 누락된 노드를 중심으로 작동하므로 반드시 문제가 되는 것은 아닐지라도 완전성을 위해 관련된 정보도 삭제해 주는 것이 좋다.&lt;/p&gt;
&lt;p&gt;따라서 &lt;code&gt;opreator.yaml&lt;/code&gt;파일에 ConfigMap을 수정할 수있는 Task를 추가하고 Step에서 이 Task를 이용하는 것으로 아래와 같이 변경한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;...
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          ...
          - name: cleanup
            tasks:
              - bootstrap_cleanup
              - config      # 추가
tasks:
  ...
  - name: bootstrap_cleanup
    kind: Delete
    spec:
      resources:
        - bootstrap_deploy.yaml
        - bootstrap_service.yaml
        - bootstrap_config.yaml
  - name: config      # 추가
    kind: Apply
    spec:
      resources:
        - galera_config.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번에는 하나의 Step에서 두 개의 Task가 동작하도록 정의했다. deploy phase의 실행 전략을 &lt;code&gt;strategy: serial&lt;/code&gt;로 지정했으므로 두 개의 Task는 순차적으로 처리된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;여기서 주의할 것은 PART 2에서 사용했던 &lt;code&gt;galera_config.yaml&lt;/code&gt;을 다시 사용한다는 것이다. 즉, 노드를 설치할 때와 삭제할 때 설정 기능이 다른데 하나의 리소스를 재 사용하는 상황인 것이다. 이를 해결하기 위해서 템플릿에서 &lt;code&gt;.StepName&lt;/code&gt; 변수를 활용해서 어떤 Step에서 호출되었는지를 식별해서 처리하는 방식을 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;StepName 정보를 기준으로 내용을 구성&lt;/code&gt;할 수 있도록 템플릿 리소스인 &lt;code&gt;templates/galera_config.yaml&lt;/code&gt; 파일을 아래와 같이 수정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Name }}-nodeconfig
  namespace: {{ .Namespace }}
data:
  galera.cnf: |
    [galera]
    wsrep_on = ON
    wsrep_provider = /usr/lib/galera/libgalera_smm.so
    wsrep_sst_method = mariabackup
    {{ if eq .StepName &amp;quot;firstboot_config&amp;quot; -}}
    wsrep_cluster_address = gcomm://{{ .Name }}-bootstrap-svc,{{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{end}}
    {{ else -}}
    wsrep_cluster_address = gcomm://{{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{end}}
    {{ end -}}
    wsrep_sst_auth = &amp;quot;{{ .Params.SST_USER }}:{{ .Params.SST_PASSWORD }}&amp;quot;
    binlog_format = ROW
  innodb.cnf: |
    [innodb]
    innodb_autoinc_lock_mode = 2
    innodb_flush_log_at_trx_commit = 0
    innodb_buffer_pool_size = 122M&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wsrep_cluster_address&lt;/code&gt; 정보를 지정할 때 템플릿에 조건부 처리를 지정했다. 즉, &lt;code&gt;firstboot_config&lt;/code&gt; Step일 경우는 부트스트랩 구성이 적용되고, 그 외는 클러스터에 참여할 노드 정보들만 구성되는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{{ if eq .StepName &amp;quot;firstboot_config&amp;quot; -}}
  wsrep_cluster_address = gcomm://{{ .Name }}-bootstrap-svc,{{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{end}}
{{ else -}}
  wsrep_cluster_address = gcomm://{{ $.Name }}-galera-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-galera-{{ $v }}.{{ $.Name }}-hs{{end}}
{{ end -}}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;if&lt;/code&gt;, &lt;code&gt;else&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt; 구문은 &lt;code&gt;-}}&lt;/code&gt; 으로 종료시키고 있다. 이는 템플릿 엔진에게 해당 라인을 빈줄로 뇌두지 말고 삭제하도록 지정하는 것이다.&lt;/p&gt;
&lt;p&gt;위와 같이 템플릿 내에 조건문을 사용해서 다른 여러 Task들에서 재 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;이제 Pod가 재 시작되면 post-bootstrap cluster에 대한 올바른 구성을 가지고 다시 백업을 시작할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo install ./&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo plan status --instance=galera-operator-instance&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 ConfigMap 및 Pod 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl describe configmap galera-operator-instance-nodeconfig

Name:         galera-operator-instance-nodeconfig
Namespace:    default
Labels:       heritage=kudo
              kudo.dev/instance=galera-operator-instance
              kudo.dev/operator=galera-operator
Annotations:  kudo.dev/last-applied-configuration:
                {&amp;quot;kind&amp;quot;:&amp;quot;ConfigMap&amp;quot;,&amp;quot;apiVersion&amp;quot;:&amp;quot;v1&amp;quot;,&amp;quot;metadata&amp;quot;:{&amp;quot;name&amp;quot;:&amp;quot;galera-operator-instance-nodeconfig&amp;quot;,&amp;quot;namespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;creationTimestamp&amp;quot;:n...
              kudo.dev/last-plan-execution-uid: 8cd36d4b-aeb5-473e-8997-1fad6090722a
              kudo.dev/phase: deploy
              kudo.dev/plan: deploy
              kudo.dev/step: cleanup

Data
====
galera.cnf:
----
[galera]
wsrep_on = ON
wsrep_provider = /usr/lib/galera/libgalera_smm.so
wsrep_sst_method = mariabackup
wsrep_cluster_address = gcomm://galera-operator-instance-galera-0.galera-operator-instance-hs,galera-operator-instance-galera-1.galera-operator-instance-hs,galera-operator-instance-galera-2.galera-operator-instance-hs
wsrep_sst_auth = &amp;quot;root:admin&amp;quot;
binlog_format = ROW

innodb.cnf:
----
[innodb]
innodb_autoinc_lock_mode = 2
innodb_flush_log_at_trx_commit = 0
innodb_buffer_pool_size = 122M

Events:  &amp;lt;none&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정상적으로 ConfigMap이 조건에 따라서 &lt;code&gt;wsrep_cluster_address&lt;/code&gt; 설정 값이 부트스트랩 정보없이 노드들의 정보만으로 구성된 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get pods

NAME                                         READY   STATUS    RESTARTS   AGE
galera-operator-instance-galera-operator-0   1/1     Running   0          3m28s
galera-operator-instance-galera-operator-1   1/1     Running   0          3m3s
galera-operator-instance-galera-operator-2   1/1     Running   0          2m35s
nfs-client-provisioner-b84668c6d-zxt8d       1/1     Running   0          91m&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;부트스트랩 노드가 제거되고 나머지 노드들만 존재하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Add service for connect from client&lt;/h2&gt;
&lt;p&gt;이제 클라이언트가 Galera Cluster에 연결할 수 있는 방법이 필요하다. Galera Cluster는 다중 마스터 구조이기 때문에 어디로 연결되더라도 안전하게 읽고 쓸 수 있다. 서비스는 어떤 의존성도 갖지않기 때문에 deploy plan의 어떤 단계에서 생성하던지 상관이 없지만 이미 클러스터 내부의 서비스를 생성한 단계가 있으므로 여기에 추가하도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;operator.yaml&lt;/code&gt; 파일의 이미 존재하는 &lt;code&gt;cluster_service&lt;/code&gt; Task에 아래와 같이 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;...
  - name: cluster_services
    kind: Apply
    spec:
      resources:
        - hs-service.yaml
        - cs-service.yaml   # 추가
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 사용한 서비스 리소스인 &lt;code&gt;templates/cs-service.yaml&lt;/code&gt; 파일을 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Service
metadata:
  name: {{ .Name }}-cs
  namespace: {{ .Namespace }}
  labels:
    app: galera
    galera: {{ .Name }} 
spec:
  ports:
    - port: {{ .Params.MYSQL_PORT }}
      name: mysql
  selector:
    app: galera
    instance: {{ .Name }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;이 서비스는 mySQL 포트만 필요하고 클라이언트가 Galera Clustr에 연결할 수 있도록 load balancing된 ClusterIP를 제공&lt;/code&gt;한다.&lt;/p&gt;
&lt;p&gt;지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo install ./&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo plan status --instance=galera-operator-instance&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Service 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get services

NAME                          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                               AGE
galera-operator-instance-cs   ClusterIP   10.104.170.201   &amp;lt;none&amp;gt;        3306/TCP                              5m3s
galera-operator-instance-hs   ClusterIP   None             &amp;lt;none&amp;gt;        3306/TCP,4444/TCP,4567/TCP,4568/TCP   5m3s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행 중인 클러스터에 모든 서비스가 생성된 것을 확인할 수 있다. 서비스의 상세 정보를 통해서 정상적으로 생성되었는지 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl describe service galera-operator-instance-cs

Name:              galera-operator-instance-cs
Namespace:         default
Labels:            app=galera
                  galera=galera-operator-instance
                  heritage=kudo
                  kudo.dev/instance=galera-operator-instance
                  kudo.dev/operator=galera-operator
Annotations:       kudo.dev/last-applied-configuration:
                    {&amp;quot;kind&amp;quot;:&amp;quot;Service&amp;quot;,&amp;quot;apiVersion&amp;quot;:&amp;quot;v1&amp;quot;,&amp;quot;metadata&amp;quot;:{&amp;quot;name&amp;quot;:&amp;quot;galera-operator-instance-cs&amp;quot;,&amp;quot;namespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;creationTimestamp&amp;quot;:null,&amp;quot;label...
                  kudo.dev/last-plan-execution-uid: f5b71f8e-b9ef-49b0-81d1-8f82315e63d4
                  kudo.dev/phase: deploy
                  kudo.dev/plan: deploy
                  kudo.dev/step: cluster_services
Selector:          app=galera,instance=galera-operator-instance
Type:              ClusterIP
IP:                10.104.170.201
Port:              mysql  3306/TCP
TargetPort:        3306/TCP
Endpoints:         10.244.247.12:3306,10.244.84.132:3306
Session Affinity:  None
Events:
  Type     Reason                        Age    From                       Message
  ----     ------                        ----   ----                       -------
  Warning  FailedToUpdateEndpointSlices  4m37s  endpoint-slice-controller  Error updating Endpoint Slices for Service default/galera-operator-instance-cs: failed to update galera-operator-instance-cs-brmb9 EndpointSlice for Service default/galera-operator-instance-cs: Operation cannot be fulfilled on endpointslices.discovery.k8s.io &amp;quot;galera-operator-instance-cs-brmb9&amp;quot;: the object has been modified; please apply your changes to the latest version and try again&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;클라이언트 연결 확인&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;생성한 서비스를 통해서 클라이언트가 Galera Cluster에 접속할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl run mysql-client --image=mysql:5.7 -it --rm --restart=Never -- mysql -u root -h galera-operator-instance-cs -p&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;진행 중에 아래와 같은 메시지가 보이면 &lt;code&gt;params.yaml&lt;/code&gt;에 정의했던 password를 입력하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;If you don&amp;#39;t see a command prompt, try pressing enter.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;mysql&amp;gt;&lt;/code&gt; 프롬프트가 보이면 정상적으로 연결이 된 것이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Scale up/down without service interruption&lt;/h2&gt;
&lt;p&gt;KUDO는 파라미터의 값을 변경함으로써 연계된 Plan 이 구동된다. 파라미터로 정의한 &lt;code&gt;NODE_COUNT&lt;/code&gt;를 변경하면 deploy plan이 구동될 것이기 때문에 ConfigMap을 변경하고 클러스터에 새로운 노드 수를 반영하기 위해서 StatefulSet이 재 시작될 필요가 있다.&lt;/p&gt;
&lt;p&gt;이런 작업을 반영하기 위해 &lt;code&gt;operator.yaml&lt;/code&gt; 파일에 새로운 plan을 아래와 같이 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;...
plans: 
  deploy:
    ...
  node_update:      # 추가
    strategy: serial
    phases:
      - name: deploy
        stratege: serial
        steps:
          - name: config
            tasks:
              - config
          - name: stateful
            tasks:
              - statefulset
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 Plan은 기존의 Task를 재 사용하는 것이기 때문에 별도의 Task 추가는 필요하지 않고 두 개의 Step이 순차적으로 실행되도록 정의한 것이다.&lt;/p&gt;
&lt;p&gt;이제 &lt;code&gt;NODE_COUNT&lt;/code&gt; 파라미터가 변경되면 위에 정의한 새로운 Plan이 구동될 수 있도록 &lt;code&gt;params.yaml&lt;/code&gt;의 파라미터 정보를 아래와 같이 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
parameters:
  ...
  - name: NODE_COUNT
    description: &amp;quot;Number of nodes to create in the cluster&amp;quot;
    default: &amp;quot;3&amp;quot;
    trigger: node_update      # 추가
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PART 2 에서는 &lt;code&gt;trigger&lt;/code&gt; 정보가 없었으며 이런 경우는 기본 값으로 &lt;code&gt;deploy&lt;/code&gt; plan이 호출되게 된다. trigger를 지정해서 &lt;code&gt;node_update&lt;/code&gt; plan을 호출하도록 한 것이다.&lt;/p&gt;
&lt;p&gt;지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo install ./&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo plan status --instance=galera-operator-instance&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NODE_COUNT 변경 검증&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo update --instance galera-operator-instance -p NODE_COUNT=3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;파라미터를 통한 변경이 적용되었기 때문에 변경에 따른 처리를 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo plan status --instance=galera-operator-instance&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pod가 재 시작되는 시간이 있기 때문에 &lt;code&gt;IN_PROGRESS&lt;/code&gt; 상태로 나타나며, 이 작업이 진행 중인 동안에 &lt;code&gt;kubectl get pods&lt;/code&gt; 명령을 수행하면 추가된 수 만큼 노드가 배포가 되고, 이전에 동작하던 노드들은 한번에 하나씩 재 시작되는 것을 볼 수 있다. 반대로 &lt;code&gt;NODE_COUNT&lt;/code&gt; 파라미터의 수를 줄이면 동작하던 노드들이 제거되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Process syncronization before shuting down nodes&lt;/h2&gt;
&lt;p&gt;Scale up은 상관이 없지만 down의 경우는 노드가 제거될 때 Galera Cluster의 노드가 동기화되지 않으면 클러스터의 상태에 문제가 발생할 수 있으므로 노드가 종료되기 전에 동기화를 해야할 필요가 있다.&lt;/p&gt;
&lt;p&gt;이를 위해서 상태를 검증하는 스크립트를 추가하고 StatefulSet의 spec에 &lt;a href=&quot;https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/&quot;&gt;preStop check&lt;/a&gt;를 수정해서 처리를 수행하도록 &lt;code&gt;operator.yaml&lt;/code&gt; 파일의 &lt;code&gt;deploy plan의 Statefulset Step 이전&lt;/code&gt;에 적용될 수 있도록 Step과 Task를 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;...
plans:
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          ...
          - name: node_scripts    # 추가
            tasks:
              - node_scripts
          ...
          - name: statefulset
          ...
  ...
tasks:
  ...
  - name: node_scripts    # 추가
    kind: Apply
    spec:
      resources:
        - node_scripts.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;상태를 검증하는 스크립트를 위한 ConfigMap 리소스인 &lt;code&gt;templates/node_scripts.yaml&lt;/code&gt; 파일을 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Name }}-node-scripts
  namespace: {{ .Namespace }}
  wait-for-sync.sh: |
    until mysql -u root -p{{ .Params.MYSQL_ROOT_PASSWORD }} -e &amp;quot;SHOW GLOBAL STATUS LIKE &amp;#39;wsrep_local_state_comment&amp;#39;;&amp;quot; | grep -q Synced 
    do
        echo &amp;quot;Waiting for sync&amp;quot;
        sleep 5
    done&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 이 스크립트가 StatefulSet에 정상적으로 마운트될 수 있도록 &lt;code&gt;templates/statefulset.yaml&lt;/code&gt;에 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;        volumeMounts:
        - name: {{ .Name }}-config
          mountPath: /etc/mysql/conf.d
        - name: {{ .Name }}-datadir
          mountPath: /var/lib/mysql
        - name: {{ .Name }}-node-scripts    # 추가
          mountPath: /etc/galera/wait-for-sync.sh
          subPath: wait-for-sync.sh
      volumes:
      - name: {{ .Name }}-config
        configMap:
          name: {{ .Name }}-nodeconfig
          items:
            - key: galera.cnf
              path: galera.cnf
            - key: innodb.cnf
              path: innodb.cnf
      - name: {{ .Name }}-node-scripts    # 추가
        configMap:
          name: {{ .Name }}-node-scripts
          defaultMode: 0755&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ConfigMap에서 subPath로 스크립트을 직접 마운트했다. subPath를 사용할 때 &lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/&quot;&gt;제한들&lt;/a&gt;이 있지만 여기서는 큰 의미가 없다.&lt;/p&gt;
&lt;p&gt;또한 Kubernetes 클러스터를 구성하는 노드들의 장애로 부터 보호하기 위해서 분리된 노드들로 배포되도록 스케줄되어 있는지에 대한 확인이 필요하다. 이 작업은 AntiAffinity (반 선호도) 규칙을 사용해서 처리가 가능하며 테스트를 위해서 이 규칙을 비활성화 시킬 수 있다. &lt;code&gt;templates/statefulset.yaml&lt;/code&gt; 파일에 아래와 같이 조건 섹션을 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;...
spec:
  {{ if eq .Params.ANTI_AFFINITY &amp;quot;true&amp;quot; }}
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: &amp;quot;app&amp;quot;
                operator: In
                values:
                - galera
              - key: &amp;quot;instance&amp;quot;
                operator: In
                values:
                - {{ .Name }}
          topologyKey: &amp;quot;kubernetes.io/hostname&amp;quot;
  {{ end }}
...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;상기 예제의 Anti-Affinity는 반 선호도에 대한 Pod Selector 개념으로 &lt;code&gt;topologyKey를 kubernetes.io/hostname으로 설정해서 Pod가 존재하는 동일한 Host가 아닌 다른 곳으로 배치&lt;/code&gt;하나는 의미가 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;사용된 &lt;code&gt;ANTI_AFFINITY&lt;/code&gt; 파라미터를 &lt;code&gt;params.yaml&lt;/code&gt; 파일에 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;...
  - name: ANTI_AFFINITY
    description: &amp;quot;Enforce pod anit-affinity&amp;quot;
    default: False
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;지금까지의 작업으로 Operator는 Node 선호도를 관리하고 안전하게 Scale up/down이 가능하고, 외부의 클라이언트가 접근할 수 있도록 구성되었다.&lt;/p&gt;
&lt;p&gt;이 시점에서 몇 가지 벤치마크와 클러스터를 확장하고 축소하는 동시에 모든 것이 정상적으로 작동하는지를 확인해야 한다. &lt;a href=&quot;https://www.percona.com/blog/2020/02/07/how-to-measure-mysql-performance-in-kubernetes-with-sysbench/&quot;&gt;how to measure mysql performance in kubernetes with sysbench&lt;/a&gt;를 이용해서 확인이 가능하다.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;지금까지 Galera Cluster를 구성하고 운영하는 Operator를 단계별로 확인해 보았으며, 그 과정을 통해서 KUDO에서 제공하는 내장 변수들과 Sprig 템플릿 함수를 이용해서 조건부 템플릿을 적용해 리소스 파일들을 재 사용할 수 있다는 것도 확인했다.&lt;/p&gt;
&lt;p&gt;향후에는 시간이 허락하고 관련된 작업을 진행하게 된다면 지금까지 확인한 설치관련 (Day 1 Operation) 뿐만 아니라 운영 중 (Day 2 Operation) 에 발생할 수 있는 상황들에 대한 부분을 추가로 검토해 볼 예정이다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/blog/blog-2020-08-05-building-your-first-operator-3.html&quot;&gt;Building your first KUDO Operator - Part 3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>galera</category>
      <category>Galera Cluster</category>
      <category>Kubernetes</category>
      <category>Kudo</category>
      <category>operator</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/65</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-3-Bootstrap-%EC%82%AD%EC%A0%9C-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A4%91%EB%8B%A8-%EC%97%86%EC%9D%B4-Scale-UpDown-%EC%B2%98%EB%A6%AC#entry65comment</comments>
      <pubDate>Tue, 5 Jan 2021 19:24:31 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - Operator] KUDO를 활용한 Galera Operator 단계별로 적용하기 - PART 2: Nodes 참여와 서비스 생성 및 StatefulSet 구성</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-2-Nodes-%EC%B0%B8%EC%97%AC%EC%99%80-%EC%99%B8%EB%B6%80-%EC%A0%91%EC%86%8D%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%83%9D%EC%84%B1-%EB%B0%8F-StatefulSet-%EA%B5%AC%EC%84%B1</link>
      <description>&lt;h1&gt;How to building real world sample step by step - Part 2&lt;/h1&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;게시글 참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이 게시글은 KUDO Blog 샘플을 기준으로 테스트하면서 발생한 문제점들을 처리하고 동작을 검증하면서 정리한 내용입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&quot;&gt;PART 1&lt;/a&gt;에서는 부트스트랩 노드 구성&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-3-Bootstrap-%EC%82%AD%EC%A0%9C-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A4%91%EB%8B%A8-%EC%97%86%EC%9D%B4-Scale-UpDown-%EC%B2%98%EB%A6%AC&quot;&gt;PART 3&lt;/a&gt;에서는 사용하지 않는 부트스트랩을 제거하고, 외부 접속을 위한 서비스를 생성하며, 안전한 Scale Up/Down 처리 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 문서는 KUDO Blog 샘플을 기준으로 테스트하면서 발생한 문제점들을 해결하고 동작을 검증하면서 정리한 내용으로 초기 구성된 Galera Cluster의 부트스트랩을 이용해서 클러스터에 노드들이 참여할 떄 사용할 서비스와 설정등을 구성하고 StatefulSet을 구성하는 방법을 정리한다.&lt;/p&gt;
&lt;h2&gt;Cluster에 참여하는 Node를 위한 초기 설정 구성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;operator.yaml&lt;/code&gt; 파일에 아래와 같이 &lt;code&gt;firstboot_config&lt;/code&gt;라는 Step과 Task를 추가하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: galera-operator
operatorVersion: 0.1.0
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          - name: bootstrap_config
            tasks:
              - bootstrap_config
          - name: bootstrap_service
            tasks:
              - bootstrap_service
          - name: bootstrap_deploy
            tasks:
              - bootstrap_deploy
          - name: firstboot_config    # 추가
            tasks:
              - firstboot_config
tasks:
  - name: bootstrap_config
    kind: Apply
    spec:
      resources:
        - bootstrap_config.yaml
  - name: bootstrap_service
    kind: Apply
    spec:
      resources:
        - bootstrap_service.yaml
  - name: bootstrap_deploy
    kind: Apply
    spec:
      resources:
        - bootstrap_deploy.yaml
  - name: firstboot_config      # 추가
    kind: Apply
    spec:
      resources:
        - galera_config.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;위의 예제에서는 deploy Phase에서 개별적인 여러 개의 Step들을 정의했고, 이 Step들은 KUDO에 의해서 순차적으로 실행된다.&lt;br&gt;작업한 몇 개의 Step들은 서로 결과를 공유하는 등의 의존성이 없고, 이론적으로는 하나의 Step내에 여러 개의 Task를 지정해서 처리하는 것도 가능하고 Phase 자체를 병렬로 처리하게 하는 것도 가능하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위의 firstboot_config Task에서 사용할 &lt;code&gt;templates/galera_config.yaml&lt;/code&gt; 파일을 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Name }}-nodeconfig
  namespace: {{ .Namespace }}
data:
  galera.cnf: |
    [galera]
    wsrep_on = ON
    wsrep_provider = /usr/lib/galera/libgalera_smm.so
    wsrep_sst_method = mariabackup
    wsrep_cluster_address = gcomm://{{ .Name }}-bootstrap-svc,{{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{end}}
    wsrep_sst_auth = &amp;quot;{{ .Params.SST_USER }}:{{ .Params.SST_PASSWORD }}&amp;quot;
    binlog_format = ROW
  innodb.cnf: |
    [innodb]
    innodb_autoinc_lock_mode = 2
    innodb_flush_log_at_trx_commit = 0
    innodb_buffer_pool_size = 122M&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;노드가 Galera Cluster에 참여할 때 처리되어야 하는 두 개의 설정을 포함하는 ConfigMap을 구성한 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;galera.cnf&lt;/code&gt;: Galera Cluster와 연동을 위한 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;innodb.cnf&lt;/code&gt;: Galera 사용할 시에 InnoDB Engine에 권장되는 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Galera Cluster에 추가될 노드는 부트스트랩 노드의 서비스 주소와 클러스터에 포함될 노드들의 주소 정보가 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wsrep_cluster_address&lt;/code&gt; 설정은 KUDO에서 지원하는 &lt;a href=&quot;http://masterminds.github.io/sprig/&quot;&gt;Sprig&lt;/a&gt; 템플릿 방식을 사용해서 다음과 같이 표현된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;wsrep_cluster_address = gcomm://{{ .Name }}-bootstrap-svc,{{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{end}}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;노드들은 StatefulSet이 될 것이기 때문에 파드의 이름을 &lt;code&gt;.Name-.OperatorName-number&lt;/code&gt; 형식이 될 것으로 유추할 수 있다. 여기서 .Name과 .OperatorName은 KUDO에서 생성된 변수이고 번호는 인스턴스화된 StatefulSet의 Replicas의 순서에 기반해서 Kubernetes가 할당한 값이 된다.&lt;/li&gt;
&lt;li&gt;노드들에 접속하기 위한 Headless 서비스를 만들고 유일함을 보장하기 위해 &lt;code&gt;.Name-hs&lt;/code&gt; 형식의 이름을 가질 것이다. 뒤에서 다시 언급할 것이기는 하지만 StatefulSet의 각 노드들에 대한 DNS 항목들은 예측 가능한 .Name-OperatorName-number.Name-hs 포맷의 이름이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위와 같은 가정을 기준으로 연결 문자열을 구성할 수 있다.&lt;/p&gt;
&lt;p&gt;우선 부트스트랩 노드의 서비스 주소를 추가하고 다음 연결 문자열을 위해 &amp;#39;,&amp;#39; 로 연계한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;gcomm://{{ .Name }}-bootstrap-svc,&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;노드는 최소한 1개 이상이 존재해야 하기 때문에 0 순번의 노드가 고정되어 있는다는 것을 알기 때문에 DNS 항목을 추가할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;gcomm://{{ .Name }}-bootstrap-svc, {{ $.Name }}-{{ $.OperatorName }}-0.{{ $.Name }}-hs&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Galera Cluster에 참여할 노드들을 구성하기 위한 &lt;code&gt;NODE_COUNT&lt;/code&gt; 파라미터를 &lt;code&gt;params.yaml&lt;/code&gt;파일에 아래와 같이 가장 마지막에 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
parameters:
  ...
  - name: NODE_COUNT
    description: &amp;quot;Number of nodes to create in the cluster&amp;quot;
    default: &amp;quot;3&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그 다음은 &lt;code&gt;params.yaml&lt;/code&gt; 에 정의된 NODE_COUNT 파라미터의 값을 참고해서 나머지 노드들을 처리할 수 있다. 이를 활용하는 Sprig 템플릿은 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }}, {{ $.Name }}-{{ $.OperatorName }}-{{ $v }}.{{ $.Name }}-hs{{ end }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 내용은 표준 Go 템플릿의 Range 함수를 사용해서 인덱스 ($i)와 값 ($v)을 제공하고 Sprig의 untilStep 함수를 사용해서 생성된 범위 값을 전달한다. 이를 통해서 1 부터 시작해서 NODE_COUNT 까지 1 단위로 증가한 정수 목록을 받아서 사용하게 된다. NODE_COUNT가 3이라면 정수 범위는 1, 2 가 된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;params.yaml&lt;/code&gt; 정의된 NODE_COUNT 파라미터는 문자열로 기본처리가 되므로 사용하기 전에 &lt;code&gt;(int .Params.NODE_COUNT)&lt;/code&gt;와 같이 int 형식으로 캐스팅해 줘야 한다.&lt;/li&gt;
&lt;li&gt;Ranage 함수가 호출되면 변수들이 함수 내로 Scope 조정이 되기 때문에 최 상위 컨텍스트를 참조하기 위해서는 &lt;code&gt;$&lt;/code&gt; 접두사를 이용해서 파라미터들에 접근해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;위의 템플릿 범위 함수를 통해서 &amp;#39;,&amp;#39;로 분리되는 형식의 클러스터의 모든 노드들에 대한 DNS 엔트리를 구성할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;복잡한 템플릿을 작업을 하는 것은 상당히 어려울 수 있기 때문에 테스트를 수행할 수 있는 go 코드를가지고 검증하는 것이 유용할 수 있다. 따라서 &lt;a href=&quot;https://github.com/mattj-io/template_test&quot;&gt;Template example&lt;/a&gt; 과 같이 템플릿을 stdout으로 출력하는 것과 같은 코드를 잘 활용하는 것이 좋다.&lt;/p&gt;
&lt;p&gt;구성된 KUDO Package를 &lt;code&gt;kubectl kudo package verify&lt;/code&gt; 명령을 통해서 논리적인 오류는 검증할 수 없겠지만 구문 오류 등을 확인할 수는 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;KUDO CLI의 Package 관련 명령을 통해서 새로운 파라미터들과 Task들이 올바르게 정의되었는지 등의 YAML 구성에 대한 검증도 가능하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Package Plans 구성 검증&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo package list plans ./    # Operator 상대 경로 또는 Package 명 지정 (여기서는 로컬 디렉터리를 사용하므로 ~/kudo_sample/galera-operator/operator 경로 사용)

plans
└── deploy (serial)
    └── [phase]  deploy (serial)
        ├── [step]  bootstrap_config
        │   └── bootstrap_config
        ├── [step]  bootstrap_service
        │   └── bootstrap_service
        ├── [step]  bootstrap_deploy
        │   └── bootstrap_deploy
        └── [step]  firstboot_config
            └── firstboot_config&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Package Tasks 구성 검증&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo package list tasks ./

Name                    Kind    Resources               
bootstrap_config        Apply   [bootstrap_config.yaml] 
bootstrap_service       Apply   [bootstrap_service.yaml]
bootstrap_deploy        Apply   [bootstrap_deploy.yaml] 
firstboot_config        Apply   [galera_config.yaml]    &lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Package Paramters 구성 검증&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo package list parameters ./

Name                    Default Required        Immutable
IST_PORT                4568    true            false    
MYSQL_PORT              3306    true            false    
MYSQL_ROOT_PASSWORD     admin   true            false    
REPLICATION_PORT        4567    true            false    
SST_PASSWORD            admin   true            false    
SST_PORT                4444    true            false    
SST_USER                root    true            false    &lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 검증을 통해서 올바르게 설정된 것이 확인 되었다면 지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo install ./&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo plan status --instance=galera-operator-instance&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 ConfigMap 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;새로운 Step과 Task인 &lt;code&gt;firstboot_config&lt;/code&gt;을 통해서 템플릿을 활용한 ConfigMap이 생성되었을 것이므로 템플릿이 제대로 동작했는지를 아래와 같이 확인해 본다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get configmaps

NAME                                  DATA   AGE
galera-operator-instance-bootstrap    1      27s
galera-operator-instance-nodeconfig   2      12s

$ kubectl describe configmap galera-operator-instance-nodeconfig

Name:         galera-operator-instance-nodeconfig
Namespace:    default
Labels:       heritage=kudo
              kudo.dev/instance=galera-operator-instance
              kudo.dev/operator=galera-operator
Annotations:  kudo.dev/last-applied-configuration:
                {&amp;quot;kind&amp;quot;:&amp;quot;ConfigMap&amp;quot;,&amp;quot;apiVersion&amp;quot;:&amp;quot;v1&amp;quot;,&amp;quot;metadata&amp;quot;:{&amp;quot;name&amp;quot;:&amp;quot;galera-operator-instance-nodeconfig&amp;quot;,&amp;quot;namespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;creationTimestamp&amp;quot;:n...
              kudo.dev/last-plan-execution-uid: 64db8c01-d078-404d-8344-1a6bd98733fe
              kudo.dev/phase: deploy
              kudo.dev/plan: deploy
              kudo.dev/step: firstboot_config

Data
====
innodb.cnf:
----
[innodb]
innodb_autoinc_lock_mode = 2
innodb_flush_log_at_trx_commit = 0
innodb_buffer_pool_size = 122M

galera.cnf:
----
[galera]
wsrep_on = ON
wsrep_provider = /usr/lib/galera/libgalera_smm.so
wsrep_sst_method = mariabackup
wsrep_cluster_address = gcomm://galera-operator-instance-bootstrap-svc,galera-operator-instance-galera-operator-0.galera-operator-instance-hs,galera-operator-instance-galera-operator-1.galera-operator-instance-hs,galera-operator-instance-galera-operator-2.galera-operator-instance-hs
wsrep_sst_auth = &amp;quot;root:admin&amp;quot;
binlog_format = ROW

Events:  &amp;lt;none&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ConfigMap이 클러스터에 배포가 되었고, &lt;code&gt;wsrep_cluster_address&lt;/code&gt; 정보를 통해서 &lt;code&gt;NODE_COUNT&lt;/code&gt; 수 만큼 올바른 항목들이 생성되어 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cluster에 참여하는 Node들이 연결할 Service 구성&lt;/h2&gt;
&lt;p&gt;클러스터에 참여할 노드들을 StatefulSet에 배포할 예정이므로 이들을 연결할 방법인 서비스를 구성해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;operator.yaml&lt;/code&gt; 파일에 Steps, Tasks, Resources 를 아래와 같이 추가하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: galera-operator
operatorVersion: 0.1.0
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          - name: bootstrap_config
            tasks:
              - bootstrap_config
          - name: bootstrap_service
            tasks:
              - bootstrap_service
          - name: bootstrap_deploy
            tasks:
              - bootstrap_deploy
          - name: firstboot_config
            tasks:
              - firstboot_config
          - name: cluster_services    # 추가
            tasks:
              - cluster_services
tasks:
  - name: bootstrap_config
    kind: Apply
    spec:
      resources:
        - bootstrap_config.yaml
  - name: bootstrap_service
    kind: Apply
    spec:
      resources:
        - bootstrap_service.yaml
  - name: bootstrap_deploy
    kind: Apply
    spec:
      resources:
        - bootstrap_deploy.yaml
  - name: firstboot_config
    kind: Apply
    spec:
      resources:
        - galera_config.yaml
  - name: cluster_services      # 추가
    kind: Apply
    spec:
      resources:
        - hs-service.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cluster_services&lt;/code&gt; Task에서 사용할 서비스 리소스인 &lt;code&gt;templates/hs-service.yaml&lt;/code&gt; 파일을 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Service
metadata:
  name: {{ .Name }}-hs
  namespace: {{ .Namespace }}
  labels:
    app: galera
    galera: {{ .Name }} 
spec:
  ports:
    - port: {{ .Params.MYSQL_PORT }}
      name: mysql
    - port: {{ .Params.SST_PORT }}
      name: sst
    - port: {{ .Params.REPLICATION_PORT }}
      name: replication
    - port: {{ .Params.IST_PORT }}
      name: ist
  clusterIP: None
  selector:
    app: galera
    instance: {{ .Name }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ClusterIP를 가지지 않는 Headless 서비스를 생성한다. &lt;code&gt;params.yaml&lt;/code&gt;에 정의된 Galera 구성에 필요한 모든 포트들을 설정하고 각 배포의 유일함을 보장하기 위해서 app와 instance라는 레이블을 선택하고 있다.&lt;/p&gt;
&lt;p&gt;Headless Service인 이유는 Load balancing 주소가 필요하지 않고 StatefulSet의 각 노드에 때한 DNS 항목만 필요하기 때문이다.&lt;/p&gt;
&lt;p&gt;지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo install ./&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo plan status --instance=galera-operator-instance&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Service 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get services

NAME                                     TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                               AGE
galera-operator-instance-bootstrap-svc   ClusterIP   None         &amp;lt;none&amp;gt;        3306/TCP,4444/TCP,4567/TCP,4568/TCP   25s
galera-operator-instance-hs              ClusterIP   None         &amp;lt;none&amp;gt;        3306/TCP,4444/TCP,4567/TCP,4568/TCP   9s
kubernetes                               ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP                               28d

$ kubectl describe service galera-operator-instance-hs

Name:              galera-operator-instance-hs
Namespace:         default
Labels:            app=galera
                  galera=galera-operator-instance
                  heritage=kudo
                  kudo.dev/instance=galera-operator-instance
                  kudo.dev/operator=galera-operator
Annotations:       kudo.dev/last-applied-configuration:
                    {&amp;quot;kind&amp;quot;:&amp;quot;Service&amp;quot;,&amp;quot;apiVersion&amp;quot;:&amp;quot;v1&amp;quot;,&amp;quot;metadata&amp;quot;:{&amp;quot;name&amp;quot;:&amp;quot;galera-operator-instance-hs&amp;quot;,&amp;quot;namespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;creationTimestamp&amp;quot;:null,&amp;quot;label...
                  kudo.dev/last-plan-execution-uid: 89dc7134-f4ae-4fee-9fbf-ecee27b5a311
                  kudo.dev/phase: deploy
                  kudo.dev/plan: deploy
                  kudo.dev/step: cluster_services
Selector:          app=galera,instance=galera-operator-instance
Type:              ClusterIP
IP:                None
Port:              mysql  3306/TCP
TargetPort:        3306/TCP
Endpoints:         &amp;lt;none&amp;gt;
Port:              sst  4444/TCP
TargetPort:        4444/TCP
Endpoints:         &amp;lt;none&amp;gt;
Port:              replication  4567/TCP
TargetPort:        4567/TCP
Endpoints:         &amp;lt;none&amp;gt;
Port:              ist  4568/TCP
TargetPort:        4568/TCP
Endpoints:         &amp;lt;none&amp;gt;
Session Affinity:  None
Events:            &amp;lt;none&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서비스가 클러스터에 생성되었고, 올바른 Selector를 사용하고 있고, 역시 노드가 배포되지 않았기 때문에 &lt;code&gt;Endpoints&lt;/code&gt;가 설정되지 않았다는 것을 확인할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cluster에 참여할 Node 구성을 위한 StatefulSet 설정&lt;/h2&gt;
&lt;p&gt;위에서 클러스터에 참여할 노드들을 위한 ConfigMap과 Service가 정의되었기 때문에 StatefulSet을 &lt;code&gt;operator.yaml&lt;/code&gt;에 Step과 Task로 설정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: galera-operator
operatorVersion: 0.1.0
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          - name: bootstrap_config
            tasks:
              - bootstrap_config
          - name: bootstrap_service
            tasks:
              - bootstrap_service
          - name: bootstrap_deploy
            tasks:
              - bootstrap_deploy
          - name: firstboot_config
            tasks:
              - firstboot_config
          - name: cluster_services
            tasks:
              - cluster_services
          - name: statefulset    # 추가
            tasks:
              - statefulset
tasks:
  - name: bootstrap_config
    kind: Apply
    spec:
      resources:
        - bootstrap_config.yaml
  - name: bootstrap_service
    kind: Apply
    spec:
      resources:
        - bootstrap_service.yaml
  - name: bootstrap_deploy
    kind: Apply
    spec:
      resources:
        - bootstrap_deploy.yaml
  - name: firstboot_config
    kind: Apply
    spec:
      resources:
        - galera_config.yaml
  - name: cluster_services
    kind: Apply
    spec:
      resources:
        - hs-service.yaml
  - name: statefulset      # 추가
    kind: Apply
    spec:
      resources:
        - statefulset.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;아래의 StatefulSet 설정은 실제 mariadb가 설치되어 동작하는 노드 컨테이너를 생성하는 것이기 때문에 Persistent Volume 구성이 필요하지만 샘플에서는 Kubernetes에 기본 생성되어 있는 &lt;code&gt;Default Storage Class&lt;/code&gt;를 이용하는 것으로 되어 있고 Persistent Volume에 대한 언급이 없다.&lt;/p&gt;
&lt;p&gt;Kubernetes Cluster를 구성하고 나면 (Test는 Openstack 베이스로 구성) Default Storage가 존재하지 않고, Galera Cluster에 여러 개의 Node들이 참여를 하고 각기 Persitentn Volume을 사용해야 하기 때문에 별도로 NFS (Network File System) 서버를 CentOS 8에 설치하고 Kubernetes에 Dynamic NFS Client Provisioner와 Storage Class를 구성한 것으로 적용했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;참고 문서&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/CentOS-NFS-CentOS-8%EC%97%90-NFS-%EC%84%A4%EC%A0%95-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot;&gt;CentOS 8에 NFS 서버 구성하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Storage-CentOS-8%EC%97%90-Dynamic-NFS-Client-Provisioner-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0&quot;&gt;Kubernetes에 NFS Client Provisioner 구성하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;이제 &lt;code&gt;statefulset&lt;/code&gt; Task에서 사용할 리소스인 &lt;code&gt;templates/statefulset.yaml&lt;/code&gt; 파일을 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ .Name }}-{{ .OperatorName }}
  namespace: {{ .Namespace }}
  labels:
    galera: {{ .OperatorName }}
    app: galera
    instance: {{ .Name }}
  annotations:
    reloader.kudo.dev/auto: &amp;quot;true&amp;quot;
spec:
  selector:
    matchLabels:
      app: galera
      galera: {{ .OperatorName }}
      instance: {{ .Name }}
  serviceName: {{ .Name }}-hs
  replicas: {{ .Params.NODE_COUNT }}
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: galera
        galera: {{ .OperatorName }}
        instance: {{ .Name }}
    spec:
      initContainers:
      # Stop the image bootstrap script from trying to set up single master
      - name: {{ .Name }}-init
        image: busybox:1.28
        command: [&amp;#39;sh&amp;#39;, &amp;#39;-c&amp;#39;, &amp;#39;set -x; if [ ! -d /datadir/mysql ]; then mkdir /datadir/mysql; fi&amp;#39;]
        volumeMounts:
          - name: {{ .Name }}-datadir
            mountPath: &amp;quot;/datadir&amp;quot;
      containers:
      - name: mariadb
        image: mariadb:latest
        args:
        - &amp;quot;--ignore_db_dirs=lost+found&amp;quot;
        env:
          # Use secret in real usage
        - name: MYSQL_ROOT_PASSWORD
          value: {{ .Params.MYSQL_ROOT_PASSWORD }}
        ports:
        - containerPort: {{ .Params.MYSQL_PORT }}
          name: mysql
        - containerPort: {{ .Params.SST_PORT }}
          name: sst
        - containerPort: {{ .Params.REPLICATION_PORT }}
          name: replication
        - containerPort: {{ .Params.IST_PORT }}
          name: ist
        livenessProbe:
          exec:
            command: [&amp;quot;mysqladmin&amp;quot;, &amp;quot;-p{{ .Params.MYSQL_ROOT_PASSWORD }}&amp;quot;, &amp;quot;ping&amp;quot;]
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
        readinessProbe:
          exec:
            # Check we can execute queries over TCP (skip-networking is off).
            command: [&amp;quot;mysql&amp;quot;, &amp;quot;-p{{ .Params.MYSQL_ROOT_PASSWORD }}&amp;quot;, &amp;quot;-h&amp;quot;, &amp;quot;127.0.0.1&amp;quot;, &amp;quot;-e&amp;quot;, &amp;quot;SELECT 1&amp;quot;]
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
        volumeMounts:
        - name: {{ .Name }}-config
          mountPath: /etc/mysql/conf.d
        - name: {{ .Name }}-datadir
          mountPath: /var/lib/mysql
      volumes:
      - name: {{ .Name }}-config
        configMap:
          name: {{ .Name }}-nodeconfig
          items:
            - key: galera.cnf
              path: galera.cnf
            - key: innodb.cnf
              path: innodb.cnf
  volumeClaimTemplates:
    - metadata:
        name: {{ .Name }}-datadir
      spec:
        accessModes: [ &amp;quot;ReadWriteOnce&amp;quot; ]
        resources:
          requests:
            storage: {{ .Params.VOLUME_SIZE }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 StatefulSet 구성에는 몇 가지 주의해야할 부분들이 존재한다.&lt;/p&gt;
&lt;p&gt;mySQL 데이터 디렉터리를 &lt;code&gt;/var/lib/mysql&lt;/code&gt; 경로로 Persistent Volumes를 사용하고, &lt;code&gt;/etc/mysql/conf.d&lt;/code&gt;에 configMap의 key/path 정보를 이용해서 설정 파일들을 시작할 때 사용할 수 있도록 마운트한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;  volumeMounts:
  - name: {{ .Name }}-config
    mountPath: /etc/mysql/conf.d
  - name: {{ .Name }}-datadir
    mountPath: /var/lib/mysql
volumes:
- name: {{ .Name }}-config
  configMap:
    name: {{ .Name }}-nodeconfig
    items:
      - key: galera.cnf
        path: galera.cnf
      - key: innodb.cnf
        path: innodb.cnf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;StatefulSet이므로 각 Replica들에 필요한 VolumeClaims들을 추가하는 VolumeClaimTemplate를 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;volumeClaimTemplates:
  - metadata:
      name: {{ .Name }}-datadir
    spec:
      accessModes: [ &amp;quot;ReadWriteOnce&amp;quot; ]
      resources:
        requests:
          storage: {{ .Params.VOLUME_SIZE }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MariaDB 컨테이너 이미지는 컨테이너가 처음 시작될 때 단일 노드 인스턴스를 설정하기 위한 부트스트랩 스크립트를 가지고 있다. 샘플에서는 이를 재 정의해서 &lt;code&gt;/var/lib/mysql&lt;/code&gt; 경로를 확인해서 데이터베이스가 이미 구성되었다면 부트스트랩 과정을 생략하도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;init container&lt;/code&gt;를 이용해서 데이터 디렉터리를 마운트하고 스크립트가 실행되기 전에 디렉터리를 생성해서 ConfigMap에서 생성한 설정에 따라서 Galera 프로세스가 올바르게 실행되고, 클러스터에 동기화될 수 있도록 재 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;initContainers:
# Stop the image bootstrap script from trying to set up single master
- name: {{ .Name }}-init
  image: busybox:1.28
  command: [&amp;#39;sh&amp;#39;, &amp;#39;-c&amp;#39;, &amp;#39;set -x; if [ ! -d /datadir/mysql ]; then mkdir /datadir/mysql; fi&amp;#39;]
  volumeMounts:
    - name: {{ .Name }}-datadir
      mountPath: &amp;quot;/datadir&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NODE_COUNT&lt;/code&gt; 파라미터를 이용해서 StatefulSet내에 포함되어야할 Replica의 수를 설정했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;replicas: {{ .Params.NODE_COUNT }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PART 1 에서 정의했던 포트 관련 파라미터들을 사용해서 노드들의 mySQL에서 사용할 포트들을 정의했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;ports:
  - containerPort: {{ .Params.MYSQL_PORT }}
    name: mysql
  - containerPort: {{ .Params.SST_PORT }}
    name: sst
  - containerPort: {{ .Params.REPLICATION_PORT }}
    name: replication
  - containerPort: {{ .Params.IST_PORT }}
    name: ist&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;새롭게 사용된 &lt;code&gt;VOLUME_SIZE&lt;/code&gt; 라는 파라미터를 &lt;code&gt;params.yaml&lt;/code&gt;에 추가하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
parameters:
  - name: SST_USER
    description: &amp;quot;User to perform SST as&amp;quot;
    default: &amp;quot;root&amp;quot;
  - name: SST_PASSWORD
    description: &amp;quot;Password for SST user&amp;quot;
    default: &amp;quot;admin&amp;quot;
  - name: MYSQL_PORT
    description: &amp;quot;MySQL port&amp;quot;
    default: &amp;quot;3306&amp;quot;
  - name: SST_PORT
    description: &amp;quot;SST port&amp;quot;
    default: &amp;quot;4444&amp;quot;
  - name: REPLICATION_PORT
    description: &amp;quot;Replication port&amp;quot;
    default: &amp;quot;4567&amp;quot;
  - name: IST_PORT
    description: &amp;quot;IST port&amp;quot;
    default: &amp;quot;4568&amp;quot;
  - name: MYSQL_ROOT_PASSWORD
    description: &amp;quot;MySQL root password&amp;quot;
    default: &amp;quot;admin&amp;quot;
  - name: NODE_COUNT
    description: &amp;quot;Number of nodes to create in the cluster&amp;quot;
    default: &amp;quot;3&amp;quot;
  - name: VOLUME_SIZE     # 추가
    description: &amp;quot;Size of persistent volume&amp;quot;
    default: &amp;quot;10M&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;테스트를 위한 것이기 때문에 아주 작은 기본 크기를 추가했다.&lt;/p&gt;
&lt;p&gt;지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다. 테스트를 위한 것이기 때문에 &lt;code&gt;params.yaml&lt;/code&gt; 파일에 정의된 &lt;code&gt;NODE_COUNT&lt;/code&gt; 파라미터의 기본 값을 1로 변경하고 진행해도 상관없다. StatefulSet은 실제 노드 및 MariaDB도 구성하므로 시간이 오래 걸릴 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo install ./&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo plan status --instance=galera-operator-instance&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Pod 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get pods&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행 중인 클러스터에 모든 인스턴스들이 생성된 것을 확인할 수 있다. 하나의 파드를 선택해서 로그 정보를 통해서 Galera가 정상적으로 동작하고 있는지를 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl logs galera-operator-instance-galera-operator-0&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;지금까지의 작업으로 StatefulSet이 올바르게 설정되었고, 모든 노드가 Galera Cluster에 참여하고 있다는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-3-Bootstrap-%EC%82%AD%EC%A0%9C-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A4%91%EB%8B%A8-%EC%97%86%EC%9D%B4-Scale-UpDown-%EC%B2%98%EB%A6%AC&quot;&gt;PART 3&lt;/a&gt; 에서는 이제 필요하지 않은 부트스트랩 노드를 제거하고, 클러스터를 외부에서 연결할 수 있도록 설정하고, 운영되는 동안 안정적으로 Scale Up/Down 할 수 있도록 기능을 추가할 것이다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/blog/blog-2020-07-13-building-your-first-operator-2.html&quot;&gt;Building your first KUDO operator - Part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>dynamic nfs client provisioner</category>
      <category>galera</category>
      <category>Galera Cluster</category>
      <category>Kubernetes</category>
      <category>Kudo</category>
      <category>NFS</category>
      <category>operator</category>
      <category>provisioner</category>
      <category>statefulset</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/64</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-2-Nodes-%EC%B0%B8%EC%97%AC%EC%99%80-%EC%99%B8%EB%B6%80-%EC%A0%91%EC%86%8D%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%83%9D%EC%84%B1-%EB%B0%8F-StatefulSet-%EA%B5%AC%EC%84%B1#entry64comment</comments>
      <pubDate>Tue, 5 Jan 2021 19:17:53 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes-Storage] CentOS 8에 Dynamic NFS Client Provisioner 구성하기</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Storage-CentOS-8%EC%97%90-Dynamic-NFS-Client-Provisioner-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;[Kubernetes - Storage] How to configure a dynamic storage provisioner for Kubernetes using a network file system on CentOS 8&lt;/h1&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이 문서는 Network File System을 CentOS 8에 설치하여 NFS Server로 운영하면서 Kubernetes의 PVC (Perssistent Volume Claim) - StorageClass - NFS 로 연동되는 PV (Persistent Volume)를 자동으로 구성하는 방법을 정리한 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;CentOS 8에 설치되는 NFS를 Kubernetes의 Dynamic Storage Provisioning 으로 활용해서 PV (Persistent Volume)를 구성해 본다.&lt;/p&gt;
&lt;h2&gt;Network file system 구성&lt;/h2&gt;
&lt;p&gt;NFS 서버를 구성하는 부분은 &lt;a href=&quot;https://ccambo.tistory.com/entry/CentOS-NFS-CentOS-8%EC%97%90-NFS-%EC%84%A4%EC%A0%95-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot;&gt;CentOS 8에 NFS 설정 및 테스트&lt;/a&gt; 글을 참고해서 진행하도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NFS 서버는 물리적인 머신으로 네트워크 상에 존재하면 되며, Network을 통해서 Kubernetes Cluster에서 NFS 서버로 접근할 수 있어야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;위에서 구성한 NFS 서버를 사용할 수 있도록 처리하는 NFS Provisioner Pod가 Kubernetes Cluster에 PV를 배포할 수 있도록 하기 위해서는 필요한 권한 설정이 필요하다. 따라서 PV를 배포할 수 있도록 ClusterRole, ClusterRoleBinding, Role, RoleBinding 설정을 가지는 Service Account를 생성해 줘야 한다.&lt;/p&gt;
&lt;h2&gt;Service Account 생성&lt;/h2&gt;
&lt;p&gt;서비스 계정을 생성한다. (serviceaccount.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-provisioner&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령을 사용해서 Kubernetes에 적용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl apply -f serviceaccount.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ClusterRole 생성&lt;/h2&gt;
&lt;p&gt;Cluster를 위한 역할을 생성한다. (clusterrole.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-provisioner-runner
rules:
  - apiGroups: [&amp;quot;&amp;quot;]
    resources: [&amp;quot;persistentvolumes&amp;quot;]
    verbs: [&amp;quot;get&amp;quot;, &amp;quot;list&amp;quot;, &amp;quot;watch&amp;quot;, &amp;quot;create&amp;quot;, &amp;quot;delete&amp;quot;]
  - apiGroups: [&amp;quot;&amp;quot;]
    resources: [&amp;quot;persistentvolumeclaims&amp;quot;]
    verbs: [&amp;quot;get&amp;quot;, &amp;quot;list&amp;quot;, &amp;quot;watch&amp;quot;, &amp;quot;update&amp;quot;]
  - apiGroups: [&amp;quot;storage.k8s.io&amp;quot;]
    resources: [&amp;quot;storageclasses&amp;quot;]
    verbs: [&amp;quot;get&amp;quot;, &amp;quot;list&amp;quot;, &amp;quot;watch&amp;quot;]
  - apiGroups: [&amp;quot;&amp;quot;]
    resources: [&amp;quot;events&amp;quot;]
    verbs: [&amp;quot;watch&amp;quot;, &amp;quot;create&amp;quot;, &amp;quot;update&amp;quot;, &amp;quot;patch&amp;quot;]
  - apiGroups: [&amp;quot;&amp;quot;]
    resources: [&amp;quot;services&amp;quot;, &amp;quot;endpoints&amp;quot;]
    verbs: [&amp;quot;get&amp;quot;]
  - apiGroups: [&amp;quot;extensions&amp;quot;]
    resources: [&amp;quot;podsecuritypolicies&amp;quot;]
    resourceNames: [&amp;quot;nfs-provisioner&amp;quot;]
    verbs: [&amp;quot;use&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령을 사용해서 Kubernetes에 적용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl apply -f clusterrole.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ClusterRoleBinding 생성&lt;/h2&gt;
&lt;p&gt;Cluster를 위한 역할 바인딩을 생성한다. (clusterbinding.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-provisioner
    namespace: default
roleRef:
  kind: ClusterRole
  name: nfs-provisioner-runner
  apiGroup: rbac.authorization.k8s.io

---

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: nfs-provisioner-otherRoles
rules:
  - apiGroups: [&amp;quot;&amp;quot;]
    resources: [&amp;quot;endpoints&amp;quot;]
    verbs: [&amp;quot;get&amp;quot;, &amp;quot;list&amp;quot;, &amp;quot;watch&amp;quot;, &amp;quot;create&amp;quot;, &amp;quot;update&amp;quot;, &amp;quot;patch&amp;quot;]

---

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: nfs-provisioner-otherRoles
subjects:
  - kind: ServiceAccount
    name: nfs-provisioner
    namespace: default    # provisioner가 배포될 네임스페이스
roleRef:
  kind: Role
  name: nfs-provisioner-otherRoles
  apiGroup: rbac.authorization.k8s.io&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령을 사용해서 Kubernetes에 적용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl apply -f clusterbinding.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NFS Client Provisioner 생성&lt;/h2&gt;
&lt;p&gt;Provisioner는 PVC와 StorageClass를 통해서 자동으로 PV를 구성해 주는 역할을 담당한다.&lt;/p&gt;
&lt;p&gt;NFS 서버를 Dynamic Storage Provisioning으로 사용할 수 있도록 NFS Client Dynmaic Provisioner Pod를 배포하도록 구성한다. (deployment-nfs.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;kind: Deployment
apiVersion: apps/v1
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccount: nfs-provisioner
      containers:
        - name: nfs-client-provisioner
          image: registry.cn-hangzhou.aliyuncs.com/open-ali/nfs-client-provisioner
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: fuseim.pri/ifs
            - name: NFS_SERVER
              value: 10.0.1.20
            - name: NFS_PATH
              value: /data/NFS
      volumes:
        - name: nfs-client-root
          nfs:
            server: 10.0.1.20
            path: /data/NFS&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령을 사용해서 Kubernetes에 적용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl apply -f deployment-nfs.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;StorageClass 생성&lt;/h2&gt;
&lt;p&gt;NFS 연계를 위한 StorageClass를 생성한다. (storageclass-nfs.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
  name: managed-nfs-storage 
provisioner: fuseim.pri/ifs&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령을 사용해서 Kubernetes에 적용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl apply -f storageclass-nfs.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Statefulset 생성 및 PV 검증&lt;/h2&gt;
&lt;p&gt;NFS 연계를 검증하기 위한 Statefulset을 생성하고 PV의 생성여부를 검증한다. (statefulset-nfs.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: &amp;quot;nginx1&amp;quot;
  replicas: 1
  selector:
    matchLabels:
      app: nginx1
  volumeClaimTemplates:
  - metadata:
      name: test 
      annotations:
        volume.beta.kubernetes.io/storage-class: &amp;quot;managed-nfs-storage&amp;quot;
    spec:
      accessModes: [ &amp;quot;ReadWriteOnce&amp;quot; ]
      resources:
        requests:
          storage: 2Gi 
  template:
    metadata:
     labels:
       app: nginx1
    spec:
     serviceAccount: nfs-provisioner
     containers:
     - name: nginx1
       image: nginx:1.7.9
       volumeMounts:
       - mountPath: &amp;quot;/mnt&amp;quot;
         name: test&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령을 사용해서 Kubernetes에 적용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl apply -f statefulset-nfs.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dynamic Provisioner Pod 실행 상태 확인&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get all
NAME                                       READY   STATUS    RESTARTS   AGE
pod/nfs-pod-provisioner-5f8686f8dd-bfkgz   1/1     Running   0          8m51s&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;만일 어느 정도의 시간이 지나도 STATUS가 &lt;code&gt;ContainerCreating&lt;/code&gt; 또는 &lt;code&gt;Pending&lt;/code&gt; 이라고 나오는 경우는 NFS 설정에 따라서 오류가 발생했을 수 있다.&lt;br&gt;&lt;code&gt;kubectl describe &amp;lt;pod name&amp;gt;&lt;/code&gt;으로 확인해 보면 오류 상황을 알 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;connection timeout&lt;/strong&gt;: NFS 서버의 IP에 접근이 안되는 경우이므로 실제 접근 가능한 IP인지 확인해야 한다. IP가 맞다면 NFS 서버에서 /etc/exports 설정을 다시 확인해 보고 &lt;code&gt;sudo exportfs -ra&lt;/code&gt;를 수행하고 다시 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;access denied by server&lt;/strong&gt;: NFS 서버에는 접근을 했지만 권한이 없는 경우이므로 NFS 서버에서 &lt;code&gt;sudo chmod 666 &amp;lt;nfs directory&amp;gt;&lt;/code&gt;를 수행하고 다시 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wrong fs type, bad option...&lt;/strong&gt;: NFS 관련 패키지가 설치되지 않았을 경우이므로 클러스터의 각 노드에 &lt;code&gt;sudo dnf install -y nfs-utils&lt;/code&gt;를 수행하고 다시 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그 이외의 오류가 발생했다면 관련된 메시지를 통해서 원인을 파악하고 해결해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;PVC (Persistent Volume Claim)를 통한 PV 생성 검증&lt;/h2&gt;
&lt;p&gt;StorageClass를 이용해서 간단한 테스트를 위한 PVC를 구성한다. (fns-pvc.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc-test
spec:
  storageClassName: managed-nfs-storage # StorageClass 명
  accessModes:
    - ReadWriteMany # 생성될 PV에 적용할 사용 모드
  resources:
    requests:
      storage: 10Mi  # 생성될 PV 크기&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;사용할 수 있는 AccessMode는 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ReadWriteOnce (RWO): 하나의 Pod에만 마운트되고 하나의 Pod에서만 읽고 쓰기 가능&lt;/li&gt;
&lt;li&gt;ReadWriteMany (RWX): 여러 개의 Pod에 마운트 가능하고 여러 개의 Pod에서 읽고 쓰기 가능&lt;/li&gt;
&lt;li&gt;ReadOnlyMany (ROX): 여러 개의 Pod에 마운트 가능하고 여러 개의 Pod에서 읽기는 가능하지만 쓰기는 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;테스트이므로 작은 크기로 설정하고 Kubernete에 적용하고 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl apply -f nfs-pvc.yaml
persistentvolumeclaim/nfs-pvc-test created

$ kubectl get pv,pvc
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS         REASON   AGE
persistentvolume/pvc-2dca7757-d8c1-4e80-8a21-f1d661d69b19   10Mi       RWX            Delete           Bound    default/nfs-pvc-test   managed-nfs-storage            7s

NAME                                  STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS         AGE
persistentvolumeclaim/nfs-pvc-test    Bound     pvc-2dca7757-d8c1-4e80-8a21-f1d661d69b19   10Mi       RWX            managed-nfs-storage   7s&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NFS 서버에서 실제 생성된 정보 확인&lt;/h2&gt;
&lt;p&gt;PVC를 통해서 생성된 PV를 확인했으므로 실제 NFS 서버에도 생성이 되었는지 확인해 보면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# NFS 서버에서 공유 디렉터리 확인
$ sudo ls /data/NFS
default-nfs-pvc-test-pvc-2dca7757-d8c1-4e80-8a21-f1d661d69b19&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 PVC와 PV 관련된 이름으로 유추할 수 있는 디렉터리가 생성된 것을 확인할 수 있다.&lt;/p&gt;
&lt;h2&gt;Deploy된 Pod의 Volume Mount 확인&lt;/h2&gt;
&lt;p&gt;실제 사용할 애플리케이션 Pod에서도 정상적으로 동작하는지를 확인하기 위해서 간단한 샘플을 구성해 보도록 한다. (nfs-test-app.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: ccambo-nfs-test
  labels:
    app: nginxnfs
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginxnfs
  template:
    metadata:
      labels:
        app: nginxnfs
    spec:
      containers:
      - name: nginxnfs
        image: nginx:latest
        ports:
        - containerPort: 80
        volumeMounts:                    
          - mountPath: /nfs
            name: nfsvolume          
      volumes:                           
      - name: nfsvolume
        persistentVolumeClaim:           
          claimName: nfs-pvc-test&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 YAML을 Kubernetes에 적용하고 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl apply -f nfs-test-app.yaml
deployment.apps/ccambo-nfs-test created

# Deployment 확인
$ kubectl get deploy
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
ccambo-nfs-test           0/1   1            0           15s
nfs-client-provisioner    1/1   1            1           75m

# Pod 확인
$ kubectl get pods
NAME                                     READY   STATUS    RESTARTS   AGE
ccambo-nfs-test-9d7fc4c6c-25xr4            1/1   Running   0          39s
ccambo-nfs-test-9d7fc4c6c-js5pl            1/1   Running   0          39s
nfs-client-provisioner-775496b5bb-vpglp    1/1   Running   0          82m&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;생성된 두 개의 Pod 중에 하나의 Pod에 접속해서 마운트된 경로에서 테스트용 파일을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl exec -it ccambo-nfs-test-9d7fc4c6c-25xr4 -- /bin/bash

# 연결된 Pod에서 마운트된 /nfs 디렉터리로 이동해서 테스트용 파일을 하나 생성한다.
&amp;gt; cd /nfs
&amp;gt; echo &amp;quot;This is NFS test&amp;quot; &amp;gt; /nfs/test.log
&amp;gt; cat /nfs/test.log
This is NFS test&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다른 Pod에 연결해서 마운트된 /nfs 디렉터리에 파일이 존재하는지 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl exec -it ccambo-nfs-test-9d7fc4c6c-js5pl -- /bin/bash

# 연결된 Pod에서 마운트된 /nfs 디렉터리로 이동해서 테스트용 파일을 확인한다.
&amp;gt; ls /nfs
test.log

&amp;gt; cat /nfs/test.log
This is NFS test&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 테스트와 같이 생성된 POD가 동일한 NFS를 PV로 마운트해서 사용하고 있는 것을 확인할 수 있다.&lt;/p&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>CentOS</category>
      <category>CentOS 8</category>
      <category>dynamic nfs client provisioner</category>
      <category>Kubernetes</category>
      <category>NFS</category>
      <category>provisioner</category>
      <category>Storage</category>
      <category>Storage Class</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/63</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Storage-CentOS-8%EC%97%90-Dynamic-NFS-Client-Provisioner-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0#entry63comment</comments>
      <pubDate>Tue, 5 Jan 2021 19:03:27 +0900</pubDate>
    </item>
    <item>
      <title>[CentOS - NFS] CentOS 8에 NFS 설정 및 테스트</title>
      <link>https://ccambo.tistory.com/entry/CentOS-NFS-CentOS-8%EC%97%90-NFS-%EC%84%A4%EC%A0%95-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
      <description>&lt;h1&gt;How to set up a network file system on CentOS 8&lt;/h1&gt;
&lt;p&gt;클라이언트 / 서버 파일 시스템이라고도 부르는 NFS (Network File System)는 클라이언트가 네트워크를 통해 다른 사용자와 디렉토리 및 파일을 공유하고 상호 작용할 수 있도록 마치 로컬에 마운트된 것 처럼 네트워크를 통해 로컬 파일 시스템을 내보내는데 널리 사용되는 교차 플랫폼 및 분산 파일 시스템 프로토콜이다.&lt;/p&gt;
&lt;p&gt;두 대의 머신에 CentOS 8을 설치하고 NFS 설정을 통해서 클라이언트 / 서버간의 파일 공유를 검증해 본다.&lt;/p&gt;
&lt;h2&gt;NFS Server 구성&lt;/h2&gt;
&lt;p&gt;NFS (Network File System)는 네트워크 상의 다른 머신에서 파일 시스템으로 마운트하여 사용할 수 있도록 공유하는 방법이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NFS 서버 패키지 설치&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 이미 설치되어 있는 경우는 최신 버전으로 업그레이드 된다. (nfs-utils-1:2.3.3-35.el8.x86_64)
$ sudo dnf install -y nfs-utils&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NFS 서버 Exports 설정&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;공유할 디렉터리를 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo mkdir /data/NFS&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NFS 서버의 특정 IP 호스트 접속을 허용하는 설정을 구성한다. (/etc/exports)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo vi /etc/exports

/data/NFS 10.0.1.*(rw,sync,no_root_squash)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;위의 /etc/exports 파일 내에 옵션을 설정할 때는 빈 공백이 없이 붙여서 작성해야 한다.&lt;/li&gt;
&lt;li&gt;10.0.1.* 는 10.0.1.0/24 와 같은 의미로 어떤 것을 사용해도 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;사용할 수 있는 옵션들은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;rw&lt;/strong&gt;: 읽기 및 쓰기 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ro&lt;/strong&gt;: 읽기만 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;secure&lt;/strong&gt;: 클라이언트 마운트 요청시 포트를 1024 이하로 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;noaccess&lt;/strong&gt;: 액세스 거부&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;root_squash&lt;/strong&gt;: 클라이언트의 root 사용자가 서버의 root 권한을 획득하는 것을 방지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;no_root_squach&lt;/strong&gt;: 클라이언트의 root 사용자와 서버의 root를 동일하게 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sync&lt;/strong&gt;: 파일 시스템이 변경되면 즉시 동기화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;all_sqash&lt;/strong&gt;: root를 제외하고 서버와 클라이언트의 사용자를 동일한 권한으로 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;no_all_sqash&lt;/strong&gt;: root를 제외하고 서버와 클라이언트의 사용자들을 하나의 권한을 가지도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NFS 실행&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# NFS Server 서비스 시작
$ sudo systemctl start nfs-server.service

# NFS Server 서비스가 재 부팅시 자동으로 시작될 수 있도록 설정
$ sudo systemctl enable nfs-server.service
Created symlink /etc/systemd/system/multi-user.target.wants/nfs-server.service → /usr/lib/systemd/system/nfs-server.service.

# NFS Server 서비스 상태 확인
$ sudo systemctl status nfs-server.service
● nfs-server.service - NFS server and services
  Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; vendor preset: disabled)
  Drop-In: /run/systemd/generator/nfs-server.service.d
          └─order-with-mounts.conf
  Active: active (exited) since Wed 2020-12-30 10:04:37 UTC; 1min 21s ago
Main PID: 13041 (code=exited, status=0/SUCCESS)
    Tasks: 0 (limit: 23978)
  Memory: 0B
  CGroup: /system.slice/nfs-server.service

12월 30 10:04:37 ccambo-nfs systemd[1]: Starting NFS server and services...
12월 30 10:04:37 ccambo-nfs systemd[1]: Started NFS server and services.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 명령을 통해서 NFS 서버 서비스를 실행하거나 NFS 공유를 마운트하는데 필요한 다른 서비스들 (nfsd, nfs-idmapd, rpcbind, rpc.mountd, lockd, rpc.startd, rpc.rquotad, rpc.idmapd)은 자동으로 시작된다.&lt;/p&gt;
&lt;p&gt;NFS 서버의 구성 파일들은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;/etc/nfs.conf&lt;/strong&gt;: NFS Daemon 및 도구에 대한 기본 구성 파일&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/etc/nfsmount.conf&lt;/strong&gt;: NFS 마운트 구성 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NFS 공유 디렉터리 정상 동작 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NFS로 공유할 디렉터리들이 정상적으로 동작하는지 확인은 아래와 같이 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo exportfs -v
/data/NFS       10.0.1.*(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)

$ sudo exportfs -arv
exporting 10.0.1.*:/data/NFS

$ sudo exportfs -s
/data/NFS  10.0.1.*(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;옵션은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;a&lt;/strong&gt;: 모든 공유 디렉터리를 내보내거나 내보내지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;r&lt;/strong&gt;: 모든 공유 디렉터리를 다시 내보내고, /var/lib/nfs/etab과 /etc/exports 파일, 그리고 /etc/exports.d 디렉터리 밑의 모든 파일들에 대한 동기화 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;v&lt;/strong&gt;: 자세한 출력&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;s&lt;/strong&gt;: 현재 내보내기 대상 리스트 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;방화벽 처리&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;firewalld 서비스가 동작 중이라면 NFS 서버 서비스에 필요한 포트들 (mountd, nfs, rpc-bind)을 열어 주어야 한다. (CentOS 8에는 기본 설치되어 있지 않다)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo firewall-cmd --permanent --add-service=nfs
$ sudo firewall-cmd --permanent --add-service=rpc-bind
$ sudo firewall-cmd --permanent --add-service=mountd

$ sudo firewall-cmd --reload&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;실제 여러 가지 환경에서 NFS 서버에 연결을 하다보면 (예를 들면 Kubernetes NFS Dynamic Provisiner 설정 등) 다양한 오류가 발생할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;접근 권한 관련&lt;/strong&gt;: &lt;code&gt;sudo chmod 666 &amp;lt;nsf directory&amp;gt;&lt;/code&gt;로 권한을 지정해 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;여기까지 작업으로 NFS 서버 설정은 완료되었다. 이제 클라이언트를 구성해서 접속 여부를 검증하면 된다.&lt;/p&gt;
&lt;h2&gt;NFS 클라이언트 구성&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NFS 패키지 설치&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo dnf install -y nfs-utils nfs4-acl-tools&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mount 후 정상 동작 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아래의 명령으로 NFS 서버의 공유 가능한 정보를 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# NFS Server에 내보내기된 목록 확인
$ showmount -e 10.0.1.20&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령으로 공유된 NFS 디렉터리를 마운트 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 로컬 마운트 경로 생성
$ sudo mkdir -p /data/mountNFS

# NFS 공유 디렉터리를 로컬 경로로 마운트
$ sudo mount -t nfs 10.0.1.20:/data/NFS /data/mountNFS

# 마운트 확인
$ mount | grep nfs
sunrpc on /var/lib/nfs/rpc_pipefs type rpc_pipefs (rw,relatime)
10.0.1.20:/data/NFS on /data/mountNFS type nfs4 (rw,relatime,vers=4.2,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.1.117,local_lock=none,addr=10.0.1.20)

# NFS 서버에서 파일 생성
$ sudo touch /data/NFS/file-created-on-server.txt

# 클라이언트에서 파일 생성 확인
$ ls -l /data/mountNFS
합계 0
-rw-r--r--. 1 root root 0 12월 30 10:53 file-created-on-server.txt

# 클라이언트에서 파일 생성
$ sudo touch /data/mountNFS/fil-created-on-client.txt

# NFS 서버에서 파일 생성 확인
$ ls -l /data/NFS
합계 0
-rw-r--r--. 1 root root 0 12월 30 10:55 file-create-on-client.txt
-rw-r--r--. 1 root root 0 12월 30 10:53 file-created-on-server.txt&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;마운트를 하는 중에 &lt;code&gt;mount.nfs: access denied by server while mounting 10.0.1.20:/data/NFS&lt;/code&gt;와 같은 오류가 발생할 수 있다.&lt;br&gt;이 오류를 확인하기 위해서는 NFS 서버에서 &lt;code&gt;sudo tail -f /var/log/messages&lt;/code&gt; 명령을 수행하면 &lt;code&gt;rpc.mountd[13025]: refused mount request from 10.0.1.117 for /data/NFS (/data/NFS): unmatched host&lt;/code&gt; 라는 오류를 확인할 수 있다.&lt;br&gt;이 경우는 NFS도 도메인 이름과 관련이 있기 때문에 &lt;code&gt;/etc/hosts&lt;/code&gt; 파일에 요청한 IP에 대한 도메인 설정이 있는지를 확인하고 도메인 설정된 호스트 명을 /etc/exports 파일에 추가해 주면 된다.&lt;br&gt;도메인 이름도 없는데 동일한 오류가 계속된다면 &lt;code&gt;10.0.1.* 설정을 10.0.1.0/24로&lt;/code&gt; 변경하고 다시 &lt;code&gt;exportfs -a&lt;/code&gt; 명령을 수행하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;재 부팅시 마운트 설정&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/fstab&lt;/code&gt; 에 마운트에 대한 설정을 추가해서 재 부팅해도 동작할 수 있도록 설정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# fstab 등록
$ echo &amp;quot;10.0.1.20:/data/NFS     /data/mountNFS  nfs     defaults 0 0&amp;quot;&amp;gt;&amp;gt;/etc/fstab

# 등록 확인
$ cat /etc/fstab&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unmount 처리&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ unmount /data/mountNFS&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;위의 명령을 /data/mountNFS 경로에서 실행하면 &lt;code&gt;/data/mountNFS: device is busy&lt;/code&gt;라는 메시지가 출력되고 처리되지 않는다. 따라서 다른 경로로 이동해서 처리하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tecmint.com/install-nfs-server-on-centos-8/&quot;&gt;How to set up NFS Server and Client on CentOS 8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>CentOS</category>
      <category>CentOS 8</category>
      <category>Network File System</category>
      <category>NFS</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/62</guid>
      <comments>https://ccambo.tistory.com/entry/CentOS-NFS-CentOS-8%EC%97%90-NFS-%EC%84%A4%EC%A0%95-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8#entry62comment</comments>
      <pubDate>Thu, 31 Dec 2020 10:42:18 +0900</pubDate>
    </item>
    <item>
      <title>[Git] macOS에서 현재 상태로 원격 저장소 재 구성하기</title>
      <link>https://ccambo.tistory.com/entry/Git-%ED%98%84%EC%9E%AC-%EC%83%81%ED%83%9C%EB%A1%9C-%EC%9B%90%EA%B2%A9-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%9E%AC-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;[Git] How to initialize remote repository&lt;/h1&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;아래의 정리된 명령들을 수행하면 원격 저장소의 데이터가 모두 초기화되므로 미리 백업등을 해 놓고 진행해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;가장 기본적인 방법은 새로운 Repository를 만들고 다시 remote server 연결하는 것이지만, Repository에서 많은 수의 파일을 삭제하고 현재 상태로 Repository를 재 구성(초기화)해야 할 경우도 존재한다.&lt;/p&gt;
&lt;p&gt;여기서는 기존에 계속 사용 중이던 Remote Repository를 현재 상태로 Repository를 재 구성하는 경우를 정리한다. (전체적인 과정은 초기 git 구성하는 방식과 크게 다르지 않다)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;로컬에 존재하는 프로젝트 디렉터리에서 숨겨진 &lt;code&gt;.git&lt;/code&gt; 서브 디렉터리를 삭제한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# .git 디렉터리 삭제
$ rm -rf ./.git&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;git init&lt;/code&gt; 을 다시 수행해서 git를 초기화 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# git 초기화
$ git init&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;현재 상태로 &lt;code&gt;commit&lt;/code&gt;을 진행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 현재 경로의 모든 파일/디렉터리들 추가
$ git add .

# 추가된 정보들에 대한 커밋
$ git commit -m &amp;quot;&amp;lt;commit comment&amp;gt;&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;remote repository&lt;/code&gt;를 연결한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# remote repository 연결
$ git remote add origin &amp;lt;url&amp;gt;

# 연결된 remote repository 확인
$ git remote -v&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;만일 github의 여러 계정을 사용하는 경우는 &lt;a href=&quot;https://ccambo.tistory.com/entry/Git-macOS%EC%97%90%EC%84%9C-%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-Githubcom-%EA%B3%84%EC%A0%95-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&quot;&gt;macOS에서 여러 개의 Github.com 계정 사용하기&lt;/a&gt; 글에서와 같이 SSH를 사용한다면 &lt;code&gt;ssh config 파일 설정 과 사용방법&lt;/code&gt;을 참고해서 &lt;code&gt;&amp;lt;url&amp;gt;을 맞춰줘야&lt;/code&gt; 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;현재 상태를 &lt;code&gt;push&lt;/code&gt;한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git push --force --set-upstream origin master&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Branch를 새로 생성해서 push한 후에 기존 Branch를 삭제해도 된다.&lt;br&gt;branch 삭제는 Repository의 Branch 선택에서 &lt;code&gt;Show all branches&lt;/code&gt;를 클릭해서 branch 관리 화면에서 &lt;code&gt;default&lt;/code&gt;를 새로 생성한 branch로 변경하고 기존 branch의 &lt;code&gt;휴지통 icon&lt;/code&gt;을 눌러서 삭제하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en&quot;&gt;github docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>GIT</category>
      <category>intialize</category>
      <category>macos</category>
      <category>reconfig</category>
      <category>remote repository</category>
      <category>맥</category>
      <category>원격 저장소 초기화</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/61</guid>
      <comments>https://ccambo.tistory.com/entry/Git-%ED%98%84%EC%9E%AC-%EC%83%81%ED%83%9C%EB%A1%9C-%EC%9B%90%EA%B2%A9-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%9E%AC-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0#entry61comment</comments>
      <pubDate>Tue, 29 Dec 2020 02:56:55 +0900</pubDate>
    </item>
    <item>
      <title>[Git] macOS에서 여러 개의 Github.com 계정 사용하기</title>
      <link>https://ccambo.tistory.com/entry/Git-macOS%EC%97%90%EC%84%9C-%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-Githubcom-%EA%B3%84%EC%A0%95-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;[Git] How to use multiple account of github on macOS&lt;/h1&gt;
&lt;p&gt;github에 여러 개의 계정이 있고, Visual Studo Code나 터미널에서 사용하다 보면 &lt;code&gt;Permission error&lt;/code&gt; 나 &lt;code&gt;User Denied&lt;/code&gt; 등의 오류를 만날 수 있다.&lt;/p&gt;
&lt;p&gt;이런 문제들을 해결하고 &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%8B%9C%ED%81%90%EC%96%B4_%EC%85%B8&quot;&gt;SSH&lt;/a&gt; Key를 이용해서 여러 개의 github 계정을 설정하고 사용하는 방법을 정리해 놓는다.&lt;/p&gt;
&lt;h2&gt;문제점&lt;/h2&gt;
&lt;p&gt;단일 계정만 사용할 경우라면 pull, push를 처음할 때 계정 권한을 확인하는 프롬프트가 뜨고, 계정 정보를 입력하면 &lt;code&gt;키 체인&lt;/code&gt;에 보관되기 때문에 다음 부터는 입력하지 않아도 정상적으로 처리가 된다.&lt;/p&gt;
&lt;p&gt;그러나 또 하나의 계정을 더 만들어서 Repository 동기화를 하려고 하면 키 체인에 저장된 정보 때문에 이전 계정의 정보를 사용하기 때문에 계속 오류가 발생하게 된다.&lt;/p&gt;
&lt;h2&gt;해결 방법&lt;/h2&gt;
&lt;p&gt;해결 방법은 간단하다. 각 계정에 대한 &lt;code&gt;SSH Key&lt;/code&gt; 생성하고 github에 등록해서 관리하면 각 계정별 Repository에 대한 걱정 없이 편리하게 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;우선 Remote Repository를 확인해 보도록 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 현재 설정된 원격 저장소 보기
$ git remote -v

# 현재 저장소 설정 조회 (저장소 루트 폴더에 .git/config 파일에 정보가 존재한다)
$ git config --list&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;기존에 입력되어 있는 키 체인 정보 삭제&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spotlight 검색(F12) 등으로 &lt;code&gt;키체인 접근&lt;/code&gt; 앱 실행&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;![Spotlight 검색](&lt;a href=&quot;http://ccambo.github.io/images/content&quot;&gt;http://ccambo.github.io/images/content&lt;/a&gt; /spotlight_key_chain_access.png?featherlight=false)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;키체인 접근 앱에서 &lt;code&gt;로그인&lt;/code&gt;을 선택하고 &lt;code&gt;github.com&lt;/code&gt;을 찾아서 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/key_chain_access.png?featherlight=false&quot; alt=&quot;Key chain access&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;macOS는 SSH 키들을 &lt;code&gt;~/.ssh&lt;/code&gt; 디렉터리에 관리한다.&lt;/li&gt;
&lt;li&gt;이후 작업은 모두 이 경로에서 처리하는 것을 기준으로 한다.&lt;/li&gt;
&lt;li&gt;github는 계정당 하나의 SSH Key만 등록 가능하므로 사용할 계정만큼 생성하고 처리해 주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 문서에서는 &lt;code&gt;ccambo&lt;/code&gt;와 &lt;code&gt;morris&lt;/code&gt;라는 두 개의 계정을 사용하는 것으로 가정한다.&lt;/p&gt;
&lt;h3&gt;SSH Key 생성 및 등록&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ssh key 생성&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ssh-keygen -t rsa -b 4096 -C &amp;quot;ccambo@gmail.com&amp;quot;

Generating public/private rsa key pair.
Enter file in which to save the key (/Users/morris/.ssh/id_rsa):   id_rsa_ccambo  # 생성할 파일 명 입력 (아니면 그냥 Enter key, 이 경우는   id_rsa라는 기본 파일로 생성됨)
Enter passphrase (empty for no passphrase): # 비밀번호 설정할 경우 입력   (아니면 Enter key)
Enter same passphrase again: # 비밀번호 설정할 경우 입력 (아니면 Enter key)
...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ssh key macOS에 등록&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ssh agent 데몬으로 동작 시키기 (이 명령은 한번만 실행)
$ eval &amp;quot;$(ssh-agent -s)&amp;quot;
Agent pid 14194

# SSH Key 추가
$ ssh-add -K ~/.ssh/id_rsa_ccambo
Identity added: /Users/&amp;lt;사용자 계정&amp;gt;/.ssh/id_rsa_ccambo (ccambo@gmail.   com)&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 단계를 사용할 계정 갯수만큼 반복한다.&lt;/p&gt;
&lt;p&gt;이 문서의 샘플 기준이라면 아래와 같이 네 개의 파일이 생성되어야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;id_rsa_ccambo&lt;/strong&gt;: ccambo 계정 비밀 키&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;id_rsa_ccambo.pub&lt;/strong&gt;: ccambo 계정 공개 키&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;id_rsa_morris&lt;/strong&gt;: morris 계정 비밀 키&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;id_rsa_morris.pub&lt;/strong&gt;: morris 계정 공개 키&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;github에 공개키 등록&lt;/h3&gt;
&lt;p&gt;생성한 계정별 공개키 파일의 내용을 github에 등록한다.&lt;/p&gt;
&lt;p&gt;github.com에 로그인 하면 &lt;code&gt;우측 상단의 계정 이미지&lt;/code&gt;를 클릭하고 펼쳐진 서브 메뉴들에서 &lt;code&gt;Settings&lt;/code&gt; 를 선택한다.&lt;/p&gt;
&lt;p&gt;계정 Settings 화면의 좌측 메뉴에서 &lt;code&gt;SSH and GPG keys&lt;/code&gt; 메뉴를 클릭하면 &lt;code&gt;SSH Keys&lt;/code&gt; 화면의 우측 상단에 &lt;code&gt;New SSH Key&lt;/code&gt; 버튼을 눌러서 다음과 같이 설정한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Title&lt;/strong&gt;: 식별할 수 있는 아무 이름이든 지정한다. (여기서는 ccambo SSH)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key&lt;/strong&gt;: 생성된 &lt;code&gt;id_rsa_ccambo.pub&lt;/code&gt;파일의 내용을 그대로 복사해 넣는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;기존에 사용하던 SSH Key각 존재할 수 있다. 여러 개를 생성해서 (Title까지 동일해도 상관없다) 사용할 수 있지만, 가능하면 하나의 SSH Key만 유지하는 것이 좋다. 이전에 등록한 것이 있다면 삭제하도록 한다.&lt;/li&gt;
&lt;li&gt;공개키 파일의 내용을 복사하지 않고 처리하려면 터미널에서 아래와 같이 명령을 사용해도 된다.&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ pbcopy &amp;lt; ~/.ssh/id_rsa_ccambo.pub&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;ssh config 파일 설정&lt;/h3&gt;
&lt;p&gt;대 부분 여러 개의 SSH Key를 운영하기 때문에 매번 서버에 접속할 때마다 SSH Key를 지정하는 것도 번거롭기 때문에 설정으로 저장해 놓고 사용하는 방법을 권장한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.ssh&lt;/code&gt; 디렉터리에 &lt;code&gt;config&lt;/code&gt; 파일이 존재하는지를 확인하고 없다면 새로 생성하도록 한다.&lt;/p&gt;
&lt;p&gt;github.com에 대한 접속 정보를 아래와 같이 설정해 주면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host github.com-ccambo    # ccambo 계정 용
  HostName github.com
  IdentityFile ~/.ssh/id_rsa_ccambo   # 비밀 키 파일
  User ccambo             # github.com 계정 명

Host github.com-morris    # Morris 계정 용
  HostName github.com
  IdentityFile ~/.ssh/id_rsa_morris   # 비밀 키 파일
  User morris             # github.com 계정 명&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;사용방법&lt;/h3&gt;
&lt;p&gt;github.com에 Repository 정보에서 &lt;code&gt;Code&lt;/code&gt;를 클릭하면 &lt;code&gt;HTTPS&lt;/code&gt;, &lt;code&gt;Github CLI&lt;/code&gt;, &lt;code&gt;SSH&lt;/code&gt; 의 세 가지 Clone 용 주소가 보인다. 이 중에서 SSH 방식을 선택하고 사용하면 된다.&lt;/p&gt;
&lt;p&gt;SSH를 이용하는 포맷은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;format : git@github.com:&amp;lt;account name&amp;gt;/&amp;lt;repository name&amp;gt;.git
Sample :  git@github.com:ccambo/ImageCDN.git&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그런데 SSH Config를 구성할 때 다음과 같이 Host Name을 구성했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;github.com-&amp;lt;account name&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;따라서 Repository를 Clone 할 떄나 Remote Add 를 수행할 떄 Repository URL은 다음과 같이 구성되어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;format : git@&amp;lt;ssh config에 정의한 HOST 명&amp;gt;:&amp;lt;account name&amp;gt;/&amp;lt;repository name&amp;gt;.git
Sample : git@github.com-ccambo:ccambo/ImageCDN.git&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;신규로 Clone&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git clone git@github.com-ccambo:ccambo/ImageCDN.git&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;신규로 Remote Repository 추가&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git remote add origin git@github.com-ccambo:ccambo/ImageCDN.git&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;이전 Remote Repository 변경&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git remote set-url origin git@github.com-ccambo:ccambo/ImageCDN.git&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/&quot;&gt;Generating a new SSH key and adding it to the ssh-agent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>account</category>
      <category>GIT</category>
      <category>github.com</category>
      <category>macos</category>
      <category>multiple account</category>
      <category>ssh</category>
      <category>다중 계정</category>
      <category>맥북</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/60</guid>
      <comments>https://ccambo.tistory.com/entry/Git-macOS%EC%97%90%EC%84%9C-%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-Githubcom-%EA%B3%84%EC%A0%95-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0#entry60comment</comments>
      <pubDate>Mon, 28 Dec 2020 00:29:55 +0900</pubDate>
    </item>
    <item>
      <title>[Vue] Vue - 키포인트 이해하기</title>
      <link>https://ccambo.tistory.com/entry/Vue-Vue-%ED%82%A4%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;[Vue] Keypoints of Vue&lt;/h1&gt;
&lt;p&gt;프로젝트에서 사용할 Front-End Framework을 고심하던 중에 Gregg이 Vue.js를 소개하는 공식 자료를 보고 Vue를 선택했다. 아래의 내용은 참고자료에 언급한 원문을 번역하면서 나름대로 정리한 것으로 왜 Vue가 43%의 개발자들이 vue.js를 배우려고 하는지를 알 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;현재 TOP 3 (Angular, React, Vue)의 자바스크립트들에 대한 비교는 &lt;a href=&quot;https://kr.vuejs.org/v2/guide/comparison.html&quot;&gt;vue.js 공식문서&lt;/a&gt;를 참고하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;strong&gt;점진적인 프레임워크&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Vue는 &lt;font color=&quot;darkpuple&quot; size=&quot;3&quot;&gt;진입장벽이 낮고, 유연하고, 성능이 우수하고, 유지보수와 테스트가 편한 자바스크립트 프레임워크로 점진적인 프레임워크를 지향&lt;/font&gt;하고 있다. 점진적인 프레임워크라는 것은 &lt;font color=&quot;darkpuple&quot; size=&quot;3&quot;&gt;웹 어플리케이션 전체를 한꺼번에 하나의 프레임워크 구조로 구조화하지 않아도 일정 부분만 적용 가능&lt;/font&gt;해서 사용자에게 더 좋은 사용자 경험을 제공할 수 있다는 것을 의미한다. 물론 전체를 처음부터 Vue로 구현할 수도 있다. Vue가 큰 규모의 어플리케이션을 개발할 수 있도록 핵심 라이브러리와 주변 생태계를 제공하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_flexibility.gif?featherlight=false&quot; alt=&quot;Vue - 점진적인 프레임워크&quot;&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;재 사용 가능한 컴포넌트 (단일 파일 컴포넌트)&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;다른 프레임워크들처럼 재 사용이 가능한 컴포넌트로 웹 페이지를 구성할 수 있으며, 각 컴포넌트는 페이지 영역을 표시하는데 필요한 &lt;strong&gt;HTML, CSS, Script&lt;/strong&gt; 를 가지는 단일 파일 컴포넌트 구조를 제공한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_components_in_page.png?featherlight=false&quot; alt=&quot;컴포넌트로 페이지 구성&quot;&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;프로젝트로 알아보기&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;아주 간략하고 특징적인 Vue의 주요 컨셉을 알아볼 수 있다.&lt;/p&gt;
&lt;p&gt;대 부분의 자바스크립트와 같이 페이지에 데이터를 표시하는 것부터 시작한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_insert_data_onpage.gif?featherlight=false&quot; alt=&quot;페이지에 데이터 표시&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 그림에 표시된 것과 같이 &amp;quot;X&amp;quot;의 위치에 스크립트에서 설정한 &amp;quot;Boots&amp;quot;를 표시하고 싶으면 아래와 같이 구현하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_instance.gif?featherlight=false&quot; alt=&quot;Vue로 페이지에 데이터 표시&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 내용을 좀 더 자세히 확인하면 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;정보를 표시할는 것은 Vue의 데이터를 바인딩할 때 사용하는 &lt;code&gt;{{ ... }}&lt;/code&gt; 를 사용한다. 위의 예는 Vue 에서 관리하는 데이터 중에서 &amp;quot;product&amp;quot;라는 정보를 출력한다는 의미다. 물론 아직 데이터는 없다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;script&amp;gt;...&amp;lt;/script&amp;gt;&lt;/code&gt; 로 Vue 라이브러리를 포함시키고 구동 가능한 상태로 만든다.&lt;/li&gt;
&lt;li&gt;const app = &lt;code&gt;new Vue({ ... })&lt;/code&gt; 코드로 Vue 인스턴스를 생성한다.&lt;/li&gt;
&lt;li&gt;Vue 인스턴스를 생성할 떄 동작에 필요한 옵션을 지정한다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;el&lt;/code&gt;&lt;/strong&gt; : Vue 인스턴스가 DOM과 연계될 수 있도록 대상 엘리먼트를 지정한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;data&lt;/code&gt;&lt;/strong&gt; : Vue 동작에 필요한 데이터를 JSON 형식으로 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 코드를 실행하면 아래와 같이 결과를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_first_result.gif?featherlight=false&quot; alt=&quot;정적 데이터 표시&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 결과까지는 일반적인 수준이지만 정적이 아닌 동적으로 데이터가 변경될 때 Vue의 마법을 경험할 수 있다. 아래와 같이 개발자 도구의 콘솔에서 데이터를 변경하면 어떻게 데이터가 연동되는지를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_first_result_with_console.gif?featherlight=false&quot; alt=&quot;동적 데이터 표시&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 결과에서 알 수 있듯이 Vue는 데이터가 변경되면 Vue에서 알아서 변경된 내용을 처리하는 방식을 제공한다. 이 동작은 문자열뿐만 아니라 모든 유형의 데이터에 모두 적용된다. 이를 확인해 보기 위해서 위의 샘플 코드를 배열을 처리할 수 있도록 &lt;code&gt;&amp;quot;ul&amp;quot;&lt;/code&gt; 태그와 &lt;code&gt;&amp;quot;li&amp;quot;&lt;/code&gt; 태그 구조로 변경한다. 이때 템플릿처럼 구성하는 것이기 때문에 &lt;font color=&quot;darkpuple&quot; size=&quot;3&quot;&gt;v-for라는 특별한 속성을 지정 (foreach 문장구조와 비슷)&lt;/font&gt;해야 한다. 그리고 Vue 인스턴스 생성할 떄의 data 부분을 배열구조인 products로 변경한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-for_directive_usage.png?featherlight=false&quot; alt=&quot;동적 데이터 표시 (v-for 지시자 사용)&quot;&gt;&lt;/p&gt;
&lt;p&gt;변경된 코드를 브라우저에서 실행해 보면 아래와 같은 결과를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-for_directive_result.gif?featherlight=false&quot; alt=&quot;동적 데이터 표시 결과&quot;&gt;&lt;/p&gt;
&lt;p&gt;좀 더 확장을 해서 서버를 호출해서 데이터를 받아온 결과를 기준으로 화면에 동적으로 출력하는 것으로 가정하면 아래와 같이 코드를 구성하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-for_directive_with_apis.gif?featherlight=false&quot; alt=&quot;동적 데이터 표시 (API 호출)&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 코드를 살펴보면 다음과 같은 변화가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;data를 products라는 빈 배열로 초기화만 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;created&lt;/code&gt;&lt;/strong&gt; 라는 vue 인스턴스의 생성시점에 호출되는 이벤트를 사용해서 API 호출 코드를 작성한다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fetch&lt;/code&gt; 메서드를 통해서 API를 호출한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;then&lt;/code&gt; 메서드를 이용해서 API 호출 결과를 Vue에서 관리하는 데이터로 변환해서 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 코드에서 호출된 API의 결과는 JSON형식으로 실행된 화면은 아래와 같이 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-for_directive_with_apis_result.gif?featherlight=false&quot; alt=&quot;동적 데이터 표시 (API 호출 결과)&quot;&gt;&lt;/p&gt;
&lt;p&gt;API에서 반환된 JSON을 그대로 출력한 것이기 떄문에 이를 좀 더 사용자에게 친숙한 형식으로 처리하기 위해서 아래와 같이 View 처리를 변경한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-for_expression.gif?featherlight=false&quot; alt=&quot;v-for 지시자 표현식&quot;&gt;&lt;/p&gt;
&lt;p&gt;수행된 결과는 반환된 결과에서 product.quantity와 product.name 만을 출력하는 것으로 변경했기 때문에 아래와 같이 나타난다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-for_exp_result.gif?featherlight=false&quot; alt=&quot;v-for 지시자 표현식 결과&quot;&gt;&lt;/p&gt;
&lt;p&gt;각 데이터 항목중에서 갯수가 0인게 있으면 사용자가 좀 더 잘 인식할 수 있도록 &amp;quot;&amp;lt;span&amp;gt;&amp;quot; 태그를 사용해서 &lt;strong&gt;item.quantity === 0&lt;/strong&gt; 일 때만 &amp;quot;OUT OF STOCK&amp;quot; 텍스트가 보이도록 아래와 같이 변경한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-for_another.gif?featherlight=false&quot; alt=&quot;v-for 지시자 표현식&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 표현식에서는 &lt;code&gt;v-if&lt;/code&gt; 지시자를 이용해서 조건식을 구성해서 재고 여부를 좀 더 확실하게 구분할 수 있도록 변경했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-if_directive_result.gif?featherlight=false&quot; alt=&quot;v-if 지시자 표현식&quot;&gt;&lt;/p&gt;
&lt;p&gt;이제 모든 상품의 총 재고량을 목록 아래쪽에 표시하려면 totalProducts라는 computed 속성을 활용하면 된다. 만일 자바스크립트의 reduce() API가 익숙하지 않다면 그냥 재고 총합을 구하는 동작이라고 이해하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_computed_prop.gif?featherlight=false&quot; alt=&quot;computed 속성&quot;&gt;&lt;/p&gt;
&lt;p&gt;실행을 해 보면 아래와 같이 모든 상품의 총 재고량이 계산되어 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_computed_prop_result.gif?featherlight=false&quot; alt=&quot;Computed 속성 결과&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=ko&quot;&gt;Vue 개발자 도구(Vue.js Chrome Extension)&lt;/a&gt;의 장점 중에 하나는 페이지상에 표시된 데이터를 살펴볼 수 있다는 점이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_devtool.gif?featherlight=false&quot; alt=&quot;Vue Dev Tools&quot;&gt;&lt;/p&gt;
&lt;p&gt;앞에서 보았던 데이터의 변화가 View에 동적으로 표현되는 부분을 다시 한번 확인할 겸해서 개발자 도구의 콘솔에서 2개의 product를 제거해 보면 아래와 같이 리스트뿐만 아니라 총 재고량도 변경되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_reactivity.gif?featherlight=false&quot; alt=&quot;Vue Dev Tools에서 데이터 변경 결과&quot;&gt;&lt;/p&gt;
&lt;p&gt;이번에는 버튼을 사용해서 페이지에 이벤트를 추가하고 버튼을 눌렀을 때 각 상품의 재고량을 1개씩 늘리는 것으로 변경해 보도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-on_click.gif?featherlight=false&quot; alt=&quot;v-on click 이벤트&quot;&gt;&lt;/p&gt;
&lt;p&gt;실행해 보면 Add 버튼을 눌렀을 때 각 상품의 재고와 총 재고량의 숫자를 증가시키는 것과 Jacket 상품에서는 OUT OF STOCK 글씨도 사라지는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-on_click_result.gif?featherlight=false&quot; alt=&quot;v-on click 이벤트 결과&quot;&gt;&lt;/p&gt;
&lt;p&gt;좀 더 확장해서 재고량을 직접 입력할 수 있도록 input 박스를 추가하고 v-model 지시자를 연결하고 입력되는 값은 항상 숫자인 것으로 지정하도록 변경한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-model.gif?featherlight=false&quot; alt=&quot;v-model 지시자&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 input 박스에는 양방향 바인딩을 위한 &lt;code&gt;v-model&lt;/code&gt; 지시자를 이용해서 숫자인 데이터를 Vue에서 관리하고 있는 데이터 항목에 연동하는 설정을 한 것이다.&lt;/p&gt;
&lt;p&gt;이제 버튼을 통한 것뿐만 아니라 사용자가 직접 숫자를 입력해서 재고를 변경할 수도 있다. 물론 0으로 숫자를 설정하는 OUT OF STOCK이 표시되는 것도, Add 버튼 처리도 모두 잘 동작하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_v-model_result.gif?featherlight=false&quot; alt=&quot;v-model 지시자 결과&quot;&gt;&lt;/p&gt;
&lt;p&gt;지금까지 설명하고 실행해서 검증했던 소스는 &lt;a href=&quot;https://jsfiddle.net/greggpollack/gr1cs2tv/&quot;&gt;JSFIDDLE&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;h2&gt;추가적인 Vue 특징&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;규모가 있는 어플리케이션을 구축한다면 여러 개의 컴포넌트를 조합해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_single_file_components.png?featherlight=false&quot; alt=&quot;Single file component&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vue는 CLI (Command Line Interface) 명령어 도구를 이용해서 쉽게 프로젝트를 생성할 수 있다. 아래의 명령으로 프로젝트를 시작하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue-cli.png?featherlight=false&quot; alt=&quot;Vue-CLI&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;컴포넌트 별로 .vue 파일을 생성해서 관리할 수 있고, 각 .vue 파일에는 HTML, Javascript, CSS &amp;amp; SCSS 가 들어간다. (Single file component)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://ccambo.github.io/images/content/vue_components_system.gif?featherlight=false&quot; alt=&quot;Vue Component System&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;지금까지 Vue가 할 수 있는 기능들 중에서 아주 기초적인 일부만 확인해 봤다. Vue에는 프론트엔드 화면을 개발하고, 구성하고, 빌드하기 위한 더 많은 기능들이 존재한다. 만일 Vue를 배우고 싶다면 아래의 2가지 자료를 추천한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.vuemastery.com&quot;&gt;VueMastery의 Vue Guide &amp;amp; CheatSheet&lt;/a&gt; : &lt;a href=&quot;https://www.vuemastery.com/courses/intro-to-vue-js/vue-instance/&quot;&gt;무료 소개 과정&lt;/a&gt;과 계정을 만들면 CheatSheet를 받을 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kr.vuejs.org/v2/guide/index.html&quot;&gt;Vue.js 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/vue-mastery/why-43-of-front-end-developers-want-to-learn-vue-js-7f23348bc5be&quot;&gt;Why 43% of Front-End Developers want to learn Vue.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Web</category>
      <category>SFC</category>
      <category>single file component</category>
      <category>VUE</category>
      <category>vue-cli</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/59</guid>
      <comments>https://ccambo.tistory.com/entry/Vue-Vue-%ED%82%A4%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#entry59comment</comments>
      <pubDate>Fri, 25 Dec 2020 21:29:33 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - Operator] KUDO를 활용한 Galera Operator 단계별로 적용하기 - PART 1: Bootstrap Node 구성</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to building real world sample step by step - Part 1&lt;/h1&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;게시글 참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이 게시글은 KUDO Blog 샘플을 기준으로 테스트하면서 발생한 문제점들을 처리하고 동작을 검증하면서 정리한 내용입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-2-Nodes-%EC%B0%B8%EC%97%AC%EC%99%80-%EC%99%B8%EB%B6%80-%EC%A0%91%EC%86%8D%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%83%9D%EC%84%B1-%EB%B0%8F-StatefulSet-%EA%B5%AC%EC%84%B1&quot;&gt;PART 2&lt;/a&gt;에서는 Galera Cluster에 참여할 노드들 구성&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-3-Bootstrap-%EC%82%AD%EC%A0%9C-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A4%91%EB%8B%A8-%EC%97%86%EC%9D%B4-Scale-UpDown-%EC%B2%98%EB%A6%AC&quot;&gt;PART 3&lt;/a&gt;에서는 사용하지 않는 부트스트랩을 제거하고, 외부 접속을 위한 서비스를 생성하며, 안전한 Scale Up/Down 처리 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 문서는 KUDO Blog 샘플을 기준으로 테스트하면서 발생한 문제점들을 해결하고 동작을 검증하면서 정리한 내용으로 &lt;a href=&quot;https://galeracluster.com/&quot;&gt;Galera 클러스터&lt;/a&gt; 구성을 위한 Bootstap Node 설정을 다룬다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이 문서는 KUDO에 대한 이해를 어느 정도 하고 있다는 것을 전제로 하며 MariaDB의 오픈 소스 클러스터링 솔루션인 Galera를 위한 Operator를 KUDO를 이용해서 만드는 과정을 정리한 것이다.&lt;/li&gt;
&lt;li&gt;이 문서는 KUDO v0.17.0 기준으로 작성되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KUDO에 관련된 정보는 이전 글들 참고&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-%EC%A0%95%EB%A6%AC-Kubernetes-Universal-Declarative-Operator-Kudobuilder&quot;&gt;KUDO 정리 (Kubernetes Universal Declarative Operator - Kudobuilder)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%82%AC%EC%9A%A9%EB%B2%95-%EA%B2%80%EC%A6%9D&quot;&gt;KUDO 설치 및 간단한 사용법 검증&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-CLI-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%A0%95%EB%A6%AC&quot;&gt;KUDO CLI 명령어 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;환경 설정&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;파일 시스템 레이아웃 구성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;우선 작업할 내용은 개발 및 테스트 환경을 구성하는 것이다. 이전 글들에서 언급했던 것과 같이 아래와 같은 구성이 필요하다. (KUDO-CLI 명령을 통해서 구조 및 기본 파일들을 생성할 수 있다.)&lt;/p&gt;
&lt;p&gt;작업할 대상은 &lt;code&gt;~/kudo_sample/galera-operator&lt;/code&gt; 폴더를 기준으로 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
└── operator
    ├── operator.yaml
    ├── params.yaml
    └── templates

2 directories, 2 files&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;생성된 operator.yaml 파일은 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: galera-operator
operatorVersion: 0.1.0
plans: {}
tasks: []&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;생성된 params.yaml 파일은 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
parameters: []&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그리고 자동으로 생성되지 않는 &lt;code&gt;templates&lt;/code&gt; 디렉터리도 생성해 놓는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;배포 계획 생성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 먼저 작업할 내용은 모든 Operator가 반드시 가지고 있어야 할 배포 계획을 구성하는 것이다. (operator.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
plans:
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위와 같이 기본 배포 계획 구성을 추가한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bootstrap Config 설정&lt;/h2&gt;
&lt;p&gt;Galera 클러스터를 배포할 때 Bootstrap을 위해 수행할 단계가 존재하기 때문에 부트스트랩 노드를 구성해야 한다. 이 부트스트랩 노드는 클러스터의 다른 노드와 구성이 다르므로 이를 배포하기 위해서 구성해야 한다.&lt;/p&gt;
&lt;p&gt;ConfigMap 리소스를 이용해서 부트스트랩 컨테이너에 전달해야 한다. 따라서 배포 계획에 Phases와 Steps를 추가해서 이를 처리하기 위한 구성을 아래와 같이 처리한다. (operator.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
plans:
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          - name: bootstrap_config
            tasks:
              - bootstrap_config
tasks:
  - name: bootstrap_config
    kind: Apply
    spec:
      resources:
        - bootstrap_config.yaml
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;지금까지의 작업으로 완성한 &lt;code&gt;~/kudo_sample/galera-operator/operator/operator.yaml&lt;/code&gt; 파일의 내용은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: galera-operator
operatorVersion: 0.1.0
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          - name: bootstrap_config
            tasks:
              - bootstrap_config
tasks:
  - name: bootstrap_config
    kind: Apply
    spec:
      resources:
        - bootstrap_config.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위에서 추가한 Step/Task 의 내용은 부트스트랩 노드를 위한 ConfigMap 리소스를 사용하겠다는 의미로 &lt;code&gt;templates/bootstrap_config.yaml&lt;/code&gt; 파일을 생성하고 아래와 같이 구성한다. (bootstrap_config.yaml)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Name }}-bootstrap
  namespace: {{ .Namespace }}
data:
  galera.cnf: |
    [galera]
    wsrep_on = ON
    wsrep_provider = /usr/lib/galera/libgalera_smm.so
    wsrep_sst_method = mariabackup
    wsrep_cluster_address = gcomm://
    wsrep_sst_auth = &amp;quot;{{ .Params.SST_USER }}:{{ .Params.SST_PASSWORD }}&amp;quot;
    binlog_format = ROW&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 설정 내용은 ConfigMap 리소스로 단일 데이터로 galera.cnf 파일의 내용으로 Galera 설정 내용이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wsrep_cluster_address = gcomm://&lt;/code&gt;: 이 설정을 통해서 이 노드가 부트스트랩 노드가 될 것을 지정한 것이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name: {{ .Name }}-bootstrap&lt;/code&gt;: 각 KUDO Instance에 유일한 값을 지정하는 것으로 ConfigMap도 유일한 이름으로 지정하기 위한 것이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;namespace: {{ .Namespace }}&lt;/code&gt;: 네임스페이스에 속한 리소스로 한정되도록 하는 것이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wsrep_sst_auth = &amp;quot;{{ .Params.SST_USER }}:{{ .Params.SST_PASSWORD }}&amp;quot;&lt;/code&gt;: 노드 간의 동기화를 위해 Galera 클러스터 내부적으로 사용할 자격 증명을 지정한 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 ConfigMap에서 사용하도록 지정한 파라미터들 (&lt;code&gt;.Params&lt;/code&gt;로 시작하는)을 정의하기 위해 &lt;code&gt;params.yaml&lt;/code&gt;을 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
parameters:
  - name: SST_USER
    description: &amp;quot;User to perform SST as&amp;quot;
    default: &amp;quot;root&amp;quot;
  - name: SST_PASSWORD
    description: &amp;quot;Password for SST user&amp;quot;
    default: &amp;quot;admin&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 내용은 SST_USER, SST_PASSWORD 파라미터를 정의하고 기본 값을 설정한 것이다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;일반적으로 &lt;code&gt;Secret&lt;/code&gt; 리소스에 민감한 정보를 구성하는 것을 권장한다. 위와 같이 파라미터로 설정하는 방식은 암호화되지 않고 클러스터에 저장되기 때문에 쉽게 노출될 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 먼저 작업할 내용은 KUDO Operator 설치로 아래와 같이 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리

$KUDO_HOME has been configured at /home/centos/.kudo
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
✅ installed crds
✅ installed namespace
✅ installed service account
Warning: admissionregistration.k8s.io/v1beta1 MutatingWebhookConfiguration is deprecated in v1.16+, unavailable in v1.22+; use admissionregistration.k8s.io/v1 MutatingWebhookConfiguration
✅ installed webhook
✅ installed kudo controller
⌛Waiting for KUDO controller to be ready in your cluster...
✅ KUDO is ready!&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;설치된 KUDO Operator에 작성한 Galera Operator를 설치한다. (KUDO는 Respository를 지원하지만 이번에는 로컬 디렉터리 (~/kudo_sample/galera-operator/operator)를 기준으로 설치한다.)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo install ./

Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
operator default/galera-operator created
operatorversion default/galera-operator-0.1.0-0.1.0 created
instance default/galera-operator-instance created&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;설치할 때 Instance 이름을 별도로 지정하지 않았기 때문에 패키지 이름을 그대로 사용한다. (galera-operator =&amp;gt; galera-operator-instance가 된다)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;설치된 Instance 정보는 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo plan status --instance=galera-operator-instance

Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
Plan(s) for &amp;quot;galera-operator-instance&amp;quot; in namespace &amp;quot;default&amp;quot;:
.
└── galera-operator-instance (Operator-Version: &amp;quot;galera-operator-0.1.0-0.1.0&amp;quot; Active-Plan: &amp;quot;deploy&amp;quot;)
    └── Plan deploy (serial strategy) [COMPLETE], last updated 2020-12-23 06:17:06
        └── Phase deploy (serial strategy) [COMPLETE]
            └── Step bootstrap_config [COMPLETE]&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 ConfigMap 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;deploy plan의 bootstrap_config step으로 생성된 ConfigMap은 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get configmaps

NAME                                 DATA   AGE
galera-operator-instance-bootstrap   1      21m&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;생성된 ConfigMap의 내용은 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl describe configmap galera-operator-instance-bootstrap

Name:         galera-operator-instance-bootstrap
Namespace:    default
Labels:       heritage=kudo
              kudo.dev/instance=galera-operator-instance
              kudo.dev/operator=galera-operator
Annotations:  kudo.dev/last-applied-configuration:
                {&amp;quot;kind&amp;quot;:&amp;quot;ConfigMap&amp;quot;,&amp;quot;apiVersion&amp;quot;:&amp;quot;v1&amp;quot;,&amp;quot;metadata&amp;quot;:{&amp;quot;name&amp;quot;:&amp;quot;galera-operator-instance-bootstrap&amp;quot;,&amp;quot;namespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;creationTimestamp&amp;quot;:nu...
              kudo.dev/last-plan-execution-uid: 3bb3d148-b420-47d5-83d9-baef16c4095c
              kudo.dev/phase: deploy
              kudo.dev/plan: deploy
              kudo.dev/step: bootstrap_config

Data
====
galera.cnf:
----
[galera]
wsrep_on = ON
wsrep_provider = /usr/lib/galera/libgalera_smm.so
wsrep_sst_method = mariabackup
wsrep_cluster_address = gcomm://
wsrep_sst_auth = &amp;quot;root:admin&amp;quot;
binlog_format = ROW

Events:  &amp;lt;none&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;지금까지 작업한 내용이 적용된 것을 확인했으므로 다음 작업을 위해서 KUDO Operator를 포함한 설치된 모든 리소스들을 삭제하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -

Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
customresourcedefinition.apiextensions.k8s.io &amp;quot;operators.kudo.dev&amp;quot; deleted
customresourcedefinition.apiextensions.k8s.io &amp;quot;operatorversions.kudo.dev&amp;quot; deleted
customresourcedefinition.apiextensions.k8s.io &amp;quot;instances.kudo.dev&amp;quot; deleted
namespace &amp;quot;kudo-system&amp;quot; deleted
serviceaccount &amp;quot;kudo-manager&amp;quot; deleted
clusterrolebinding.rbac.authorization.k8s.io &amp;quot;kudo-manager-rolebinding&amp;quot; deleted
Warning: admissionregistration.k8s.io/v1beta1 MutatingWebhookConfiguration is deprecated in v1.16+, unavailable in v1.22+; use admissionregistration.k8s.io/v1 MutatingWebhookConfiguration
mutatingwebhookconfiguration.admissionregistration.k8s.io &amp;quot;kudo-manager-instance-admission-webhook-config&amp;quot; deleted
secret &amp;quot;kudo-webhook-server-secret&amp;quot; deleted
service &amp;quot;kudo-controller-manager-service&amp;quot; deleted
statefulset.apps &amp;quot;kudo-controller-manager&amp;quot; deleted&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bootstrap 서비스 구성&lt;/h2&gt;
&lt;p&gt;다음 작업은 부트스트랩 서비스를 정의하는 것으로, 클러스터 노드가 배포되면 부트스트랩 노드에 연결하기 때문에 이를 위한 서비스를 정의하는 과정이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;operator.yaml 파일에 &lt;code&gt;bootstrap_service step&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;operator.yaml 파일에 &lt;code&gt;bootstrap_service task&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;task에서 사용할 &lt;code&gt;bootstrap_service.yaml&lt;/code&gt; 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 같은 작업을 처리한 operator.yaml은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: galera-operator
operatorVersion: 0.1.0
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          - name: bootstrap_config
            tasks:
              - bootstrap_config
          - name: bootstrap_service
            tasks:
              - bootstrap_service
tasks:
  - name: bootstrap_config
    kind: Apply
    spec:
      resources:
        - bootstrap_config.yaml
  - name: bootstrap_service
    kind: Apply
    spec:
      resources:
        - bootstrap_service.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제 Task에 리소스로 정의한 부트스트랩 서비스 리소스 파일을 &lt;code&gt;templates/bootstrap_service.yaml&lt;/code&gt; 파일에 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: {{ .Name }}-bootstrap-svc
  namespace: {{ .Namespace }}
  labels:
    app: galera-bootstrap
spec:
  ports:
    - port: {{ .Params.MYSQL_PORT }}
      name: mysql
    - port: {{ .Params.SST_PORT }}
      name: sst 
    - port: {{ .Params.REPLICATION_PORT }}
      name: replication
    - port: {{ .Params.IST_PORT }}
      name: ist 
  selector:
    app: galera-bootstrap
    instance: {{ .Name }}
  clusterIP: None&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name: {{ .Name }}-bootstrap-svc&lt;/code&gt;: 서비스의 이름도 고유한 값으로 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app: galera-bootstrap&lt;/code&gt;: 이 서비스를 사용하는 모든 인스턴스들을 관리하는데 사용할 레이블 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port: {{ .Params.MYSQL_PORT }}&lt;/code&gt;: mySQL 사용 포트를 파라미터와 연동하도록 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port: {{ .Params.SST_PORT }}&lt;/code&gt;: SST 연계용 포트를 파라미터와 연동하도록 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port: {{ .Params.REPLICATION_PORT }}&lt;/code&gt;: Replication용 포트를 파라미터와 연동하도록 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port: {{ .Params.IST_PORT }}&lt;/code&gt;: IST용 포트를 파라미터와 연동하도록 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;instance: {{ .Name }}&lt;/code&gt;: 부트스트랩 인스턴스 Selector 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 특별한 서비스는 하나의 인스턴스만 사용하기 때문에 클러스터 IP는 필요하지 않고, 서비스는 Headless일 수 있으며 Kubernetes는 관련된 DNS Endpoint를 생성하게 된다.&lt;/p&gt;
&lt;p&gt;서비스를 정의하면서 추가로 사용한 파라미터들을 &lt;code&gt;params.yaml&lt;/code&gt; 파일에 아래와 같이 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
parameters:
  - name: SST_USER
    description: &amp;quot;User to perform SST as&amp;quot;
    default: &amp;quot;root&amp;quot;
  - name: SST_PASSWORD
    description: &amp;quot;Password for SST user&amp;quot;
    default: &amp;quot;admin&amp;quot;
  - name: MYSQL_PORT
    description: &amp;quot;MySQL port&amp;quot;
    default: &amp;quot;3306&amp;quot;
  - name: SST_PORT
    description: &amp;quot;SST port&amp;quot;
    default: &amp;quot;4444&amp;quot;
  - name: REPLICATION_PORT
    description: &amp;quot;Replication port&amp;quot;
    default: &amp;quot;4567&amp;quot;
  - name: IST_PORT
    description: &amp;quot;IST port&amp;quot;
    default: &amp;quot;4568&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo install ./&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo plan status --instance=galera-operator-instance

Plan(s) for &amp;quot;galera-operator-instance&amp;quot; in namespace &amp;quot;default&amp;quot;:
.
└── galera-operator-instance (Operator-Version: &amp;quot;galera-operator-0.1.0-0.1.0&amp;quot; Active-Plan: &amp;quot;deploy&amp;quot;)
    └── Plan deploy (serial strategy) [COMPLETE], last updated 2020-12-23 07:48:27
        └── Phase deploy (serial strategy) [COMPLETE]
            ├── Step bootstrap_config [COMPLETE]
            └── Step bootstrap_service [COMPLETE]&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Service 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;deploy plan의 bootstrap_service step으로 생성된 service는 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get services

NAME                                     TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                               AGE
galera-operator-instance-bootstrap-svc   ClusterIP   None         &amp;lt;none&amp;gt;        3306/TCP,4444/TCP,4567/TCP,4568/TCP   80s
kubernetes                               ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP                               21d&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;생성된 서비스의 상세 정보는 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl describe service galera-operator-instance-bootstrap-svc

Name:              galera-operator-instance-bootstrap-svc
Namespace:         default
Labels:            app=galera-bootstrap
                  heritage=kudo
                  kudo.dev/instance=galera-operator-instance
                  kudo.dev/operator=galera-operator
Annotations:       kudo.dev/last-applied-configuration:
                    {&amp;quot;kind&amp;quot;:&amp;quot;Service&amp;quot;,&amp;quot;apiVersion&amp;quot;:&amp;quot;v1&amp;quot;,&amp;quot;metadata&amp;quot;:{&amp;quot;name&amp;quot;:&amp;quot;galera-operator-instance-bootstrap-svc&amp;quot;,&amp;quot;namespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;creationTimestamp&amp;quot;:...
                  kudo.dev/last-plan-execution-uid: 7fdc50f2-86b3-4a5f-adb1-eb6c57a90c81
                  kudo.dev/phase: deploy
                  kudo.dev/plan: deploy
                  kudo.dev/step: bootstrap_service
Selector:          app=galera-bootstrap,instance=galera-operator-instance
Type:              ClusterIP
IP:                None
Port:              mysql  3306/TCP
TargetPort:        3306/TCP
Endpoints:         &amp;lt;none&amp;gt;
Port:              sst  4444/TCP
TargetPort:        4444/TCP
Endpoints:         &amp;lt;none&amp;gt;
Port:              replication  4567/TCP
TargetPort:        4567/TCP
Endpoints:         &amp;lt;none&amp;gt;
Port:              ist  4568/TCP
TargetPort:        4568/TCP
Endpoints:         &amp;lt;none&amp;gt;
Session Affinity:  None
Events:            &amp;lt;none&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 결과와 같이 레이블과 포트 및 정의된 정보들이 제대로 설정된 것을 확인할 수 있다. 지금 시점에는 서비스를 사용할 부트스트랩 노드의 실제 인스턴스가 없기 때문에 &lt;code&gt;Endpoint&lt;/code&gt; 정보가 없다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bootstrap Node 배포&lt;/h2&gt;
&lt;p&gt;마지막 작업으로 위에서 설정한 Config, Service를 사용할 부트스트랩 노드의 실제 인스턴스를 배포하는 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;operator.yaml 파일에 &lt;code&gt;bootstrap_deploy step&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;operator.yaml 파일에 &lt;code&gt;bootstrap_deploy task&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;task에서 사용할 &lt;code&gt;templates/bootstrap_deploy.yaml&lt;/code&gt; 파일 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 같은 작업을 처리한 operator.yaml은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: galera-operator
operatorVersion: 0.1.0
plans: 
  deploy:
    strategy: serial
    phases:
      - name: deploy
        strategy: serial
        steps:
          - name: bootstrap_config
            tasks:
              - bootstrap_config
          - name: bootstrap_service
            tasks:
              - bootstrap_service
          - name: bootstrap_deploy
            tasks:
              - bootstrap_deploy
tasks:
  - name: bootstrap_config
    kind: Apply
    spec:
      resources:
        - bootstrap_config.yaml
  - name: bootstrap_service
    kind: Apply
    spec:
      resources:
        - bootstrap_service.yaml
  - name: bootstrap_deploy
    kind: Apply
    spec:
      resources:
        - bootstrap_deploy.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제 Task에 리소스로 정의한 부트스트랩 배포 리소스 파일을 &lt;code&gt;templates/bootstrap_deploy.yaml&lt;/code&gt; 파일에 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Name }}-bootstrap
  namespace: {{ .Namespace }}
  labels:
    app: galera-bootstrap
    instance: {{ .Name }}
spec:
  selector:
    matchLabels:
      app: galera-bootstrap
      instance: {{ .Name }}
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: galera-bootstrap
        instance: {{ .Name }}
    spec:
      containers:
      - image: mariadb:latest
        name: mariadb
        args:
        - &amp;quot;--ignore_db_dirs=lost+found&amp;quot;
        env:
          # Use secret in real usage
        - name: MYSQL_ROOT_PASSWORD
          value: {{ .Params.MYSQL_ROOT_PASSWORD }}
        ports:
        - containerPort: {{ .Params.MYSQL_PORT }}
          name: mysql
        - containerPort: {{ .Params.SST_PORT }}
          name: sst
        - containerPort: {{ .Params.REPLICATION_PORT }}
          name: replication
        - containerPort: {{ .Params.IST_PORT }}
          name: ist
        livenessProbe:
          exec:
            command: [&amp;quot;mysqladmin&amp;quot;, &amp;quot;-p{{ .Params.MYSQL_ROOT_PASSWORD }}&amp;quot;, &amp;quot;ping&amp;quot;]
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
        readinessProbe:
          exec:
            # Check we can execute queries over TCP (skip-networking is off).
            command: [&amp;quot;mysql&amp;quot;, &amp;quot;-p{{ .Params.MYSQL_ROOT_PASSWORD }}&amp;quot;, &amp;quot;-h&amp;quot;, &amp;quot;127.0.0.1&amp;quot;, &amp;quot;-e&amp;quot;, &amp;quot;SELECT 1&amp;quot;]
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
        volumeMounts:
        - name: {{ .Name }}-bootstrap
          mountPath: /etc/mysql/conf.d
      volumes:
        - name: {{ .Name }}-bootstrap
          configMap:
            name: {{ .Name }}-bootstrap
            items:
            - key: galera.cnf
              path: galera.cnf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이전에 정의한 리소스들에 비해서는 복잡해 보이지만 몇 가지만 추가된 것이다. 주요한 몇 가지는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;selector:
  matchLabels:
    app: galera-bootstrap
    instance: {{ .Name }}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;리소스를 연결하는데 사용할 레이블과 인스턴스를 지정한 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ports:
- containerPort: {{ .Params.MYSQL_PORT }}
  name: mysql
- containerPort: {{ .Params.SST_PORT }}
  name: sst
- containerPort: {{ .Params.REPLICATION_PORT }}
  name: replication
- containerPort: {{ .Params.IST_PORT }}
  name: ist&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;컨테이너에서 사용할 포트들을 파라미터와 연계해서 지정한 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;env:
  # Use secret in real usage
- name: MYSQL_ROOT_PASSWORD
  value: {{ .Params.MYSQL_ROOT_PASSWORD }}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;env 섹션을 통해서 파라미터와 연계된 mySQL root 사용자 비밀번호를 저정한 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;livenessProbe:
  exec:
    command: [&amp;quot;mysqladmin&amp;quot;, &amp;quot;-p{{ .Params.MYSQL_ROOT_PASSWORD }}&amp;quot;, &amp;quot;ping&amp;quot;]
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
readinessProbe:
  exec:
    # Check we can execute queries over TCP (skip-networking is off).
    command: [&amp;quot;mysql&amp;quot;, &amp;quot;-p{{ .Params.MYSQL_ROOT_PASSWORD }}&amp;quot;, &amp;quot;-h&amp;quot;, &amp;quot;127.0.0.1&amp;quot;, &amp;quot;-e&amp;quot;, &amp;quot;SELECT 1&amp;quot;]
  initialDelaySeconds: 5
  periodSeconds: 2
  timeoutSeconds: 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;livenessProbe와 readinessProbe를 통해서 접근이 가능한지와 사용할 수 있는지에 대한 검증 명령을 지정한 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  volumeMounts:
  - name: {{ .Name }}-bootstrap
    mountPath: /etc/mysql/conf.d
volumes:
  - name: {{ .Name }}-bootstrap
    configMap:
      name: {{ .Name }}-bootstrap
      items:
      - key: galera.cnf
        path: galera.cnf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ConfigMap이 정의된 Volume을 설정하고 컨테이너에서 사용할 수 있도록 마운트하는 것을 지정한 것이다. Volume도 유일한 이름으로 지정되어야 하고, ConfigMap에서 찾아야할 부분을 Key/Path로 지정해서 마운트될 때 &lt;code&gt;/etc/mysql/conf.d&lt;/code&gt; 라는 이름의 파일로 처리될 수 있도록 지정한 것이다.&lt;/p&gt;
&lt;p&gt;배포를 정의하면서 추가로 사용한 파라미터들을 &lt;code&gt;params.yaml&lt;/code&gt; 파일에 아래와 같이 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
parameters:
  - name: SST_USER
    description: &amp;quot;User to perform SST as&amp;quot;
    default: &amp;quot;root&amp;quot;
  - name: SST_PASSWORD
    description: &amp;quot;Password for SST user&amp;quot;
    default: &amp;quot;admin&amp;quot;
  - name: MYSQL_PORT
    description: &amp;quot;MySQL port&amp;quot;
    default: &amp;quot;3306&amp;quot;
  - name: SST_PORT
    description: &amp;quot;SST port&amp;quot;
    default: &amp;quot;4444&amp;quot;
  - name: REPLICATION_PORT
    description: &amp;quot;Replication port&amp;quot;
    default: &amp;quot;4567&amp;quot;
  - name: IST_PORT
    description: &amp;quot;IST port&amp;quot;
    default: &amp;quot;4568&amp;quot;
  - name: MYSQL_ROOT_PASSWORD
    description: &amp;quot;MySQL root password&amp;quot;
    default: &amp;quot;admin&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;지금까지 작업한 것을 기준으로 정상적으로 동작하는지 테스트를 하도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 설치 (using init)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait  # Webhook TLS를 자체 서명한 버전으로 처리&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Galera Operator 설치 (using install)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo install ./&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Operator Instance 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo plan status --instance=galera-operator-instance

Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
Plan(s) for &amp;quot;galera-operator-instance&amp;quot; in namespace &amp;quot;default&amp;quot;:
.
└── galera-operator-instance (Operator-Version: &amp;quot;galera-operator-0.1.0-0.1.0&amp;quot; Active-Plan: &amp;quot;deploy&amp;quot;)
    └── Plan deploy (serial strategy) [IN_PROGRESS], last updated 2020-12-23 09:23:50
        └── Phase deploy (serial strategy) [IN_PROGRESS]
            ├── Step bootstrap_config [COMPLETE]
            ├── Step bootstrap_service [COMPLETE]
            └── Step bootstrap_deploy [IN_PROGRESS]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 Plan 정보 중에 Phase, Step 이 &lt;code&gt;IN_PROGRESS&lt;/code&gt;로 나타나는 것은 실제 컨테이너가 구동되는 시간이 (이미지를 다운로드하고 구성하는 등의 시간) 필요하기 때문에 나타나는 것으로 시간이 지나면 정상적으로 표시된다. 만일 실패가 되면 원인을 찾아서 다시 처리해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker의 상용화 정책으로 인해서 Docker Repository에서 Image를 다운로드하는 부분에 Rate Limit가 설정되어 있기 때문에 오류가 발생할 수도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;설치된 Bootstrap Node 확인&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;부트스트랩 노드 인스턴스를 배포한 것이기 때문에 Pod가 생성되어 동작하는지를 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get pods
NAME                                                  READY   STATUS    RESTARTS   AGE
galera-operator-instance-bootstrap-59855f6884-gz4rr   1/1     Running   0          5m7s&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;정상적으로 생성되어 동작하는 로그 정보는 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl logs galera-operator-instance-bootstrap-59855f6884-gz4rr

...
2020-12-23  9:24:04 0 [Note] WSREP: Server initialized
2020-12-23  9:24:04 0 [Note] WSREP: Server status change initializing -&amp;gt; initialized
2020-12-23  9:24:04 0 [Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
2020-12-23  9:24:04 2 [Note] WSREP: Bootstrapping a new cluster, setting initial position to 00000000-0000-0000-0000-000000000000:-1
2020-12-23  9:24:04 4 [Note] WSREP: Cluster table is empty, not recovering transactions
2020-12-23  9:24:04 2 [Note] WSREP: Server status change initialized -&amp;gt; joined
2020-12-23  9:24:04 2 [Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
2020-12-23  9:24:04 2 [Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
2020-12-23  9:24:04 0 [Note] Reading of all Master_info entries succeeded
2020-12-23  9:24:04 0 [Note] Added new Master_info &amp;#39;&amp;#39; to hash table
2020-12-23  9:24:04 0 [Note] mysqld: ready for connections.
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 로그 결과와 같이 Galera 가 구성되었고 가입이 가능하도록 준비된 새 클러스터를 생성한 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;그리고 ConfigMap이 제대로 마운트되어 실행되어 있는지는 아래와 같이 동작하는 컨테이너에 연결해서 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl exec -it galera-operator-instance-bootstrap-59855f6884-gz4rr /bin/bash

kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.

root@galera-operator-instance-bootstrap-59855f6884-gz4rr:/# ls /etc/mysql/conf.d/
galera.cnf

root@galera-operator-instance-bootstrap-59855f6884-gz4rr:/# cat /etc/mysql/conf.d/galera.cnf

[galera]
wsrep_on = ON
wsrep_provider = /usr/lib/galera/libgalera_smm.so
wsrep_sst_method = mariabackup
wsrep_cluster_address = gcomm://
wsrep_sst_auth = &amp;quot;root:admin&amp;quot;
binlog_format = ROW&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그리고 bootstrap_service를 구성했을 때 지정되지 않았던 &lt;code&gt;endpoint&lt;/code&gt;가 제대로 설정되었는지는 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl describe service galera-operator-instance-bootstrap-svc

Name:              galera-operator-instance-bootstrap-svc
Namespace:         default
Labels:            app=galera-bootstrap
                  heritage=kudo
                  kudo.dev/instance=galera-operator-instance
                  kudo.dev/operator=galera-operator
Annotations:       kudo.dev/last-applied-configuration:
                    {&amp;quot;kind&amp;quot;:&amp;quot;Service&amp;quot;,&amp;quot;apiVersion&amp;quot;:&amp;quot;v1&amp;quot;,&amp;quot;metadata&amp;quot;:{&amp;quot;name&amp;quot;:&amp;quot;galera-operator-instance-bootstrap-svc&amp;quot;,&amp;quot;namespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;creationTimestamp&amp;quot;:...
                  kudo.dev/last-plan-execution-uid: 11aebdd4-ff44-4f5c-a166-1bd56a9213ff
                  kudo.dev/phase: deploy
                  kudo.dev/plan: deploy
                  kudo.dev/step: bootstrap_service
Selector:          app=galera-bootstrap,instance=galera-operator-instance
Type:              ClusterIP
IP:                None
Port:              mysql  3306/TCP
TargetPort:        3306/TCP
Endpoints:         10.244.84.141:3306
Port:              sst  4444/TCP
TargetPort:        4444/TCP
Endpoints:         10.244.84.141:4444
Port:              replication  4567/TCP
TargetPort:        4567/TCP
Endpoints:         10.244.84.141:4567
Port:              ist  4568/TCP
TargetPort:        4568/TCP
Endpoints:         10.244.84.141:4568
Session Affinity:  None
Events:            &amp;lt;none&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO 및 Operator 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;지금까지의 작업으로 완전하게 동작하는 Galera Bootstrap Node가 구성되어 연결을 수신힐 준비가 되었다는 것을 확인해 보았다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-PART-2-Nodes-%EC%B0%B8%EC%97%AC%EC%99%80-%EC%99%B8%EB%B6%80-%EC%A0%91%EC%86%8D%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%83%9D%EC%84%B1-%EB%B0%8F-StatefulSet-%EA%B5%AC%EC%84%B1&quot;&gt;PART 2&lt;/a&gt;에서는 더 많은 Operator 기능을 구성해서 노드들을 추가하고 클러스터에 참여하도록 하는 방법을 검토해 본다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/blog/blog-2020-06-building-your-first-operator-1.html&quot;&gt;Building your first KUDO Operator - Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>bootstrap node</category>
      <category>galera</category>
      <category>Kubernetes</category>
      <category>Kudo</category>
      <category>kudo-cli</category>
      <category>mariadb</category>
      <category>mysql</category>
      <category>operator</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/58</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Galera-Operator-%EB%8B%A8%EA%B3%84%EB%B3%84%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0#entry58comment</comments>
      <pubDate>Wed, 23 Dec 2020 20:20:24 +0900</pubDate>
    </item>
    <item>
      <title>[OpenStack] CLI - 유동 IP 관련 명령 정리</title>
      <link>https://ccambo.tistory.com/entry/OpenStack-CLI-%EC%9C%A0%EB%8F%99-IP-%EA%B4%80%EB%A0%A8-%EB%AA%85%EB%A0%B9-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;h1&gt;유동 (Floating) IP 관련 명령들&lt;/h1&gt;
&lt;h2&gt;유동 IP 생성&lt;/h2&gt;
&lt;p&gt;오픈스택 대시보드를 통해서도 유동 IP를 생성할 수는 있지만 랜덤하게 생성되는 문제가 았기 때문에 CLI를 이용해서 연속적인 IP를 할당할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;대시보드 유동 IP 생성&lt;/strong&gt;&lt;br&gt;왼쪽 메뉴 트리에서 &lt;code&gt;네트워크&lt;/code&gt; &amp;gt; &lt;code&gt;Floating IP&lt;/code&gt; 화면을 열고 상단의 &lt;code&gt;프로젝트에 IP 할당&lt;/code&gt; 버튼을 눌러서 생성 가능&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;openstack floating ip create
  [--subnet &amp;lt;subnet&amp;gt;]
  [--port &amp;lt;port&amp;gt;]
  [--floating-ip-address &amp;lt;ip-address&amp;gt;]
  [--fixed-ip-address &amp;lt;ip-address&amp;gt;]
  [--description &amp;lt;description&amp;gt;]
  [--project &amp;lt;project&amp;gt; [--project-domain &amp;lt;project-domain&amp;gt;]]
  &amp;lt;network&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;--subnet &lt;subnet&gt;&lt;/strong&gt; : 유동 IP (이름 또는 ID)를 만들려는 서브넷 네트워크 지정 (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--port &lt;port&gt;&lt;/strong&gt; : 유동 IP (이름 또는 ID)와 연결될 포트 지정 (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--floating-ip-address &lt;ip-address&gt;&lt;/strong&gt; : 만들 유동 IP 지정 (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--fixed-ip-address &lt;ip-address&gt;&lt;/strong&gt; : 유동 IP가 연결될 고정 IP 주소 (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--description &lt;description&gt;&lt;/strong&gt; : 유동 IP 설명 (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--project &lt;project&gt;&lt;/strong&gt; : 소유지의 프로젝트 (이름 또는 ID) (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--project-domain &lt;project-domain&gt;&lt;/strong&gt; : 프로젝트가 속한 도메인 (이름 또는 ID), 프로젝트 이름이 충돌할 경우에 사용 (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;network&lt;/strong&gt; : 유동 IP를 할당할 네트워크 (이름 또는 ID)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;유동 IP 삭제&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ openstack floating ip delete &amp;lt;floating-ip&amp;gt; [&amp;lt;floating-ip&amp;gt; ...]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;floating-ip&lt;/strong&gt; : 삭제할 유동 IP (IP 주소 또는 ID)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;유동 IP 리스트&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;openstack floating ip list
    [--network &amp;lt;network&amp;gt;]
    [--port &amp;lt;port&amp;gt;]
    [--fixed-ip-address &amp;lt;ip-address&amp;gt;]
    [--long]
    [--status &amp;lt;status&amp;gt;]
    [--project &amp;lt;project&amp;gt; [--project-domain &amp;lt;project-domain&amp;gt;]]
    [--router &amp;lt;router&amp;gt;]&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;--network &lt;network&gt;&lt;/strong&gt; : 지정한 네트워크 (이름 또는 ID) 에 연계된 유동 IP (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--port &lt;port&gt;&lt;/strong&gt; : 지정한 포트 (이름 또는 ID) 에 연계된 유동 IP (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--fixed-ip-address &lt;ip-address&gt;&lt;/strong&gt; : 지정한 고정 IP에 연계된 유동 IP (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--long&lt;/strong&gt; : 출력에 추가 필드 정보 포함 (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--status &lt;status&gt;&lt;/strong&gt; : 지정한 상태 (&amp;#39;Active&amp;#39;, &amp;#39;Down&amp;#39; 등) 의 유동 IP (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--project &lt;project&gt;&lt;/strong&gt; : 지정한 프로젝트 (이름 또는 ID) 에 연계된 유동 IP (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--project-domain &lt;project-domain&gt;&lt;/strong&gt; : 지정한 프로젝트가 속한 도메인 (이름 또는 ID)에 연계된 유동 IP, 프로젝트 이름이 충돌할 경우에 사용 (버전 2만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;--router &lt;router&gt;&lt;/strong&gt; : 지정한 라우터 (이름 또는 ID) 에 연계된 유동 IP (버전 2만)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;유동 IP 설정&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;openstack floating ip set
  --port &amp;lt;port&amp;gt;
  [--fixed-ip-address &amp;lt;ip-address&amp;gt;]
  &amp;lt;floating-ip&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;--port &lt;port&gt;&lt;/strong&gt; : 유동 IP에 연결할 포트 (이름 또는 ID)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fixed-ip-address &lt;ip-address&gt;&lt;/strong&gt; : 유동 IP에 연결될 고정 IP 주소 (포트에 여러 IP가 있는 경우만)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;floating-ip&lt;/strong&gt; : 속성을 설정할 유동 IP (IP 주소 또는 ID)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;유동 IP 상세 보기&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;openstack floating ip show &amp;lt;floating-ip&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;floating-ip&lt;/strong&gt; : 표시할 대상 유동 IP (IP 주소 또는 ID)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;유동 IP 설정 해제&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;openstack floating ip unset
  --port
  &amp;lt;floating-ip&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;--port&lt;/strong&gt; : 유동 IP 에 연결된 모든 포트 해제&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;floating-ip&lt;/strong&gt; : 연결 해제할 유동 IP (IP 주소 또는 ID)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.openstack.org/python-openstackclient/pike/cli/command-objects/floating-ip.html&quot;&gt;floating IP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/OpenStack</category>
      <category>floating ip</category>
      <category>openstack</category>
      <category>openstack-cli</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/56</guid>
      <comments>https://ccambo.tistory.com/entry/OpenStack-CLI-%EC%9C%A0%EB%8F%99-IP-%EA%B4%80%EB%A0%A8-%EB%AA%85%EB%A0%B9-%EC%A0%95%EB%A6%AC#entry56comment</comments>
      <pubDate>Mon, 21 Dec 2020 19:25:36 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - Operator] KUDO 설치 및 간단한 사용법 검증</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%82%AC%EC%9A%A9%EB%B2%95-%EA%B2%80%EC%A6%9D</link>
      <description>&lt;h1&gt;How to use KUDO&lt;/h1&gt;
&lt;p&gt;당연한 것이지만 KUDO를 테스트하기 위해서는 Kubernetes Cluster가 존재해야 한다. &lt;a href=&quot;https://github.com/kubernetes-sigs/kind&quot;&gt;Kind&lt;/a&gt; 와 &lt;a href=&quot;https://github.com/kubernetes/minikube&quot;&gt;Minikube&lt;/a&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;font color=&quot;darkpuple&quot; size=&quot;3&quot;&gt;

&lt;ul&gt;
&lt;li&gt;Setup a Kubernetes Cluster 1.13+&lt;/li&gt;
&lt;li&gt;Install &lt;a href=&quot;https://kubernetes.io/docs/tasks/tools/install-kubectl/&quot;&gt;kubectl&lt;/a&gt; 1.13+&lt;/li&gt;
&lt;li&gt;Install &lt;a href=&quot;https://cert-manager.io/docs/installation/kubernetes/&quot;&gt;cert-manager&lt;/a&gt;. KUDO는 TLS가 필요한 &lt;a href=&quot;https://kubernetes.io/docs/reference/access-authn-authz/webhook/&quot;&gt;webhook&lt;/a&gt;를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/font&gt; 

&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Minikube&lt;br&gt;Minikue를 통해 KUDO를 로컬에서 개발하고 테스트하기 위해서는 적절한 양의 메모리가 필요한데 Minikube는 2G 메모리를 기준으로 하는데 Kafka의 경우는 최소 10GB를 권장하므로 로컬에서 메모리를 구성하기는 좀 힘들다. 그러나 가능한 리소스가 존재한다면 아래와 같이 기본적인 설정을 바꿔 실행이 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ minikube start --cpus=4 --memory=10240 --disk-size=40g&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kind&lt;br&gt;KIND를 storage operator와 같이 사용하기 위해서는 KIND v0.7.0 이상을 사용해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cert-Manager&lt;br&gt;cert-manager 의존성을 테스트 환경에서 제외할 경우는 KUDO를 보안이 되지 않는 상황으로 자체 서명된 CA 번들을 사용하도록 초기화할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이와 관련된 개발을 위해서 &lt;a href=&quot;https://kudo.dev/blog/blog-2020-07-10-webhook-development.html&quot;&gt;Blog post&lt;/a&gt;를 참고하는 것이 좋다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Install KUDO CLI&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KUDO CLI&lt;/code&gt;는 kubectl에 KUDO 기능을 제공하는 플러그인이다.&lt;/p&gt;
&lt;p&gt;CLI 바이너리를 &lt;a href=&quot;https://github.com/kudobuilder/kudo/releases&quot;&gt;Release Page&lt;/a&gt;에서 다운로드해서 설치가 가능하다. 맥인 경우는 &lt;code&gt;brew&lt;/code&gt;를 통해서 설치가 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ brew tap kudobuilder/tap
$ brew instll kudo-cli&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;맥 OSX에서는 명령 사용을 명시적으로 허가해야 할 수도 있으므로 &lt;a href=&quot;https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac&quot;&gt;Apple support site&lt;/a&gt;를 참고하도록 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;다른 방식은 kubectl 플러그인들을 위한 패키지 매니저인 &lt;a href=&quot;https://github.com/kubernetes-sigs/krew&quot;&gt;krew&lt;/a&gt;를 이용하는 것이다. &lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-KREW-KREW%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C&quot;&gt;KREW란 무엇일까?&lt;/a&gt; 문서를 참고한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl krew install kudo&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그외는 &lt;a href=&quot;https://github.com/kudobuilder/kudo/releases/latest&quot;&gt;최신 릴리즈&lt;/a&gt;에서 플랫폼과 OS에 따라서 바이너리를 다운로드하고 실행가능한 상태로 전환하고 Path에 추가하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ VERSION=x.y.z # look up the current stable release at https://github.com/kudobuilder/kudo/releases/latest
$ OS=$(uname | tr &amp;#39;[:upper:]&amp;#39; &amp;#39;[:lower:]&amp;#39;)
$ ARCH=$(uname -m)
$ wget -O kubectl-kudo https://github.com/kudobuilder/kudo/releases/download/v${VERSION}/kubectl-kudo_${VERSION}_${OS}_${ARCH}
$ chmod +x kubectl-kudo
# add to your path
$ sudo mv kubectl-kudo /usr/local/bin/kubectl-kudo&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Manage KUDO on Cluster&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;KUDO는 Cluster 내부에 설치되는 컴포넌트&lt;/code&gt;이기 떄문에 KUDO CLI를 통해서 초기화(설치) 및 관리 (Upgrade/Delete)를 할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;Requirement&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes 1.15+&lt;/li&gt;
&lt;li&gt;100m &lt;a href=&quot;https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu&quot;&gt;CPU&lt;/a&gt;, 50Mi &lt;a href=&quot;https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory&quot;&gt;Memory&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Install KUDO into Cluster&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init

# 자체 인증서와 시간 설정 추가
$ kubectl kudo init --unsafe-self-signed-webhook-ca --wait

$KUDO_HOME has been configured at /home/centos/.kudo
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
✅ installed crds
✅ installed namespace
✅ installed service account
Warning: admissionregistration.k8s.io/v1beta1 MutatingWebhookConfiguration is deprecated in v1.16+, unavailable in v1.22+; use admissionregistration.k8s.io/v1 MutatingWebhookConfiguration
✅ installed webhook
✅ installed kudo controller
⌛Waiting for KUDO controller to be ready in your cluster...
✅ KUDO is ready!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 명령은 KUDO가 동작하는데 필요한 Controller 배포, Webhook 및 기타 모든 컴포넌트들을 설치한다. 만일 기존에 설치된 KUDO가 존재하는 경우는 우발적인 업그레이드가 발생하지 않도록 중단된다. KUDO 초기화에 필요한 명령의 자세한 옵션들은 &lt;a href=&quot;https://kudo.dev/docs/cli/commands.html#init&quot;&gt;init command&lt;/a&gt;를 참고한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deployment of KUDO CRDs : &lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/&quot;&gt;Custom Resource Definitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Create of kudo-system namespace&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/&quot;&gt;Service Account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/reference/access-authn-authz/rbac/&quot;&gt;Role Binding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/docs/architecture.html#components&quot;&gt;Instance of KUDO Controller&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;수동 설치 또는 다른 설정등의 작업을 해서 처리할 경우는 아래와 같이 처리도 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --dry-run -o=yaml &amp;gt; kudo.yaml
$ kubectl apply -f kudo.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;KUDO init 명령은 Kubernetes에 처리할 명령을 전달하고 종료되기 때문에 실제 클러스터 내부에 필요한 리소스들이 성공적으로 실행되고 있는지를 확인하지 않는다. 따라서 &lt;code&gt;--wait-timeout&lt;/code&gt; 옵션으로 300초 정도 지연을 걸어서 KUDO 관련 리소스들이 구성될 시간을 주는 것이 좋다.&lt;/p&gt;
&lt;p&gt;KUDO가 정상적으로 동작하고 있는지는 &lt;code&gt;kudo-controller-manager-0&lt;/code&gt; 파드가 정상적으로 실행되고 있는지를 확인하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl get -n kudo-system pod

NAME                        READY   STATUS    RESTARTS   AGE
kudo-controller-manager-0   1/1     Running   0          11m&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KUDO 관련 리소스들은 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl api-resources --api-group kudo.dev

NAME               SHORTNAMES   APIGROUP   NAMESPACED   KIND
instances                       kudo.dev   true         Instance
operators                       kudo.dev   true         Operator
operatorversions                kudo.dev   true         OperatorVersion&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;KUDO Upgrades&lt;/h3&gt;
&lt;p&gt;KUDO 업그레이드는 KUDO 초기화 명령을 그대로 사용하며, &lt;code&gt;--upgrade&lt;/code&gt; 옵션을 추가하는 것이다. 기존 초기화에 사용했던 옵션들을 똑같이 지정해 줘야 한다. 그렇지 않으면 KUDO의 기존 초기화 정보를 올바르게 감지하지 못하고 업그레이드가 중단된다.&lt;/p&gt;
&lt;p&gt;예를 들어 아래와 같이 설치를 했다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --namespace kudo-custom --service-account my-kudo-sa&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;업그레이드는 위의 명령에 --upgrade 옵션만 추가하는 형식이 되어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --upgrade --namespace kudo-custom --service-account my-kudo-sa&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주된 옵션들은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;--namespace&lt;/li&gt;
&lt;li&gt;--service-account&lt;/li&gt;
&lt;li&gt;--unsafe-self-signed-ca&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;KUDO Uninstall&lt;/h3&gt;
&lt;p&gt;현재 Uninstall은 명령이 통합되어 있지 않기 때문에 아래와 같이 처리해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo init --unsafe-self-signed-webhook-ca --upgrade --dry-run --output yaml | kubectl delete -f -&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 명령을 통해서 설치된 모든 KUDO CRDs 와 Deployments 등의 리소스들이 클러스터에서 삭제된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;init --upgrade&lt;/code&gt; 명령을 사용하는 것이기 때문에 init 명령에 지정한 옵션을 동일하게 지정해야 한다.&lt;/li&gt;
&lt;li&gt;이 명령은 설치된 모든 Operator들을 삭제한다. 따라서 실행 이전에 검토를 해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;How to Debug KUDO&lt;/h2&gt;
&lt;p&gt;현재 별도로 제공하는 Debugging은 없고 log 파일을 확인하는 것이 어떤 일이 발생했는지를 파악하는 가장 좋은 방법이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl logs -n kudo-system kudo-controller-master-0&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How to Create an Operator from Scratch&lt;/h2&gt;
&lt;p&gt;KUDO CLI를 이용해서 KUDO Operator 구조를 생성하고 Operator를 생성하는 단계별 내용을 확인한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create the Core Operator Structure&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# create operator folder
$ mkdir first-operator
$ cd first-operator

$ kubectl kudo package new first-operator -i -w -v 100 --validate-install

Operator Name: first-operator
Operator directory: operator
Operator Version: 0.1.0
m: 0.1.0
Application Version: 0.1.0
Required KUDO Version: 0.17.2
Required Kubernetes Version: 0.16.0
Project URL: 
Maintainer Name: ccambo
Maintainer Email: ccambo@gmail.com&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;생성된 Operator 패키지 구조는 &lt;code&gt;tree .&lt;/code&gt; 명령으로 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ tree .

.
└── operator
    ├── operator.yaml
    └── params.yaml

1 directory, 2 files&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-i&lt;/code&gt; : 사용자가 직접 입력할 수 있도록 프롬프트를 나타내는 옵션&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-w&lt;/code&gt; : 이미 존재하는 디렉터리와 파일을 overwrite 하는 옵션&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v 100&lt;/code&gt; : verbose log로 100줄 출력 옵션&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--validate-install&lt;/code&gt; : 설치할 때를 가정해서 검증하는 옵션&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;생성된 기본 Operator.yaml의 내용은 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: first-operator
operatorVersion: 0.1.0
plans:
  deploy:
    phases:
    - name: deploy
      steps:
      - name: deploy
        tasks:
        - deploy
      strategy: serial
    strategy: serial
tasks:
- kind: Apply
  name: deploy
  spec: {}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;기본적으로 &lt;code&gt;deploy plan&lt;/code&gt; 과 &lt;code&gt;Apply 형식의 deploy task&lt;/code&gt; 가 기본 생성되어 있다. 이 부분이 아래의 내용을 처리하면서 기존에 존재하는 것을 갱신하는 명령이 없기 때문에 충돌 문제를 발생시키므로 처음에 삭제를 하고 시작하는 것이 좋다. (현재 CLI 버전과 문서의 내용이 일치하지 않는다)&lt;br&gt;따라서 아래와 같이 operator/operator.yaml 파일의 내용을 다음과 같이 처리하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: first-operator
operatorVersion: 0.1.0
plans:{}
tasks:[]&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;p&gt;Add a Maintainer&lt;/p&gt;
&lt;p&gt;위 단계에서 &lt;code&gt;-i&lt;/code&gt; 옵션으로 지정했으면 처리할 필요가 없지만, 여러 사람들을 등록할 경우라면 추가할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo package add maintainer &amp;quot;&amp;lt;your name&amp;gt;&amp;quot; &amp;quot;&amp;lt;mail address&amp;gt;&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a Task&lt;/p&gt;
&lt;p&gt;이 명령은 Operator의 실제 작업 단위인 Task를 추가하는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo package add task -i -v 100 --validate-install
folder walking through directory operator
folder walking through directory templates
Task Name: app
✔ Apply
Task Resource: deployment
✗ Add another Resource: 
folder walking through directory operator
folder walking through directory templates&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a Plan&lt;/p&gt;
&lt;p&gt;이 명령은 Operator가 처리될 떄 작업할 Plan을 추가하는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo package add plan -i -v 100 --validate-install
folder walking through directory operator
folder walking through directory templates
folder walking through directory operator
folder walking through directory templates
Plan Name: deploy
✔ serial
Phase 1 name: main
✔ parallel
Step 1 name: everything
✔ app
✗ Add another Task: 
✗ Add another Step: 
✗ Add another Phase: 
folder walking through directory operator
folder walking through directory templates&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Plan &amp;gt; Phase &amp;gt; Step &amp;gt; Task 순&lt;/code&gt;으로 지정되며, &lt;code&gt;Task, Step, Phase는 여러 개를 지정&lt;/code&gt;할 수 있고, &lt;code&gt;Plan, Phase는 순차처리 (Serial) 또는 병행처리 (Parallel) 를 선택&lt;/code&gt;해서 지정할 수 있다.&lt;br&gt;여기서는 app Task를 deploy plan &amp;gt; main phase &amp;gt; everyting step 에 설정하는 단순한 샘플로 처리한 것이다.&lt;/p&gt;
&lt;p&gt;여기까지 구성된 전체 Operator.yaml의 내용은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
appVersion: 0.1.0
kubernetesVersion: 0.16.0
kudoVersion: 0.17.2
maintainers:
- email: ccambo@gmail.com
  name: ccambo
name: first-operator
operatorVersion: 0.1.0
plans:
  deploy:
    phases:
    - name: main
      steps:
      - name: everything
        tasks:
        - app
      strategy: parallel
    strategy: serial
tasks:
- kind: Apply
  name: app
  spec:
    resources:
    - deployment.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a Parameter&lt;/p&gt;
&lt;p&gt;이 명령은 Operator가 동작할 때 제공할 Parameter 정보를 지정하는 것으로 정의는 &lt;code&gt;operator/params.yaml&lt;/code&gt; 로 저장되고, 이 파라미터를 사용하는 곳은 &lt;code&gt;operator/tempates/&lt;/code&gt; 디렉터리 밑에 추가할 리소스 파일에서 사용하는 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo package add parameter -i -v 100 --validate-install
folder walking through directory operator
folder walking through directory templates
folder walking through directory operator
folder walking through directory templates
Parameter Name: replicas
Default Value: 2
Display Name: Replicas
✔ Display Name: Replicas
✔ Description: Number of replicas that should be run as part of the deployment
✔ false
✗ Add Trigger Plan (defaults to deploy): 
folder walking through directory operator
folder walking through directory templates&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Add Trigger Plan (defaults to deploy)&lt;/code&gt;는 파라미터가 변경되었을 때 호출된 plan을 지정하는 것으로 설정하지 않아도 기본적으로 &lt;code&gt;deploy&lt;/code&gt; plan이 동작한다는 의미이며, 다른 plan을 지정할 경우는 plan 이름을 명기하면 된다.&lt;/p&gt;
&lt;p&gt;실제 파라미터 정보는 아래와 같이 &lt;code&gt;operator/params.yaml&lt;/code&gt; 파일에 추가된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: kudo.dev/v1beta1
parameters:
- default: &amp;quot;2&amp;quot;
  description: Number of replicas that should be run as part of the deployment
  displayName: Replicas
  name: replicas&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add Resource Templates&lt;/p&gt;
&lt;p&gt;지금까지의 작업을 통해서 첫 번째 Operator를 생성했다. 하지만 실제 사용할 Deploy Task에 지정된 deployment 리소스를 구성해야 하는 작업이 남았다. &lt;code&gt;templates/deployment.yaml&lt;/code&gt; 파일을 생성하고 아래와 같이 실제 작업을 지정해 줘야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; operator/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: {{ .Params.replicas }}
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.7.9
          ports:
            - containerPort: 80
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 내용에서 보이는 &lt;code&gt;{{ .Params.replicas }}&lt;/code&gt; 는 &lt;code&gt;.Params&lt;/code&gt; 는 실제 &lt;code&gt;operator/params.yaml&lt;/code&gt; 파일의 내용을 의미하고, &lt;code&gt;.replicas&lt;/code&gt;는 정의된 파라미터를 의미하는 것이다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;지금까지의 작업으로 완전히 동작할 수 있는 아주 간단한 첫 번쨰 Operator를 구성했다.&lt;/p&gt;
&lt;h2&gt;How to Package a KUDO Operator&lt;/h2&gt;
&lt;p&gt;KUDO를 통해서 (정확히는 KUDO Operator) &lt;code&gt;Operator 패키지를 배포하기 위해서는 tarball로 패키징&lt;/code&gt;을 해야 한다. KUDO CLI는 패키지 구성에 대한 검증을 포함해서 패키지를 만들 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Packing operator&lt;/p&gt;
&lt;p&gt;이 작업은 구성한 Operator를 실제 KUDO에 설치하기 위한 Package로 생성하는 부분으로 Local Repository Directory를 구성하고 Tarball을 생성하는 작업이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ rm -rf ~/kudo_sample/repo
$ mkdir -p ~/kudo_sample/repo

# 상대적인 경로를 사용한다. 이 명령은 first-operator 디렉터리에서 실행한 것이다.
$ kubectl kudo package create ~/kudo_sample/first-operator/operator/ --destination=~/kudo_sample/repo -w -v 100 --validate-install
folder walking through directory operator
folder walking through directory templates
package is valid
folder walking through directory operator
folder walking through directory templates
Package created: /home/centos/kudo_sample/repo/first-operator-0.1.0_0.1.0.tgz&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;실제 Packaging 되는 대상은 operator 디렉토리에 있는 &lt;code&gt;operator.yaml&lt;/code&gt;, &lt;code&gt;params.yaml&lt;/code&gt;, &lt;code&gt;templates/*&lt;/code&gt; 만 등록되어야 한다. 따라서 Operator 대상 폴더를 잘 지정해 줘야 한다.&lt;br&gt;현재 경로를 기준으로 ./operator/ 로 지정하면 이상하게 &lt;code&gt;operator/operator.yaml&lt;/code&gt;, &lt;code&gt;operator/params.yaml&lt;/code&gt;, &lt;code&gt;operator/templates/*&lt;/code&gt; 로 처리되어 Package에 대한 index를 생성할 때 Invalid 오류가 발생하게 된다.&lt;br&gt;생성된 후에 &lt;code&gt;vim&lt;/code&gt;을 이용해서 tarball 이 제대로 생성되었는지 확인이 필요하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check the builted package&lt;/p&gt;
&lt;p&gt;로컬 repo 디렉터리인 &lt;code&gt;~/kudo_sample/repo&lt;/code&gt;를 확인하면 생성된 패키지를 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ls ~/kudo_sample/repo
first-operator-0.1.0_0.1.0.tgz&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How to Add an Operator to a Repository&lt;/h2&gt;
&lt;p&gt;리포지토리에 Operator를 추가하는 아주 간단한 방법을 살펴본다. 예제로는 Community라는 리포지토리를 사용하며 관리 권한이 있어야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;리포지토리를 서비스할 수 있는 read/write 권한을 가지는 웹 기반 서버가 필요하다.&lt;/li&gt;
&lt;li&gt;위에서 만든 first-operator가 존재하는 로컬 ~/kudo_sample/repo 경로를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Check repository&lt;/p&gt;
&lt;p&gt;설치된 KUDO가 기본적으로 바라보고 있는 리포지토리를 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo repo list

NAME            URL                                              
*community      https://kudo-repository.storage.googleapis.com/v1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NAME 앞에 &lt;code&gt;*&lt;/code&gt; 표시는 기본 리포지토리임을 나타낸다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build an index file&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo repo index --merge-repo community --url-repo community -w -v 100 --validate-install ~/kudo_sample/repo 
repo configs: { name:community, url:https://kudo-repository.storage.googleapis.com/v1 }

WARNING: operator: /home/centos/kudo_sample/repo/first-operator-0.1.0_0.1.0.tgz is invalid     ### 이 경우가 제대로 폴더구조가 생성되지 않았을 떄 발생하는 오류 indexing 처리되지 않는다.
repo configs: { name:community, url:https://kudo-repository.storage.googleapis.com/v1 }

index /home/centos/kudo_sample/repo/index.yaml created.&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Operator Invalid 상태가 되면 대 부분 tarball의 내용이 잘 못되어 있기 때문에 Package를 create될 때는 처리 상의 문제가 없으면 valid 상태로 나타난다. 생성된 index.yaml을 열어 보면 누락된 것을 확인할 수 잆다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위의 작업은 KUDO 가 바라보고 있는 Community 리포지토리에서 index.yaml 파일을 다운로드해서 ~/kudo_sample/repo 폴더에 존재하는 Operator들을 Index.yaml 파일에 추가(머지)하는 것이다. 이때 사용할 URL은 Community URL을 그대로 사용하고 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;이제 ~/kudo_sample/repo의 모든 내용을 Community 리포지토리에 upload 하면 된다. 그러나 Community URL에 우리가 작성한 Operator를 등록할 수는 없으니 다음 내용과 같이 웹 서버로 구성된 곳의 URL을 사용하는 것으로 처리하면 된다.&lt;/p&gt;
&lt;h2&gt;How to host an Operator in a Local Repository&lt;/h2&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이 글은 테스트용으로 정리된 것이기 때문에 실제 호스팅 가능한 웹 서버를 구축한 것이 아니고, 단순히 ~/kudo_sample/repo 디렉터리를 올려서 테스트하기 위한 용도로 python3의 http server 기능을 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;local repository 폴더에 index 파일 생성&lt;/p&gt;
&lt;p&gt;이 작업은 작성한 Operator에 대한 index.yaml 파일을 생성해서 설치할 때 Operator를 식별하는 인덱스처럼 활용하기 위한 것이다.&lt;/p&gt;
&lt;p&gt;현재 작업은 &lt;code&gt;http://localhost:8090&lt;/code&gt;으로 할 것이기 때문에 이 옵션을 줘야 한다. (생략하면 &lt;a href=&quot;http://localhost:80&quot;&gt;http://localhost:80&lt;/a&gt; 인 것으로 설정된다.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo repo index -w -v 100 --validate-install --url http://localhost:8090 ~/kudo_sample/repo 
WARNING: operator: /home/centos/kudo_sample/repo/first-operator-0.1.0_0.1.0.tgz is invalid   ### 이 경우가 제대로 폴더구조가 생성되지 않았을 떄 발생하는 오류 indexing 처리되지 않는다.
index /home/centos/kudo_sample/repo/index.yaml created.&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Opeartor Invalid 오류가 발생하면 Packaging 처리부터 경로를 잘 지정하고 다시 작업해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;index.yaml 파일에 생성된 결과는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
entries:
  first-operator:
  - appVersion: 0.1.0
    digest: 3360ca9dd47d885982aac2972b170e576eeb54aa82f46302c1b409602475fa50
    maintainers:
    - email: ccambo@gmail.com
      name: ccambo
    name: first-operator
    operatorVersion: 0.1.0
    urls:
    - http://localhost/first-operator-0.1.0_0.1.0.tgz&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HTTP Server 실행&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-baseh&quot;&gt;$ cd ~/kudo_sample/repo

$ python3 -m http.server 8090   
Serving HTTP on 0.0.0.0 port 8090 (http://0.0.0.0:8090/) ...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;local repository를 kudo client에 등록&lt;/p&gt;
&lt;p&gt;이 작업은 내부에서 운영 중인 (이 문서에서는 로컬 머신에서 운영되는) http server를 KUDO Client에 등록해서 인식시키는 작업이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo repo add -v 10 --validate-install local http://localhost:8090
repo configs: { name:community, url:https://kudo-repository.storage.googleapis.com/v1 }

&amp;quot;local&amp;quot; has been added to your repositories&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치가 제대로 되었는지는 아래의 명령으로 확인 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo repo list
NAME            URL                                              
*community      https://kudo-repository.storage.googleapis.com/v1
local           http://localhost:8090&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이미 생성된 Repository를 갱신하는 기능은 없다. 따라서 &lt;code&gt;kubectl kudo repo remove -v 10 --validate-install local&lt;/code&gt; 명령으로 삭제하고 다시 생성해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;local repository를 kudo context의 기본 repository로 설정&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo repo context local

$ kubectl kudo repo list
NAME            URL                                              
community       https://kudo-repository.storage.googleapis.com/v1
*local          http://localhost:8090&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;local repository를 기준으로 설치 검증&lt;/p&gt;
&lt;p&gt;상세 출력을 해주는 &lt;code&gt;-v&lt;/code&gt; 옵션을 사용하면 Operator가 설치되는 위치에서 추적이 가능해 진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ kubectl kudo install --wait -v 50 --validate-install first-operator
repo configs: { name:community, url:https://kudo-repository.storage.googleapis.com/v1 },{ name:local, url:http://localhost:8090 }

repository used { name:local, url:http://localhost:8090 }
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
acquiring kudo client
getting operator package
no local operator discovered, looking for http
no http discovered, looking for repository
getting package reader for first-operator, _
repository using: { name:local, url:http://localhost:8090 }
attempt to retrieve package from url: http://localhost:8090/first-operator-0.1.0_0.1.0.tgz
first-operator is a repository package from { name:local, url:http://localhost:8090 }
Preparing default/first-operator:0.1.0 for installation
parameters in use: map[]
operator.kudo.dev default/first-operator does not exist

operator default/first-operator created
operatorversion default/first-operator-0.1.0-0.1.0 created
instance first-operator-instance created in namespace default
instance default/first-operator-instance created
plan status for instance &amp;quot;first-operator-instance&amp;quot; is not available

instance plan &amp;quot;deploy&amp;quot; is not not finished running: true, term: false, finished: false
...
instance plan &amp;quot;deploy&amp;quot; is not not finished running: true, term: false, finished: false
plan status for &amp;quot;first-operator-instance&amp;quot; is finished

instance default/first-operator-instance ready&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Operator install 에는 상당히 많은 옵션들이 존재한다. &lt;code&gt;kubectl kudo install --help&lt;/code&gt;를 통해서 옵션들을 확인하고 적절하게 사용하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위와 같이 명령어의 출력을 통해서 설치를 확인할 수도 있으며, 실행되고 있는 로컬 웹 서버의 출력을 통해서도 확인이 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ python3 -m http.server 8090   
Serving HTTP on 0.0.0.0 port 8090 (http://0.0.0.0:8090/) ...
127.0.0.1 - - [17/Dec/2020 08:14:06] &amp;quot;GET /index.yaml HTTP/1.1&amp;quot; 200 -
127.0.0.1 - - [17/Dec/2020 08:14:06] &amp;quot;GET /first-operator-0.1.0_0.1.0.tgz HTTP/1.1&amp;quot; 200 -&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 설치된 정보들을 확인해 봐야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# deploy plan으로 생성된 Pod 확인 (replicas 파라미터의 기본 값 2 적용)
$ kubectl get pod
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-77cfbb848f-4tgss   1/1     Running   0          3m17s
nginx-deployment-77cfbb848f-dngpl   1/1     Running   0          3m17s

# parameter를 변경해서 적용되는지 검증 (replicas 파라미터를 4로 변경 검증)
$ kubectl kudo update --wait -v 10 --validate-install --parameter replicas=4 --instance first-operator-instance
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
instance plan &amp;quot;deploy&amp;quot; is not not finished running: true, term: false, finished: false
instance plan &amp;quot;deploy&amp;quot; is not not finished running: true, term: false, finished: false
instance plan &amp;quot;deploy&amp;quot; is not not finished running: true, term: false, finished: false
plan status for &amp;quot;first-operator-instance&amp;quot; is finished

Instance first-operator-instance was updated.

# 변경된 내용 검증
$ kubectl get pod
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-77cfbb848f-2bgjg   1/1     Running   0          50s
nginx-deployment-77cfbb848f-4tgss   1/1     Running   0          19m
nginx-deployment-77cfbb848f-dngpl   1/1     Running   0          19m
nginx-deployment-77cfbb848f-kqh5f   1/1     Running   0          50s

# 설치된 Custom Opserator 정보 확인
$ kubectl kudo get all
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
List of current installed operators including versions and instances in namespace &amp;quot;default&amp;quot;:
.
└── first-operator                     # Opserator
    └── first-operator-0.1.0-0.1.0     # OperatorVersion
        └── first-operator-instance    # OpseratorInstance

# OperatorInstance 의 Plan 상태 확인
$ kubectl kudo plan status --instance=first-operator-instance
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
Plan(s) for &amp;quot;first-operator-instance&amp;quot; in namespace &amp;quot;default&amp;quot;:
.
└── first-operator-instance (Operator-Version: &amp;quot;first-operator-0.1.0-0.1.0&amp;quot; Active-Plan: &amp;quot;deploy&amp;quot;)
    └── Plan deploy (serial strategy) [COMPLETE], last updated 2020-12-17 08:54:59
        └── Phase main (parallel strategy) [COMPLETE]
            └── Step everything [COMPLETE]

# 진단
$ kubectl kudo diagnostics collect --instance=first-operator-instance
Collect ResourceGroup for 3 collectors
Collect Logs for 4 pods
Collect Logs for 1 pods

위의 명령은 수집된 데이터를 현재 경로의 diag 폴더에 생성한다. KUDO와 설치된 Operator에 대한 각종 리소스들과 로그들을 확인할 수 있다.

# Operator 삭제
$ kubectl kudo uninstall --instance=first-operator-instance
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
instance.kudo.dev/v1beta1/first-operator-instance deleted

이 명령은 실제 구동되는 대상인 OperatorInstance와 연계된 리소스들 (Deployments, Pods, ...)을 제거하지만, Operator와 OperatorVersion은 삭제하지 않는다.&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/docs/cli/installation.html#cli-installation&quot;&gt;KUDO CLI Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/docs/runbooks/admin/initialize-kudo.html&quot;&gt;RunBook - Admin - Installation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>Kind</category>
      <category>krew</category>
      <category>Kubernetes</category>
      <category>Kudo</category>
      <category>kudo-cli</category>
      <category>minikube</category>
      <category>operator</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/55</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%82%AC%EC%9A%A9%EB%B2%95-%EA%B2%80%EC%A6%9D#entry55comment</comments>
      <pubDate>Mon, 21 Dec 2020 18:57:38 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - Operator] Kubernetes 상의 Operator Tools 간략 비교</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Operator-Kubernetes-%EC%83%81%EC%9D%98-Operator-Tools-%EA%B0%84%EB%9E%B5-%EB%B9%84%EA%B5%90</link>
      <description>&lt;h1&gt;How to use Operator tools on Kubernetes&lt;/h1&gt;
&lt;p&gt;지난 게시글인 &lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-Kubernetes%EC%9D%98-Operator-%EB%82%98%EB%A6%84%EB%8C%80%EB%A1%9C-%EC%A0%95%EB%A6%AC&quot;&gt;Kubernetes상의 Operator 나름대로 정리&lt;/a&gt;에 이어서 Operator 구현하는 툴들에 대해 정리한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;font color=&quot;seablue&quot; size=&quot;4&quot;&gt;&lt;strong&gt;Kubernetes를 Operators 관점에서 보면...&lt;/strong&gt;&lt;/font&gt;&lt;br&gt;&lt;font color=&quot;DarkCyan&quot; size=&quot;3&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Orchestrate stateful applications using K8s API&lt;/li&gt;
&lt;li&gt;Extend API using Custom Resource Definitions&lt;/li&gt;
&lt;li&gt;Encode domain specific operational knowledge&lt;/li&gt;
&lt;li&gt;Upgrades&lt;/li&gt;
&lt;li&gt;Failure and Recovery Scenarios&lt;/li&gt;
&lt;li&gt;Scaling up / down&lt;/li&gt;
&lt;li&gt;Purpose built per application&lt;/li&gt;
&lt;li&gt;Kubernetes is an Operations API&lt;/li&gt;
&lt;/ul&gt;
&lt;/font&gt;
&lt;/blockquote&gt;
&lt;h2&gt;여러 상황들 검토&lt;/h2&gt;
&lt;p&gt;Kubernetes에 애플리케이션을 배포하는 것은 다양한 형태로 제공된다. 배치 패러다임, YAML 템플릿처리, IaC (Instrastructure as Code) Tooling (ansible, terraform), Controller 등에 대해서 검토한다.&lt;/p&gt;
&lt;h3&gt;IaC&lt;/h3&gt;
&lt;p&gt;IaC 툴들은 애플리케이션과 클라우드 인프라를 관리하는데 도움이 된다. 개발자는 이 툴들을 통해서 Kubernetes의 리소스들을 관리할 수 있다. 사용자는 이 툴들을 설치 및 유지보수 도구로 활용할 수 있다. 대표적인 것이 &lt;code&gt;Ansible&lt;/code&gt;과 &lt;code&gt;Terraform&lt;/code&gt; 등이며, 다른 툴들도 많이 존재한다.&lt;/p&gt;
&lt;p&gt;이 툴들은 가상머신이나 애플리케이션을 배포하는데 종종 사용되며 이런 자원들을 설명하는데 &lt;code&gt;cattle (소 떼) vs. Pets (반려동물)&lt;/code&gt; 패턴을 사용한다. 소 떼는 교체가 쉽고 상태가 중요하지 않은 구성 요소들을 의미하고 주로 웹 서비스, APIs, Jobs 등을 의미한다. 반려동물은 데이터베이스를 포함해 교체나 갱신이 어려운 구성 요소들을 의미한다.&lt;/p&gt;
&lt;p&gt;IaC 툴들은 위의 두 가지 유형의 애플리케이션을 비슷하게 다루기 때문에 설치한 이후의 변화를 알지 못하게 된다. 그러나 Operator는 애플리케이션이 동작하는 동안의 관리를 수행할 수 있으며 사용자 정의 리소스와 컨트롤러가 결합되어 있기 때문에 항상 애플리케이션의 정의된 상태와 현재의 상태가 일치하는지를 지속적으로 관리하게 된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Terraform Kubernetes Provider v1.10.0 에서는 CRD와 CR에 대한 지원을 계획하고 있지만 아직 제공되지 않는 상태이며 다른 &lt;a href=&quot;https://github.com/nabancard/terraform-provider-kubernetes-yaml&quot;&gt;3rd Terraform Provider&lt;/a&gt; 에서 지원이 되는 것들이 존재한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Static Definition Formats&lt;/h3&gt;
&lt;p&gt;일반적인 정의 (YAML) 로 정적인 포맷은 클러스터 외부에서 정의되고 이를 다른 도구와 사용하면 애플리케이션 설치, 업그레이드, 롤백할 수는 있지만 클러스터에 설치된 애플리케이션에 대해 자동으로 요청된 상태로 유지하거나 클러스터와 상호 작용하는 매커니즘은 존재하지 않는다.&lt;/p&gt;
&lt;p&gt;대부분은 클러스터에 추가로 설치되는 구성 요소가 없기 때문에 쉬운 설치와 사용자가 실행할 수 있는 권한만 있으면 된다. 즉 설치까지는 쉽지만 설치 이후의 제어를 할 수 없다는 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Easy installation&lt;/li&gt;
&lt;li&gt;No installed component in the cluster&lt;/li&gt;
&lt;li&gt;No requirements for admin permission on the cluster&lt;/li&gt;
&lt;li&gt;Limited control over the installed application&lt;/li&gt;
&lt;li&gt;No interaction with installed applications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Raw YAML 방식은 애플리케이션을 Kubernetes에 배포하는 가장 쉬운 방법이며 단순한 애플리케이션과 대 규모의 애플리케이션을 시작하는 측면에서는 적합하고 검증되어 있는 방식이지만 변화에 대한 처리 등에는 역시 대응하기 힘들다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kubernetes-sigs/kustomize&quot;&gt;Kustomizse&lt;/a&gt; 방식은 구성된 YAML을 Kubernetes 클러스터에 적용하기 전에 패치, 머지, 변경이 가능하지만 단지 재 사용적인 측면이지, 이미 설치된 애플리케이션의 업그레이드와 상태 변화에 대응하는 방법은 제공되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://helm.sh/&quot;&gt;Helm&lt;/a&gt;은 Kubernetes의 패키지 매니저로 &lt;code&gt;chart&lt;/code&gt; 형식으로 패키지를 관리할 수 있고, 패키지의 설치, 업그레이드, 롤백과 커스터마이즈가 가능하다. Helm v2는 클러스터에 Tiller가 설치되고 클라이언트가 API 서버를 통해서 상호 작용하는 방식이며, Helm v3는 구성 요소에 의존하지 않고 클라이언트가 단독으로 처리하는 새로운 아키텍처를 사용한다. 그러나 역시 설치가 된 애플리케이션과의 상호작용이나 라이프사이클의 상태를 커버하지는 못한다.&lt;/p&gt;
&lt;h3&gt;High Level Frameworks&lt;/h3&gt;
&lt;p&gt;단순한 구성과 사용자 지정 컨틀롤러 사이의 영역에 존재하며 복잡한 Kubernetes의 내부적인 부분을 감추고 간단한 API 작업을 통해 지원하는 방식이다. 그러나 모든 상세한 것들을 다 노출해 주는 것은 아니기 떄문에 완전히 커스텀하게 컨트롤러를 작성하는 것보다는 제약이 존재한다&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MetaController&lt;/code&gt;는 웹 후크 (Webhook)를 기반으로 커스텀 컨트롤러를 개발하는 프레임워크로 커스텀 리소스에 대한 동작을 쉽게 정의할 수 있고 기존 API에 기능을 추가할 수 있도록 한다. jsonnet이나 다른 언어들과 같은 기존 도구와 같이 작업할 수 있지만 이런 자유도는 오히려 라이프사이클 컨트롤러를 개발하는데 복잡성만 더 높이는 상황이 될 수 있다.&lt;/p&gt;
&lt;h3&gt;Custom Controllers&lt;/h3&gt;
&lt;p&gt;커스텀 컨트롤러를 개발하는 것은 애플리케이션의 라이프사이클을 관리하기 위한 코드를 작성하는 과정으로 다양하게 발생할 수 있는 상황들을 관리하기에 많은 노력이 필요하고 Kubernetes에 대한 많고 자세한 지식들이 필요하고 테스트, Kubernetes 클러스터의 업그레이드, Operator의 데이터 저장 변경과 API 변경 등에 대한 부분도 감안해야 하기 때문에 쉬운 작업이 아니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full control over the cluster and deployed resources&lt;/li&gt;
&lt;li&gt;Allows reaction to changes in the cluster, custom resources, validation&lt;/li&gt;
&lt;li&gt;Allows any required behavior&lt;/li&gt;
&lt;li&gt;Requires intimate knowledge of Kubernetes internals&lt;/li&gt;
&lt;li&gt;Is an extra software to develop, test and manage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://kubernetes.io/docs/reference/using-api/client-libraries/&quot;&gt;Kubernetes Client&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 많은 자유도가 있는 방식으로 어떤 Wrapper나 Layer가 없이 직접 Low level로 Kubernetes API를 접속하는 클라이언트를 작성하는 것이며, 각종 언어들 (Go, Java, JS/Typescript, ...)의 활용이 가능하다. 개발에 어떤 제한이나 권장이 없기 때문에 알려진 여러 문제들이나 Kubernetes 영역에서 공통적으로 제시하는 가이드들이 없기 때문에 스스로 해결해야 하는 문제가 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/kubernetes-sigs/kubebuilder&quot;&gt;Kubebuilder&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CRD를 이용해서 Kubernetes API를 구축하기 위한 프레임워크로 Go를 사용하고 새로운 컨틀롤러들을 개발하는데 &lt;a href=&quot;https://github.com/kubernetes-sigs/controller-runtime&quot;&gt;controller-runtime&lt;/a&gt; 라이브러리를 활용한다.&lt;/p&gt;
&lt;p&gt;Kubebuilder 접근 방식은 데이터를 CRD에 저장하고 CR들과 다른 리소스들을 감시하고 작성한 비즈니스 로직을 따르는 변경 처리를 수행하는 루프를 구현한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/operator-framework/operator-sdk&quot;&gt;Operator SDK&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;controller-runtime을 중심으로 Higher level 프레임워크로 많은 Scaffolding들을 제공하고 Helm 이나 Ansible과 통합을 허용하고 많은 Feature Set들을 제공한다.&lt;/p&gt;
&lt;h2&gt;Operator 개발을 위한 툴 비교&lt;/h2&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Kubernetes의 원래 초점은 애플리케이션의 오케스트레이션이기 때문에 상태가 저장되지 않는 일반적인 애플리케이션을 대상으로 했었지만, 실제 운용되는 애플리케이션들은 상태관리가 필요한 것들이 많기 때문에 쿠버네티스 1.5부터 StatefulSet을 추가해서 안정적인 네트워크와 스토리지 개념을 제공했지만, 실제 파드 내에서 어떤 일이 발생하는지를 모르기 때문에 단순히 애플리케이션의 중지/배포/시작의 단순한 처리로는 해결되지 않는 부분이 많으며, 이를 해결하기 위한 운영자의 노력과 지식을 자동화해 보자는 시도가 Operator라는 것으로 진행되고 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;오퍼레이터 패턴에 대해 다시 정리하면 &lt;code&gt;상태 저장 애플리케이션 (상태를 근거로 하나 이상의 인스턴스가 서로 긴밀하게 연결되는)을 구성하고, 유지 관리하기 위한 사람들의 지식을 포착해서 Kubernetes상의 API 를 확장 (CRD or API Aggregation Layer)하고 자동화하는 수단을 제공&lt;/code&gt;하는 것이다.&lt;/p&gt;
&lt;p&gt;대표적인 수행 작업들은 아래와 같으며, 애플리케이션이 동작하는 동안의 거의 모든 작업들을 처리할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;백업&lt;/li&gt;
&lt;li&gt;데이터 조정&lt;/li&gt;
&lt;li&gt;확장&lt;/li&gt;
&lt;li&gt;설정 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이와 같이 Kubernetes 만으로는 해결할 수 없는 것들을 처리히기 위한 운영자의 지식을 자동화하는 Operator는 여러 가지 방법으로 구성할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Operator Framework&lt;/li&gt;
&lt;li&gt;KubeBuilder and MetaController&lt;/li&gt;
&lt;li&gt;KUDO&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;1, 2 번이 SDK에 근거해서 코드를 작성하고 Kubernetes에 대한 지식을 기반으로 하는 것에 반해서 &lt;code&gt;KUDO의 경우는 다형성 기반의 선언적 접근 방식&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Operator Development 관점의 비교&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operator Framework&lt;/th&gt;
&lt;th&gt;Kubebuilder&lt;/th&gt;
&lt;th&gt;Kudobuilder&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Redhat / IBM Project&lt;/td&gt;
&lt;td&gt;Kubernetes SIG API Machinery sub-project&lt;/td&gt;
&lt;td&gt;Polymorphic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implement using Ansible, Helm charts, or Go&lt;/td&gt;
&lt;td&gt;Written in Go with a focus on code generation&lt;/td&gt;
&lt;td&gt;Universal Operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing implementations often don&amp;#39;t cover the entire lifecycle&lt;/td&gt;
&lt;td&gt;Existing implementations often don&amp;#39;t cover the entire lifecycle&lt;/td&gt;
&lt;td&gt;Built using community projects (Kubebuilder, Kustomize, ...)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ansible and Helm are limited. Go requires 1,000s of lines of controller code&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Write Operators as templated YAML manifests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Provide high level CRDs that represent workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Focused on higher level coordination of software lifecycles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Day 2 Operators&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;support test framework &lt;a href=&quot;https://github.com/kudobuilder/kuttl&quot;&gt;kuttl&lt;/a&gt; for check operator&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Operator Framework Architecture&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://miro.medium.com/max/1058/1*GYLAUB7KGCysjPgwek-pPA.jpeg&quot; alt=&quot;Operator Framework Architecture&quot;&gt;&lt;/p&gt;
&lt;h2&gt;KUDO Architecture&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://kudo.dev/images/kudo-architecture.jpg?10x20&quot; alt=&quot;Architecture of KUDO&quot;&gt;&lt;/p&gt;
&lt;p&gt;다음에는 가장 Kubernetes의 선언적 운영 방식에 좀 더 적합한 것으로 판단되는 &lt;a href=&quot;https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-%EC%A0%95%EB%A6%AC-Kubernetes-Universal-Declarative-Operator-Kudobuilder&quot;&gt;KUDO 정리 (Kubernetes Universal Declarative Operator - Kudobuilder)&lt;/a&gt; 해 보도록 한다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/&quot;&gt;KUDO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/reference/using-api/client-libraries/&quot;&gt;Kubernetes Client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://book.kubebuilder.io/&quot;&gt;Kubebuilder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://metacontroller.github.io/metacontroller/&quot;&gt;Webhook with Metacontroller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://operatorframework.io/&quot;&gt;Operator Frameowrk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/extend-kubernetes/api-extension/custom-resources/&quot;&gt;Custom Resource&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.atomicinc.com/2018/05/23/kubernetes-is-an-operations-api/&quot;&gt;Kubernetes is an Operations API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>kubebuilder</category>
      <category>Kubernetes</category>
      <category>kubernetes client</category>
      <category>Kudo</category>
      <category>Kustomize</category>
      <category>metacontroller</category>
      <category>operator</category>
      <category>operator framework</category>
      <category>operator sdk</category>
      <category>webhook</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/54</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Operator-Kubernetes-%EC%83%81%EC%9D%98-Operator-Tools-%EA%B0%84%EB%9E%B5-%EB%B9%84%EA%B5%90#entry54comment</comments>
      <pubDate>Mon, 21 Dec 2020 18:17:51 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - Operator] Kubernetes상의 Operator 나름대로 정리</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Operator-Kubernetes%EC%9D%98-Operator-%EB%82%98%EB%A6%84%EB%8C%80%EB%A1%9C-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;h1&gt;What is the Operator on Kubernetes&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;font color=&quot;seablue&quot; size=&quot;4&quot;&gt;&lt;strong&gt;기본 전제 및 용어 정리&lt;/strong&gt;&lt;/font&gt;&lt;br&gt;&lt;font color=&quot;DarkCyan&quot; size=&quot;3&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes는 선언적 상태관리 시스템이다.&lt;/li&gt;
&lt;li&gt;Operator란 Kubernetes 애플리케이션을 패키징, 배포, 관리하는 방법론이다. (운영자 관점)&lt;/li&gt;
&lt;li&gt;Operator Pattern은 Kuberentes에서 Operator 방법론을 적용해서 확장하는 패턴이다. (확장 개발 관점)&lt;/li&gt;
&lt;li&gt;Oeprator Framework은 Kubernetes에서 Operator를 실제 구현과 관리를 지원하는 Framework이다. (실 구현 관점)&lt;/li&gt;
&lt;li&gt;CRD (Custom Resource Definition)은 Operator로 사용할 상태 관리용 객체들의 Spec을 정의한다. (Schema 관점)&lt;/li&gt;
&lt;li&gt;CR (Custom Resource)은 CRD의 Spec을 지키는 객체들의 실제 상태 데이터 조합이다. (Desired State 관점)&lt;/li&gt;
&lt;li&gt;CC (Custom Controller)는 CR의 상태를 기준으로 현재의 상태를 규정한 상태로 처리하기 위한 컨트롤 루프다. (Current State 관점)&lt;/li&gt;
&lt;/ul&gt;
&lt;/font&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Let&amp;#39;s take a look at the conclusion.&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Operator는 운영자 주로 하는 작업들을 묶어서 자동화하는 것이 목표&lt;/code&gt;이며 다음과 같이 동작하게 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;운영자의 입장에서 관리할 대상에 대한 규정 (Spec)을 정의하고 Kubernetes에 등록한다. (Kubernetes의 CRD로 생성)&lt;/li&gt;
&lt;li&gt;관리할 대상이 유지해야 할 상태 정보를 규정에 맞도록 지정하고 Kubernetes에 등록한다. (Kubernetes의 CR 객체로 생성 - 상태 데이터로서 ETCD에 저장관리)&lt;/li&gt;
&lt;li&gt;상태 유지를 위한 컨트롤러를 구성해서 Kuberentes에 등록 (Kubernetes의 CC로 생성 - 원하는 상태 유지 작업)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CRD 방식을 사용하면 일반적인 Kubernetes 사용법 (kubectl - API Server)으로 운용이 가능하며 CRD로 등록된 리소스 객체를 그대로 사용할 수 있다. (아래 샘플 참조)&lt;/p&gt;
&lt;p&gt;단, Custom Controller 나 API Server 연계 등에 대한 자세한 Spec 등과 가이드가 충분하지 않기 때문에 직접 개발 보다는 지원되는 Framework이나 툴을 이용해서 개발하게 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Operator Fraemwork by Redhat (원래는 CoreOS 지만 Redhat에 흡수됨)&lt;/li&gt;
&lt;li&gt;KUDO : Operator를 작성 배포하기 위한 Kubernetes Addon&lt;/li&gt;
&lt;li&gt;KubeBuilder : CRD 방식이 아닌 API Server를 확장하는 개념으로 현재는 CRD 방식으로 방향성을 잡고 있으므로 용도가 좀 다르다고 생각된다.&lt;/li&gt;
&lt;li&gt;Webhook + MetaController : Custom Controller를 쉽게 작성 및 배포하기 위한 Kuberenetes Addon&lt;/li&gt;
&lt;li&gt;Operator Framework : Operator를 작성 배포하기 위한 Framework&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래의 그림은 Kuberentes의 동작 흐름이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lcom.static.linuxfound.org/sites/lcom/files/kenzan-k8s-1.png&quot; alt=&quot;Kubernetes Processing flow&quot;&gt;&lt;/p&gt;
&lt;p&gt;아래의 그림은 Operator의 동작 흐름이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lcom.static.linuxfound.org/sites/lcom/files/kenzan-k8s-2.png&quot; alt=&quot;Kuberentes Operator Processing Flow&quot;&gt;&lt;/p&gt;
&lt;p&gt;두 그림을 비교해 보면 같은 패턴의 프로세싱 흐름을 볼 수 있다. 따라서 향후 방향성은 CRD 기반의 Operator 중심으로 Kubernetes 모듈화 가속이 될 것으로 판단된다.&lt;/p&gt;
&lt;h2&gt;Operator Framework&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Operattor Framework은 CoreOS에서 시작되었고, 현재는 CoreOS를 인수한 Redhat에서 리딩하고 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Operator Framework은 Cloud에서 하나의 Architecture Pattern으로 분류되며 단순한 작업을 자동화하기 위해 등장한 패턴&lt;/code&gt;으로 보면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kubernetes 애플리케이션을 패키징, 배포, 운영 자동화를 제공하며 다음과 같은 툴을 제공한다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Operator SDK&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;SDK를 이용하면 기존의 Kubernetes client-go 라이브러리를 사용하지 않고도 추상화된 Kubernetes API 사용이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/99E6924C5C2581A10A&quot; alt=&quot;Operator SDK : Build, Test, Iterate&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Operator Lifecycle Manager (OLM)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;OLM은 Operator의 생애주기를 관리하는 매니저로 Operator를 설치하고, 업데이트, 백업, 스캐일링 등을 처리한다. 현재 Operator SDK를 사용하지 않고 OLM을 통해서도 Operator 생성이 가능하다고 되어 있어 애매한 상태이며 향후 통합 또는 변경될 가능성이 있다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Operator Metering&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Operator Framework을 통해서 만들어진 애플리케이션과 Operator 조합을 대상으로 한 미터링 툴로 애플리케이션의 CPU/Memory 등에 대한 시계열 데이터를 활용 (Prometheus와 연동)해서 미터링 리포트 데이터를 생성하고 제공하는 역할을 담당한다.&lt;/p&gt;
&lt;h3&gt;Operator vs. Helm&lt;/h3&gt;
&lt;p&gt;Operator는 Helm 기능을 포함하고 있다. Helm이 애플리케이션의 배포 및 업그레이드를 다룬다면, Operator는 배포 및 업그레이드를 포함해 애플리케이션의 운영까지 관리하는 것이다.&lt;/p&gt;
&lt;p&gt;단, Operator Framework을 도입하면 Helm Chart를 대체할 수도 있지만, Operator 자체를 Helm Chart로 만들수도 있고 (이때는 OLM 사용 배체), Operator 내부에서 애플리케이션 배포를 Helm을 통해서 할 수도 있다. 따라서 현재까지는 Helm을 대체한다기 보다는 서로 다른 용도/목적이라고 생각하면 된다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Real Sample (&lt;a href=&quot;https://github.com/coreos/prometheus-operator&quot;&gt;Prometheus Operator&lt;/a&gt;)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;순수 Prometheus만을 사용하다보면 여러 가지 문제들이 존재한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;점점 많아지는 Scrape, Rule Condition&lt;/strong&gt; : 수집해야 하는 Metric 정보와 규칙들이 많아질 수록 Prometheus Configuration이 복잡해져 효율적 관리가 힘들고 CI/CD를 적용하려다 보면 Configuration 의 부분적 재 사용이 필요한 문제 -&amp;gt; &lt;code&gt;Prometheus Configuration 을 CRD로 적용해서 결합도를 낮추고 (decoupled) 운영 효율성 높임&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상황에 따른 Logical data sharding 필요&lt;/strong&gt; : Operator를 이용하면 N개의 Prometheus를 설치/관리할 수 있다. 위의 decoupling과 연계해서 각 Prometheus른 서로 다른 Metric을 수집할 수 있고 Federation까지 활용해서 복잡한 구조의 Metric 수집도 쉽게 해결이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/991743415C259BAF33&quot; alt=&quot;Prometheus Operator 작동 방식&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 그림에서 ServiceMonitor는 동작하는 프로세스가 아니라 Configuration인 CRD에 해당한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그림의 점선 박스 전체가 CRD&lt;/li&gt;
&lt;li&gt;아래의 &amp;#39;Operator&amp;#39;와 &amp;#39;Prometheus Server&amp;#39; 가 실제 동작하는 파드다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예를 들어 운영자가 Service Monitor를 추가 생성하면 이를 Watching하고 있던 Operator가 새로 생성된 Service Monitor에 현재 동작하고 있는 Prometheus의 Configuration file을 업데이트하고 Prometheus를 재 실행 시킨다.&lt;/p&gt;
&lt;h2&gt;Kubernetes 확장&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;br&gt;Kubernetes의 확장 포인트는 다양하다. 그러나 이 문서에서는 Operator와 관련된 확장에 대해서만 다룬다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://d33wubrfki0l68.cloudfront.net/2475489eaf20163ec0f54ddc1d92aa8d4c87c96b/e7c81/images/docs/components-of-kubernetes.svg&quot; alt=&quot;Kubernetes Cluster Diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 그림은 Kubernetes의 주요 구성 요소들의 관계를 표현한 것으로 Operator와 관련된 부분은 주로 &lt;code&gt;c-m (controller-manager)&lt;/code&gt;와 &lt;code&gt;api server (with Aggregation Layer)&lt;/code&gt; 부분과 연관성이 크다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;확장은 Kubernetes Control Plane과 상호 작용하며, 당연히 장애가 발생할 수 있는 위험성이 추가되는 형식이 된다.&lt;ul&gt;
&lt;li&gt;클라이언트로 동작하는 프로그램을 작성하는 패턴을 &lt;code&gt;Controller Pattern&lt;/code&gt;이라고 한다.&lt;/li&gt;
&lt;li&gt;Kubernetes가 클라이언트로 원격 서비스를 호출하는 방식을 &lt;code&gt;Webhook&lt;/code&gt;이라고 하며 호출을 받아 처리하는 서비스를 &lt;code&gt;Webhook Backend&lt;/code&gt;라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controller는 객체의 &lt;code&gt;.spec (Wanted)&lt;/code&gt; 정보를 읽고 객체의 상태 (current state)와 비교해서 처리한 후에 &lt;code&gt;.status (to ETCD)&lt;/code&gt;를 갱신&lt;/strong&gt;하는 컨트롤 루프다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRD (Custom Resources Definition)은 API 확장의 범주&lt;/strong&gt;에 속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://docs.google.com/drawings/d/e/2PACX-1vQBRWyXLVUlQPlp7BvxvV9S1mxyXSM6rAc_cbLANvKlu6kCCf-kGTporTMIeG5GZtUdxXz1xowN7RmL/pub?w=960&amp;h=720&quot; alt=&quot;Extension Point가 Kubernetes Control Plane과 상호 작용하는 다이어그램&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 그림은 확장 포인트에 따른 Kubernetes Control Plane과의 상호 작용을 나타내고 있다.&lt;/p&gt;
&lt;h2&gt;Operator as an extension of Kubernetes&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;font color=&quot;seablue&quot; size=&quot;4&quot;&gt;&lt;strong&gt;정의&lt;/strong&gt;&lt;/font&gt;&lt;br&gt;&lt;font color=&quot;DarkCyan&quot; size=&quot;3&quot;&gt;&lt;/p&gt;
&lt;p&gt;Operator는 사용자 정의 리소스 (CRD : Custom Resource Definition)를 기반으로 애플리케이션 및 컴포넌트를 관리하는 Kubernetes API 확장 개념&lt;br&gt;&lt;/font&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;서비스 또는 서비스 세트를 관리하는 운영자의 주요 목표가 타겟&lt;/li&gt;
&lt;li&gt;운영자가 주로 사용하는 작업들을 Kubernetes가 제공하는 이상으로 자동화를 하기 위한 방법&lt;/li&gt;
&lt;li&gt;Kubernetes의 워크로드 배포, 실행 등의 자동화 및 수행 방식의 자동화&lt;/li&gt;
&lt;li&gt;Kubernetes의 컨트롤러 개념을 기반으로 하는 CRD를 위한 컨트롤러 역할을 담당하는 API 클라이언트&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What is the Custom Resource&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;font color=&quot;seablue&quot; size=&quot;4&quot;&gt;&lt;strong&gt;정의&lt;/strong&gt;&lt;/font&gt;&lt;br&gt;&lt;font color=&quot;DarkCyan&quot; size=&quot;3&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom Resource는 Custom Object의 모음이며 API 확장을 위한 기본 리소스로 결국은 사용할 객체를 정의하고 추상화한 구조적 데이터와 같다.&lt;/li&gt;
&lt;li&gt;Custom Resource에서의 객체 정의는 완전히 새로운 객체가 아닌 이미 존재하는 Deployments, Services와 같은 기본 객체를 목적에 맞게 조합하고 추상화해서 새로운 이름으로 명시할 수 있다는 의미다.&lt;/li&gt;
&lt;li&gt;Custom Resource Definition은 Costom Resource가 데이터로 어떤 항목이 정의되어야 하는지 등을 저장하는 선언적 메타데이터 객체일 뿐이다. (XML Schema 와 XML의 관게를 생각하면 이해하기 쉽다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/font&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kubernetes에서는 Resource 개념이 존재하고 &lt;code&gt;Resource는 Kubernetes Object들을 모아놓은 Kubernetes API의 Endpoint&lt;/code&gt;라고 정의하고 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Kubernetes API의 Endpoint&lt;/code&gt;라고 부르는 이유는 예를 들어 파드 리소스가 존재할 떄 이를 사용해서 작업을 하기 위해서는 파드의 CRUD 관련 API 묶음이 Kubernetes API로 등록되어야 하기 때문이라고 생각하면 편할 듯 하다.&lt;/li&gt;
&lt;li&gt;현재는 쿠버네티스의 핵심 기능들이 커스텀 리소스를 사용해서 구축되고 있으며 &lt;code&gt;쿠버네티스의 모듈화&lt;/code&gt;쪽으로 가고 있다.&lt;/li&gt;
&lt;li&gt;Custom Resource는 &lt;code&gt;kubectl&lt;/code&gt; 을 통해서 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kubernetes의 몇 가지 용어들을 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Kubernetes Objects&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;Persistent entities in the Kubernetes system&amp;quot;&lt;/strong&gt; 로 정의되어 있으며 &lt;strong&gt;&lt;code&gt;Kubernetes에 저장된 실체들&lt;/code&gt;&lt;/strong&gt; 이라고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;이 객체들로 다음과 같은 정보를 나타낼 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;애플리케이션이 배정된 노드들&lt;/li&gt;
&lt;li&gt;애플리케이션이 사용할 수 있는 리소스들&lt;/li&gt;
&lt;li&gt;애플리케이션 동작에 대한 정책 (어떻게 재 시작할지, 업데이트할지 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Kubernetes 객체들은 생성했을 떄 생성된 실체가 존재하는 것이라기 보다는 상태&lt;/code&gt;를 의미 한다. 예를 들어 kubectl을 통해 파드 1개를 생성하는 요청을 보내면 Kubernetes는 1개의 파드가 필요한 상태를 기록한다. 그리고 Kubernetes는 현재의 상태와 기록된 상태를 비교해서 원하는 상태를 맞추도록 동작하게 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Object Spec and Status&lt;/p&gt;
&lt;p&gt;모든 Kubernetes 객체들은 공통적으로 두 개의 필드를 가진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spec&lt;/strong&gt; : 객체가 가질 상태에 대한 명세 정보&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status&lt;/strong&gt; : 실체 클러스터에서 객체가 가진 상태 정보 (Kubernetes가 계속 검증하고 반영)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubernetes API&lt;/p&gt;
&lt;p&gt;Kubernetes의 객체를 이용해서 CRUD 작업을 하기 위해서는 Kubernetes API를 통해야 한다. 즉 사용자가 kubectl을 사용해서 객체 생성 명령을 실행하면 kubectl은 Kubernetes API로 요청을 하고 Kubernetes는 해당 객체를 생성하게 된다. (물론 kubectl이 아닌 Kubernetes API 클라이언트 라이브러리를 통해서 작업도 가능)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;기본 제공되는 &lt;code&gt;파드 리소스의 경우도 동일하게 생각하면 파드에 대한 정의와 상태 및 오퍼레이션&lt;/code&gt; 으로 생각하면 될 듯 하다. 물론 커스텀 리소스는 이런 기본 제공되는 리소스 이외의 사용자가 정의한 리소스를 의미하고 Kubernetes API를 확장하는 작업에 필요한 것이다.&lt;/p&gt;
&lt;h2&gt;How to use Custom Resource on Kubernetes&lt;/h2&gt;
&lt;p&gt;Custom Resource는 &lt;code&gt;CRD (Custom Resource Definition)&lt;/code&gt; 또는 &lt;code&gt;API Aggregation&lt;/code&gt; 방식으로 사용하는 것이 일반적이다.&lt;/p&gt;
&lt;h3&gt;using by CRD and Operator&lt;/h3&gt;
&lt;p&gt;가장 쉽고 일반적인 방법으로 CRD를 사용하는 것으로 CRD는 이미 Kubernetes에서 제공되는 객체의 한 종류로 &lt;code&gt;kubectl get crd&lt;/code&gt; 명령으로 목록을 확인할 수 있고 &lt;code&gt;kubectl apply -f ...&lt;/code&gt; 명령으로 CRD를 생성할 수도 있다.&lt;/p&gt;
&lt;p&gt;그러나 Custom Resource &lt;code&gt;Definition&lt;/code&gt; 이라는 이름에서 알 수 있듯이 &lt;code&gt;커스텀 리소스가 어떤 데이터로 구성되어 있는지를 정의하는 객체일 뿐 CRD만으로는 실제 Custom Resource를 생성하지는 않으며, 단지 커스텀 리소스의 데이터에 어떤 항목이 정의되어야 하는지 등을 저장하는 선언적 메타데이터 객체일 뿐&lt;/code&gt;이다. 따라서 CRD로 부터 검증되어 실질적 Custom Resource를 생성하려면 CRD에 정의된 객체의 이름, 항목 등을 명시한 YML 파일을 만들고 생성해 줘야 한다. (XML Schema와 XML 파일의 관계로 생각하면 이해하기 쉽다)&lt;/p&gt;
&lt;p&gt;이제 이해를 위한 간략한 샘플을 보도록 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;STEP 1 - 리소스 스키마인 Definition 정의와 데이터에 대한 선언적 Resource 정의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;quot;my-crd.yml&amp;quot; 파일로 CRD를 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: &amp;quot;apiextensions.k8s.io/v1&amp;quot;
kind: &amp;quot;CustomResourceDefinition&amp;quot;      # &amp;lt;- 객체 유형은 CRD
metadata:
  name: &amp;quot;ccambo.morris.com&amp;quot;
spec:                                 # &amp;lt;- 리소스 객체 정의 Spec
  group: &amp;quot;morris.com&amp;quot;
  version: &amp;quot;v1alpha1&amp;quot;
  scope: &amp;quot;Namespaced&amp;quot;
  names:
    plural: &amp;quot;ccambo&amp;quot;
    singular: &amp;quot;ccambo&amp;quot;
    kind: &amp;quot;Ccambo&amp;quot;                    # &amp;lt;- 객체 유형 정의
  validation:
    openAPIV3Schema:
      required: [&amp;quot;spec&amp;quot;]
      properties:
        spec:
          required: [&amp;quot;Pet&amp;quot;]           # &amp;lt;- 필수 정보 정의
          properties:
            Pet:                      # &amp;lt;- 데이터 형식 정의
              type: &amp;quot;string&amp;quot;
              minimum: 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 내용은 ccambo라는 리소스에 Pet 이라는 데이터 항목이 string 형식으로 반드시 존재해야 하는 것을 명시한 것이다. 따라서 정의한 규칙을 따르는 YML 파일만 리소스를 생성할 수 있다.&lt;/p&gt;
&lt;p&gt;위와 같이 CRD를 정의하고 CRD를 생성한다. (Definition 생성)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl apply -f my-crd.yml

customresourcedefinition.apiextensions.k8s.io/ccambo.morris.com created

$ kubectl get crd

NAME                                                  CREATED AT
...
ccambo.morris.com                                     2020-12-03T09:42:29Z
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;규정에 맞는 실제 사용할 커스텀 리소스는 아래와 같이 &amp;quot;ccambo-resource.yml&amp;quot; 파일로 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: &amp;quot;morris.com/v1alpha1&amp;quot;
kind: &amp;quot;Ccambo&amp;quot;                  # &amp;lt;- 정의된 객체 유형
metadata:
  name: &amp;quot;my-custom-resource&amp;quot;
spec:
  Pet: &amp;quot;I like Hedgehog&amp;quot;        # &amp;lt;- 필수 데이터 항목 정의&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 내용은 검증용 샘플로 CRD로 정의된 형식에 맞춰서 작성한 것으로 아래의 명령으로 생성한다. (Resource 생성)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl apply -f ccambo-resource.yml

ccambo.morris.com/my-custom-resource created

$ kubectl get ccambo

NAME                 AGE
my-custom-resource   2m54s&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;간단하게 커스텀 리소스를 생성해 보았다. 그러나 이런 &lt;code&gt;리소스도 결국은 ETCD에 저장되는 단순한 데이터일 뿐이고 실제 동작하는 파드나 서비스는 아니라는 것&lt;/code&gt;을 명심해야 한다.&lt;/p&gt;
&lt;p&gt;커스텀 리소스는 리소스가 어떤 목적인지를 나타내는 데이터라면 이제 리소스가 실제 어떻게 동작할 것인지를 정의할 선언적 컨트롤러를 구현해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;STEP 2 - 동작을 규정하는 선언적 컨트롤러 구현&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;쿠버네티스의 내장 객체를 예시로 생각해 보면 &lt;code&gt;Deployment 객체의 목적은 &amp;quot;ReplicaSet&amp;quot;을 만드는 것이고, ReplicaSet 객체의 목적은 Matchlabel에 맞는 &amp;quot;파드&amp;quot;를 생성하는 것&lt;/code&gt;이다. 따라서 커스텀 리소스가 어떻게, 무엇을 위해서 동작할 것인지를 정의하는 부분이다.&lt;/p&gt;
&lt;p&gt;이런 &lt;code&gt;선언적인 동작을 수행하도록 구현한 것이 컨트롤러 (Controller) 이며 커스텀 리소스가 가져야할 최종적인 상태를 유지하는 역할&lt;/code&gt;이다. 예를 들어 커스텀 리소스의 목적이 &amp;quot;CAT&amp;quot;, &amp;quot;HEDGEHOG&amp;quot; 라는 두개의 파드가 실행 상태에 있어야 한다는 것이라면 컨틀롤러는 두 개의 파드를 생성하려고 시도하고 최종적으로 두 개의 파드가 실행 중인 상태를 유지한다.&lt;/p&gt;
&lt;p&gt;이 부분부터는 Kubernetes에서 제공하는 저수준의 API를 알아야 하고 어떻게 구현해야 하는지에 대한 가이드도 별로 없기 때문에 &lt;code&gt;Operator SDK&lt;/code&gt;를 이용해서 처리해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지금까지 언급한 내용을 정리하면 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;쿠버네티스의 객체 중에 하나인 CRD를 통해서 커스텀 리소스에 대한 정의를 생성한다.&lt;/li&gt;
&lt;li&gt;CRD로 정의된 포맷대로 커스텀 리소스를 생성한다.&lt;/li&gt;
&lt;li&gt;커스텀 컨트롤러를 구현한다.&lt;/li&gt;
&lt;li&gt;커스텀 컨트롤러는 쿠버네티스의 Watch API를 통해서 커스텀 리소스가 생성된 것을 식별한다.&lt;/li&gt;
&lt;li&gt;커스텀 컨트롤러는 커스텀 리소스의 설정에 맞는 동작을 수행한다. (파드 또는 서비스 생성들...)&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;br&gt;Clound Native Infrastructure에서 자주 언급되는 단어들 중에 선언적 (Declarative) 와 명령적 (Imperative) 동작이라는 것이 있다. 쿠버네티스는 선언적 및 명령적 동작을 모두 지원하지만 가능하면 선언적 동작을 권장하고 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;선언적 동작&lt;/strong&gt; : 원하는 상태를 미리 정의하고, 현재 상태가 원하는 상태가 되도록 하는 것을 의미한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;명령적 동작&lt;/strong&gt; : 지정한 상태로 만들어 버리는 것을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f ...&lt;/code&gt; 는 선언적이고, &lt;code&gt;kubectl create ...&lt;/code&gt; 는 명령적인 것을 의미한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;using by API Aggregation&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;API Aggregation은 여러 개의 API 서버를 하나의 API 서버인 것처럼 사용할 수 있도록 하는 쿠버네티스의 기능&lt;/code&gt;을 말하는 것으로 공식 문서들이 너무 어렵게 쓰여 있지만 정리하면 그렇게 어려운 것은 아니다.&lt;/p&gt;
&lt;p&gt;쿠버네티스는 API 서버의 기능을 직접 확장해 사용할 수 있도록 API Registration이라는 것을 제공한다. 별도로 필요한 기능이 있다면 이를 직접 별도의 서버에 구현한 뒤에 쿠버네티스에 등록하는 방식이다. 그냥 보기에는 기존 쿠버네티스 API 서버 엔드포인트와 별도로 구현한 API 서버의 엔드포인트가 각각 존재하기 때문에 각각 접속해야 할 것 같지만, 쿠버네티스는 API Aggreation이라는 기능을 통해서 여러 개의 API 서버에 접근할 수 있도록 추상화된 엔드포인트를 제공한다.&lt;/p&gt;
&lt;p&gt;추가적인 기능을 구현한 별도의 API 서버는 &lt;code&gt;apiserver-builder&lt;/code&gt;나 &lt;code&gt;kubebuilder&lt;/code&gt;를 통해 구현할 수 있다. (실제 구현할 때는 &lt;a href=&quot;https://github.com/kubernetes/sample-apiserver&quot;&gt;sample-apiserver&lt;/a&gt;가 제공되고 있으므로 직접 테스트가 가능하다) 이를 통해서 &lt;code&gt;구현된 API 서버를 APIService 객체를 통해 API 서버에 등록&lt;/code&gt;하면 직접 구현한 API 기능을 기존의 API 서버를 통해서 사용할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;정리하면 아래의 그림과 같이 기존 쿠버네티스 API 서버가 일종의 프록시 역할을 대신하는 것이라고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?export=view&amp;id=11gycDo4WUbdTS2eErjeBq4PirLxLO84p&quot; alt=&quot;API Aggregation을 통한 API 서버 연계&quot;&gt;&lt;/p&gt;
&lt;p&gt;확장된 API 서버 내부에서 커스텀 리소스에 대한 API를 정의한 뒤 이를 APIService 를 통해서 등록하고 사용하면 된다. 아래의 내용은 sample-apiserver 예제에서 제공하는 APIService 내용이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1alpha1.wardle.k8s.io
spec:
  insecureSkipTLSVerify: true
  group: wardle.k8s.io
  groupPriorityMinimum: 1000
  versionPriority: 15
  service:
    namespace: wardle             # 1. wardle 이라는 네임스페이스에 존재하는
    name: api                     # 2. api 라는 서비스를 통해 확장된 API 서버에 접근할 수 있다.
  version: v1alpha1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;개념적으로는 간단해도 별도의 API 서버를 구축하는 작업은 만만치 않아 보인다. &lt;code&gt;apiserver-builder&lt;/code&gt;나 &lt;code&gt;kubebuilder&lt;/code&gt;를 이용해서 서버를 빌드해야하고 쿠버네티스 API 서버와의 인증 작업도 신경써야 하며, API Aggregation을 통해서 커스텀 리소스를 정의헀다고 해도 커스텀 컨틀로러의 구현도 직접해야 하고 별도의 API 서버를 위한 스토리지 구성등도 따로 해줘야 하는 상황이 발생할 수 밖에는 없다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/oracle/mysql-operator/issues/49&quot;&gt;mysql-operator의 mysql-apiserver 아키텍쳐&lt;/a&gt; 를 만든 사람이 구성한 아래의 그림을 살펴보는 것이 좋다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/244160/35272805-6b20706e-002e-11e8-813c-97c592aaabe2.png&quot; alt=&quot;mysql-apiserver Architecture&quot;&gt;&lt;/p&gt;
&lt;p&gt;작성자는 &lt;code&gt;CRD의 기능이 점차 API Aggregation의 기능을 따라잡고 있으니 이런 구조는 필요없을 것&lt;/code&gt;이라고 말하고 있다.&lt;/p&gt;
&lt;p&gt;결론적으로 커스텀 리소스만을 사용할 것이라면 API Aggregation보다는 Operator를 통한 CRD를 사용하는 추세로 가는 것으로 보이고, 지금까지의 설명으로도 Operator가 더 쉽게 느껴진다. 물론 API Aggregation 기능이 커스텀 리소스에만 사용되는 것이 아니기 때문에 API Server의 확장을 위해서도 필요하므로 검토해 봐야 한다. 현재라도 CRD에서 지원하지 않는 기능은 API Aggregation을 통해서 확장하는 방법외에는 없다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/extend-kubernetes/operator/&quot;&gt;Operator Pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/extend-kubernetes/api-extension/custom-resources/&quot;&gt;Custom Resource&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/&quot;&gt;Aggregation Layer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/architecture/controller/&quot;&gt;Controller&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>CRD</category>
      <category>custom resource</category>
      <category>custom resource definition</category>
      <category>Kubernetes</category>
      <category>operator</category>
      <category>operator framework</category>
      <category>operator pattern</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/53</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Operator-Kubernetes%EC%9D%98-Operator-%EB%82%98%EB%A6%84%EB%8C%80%EB%A1%9C-%EC%A0%95%EB%A6%AC#entry53comment</comments>
      <pubDate>Mon, 21 Dec 2020 17:21:59 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - Operator] KUDO 정리 (Kubernetes Universal Declarative Operator - Kudobuilder)</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-%EC%A0%95%EB%A6%AC-Kubernetes-Universal-Declarative-Operator-Kudobuilder</link>
      <description>&lt;h1&gt;What is KUDO (Kubernetes Universal Declarative Operator - Kudobuilder)&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;font color=&quot;seablue&quot; size=&quot;4&quot;&gt;&lt;strong&gt;정의&lt;/strong&gt;&lt;/font&gt;&lt;br&gt;&lt;font color=&quot;DarkCyan&quot; size=&quot;3&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KUDO는 Operator를 위한 Oeprator&lt;/li&gt;
&lt;li&gt;선언적 접근 방식을 제공해 애플리케이션의 전체 라이프사이클을 커버하는 범용 오퍼레이터&lt;/li&gt;
&lt;li&gt;대부분의 경우는 YAML 만을 사용할 수 있는 정도로 쉽게 Kubernetes Operator를 만들 수 있는 툴 킷 + 런타임&lt;/li&gt;
&lt;li&gt;라이프사이클 Operator들 간의 공통화 및 재 사용성&lt;/li&gt;
&lt;li&gt;복잡한 상태 저장 애플리케이션에 최적화&lt;/li&gt;
&lt;li&gt;Operator 구축시 개발자의 생산성 향상&lt;/li&gt;
&lt;li&gt;서비스 운영시 운영자의 생산성 향상&lt;/li&gt;
&lt;li&gt;기본 제공되는 &lt;a href=&quot;https://github.com/kudobuilder/operators/&quot;&gt;리파지토리의 여러 Operator들 중&lt;/a&gt;에서 고르거나 쉽게 커스터마이징이 가능하다.&lt;/li&gt;
&lt;li&gt;표준화된 방식으로 Operatators가 동작할 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/font&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;KUDO는 범용적이고 선언적인 접근 방식 (UD - Universal and Declarative)&lt;/code&gt;을 취하기 때문에 어떠한 코드를 사용하지 않고 오퍼레이터를 구성할 수 있도록 지원한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Developer 측면&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개념적으로 &amp;quot;runbooks&amp;quot;와 유사하게 Kubernetes 객체들과 &amp;quot;Plans&amp;quot;를 이용해서 라이프사이클 운영 시퀀스에 대한 추상화 제공&lt;/li&gt;
&lt;li&gt;라이프사이클 작업들 간의 공통성과 재 사용성&lt;/li&gt;
&lt;li&gt;작업들 간의 코드 중복과 재사용 가능한 Boilerplate 감소&lt;/li&gt;
&lt;li&gt;사용자 환경에 맞춤화를 위한 기본 Operator의 &amp;quot;Flavor&amp;quot;를 생성하는 매커님즘 제공&lt;/li&gt;
&lt;li&gt;소프트웨어와 Day 2 운영 모범사례를 제공할 수 있는 도구들을 ISV들에 제공&lt;/li&gt;
&lt;li&gt;Kubernetes 리소스의 TDD 활성화를 위한 테스트 툴 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User 측면&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;워크로드들을 배포하고, 관리하고 디버깅할 수 있는 &lt;code&gt;kubectl kudo&lt;/code&gt; 플러그인 제공&lt;/li&gt;
&lt;li&gt;KUDO는 작은 Kubernetes로 &lt;code&gt;kubectl&lt;/code&gt; 사용 지원 - KUDO is Kubernets! (단, KUDO 역시 CRD로 정의된 리소스일 뿐)&lt;/li&gt;
&lt;li&gt;클러스터에 다양한 Operator들을 배포하는 것이 일반적이기 때문에, 유사한 API 및 CLI / Workflow 경험을 제공&lt;/li&gt;
&lt;li&gt;모든 워크로드를 CRD로 관리하여 GitOps 촉진&lt;/li&gt;
&lt;li&gt;이미 존재하는 Operator들도 KUDO를 통해 관리가 가능하고, CRD, CR 및 기타 Operator들을 파악해서 다른 워크로드들의 일부로 종속성를 지원&lt;/li&gt;
&lt;li&gt;(향후) 엔터프라이즈 워크로드를 위한 중앙 집중식 지원, 메트릭/경고 기능 및 보안과 RBAC 기능을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f&lt;/code&gt; 또는 &lt;code&gt;helm install&lt;/code&gt;로 애플리케이션의 라이프사이클을 관리하기 힘든 경우에 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상태 저장 서비스를 사용하고 백업/복원과 같은 특정 워크플로우를 자동화해야 하는 경우이거나 스케일 업/다운 시 재 구성이 필요한 일반적인 작업을 수행할 떄 추가적인 로직의 실행이 필요한 경우&lt;/li&gt;
&lt;li&gt;배포 워크플로우에 순차적인 또는 병렬로 처리할 단계가 존재하는 마이크로 서비스를 관리하는 경우 (예를 들어, 동적으로 구성을 생성하거나 마이그레인션 완료를 대기해야 하는 등의 작업)&lt;/li&gt;
&lt;li&gt;혼돈 및 탄력성 테스트와 같이 애플리케이션 라이프사이클 전체에 걸쳐 반복적인 작업을 자동화해야할 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Concepts&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Operator Package&lt;/strong&gt; : &lt;code&gt;Helm Chart 나 Homebrew formula와 같은 KUDO Operator를 정의하는 파일의 모음&lt;/code&gt;이며, 로컬 (folder or tarball) 또는 원격 (tarball URL)일 수도 있고, 실행할 애플리케이션에 대한 모든 Kubernetes 리소스 및 워크플로우가 정의되어 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repository&lt;/strong&gt; : Operator 패키지를 보관하는 곳으로 로컬 폴더 또는 원격 URL일 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KUDO Manager&lt;/strong&gt; : KUDO Operator들을 이해하고 Plan들을 어떻게 실행해야 하는지를 알고 있는 Kubernetes Controller 들의 집합이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan&lt;/strong&gt; : Plan은 Operator의 주요 워크플로우 단위로, 정의된 순서에 따라 클러스터에 Kubernetes 리소스를 적용하거나 삭제하는 일련의 단계를 지정한다.&lt;br&gt;&amp;nbsp;&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;Usage&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;사용자가 리포지토리에서 Operator Package를 가져와서 KUDO Manager에 제출하면, KUDO Manager는 자동 또는 요구에 따라서 Operator의 Plan들을 실행하게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Operator Plans&lt;/p&gt;
&lt;p&gt;Operator는 몇 가지 Plan들로 구성되고 KUDO를 통해 수행될수 있는 구조화된 방법으로 쓰여진 runbook (실행 책자?)로 보면 된다. 각 Plan은 몇 개의 Phase로 이뤄지고 각 Phase에는 몇 개의 Step 들이 존재한다. 모든 Operator들은 클러스터에 애플리케이션을 배포하는 기본 Plan인 Deploy Plan을 포함해야 한다. 복잡한 애플리케이션인 경우는 백업 및 복원 또는 업그레이드 Plan을 정의한다. 각 Plan의 Phase 와 Step은 순차적 또는 병렬로 동작할 수 있으며, 별도로 지정하지 않는 경우라면 기본적으로 순차 처리한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?export=view&amp;id=1UJGi1jLVmwpQqXax7WEkRCkDtPI9Uho1&quot; alt=&quot;Kudo Plan 구성&quot;&gt;&lt;/p&gt;
&lt;p&gt;다양한 노드 유형 (부트스트랩, 마스터, 에이전트) 들을 순차적으로 처리하는 데이터 서비스의 추상적인 배포 플랜의 예시는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;plan: deploy
  phase: bootstrap-node
    step: start-bootstrap-node
  phase: master-nodes
    step: start-master-nodes
  phase: agent-nodes
    step: start-agent-nodes
  phase: deploy-cleanup
    step: remove-bootstrap-node&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;순차적인 처리의 기준은 앞 phase의 진행이 성공적이어야 다음 phase로 진행되는 방식이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Operator Paramters&lt;/p&gt;
&lt;p&gt;Operator들은 파라미터를 사용해서 커스터마이징이 가능하다. 이부분이 다른 비슷한 툴들과 다른 점으로 Operator Paramter는 Plan과 연계되어 있기 때문에 파라미터가 변경되면 자동으로 연계된 Plan이 실행된다. 아래의 예는 애플리케이션 노드의 수와 노드 컨테이너가 사용하는 메모리를 나타내는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;parameters:
  - name: NODE_COUNT
    description: &amp;quot;Number of application nodes&amp;quot;
    default: &amp;quot;3&amp;quot;
    trigger: &amp;quot;deploy&amp;quot;
  - name: NODE_MEM_MIB
    description: &amp;quot;Memory request (in MIB) for node container&amp;quot;
    default: &amp;quot;4096&amp;quot;
    trigger: &amp;quot;update&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 예에서 볼 수 있는 것처럼 각 파라미터는 &lt;code&gt;tigger&lt;/code&gt; 필드를 가지고 있는데, 이 필드 값을 통해서 파라미터의 값이 변경되었을 때 실행될 Plan을 지정하게 된다. Operator는 실행 중인 애플리케이션의 안전하게 갱신되는 것을 요구하기 때문에 파라미터의 변경에 밀접하게 연계된다. 예를 들어 NODE_COUNT의 변경은 단지 deploy plan을 실행하면 되지만, NODE_MEM_MIB는 canary 또는 blue/green deploy가 실행되어 새 버전을 Rollout 하는 것이 필요하다. 만일 파라미터에 trigger가 지정되지 않으면 기본적인 deploy plan이 실행된다.&lt;/p&gt;
&lt;p&gt;물론 파라미터와 연계되지 않고도 수동으로 Plan을 동작시킬 수도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Default Plans&lt;/p&gt;
&lt;p&gt;특별한 목적으로 KUDO가 사용하는 세 가지 Plan이 존재하며 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;deploy&lt;/strong&gt; : (필수) 기본 배포 Plan으로 위에서 설명한 파라미터에 trigger가 지정되지 않았을 떄 실행되는 기본 Plan 이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;upgrade&lt;/strong&gt; : Operator 자체를 새로운 버전으로 갱신할때 사용하는 Plan으로 이 Plan이 지정되지 않으면 deploy plan이 실행된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cleanup&lt;/strong&gt; : Operator가 삭제될 때 사용되는 plan으로 Operator가 제거될 때 graceful shutdown이나 사용한 리소스들을 해제할 수 있는 기회가 된다. 추가적인 사항은 &lt;a href=&quot;https://kudo.dev/docs/developing-operators/plans.html#cleanup-plans&quot;&gt;Cleanup Plans&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deep dive into the KUDO CRDs&lt;/p&gt;
&lt;p&gt;다시 정리하면 &lt;code&gt;KUDO  Manager는 Operator들을 처리하고, Plan들을 호출하는 등의 작업을 수행하는 클러스터에 배포된 Kubernetes Controller 세트이며, Kubernetes CDR 들을 활용해서 Kubernetes API를 확장&lt;/code&gt;한다.&lt;/p&gt;
&lt;p&gt;중요한 구성들은 다음과 같다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Operator&lt;/strong&gt; : Operator CRD는 Kubernetes 클러스터에서 실행할 애플리케이션에 대한 High levle의 설명을 포함한다. Operator 자체를 의미하는 것이 아니라 단지 대상이 되는 애플리케이션의 메타 정보를 의미하는 것이고, 특정 plan이나 리소스등은 포함되지 않는다. &lt;code&gt;클러스터에 여러 버전의 애플리케이션을 설치할 수 있도록 공통된 메타정보를 관리하는 하는 그룹 개념&lt;/code&gt;으로 생각하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OperatorVersion&lt;/strong&gt; : 애플리케이션의 특정한 버전을 의미하고, Operator에 의해서 사용되는 Kubernetes 리소스들 (deployments, services, ...) 과 Plans, Parameters 들을 모두 포함하고 있다. 따라서 &lt;code&gt;인스턴스화 되어 애플리케이션으로 동작할 수 있는 클래스처럼 생각&lt;/code&gt;하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instance&lt;/strong&gt; : &lt;code&gt;OperatorVersion의 실체화되어 배포된 애플리케이션을 의미&lt;/code&gt;하고, OperatorVersion을 클래스라고 한다면 실체화된 객체를 의미한다. OperatorVersion에 정의된 기본 값을 재 정의할 수도 있고, 누락된 파라미터 값을 제공해야 한다. 인스턴스의 생성은 대부분의 경우는 Phases, steps에 따라 Kubernetes 리소스들을 렌더링하고 적용하는 deploy plan을 실행하는 것을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;정리&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;하나의 Operator는 여러 개의 OperatorVersion을 가질 수 있다. 예를 들어 요구 사항에 따라 여러 팀이 서로 다른 Kafka 버전을 사용하는 것을 생각하면 된다. &lt;/li&gt;
&lt;li&gt;OperatorVersion과 Instance를 분리하는 것은 동일한 클러스터에 동일한 버전의 애플리케이션을 다른 팀들이 각각 사용할 때 유용하다. 예를 들어 동일한 OperatorVersion을 통하지만 별도의 구성이나 확장이 필요한 경우라고 생각하면 된다.&lt;/li&gt;
&lt;li&gt;Operator나 OperatorVersion은 관리자가 설정 및 처리한다. 사용자는 이 부분을 거의 볼 수 없다. Instance부터 사용자가 처리할 수 있으며 많은 CLI 명령들이 존재한다. 예를 들면 Instance를 생성하며 이름을 지정하는 등의 작업이 가능하다.&lt;pre&gt;&lt;code&gt;$ kubectl kudo install kafka --instance dev-kafka&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limitations&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cross-namespace ownership and cluster-scoped resources&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Instance가 생성한 모든 리소스는 Instance의 소유&lt;/li&gt;
&lt;li&gt;Instance가 삭제되면 연계된 모든 리소스도 삭제&lt;/li&gt;
&lt;li&gt;Operator, OperatorVersion, Instance 모두 namespace 범위로 한정이므로 리소스도 동일한 namespace만 가능&lt;/li&gt;
&lt;li&gt;다른 여러 namespace에 리소스를 생성하는 것은 불가능&lt;/li&gt;
&lt;li&gt;cluster-scoped 리소스는 생성가능하지만 resourceOwners 팔드가 Instance 참조로 처리되지 않기 때문에 Instance가 삭제될 때 자동으로 삭제되지 않으며, Instance의 Update/Upgrade에서 문제가 발생할 수 있다. 이 부분은 &lt;a href=&quot;https://github.com/kudobuilder/kudo/blob/master/keps/0005-cluster-resources-for-crds.md&quot;&gt;KEP-5 (cluster-Resource-for-crds)&lt;/a&gt;를 통해 해결될 가능성이 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reconciling arbitrary resources&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;KUDO는 임의의 사용자 지정 리소스 유형을 조정할 수 없다.&lt;/li&gt;
&lt;li&gt;KUDO Controller가 조정할 수 있는 것은 Operator, OperatorVersion, Instance 등의 Custom Resource들이다.&lt;/li&gt;
&lt;li&gt;KUDO Controller는 다른 임의의 리소스들을 생성하도록 설정된 정해진 Spec에 따르는 Plan을 실행해서 배포될 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;  &lt;img src=&quot;https://kudo.dev/images/kudo-architecture.jpg?10x20&quot; alt=&quot;Architecture of KUDO&quot;&gt;&lt;br&gt;  위의 그림과 같이 KUDO CRDs / KUDO Controller 가 설치되어 동작하고 있으며, 실제 Operator에 대한 정보가 KUDO로 전달되면 내부적으로 운영되는 방식이다.&lt;/p&gt;
&lt;h3&gt;Components&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;kube-kudo&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/&quot;&gt;kubectl 플러그인&lt;/a&gt; 명령줄 클라이언트로 &lt;code&gt;kubectl kudo&lt;/code&gt; 와 같은 방식으로 사용한다. 이 클라이언트는 개발자들이 KUDO Operator 생성을 지원하고, 운영팀이 Kubernetes cluster 내의 Operator들을 관리하는데 사용된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Operator 개발&lt;/li&gt;
&lt;li&gt;End to end test harness execution&lt;/li&gt;
&lt;li&gt;Repository 개발 및 관리&lt;/li&gt;
&lt;li&gt;KUDO CRDs를 통해 구현된 KUDO Controller와 상호작용&lt;ul&gt;
&lt;li&gt;Operator CRD들과 Operators 설치와 제거&lt;/li&gt;
&lt;li&gt;Operator Plan의 실행과 상태 조회&lt;/li&gt;
&lt;li&gt;Operator들의 Upgrade / Update, backup / restore&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO CRDs&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;KUDO를 지원할 수 있도록 Kubernetes API 확장&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO Controller&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;클러스터의 &lt;code&gt;kudo-system&lt;/code&gt; namespace에 배포된 컨트롤러의 모음으로 KUDO CRDs에 정의된 서비스를 제공하고, KUDO Operator들을 관리한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes의 KUDO Object들을 관찰하고 원하는 상태가 되도록 처리&lt;/li&gt;
&lt;li&gt;KUDO Operator들을 생성하고, Operator의 Plan 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO Repository&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;상당한 편의성을 제공하지만 반드시 필요한 것은 아니며 Operator들이 검색될 수 있도록 URL과 연계된 색인 정보를 제공한다. Operator Repository와 Operator Layout에 대한 자세한 정보는 &lt;a href=&quot;https://github.com/kudobuilder/operators&quot;&gt;여기&lt;/a&gt;를 참고하도록 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Implementation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;KUDO CLI와 Controller는 Go로 작성되었고, CLI는 Repository와 HTTP 통신을 사용하고 Controller와 통신하기 위해서 &lt;a href=&quot;https://github.com/kubernetes/client-go&quot;&gt;Kubernetes client-go&lt;/a&gt;를 사용하고 있다.&lt;/li&gt;
&lt;li&gt;KUDO Controller는 &lt;a href=&quot;&quot;&gt;&lt;/a&gt;를 사용해서 구축되었고, Kubernetes와 통신하는 의존성을 가지고 있다. 모든 KUDO의 상태는 CRD에 저장되기 때문에 별도의 데이터베이스 등은 필요하지 않다.&lt;/li&gt;
&lt;li&gt;KUDO Operator들은 모두 YAML로 작성되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Developing Operators&lt;/h2&gt;
&lt;p&gt;KUDO의 주요 구성 개념들은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Operator Packages&lt;/li&gt;
&lt;li&gt;Plans&lt;/li&gt;
&lt;li&gt;Tasks&lt;/li&gt;
&lt;li&gt;Parameters&lt;/li&gt;
&lt;li&gt;Templates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;간단한 샘플로 만들 Operator는 Nginx를 클러스터에 배치하는 것으로 이 정도로는 KUDO가 필요하지 않다. 단지 KUDO의 강력함 보다는 어떤 과정을 거쳐서 처리하면 되는지를 보여주기 위한 샘플이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;first-operator&lt;/code&gt; 라는 이름의 폴더를 하나 생성한다. 이 폴더는 local 패키지를 구성하는 것이다. (이 부분은 뒤에 설명할 Operator Package 단위다)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;operator.yaml&lt;/code&gt; 파일을 &lt;code&gt;first-operator&lt;/code&gt; 폴더에 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; apiVersion: kudo.dev/v1beta1
 name: &amp;quot;first-operator&amp;quot;
 operatorVersion: &amp;quot;0.1.0&amp;quot;
 appVersion: &amp;quot;1.7.9&amp;quot;
 kubernetesVersion: 1.13.0
 maintainers:
   - name: Your name
     email: &amp;lt;your@email.com&amp;gt;
 url: https://kudo.dev
 tasks:
   - name: app
     kind: Apply
     spec:
       resources:
         - deployment.yaml
 plans:
   deploy:
     strategy: serial
     phases:
       - name: main
         strategy: parallel
         steps:
           - name: everything
             tasks:
               - app&lt;/code&gt;&lt;/pre&gt;&lt;p&gt; 위의 내용은 &lt;code&gt;deploy&lt;/code&gt; 라는 하나의 plan을 가지는 Operator로 하나의 Phase와 하나의 step으로 구성된 최소 설정이다. 이 Operator의 Instance를 클러스터에 배포하면 자동으로 deploy plan이 트리거 된다. step에 지정된 task는 &lt;code&gt;app&lt;/code&gt;이고 해당 task는 참조하는 리소스로 &lt;code&gt;deployment.yaml&lt;/code&gt;을 참고하고 있다. KUDO는 이 파일이 &lt;code&gt;templates&lt;/code&gt; 폴더에 존재하는지를 검증한다. &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;deployment.yaml&lt;/code&gt; 파일을 &lt;code&gt;first-operator/templates&lt;/code&gt; 폴더에 생성하고 아래와 같이 설정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: nginx-deployment
 spec:
   selector:
     matchLabels:
       app: nginx
   replicas: {{ .Params.replicas }} # tells deployment to run 2 pods matching the template
   template:
     metadata:
       labels:
         app: nginx
     spec:
       containers:
         - name: nginx
           image: nginx:{{ .AppVersion }}
           ports:
             - containerPort: 80&lt;/code&gt;&lt;/pre&gt;&lt;p&gt; 위의 파일 내용은 일반적인 Kubernetes의 Deployment를 정의하는 YAML 파일이다. 그러나 잘 보면 KUDO의 템플릿 기능을 사용해서 &lt;code&gt;{{ .Params.replicas }}&lt;/code&gt; 처리한 것을 볼 수 있다. &lt;code&gt;.Params&lt;/code&gt;는 &lt;code&gt;params.yaml&lt;/code&gt;에 정의된 파라미터 정보를 참조하는 것으로 설치를 진행하면서 지정한 파라미터를 정의한 파라미터의 값으로 재 정의해서 처리한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;params.yaml&lt;/code&gt; 파일을 &lt;code&gt;first-operator&lt;/code&gt; 폴더에 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; apiVersion: kudo.dev/v1beta1
 parameters:
   - name: replicas
     description: Number of replicas that should be run as part of the deployment
     default: 2&lt;/code&gt;&lt;/pre&gt;&lt;p&gt; 여기까지해서 첫 번째 Operator를 클러스터에 설치할 수 있는 준비를 했다. 아래의 명령을 통해서 실행이 가능하다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;터미널에서 설치 명령 실행&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; $ kubectl kudo install ./first-operator&lt;/code&gt;&lt;/pre&gt;&lt;p&gt; 위의 명령에서 &lt;code&gt;./first-operator&lt;/code&gt; 값은 실제 Operator Package 폴더를 상대적인 경로를 기준으로 지정하면 된다.&lt;/p&gt;
&lt;p&gt; 이 작업을 위해서는 KUDO CLI 가 설치되어 있어야 한다. 설치를 하면 KUDO에서 사용자의 Operator를 처리하는데 필요로 하는 &lt;code&gt;Operator&lt;/code&gt;, &lt;code&gt;OperatorVersion&lt;/code&gt;, &lt;code&gt;Instasnce&lt;/code&gt; 와 같은 다양한 리소스들이 설치된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;클러스터에서 아래와 같이 생성된 정보들을 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; # Get instances
 $ kubectl get instances

 # or
 $ kubectl kudo get instances&lt;/code&gt;&lt;/pre&gt;&lt;p&gt; 위의 결과가 정상적이라면 아래의 명령으로 pod 정보를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; $ kubectl get pods&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이제 KUDO에서 이야기하는 Day 2 Operating에서 Day 1에 해당하는 설치 절차를 확인해 보았다. 실제 설치한 Operator에 대한 테스트는 &lt;a href=&quot;https://kudo.dev/docs/testing.html&quot;&gt;Test harness document&lt;/a&gt;룰 참고하도록 한다.&lt;/p&gt;
&lt;h3&gt;Operator Package&lt;/h3&gt;
&lt;p&gt;패키지는 Oeprator를 설명하는데 필요한 모든 파일들을 번들로 처리하며 패키지의 구조는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── operator.yaml
├── params.yaml
└── templates
    ├── deployment.yaml
    └── ...&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;operator.yaml&lt;/strong&gt; : Operator의 전체 라이프사이클과 메타데이터를 정의하는 메인 YAML 파일이다. Tasks와 Plans는 모두 이 파일에 정의되어 있다. Operator name, version, 유지관리자등의 정보를 제공하는 메타데이터도 여기에 설정된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;params.yaml&lt;/strong&gt; : Opearator에서 사용되는 파라미터들을 정의한다. 설치하는 동안 사용자에 의해서 여기에 정의된 파라미터들은 재 정의될 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;templates/&lt;/strong&gt; : operator.yaml에 정의된 워크플로우에 기반해서 설치가 된 후 클러스터에 적용할 모든 템플릿화된 Kubernetes objects이 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tasks&lt;/h3&gt;
&lt;p&gt;Task는 KUDO 워크플로우의 기본적 빌딩 블록이다. Plan, Phases, Steps 는 Task를 실행하는 제어적인 구조들이며, KUDO는 &lt;code&gt;Apply&lt;/code&gt;, &lt;code&gt;Delete&lt;/code&gt;, &lt;code&gt;Pipe&lt;/code&gt;, &lt;code&gt;Toggle&lt;/code&gt;, &lt;code&gt;KudoOperator&lt;/code&gt; 라는 주요 Task를 제공하고 oeprator를 테스트하고 디버깅하기 위한 &lt;code&gt;Dummy&lt;/code&gt; 형식을 제공하고 있다.&lt;/p&gt;
&lt;p&gt;모든 Taks들은 operator.yaml 파일에 정의되고 &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;kind&lt;/code&gt;, &lt;code&gt;spec&lt;/code&gt; 의 세 가지 필드를 반드시 포함해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - name: # Task name defined by the user
    kind: # Task kind can be: Apply, Delete, Pipe, Toggle, KudoOperator and Dummy
    spec: # Task-specific specification&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apply-Task&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;appy-task는 템플릿을 클러스터에 적용하는 것으로 &lt;code&gt;spec.resources&lt;/code&gt; 필드는 존재하지 않는다면 생성하고 존재한다면 갱신할 Kubernetes 리소스 리스트 목록을 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - name: app
    kind: Apply
    spec:
      resources:
        - deployment.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;app&lt;/code&gt; 라는 이름의 Task는 &lt;code&gt;templates/deployment.yaml&lt;/code&gt;에 정의된 리소스의 deployment를 생성한다.&lt;/p&gt;
&lt;p&gt;KUDO는 정의된 리소스 리스트를 모두 적용하고 모든 리소스가 정상화할 때까지 대기한다. 상태는 특정 리소스마다 달라지며 배포는 Spec으로 정의된 isntance 수가 모두 생성되고 실행되어야 하고 Job은 모두 정상적으로 끝나야 한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;현재의 구현은 Pod가 미리 생성된 ConfigMap을 마운트할 수 있도록 정의된 순서에 따라서 모든 리소스들을 적용하는 방식이다. 이 부분은 규격의 일부가 아니며 향후 모든 리소스가 동시에 적용될 수 있는 것처럼 변경될 수 있고 리소스간에 순서가 중요한 경우는 순차적인 steps를 사용해야 한다.&lt;/p&gt;
&lt;p&gt;apply-task는 KUDO 특정 라벨과 어노테이션을 통해서 배포된 자원을 강화한다. 파드를 셍성하거나 배포하는 리소스 (StatefulSets, Deployments, Daemonsets, ...)의 경우는 KUDO가 pod template spec (ConfigMaps and Secrets)에 의해서 사용되는 모든 리소스들의 hash를 pod template specdp 특정 라벨을 추가한다. 이 해시값의 효과는 ConfigMan을 변경하는 Operator parameter에 대한 갱신으로 해당 ConfigMap을 사용하는 Pod의 재 시작을 호출하게 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pod는 단지 상위 리소스 (StatefulSet, Deployment)가 적용되는 경우에 재 시작된다. 이런 처리는 동일한 Task의 종속성이나 동일한 plan의 이후 실행되는 Task에 의해서 발생할 수 있다. 즉 ConfigMap만 수정하고 ConfigMap을 사용하는 StatefulSet이 변경되지 않는 경우라면 plan이 실행되도 Pod는 재 시작되지 않는다는 것을 의미한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pod 재 시작을 트리거하는 경우에서 ConfigMap 또는 Secret을 제외하려면 ConfigMap 또는 Secret에 임의 값과 &lt;code&gt;kudo.dev/skip-hash-calculation&lt;/code&gt; 주석을 추가하면 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delete Task&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;apply-task를 사용해서 생성한 리소스들을 삭제하는 Task이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - name: remove
    kind: Delete
    spec:
      resources:
        - deployment.yaml&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;delete-task는 실제 성공/실패 여부와 상관없이 무조건 성공으로 처리된다. KUDO 0.9.0 에서는 API 서버가 삭제 요청을 받아들이면 실제 리소스가 삭제되는 것을 기다리지 않고 Task를 종료한다. Kubernetest에서는 Pod의 종료에 기본 시간을 30초를 지정해 놓았지만 KUDO 0.9.0 버전에서는 요청이 받아들여지면 처리된 것으로 간주한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Toggle-Task&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Operator 기능을 개발할 때 리소스를 활성/비활성 처리할 때가 존재한다. toggle-task는 파라미터의 boolean 값을 기준으로 리소스를 적용하거나 삭제한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - name: app-service
    kind: Toggle
    spec:
      parameter: enable-service
      resources:
        - service.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 작업은 &lt;code&gt;enable-service&lt;/code&gt; 파라미터의 값을 기준으로 &lt;code&gt;templates/service.yaml&lt;/code&gt;에 정의된 리소스를을 적용하거나 삭제하는 것이다. 당연히 사용하는 &lt;code&gt;enable-service&lt;/code&gt; 파라미터는 &lt;code&gt;params.yaml&lt;/code&gt;에 정의되어 있어야 한다. 만일 없다면 toggle-task는 실패하게 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pipe-Task&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;복잡한 Operator들을 개발하다보면 종종 한 step에서 파일들을 생성하고 다음 step에서 이를 사용하는 경우가 많다. 일반적인 예는 bootstrap step 에서 사용자 지정 certifications/dynamic 설정 파일을 생성하고, 서비스의 배포 step에서 이를 사용하는 것이다. 이런 상황에서 pipe-task가 도움이 될 수 있다. 예를 들어 index.html를 생성하고 Nginx 서버와 함께 배포할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - name: genwww
    kind: Pipe
    spec:
      pod: pipe-pod.yaml
      pipe:
        - file: /tmp/index.html
          kind: ConfigMap
          key: indexHtml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;pipe-task spec은 두 개의 필드를 가진다. &lt;code&gt;pod&lt;/code&gt; spec은 index.html을 생성하고 저장할 파일들의 리스트를 정의한다. 생성된 /tmp/index.html 파일은 ConfigMap으로 저장될 것이다. indexHtml 이라는 키로 템플릿 리소스에서 참조가 되며 pipe-task의 spec.pod 는 &lt;a href=&quot;https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#pod-v1-core&quot;&gt;core/v1&lt;/a&gt; Pod 템플릿을 반드시 참조해야 한다. 그러나 한계가 존재한다. 이에 대해서는 &lt;a href=&quot;https://github.com/kudobuilder/kudo/blob/master/keps/0017-pipe-tasks.md&quot;&gt;KEP&lt;/a&gt;를 참고한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pipe-pod.yaml은 &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/init-containers/&quot;&gt;initContainer&lt;/a&gt;에 파일을 생성한다.&lt;/li&gt;
&lt;li&gt;파일들이 저장될 &lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/volumes/#emptydir&quot;&gt;emptyDir Volume&lt;/a&gt; 하나를 정의하고 마운트해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래는 &lt;code&gt;templates/pipe-pod.yaml&lt;/code&gt; 이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Pod
spec:
  volumes:
  - name: shared-data
    emptyDir: {}
  initContainers:
    - name: init
      image: busybox
      command: [ &amp;quot;/bin/sh&amp;quot;, &amp;quot;-c&amp;quot; ]
      args:
        - wget -O /tmp/index.html &amp;#39;http://cowsay.morecode.org/say?message=Good+things+come+when+you+least+expect+them&amp;amp;format=html&amp;#39;
      volumeMounts:
        - name: shared-data
          mountPath: /tmp&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위는 온라인 &lt;a href=&quot;http://cowsay.morecode.org/&quot;&gt;cowsay-generator&lt;/a&gt; API를 이용해서 결과를 /temp/index.html 파일로 다운로드 한다.&lt;/p&gt;
&lt;p&gt;위의 pipe-pod.yaml과 genwww task 사양에 따라서 KUDO는 pipe-pod를 실행하고 성공적으로 index.html 파일이 생성될 때까지 기다리고 이를 ConfigMap으로 저장한다. 이 파일은 이제 Nginx 배포 spec에서 사용될 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
spec:
  containers:
    - name: nginx
      image: nginx:1.7.9
      ports:
        - containerPort: 80
      volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html/
  volumes:
    - name: www
      configMap:
        name: {{ .Pipes.indexHtml }}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ConfigMap으로 부터 마운트 볼륨을 생성했다. &lt;code&gt;{{ .Pipes.indexHtml } }&lt;/code&gt; 을 이용해서 ConfigMap 이름을 지정하고, &lt;code&gt;.Pipes&lt;/code&gt; 키워드는 pipe-artifacts를 처리할 수 있도록 하고, indexHthml은 pipe-task에서 정의한 키다.&lt;/p&gt;
&lt;p&gt;이제 Task를 기준으로 아래와 같이 deploy plan을 구성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
plans:
  deploy:
    strategy: serial
    phases:
      - name: main
        strategy: serial
        steps:
          - name: genfiles
            tasks:
              - genwww
          - name: app
            tasks:
              - app&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위에서 &lt;code&gt;strategy: serial&lt;/code&gt;은 순차적 진행으로 &lt;code&gt;genfiles&lt;/code&gt; step에서 파일이 생성되기를 기다리고 다음 step에서 이를 사용할 수 있도록 하는 것이다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;파일을 생성하는 pod는 장애가 발생했을 때 여러 번 실행될 수 있기 때문에 부작용 (side-effect - 타사 API 호출처럼 컨테이너 외부에서 관측할 수 있는 부적용을 의미함)이 없어야 한다. &lt;code&gt;restartPolicy: OnFailure&lt;/code&gt; 는 pipe-pod 에 대해서 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;1M 이하의 파일만 ConfigMap이나 Secret으로 저장할 수 있다. 1M 보다 큰 파일은 pipe-task가 실패한다.&lt;/li&gt;
&lt;li&gt;KUDO 0.9.0에서는 pipe-artifacts는 동일한 plan에서만 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;하나의 pipe-task에서 artifacts를 생성하고 후속 task에서 마운트해서 pipeline 처리를 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KudoOperator Task&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KUDO 0.15.x 기준으로 다른 Opeator들에 대한 종속성을 지정할 수 있는 KudoOperator Task를 지원한다. 종속성은 복잡한 주제지만 KudoOperator 자체는 설치 종속성에 대한 것으로 Operator Instance와 모든 종속성 (전환성 포함)를 하나의 단위로 설치하거나 제거할 수 있게 한다.&lt;/p&gt;
&lt;p&gt;KUDO Operators는 Plans, Phases, Steps라고 부르는 설치 의존성을 순차 또는 병렬 수행 전략 기준으로 수행하는 매커니즘을 가지고 있다. 이 매커니즘은 전이적 종속성과 모든 종속성 계층을 표현하기에 이미 충분히 강력하다.&lt;/p&gt;
&lt;p&gt;Zookeeper Operator version 0.3.0 (install &lt;a href=&quot;https://github.com/kudobuilder/operators/blob/master/repository/zookeeper/README.md&quot;&gt;Zookeeper v3.4.14&lt;/a&gt;)에 대한 종속성을 지정하는 간단한 샘플이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - name: deploy-zookeeper
    kind: KudoOperator
    spec:
      package: zookeeper
      operatorVersion: 0.3.0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;다른 Task와 마찬가지로 KUDO도 다음 Task로 넘어가기 전에 healty 검증을 한다. 아래의 예는 deploy plan을 사전 준비와 기본인 두 개의 Phase로 구성한 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plans:
  deploy:
    strategy: serial
    phases:
      - name: prereqs
        strategy: parallel
        steps:
          - name: first
            tasks:
              - deploy-zookeeper
      - name: main
        strategy: parallel
        steps:
          - name: second
            tasks:
              - deploy-main&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;quot;Healthy&amp;quot;는 이 경우 deploy plan이 COMPLETE 인 것을 의미한다. KudoOperator는 appVersion (최신 버전이 기본 값)과 instanceName (KUDO에 의해서 기본적으로 생성됨)을 추가적으로 지정할 수 있는 &lt;code&gt;kubectl kudo install&lt;/code&gt; 명령과 의미론적으로 근접하게 모방하는 것이다. 이 명령과 마찬가지로 local operator (또는 원격 tarball)를 Package field로 지정할 수 있다. (e.g package: &amp;quot;./child-operator&amp;quot;) 상위 Operator가 설치되는 동안 KUDO CLI는 하위 종속성을 해결하고 Operator 구문 분석과 필요한 리소스를 생성한다.&lt;/p&gt;
&lt;p&gt;만일 종속성이 local이고 상대 경로 (./child-operator)라면 다음과 같이 처리된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;정의되어 있는 operator.yaml 에 대해 상대 경로로 판단한다.&lt;/li&gt;
&lt;li&gt;kudo install command처럼 &lt;code&gt;./&lt;/code&gt; 또는 &lt;code&gt;../&lt;/code&gt; 로 접두사를 붙여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 다음과 같은 Operator Tree가 지정된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── child
│   └── operator.yaml
│
└── parent
    └── operator.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Child Operator는 parent/operator.yaml에 아래와 같이 상대 경로 지정된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  - name: child
    kind: KudoOperator
    spec:
      package: &amp;quot;../child&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Dependency Parametrization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;하위 Operator가 파라미터화 되어야할 필요성이 있다면 &lt;code&gt;parameterFile&lt;/code&gt; 필드를 사용해서 파라미터 변수 파일을 지정할 수 있다. 예를 들어 상위와 하위 Operator가 있을 때 하위가 빈 값을 기본 값으로 하는 필수 PASSWORD 파라미터가 있다면 상위는 하위를 위해서 파라미터 파일을 지정하고 연관된 KudoOperator에서 참조할 필요가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - name: deploy-child
    kind: KudoOperator
    spec:
      package: child-operator
      parameterFile: child-params.yaml &lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;child-params.yaml&lt;/code&gt;은 다른 템플릿 파일과 같이 상위 Operator의 템플릿 폴더에 위치한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# parent/templates/child-params.yaml
PASSWORD: {{ .Params.CHILD_PASSWORD }}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;하위 Oeprator에 전달될 &lt;code&gt;PASSWORD&lt;/code&gt; 값은 상위 Operator의 파라미터로 &lt;code&gt;CHILD_PASSWORD&lt;/code&gt;를 이용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# parent/params.yaml
apiVersion: kudo.dev/v1beta1
parameters:
  - name: CHILD_PASSWORD
    displayName: &amp;quot;child password&amp;quot;
    description: &amp;quot;password for the underlying instance of child operator&amp;quot;
    required: true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;따라서 사용자가 상위 Operator를 설치할 떄 평소와 같이 파라미터를 지정하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo install parent -p CHILD_PASSWORD=secret&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;상위 Operator가 합리적인 기본 값을 제공하거나 사용자에게 노출시키지 않기 위해서 하드 코드로 지정할 수도 있다. 전체적으로 Operator 캡슐화 방법을 제공해서 Operator 구성을 권장한다. 즉 포함된 Operator instance들의 파라미터를 사용자가 임의로 수정하도록 해서는 안된다는 말이다. 상위 Operator는 직접 연결된 하위 Operator가 필요로 하는 모든 파라미터를 정의해야 한다. 자세한 정보는 &lt;a href=&quot;https://github.com/kudobuilder/kudo/blob/main/keps/0029-operator-dependencies.md&quot;&gt;KEP-29&lt;/a&gt;를 참고하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dummy Task&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Dummy-Task는 처리가 성공이나 실패할 수 있으며 KUDO Operator 워크플로우를 테스트하거나 디버깅하는데 유용하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - name: breakpoint
    kind: Dummy
    spec:
    wantErr: true # If true, the task will fail with a transient error
    fatal: true # If true and wantErr: true, the task will fail with a fatal error
    done: false # if true, the task will succeed immediately&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이런 task는 Operator 워크플로우에서 중단점으로  유용하고 Operator 개발자가 임의의 step에서 실행을 일시 중지하거나 실패하도록 할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wantErr&lt;/code&gt; 은 &lt;code&gt;done&lt;/code&gt; 보다 우선한다.&lt;/li&gt;
&lt;li&gt;fatal error를 원하는 경우는 &lt;code&gt;wantErr&lt;/code&gt;과 &lt;code&gt;fatal&lt;/code&gt;을 모두 true로 설정해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wantErr&lt;/code&gt;, &lt;code&gt;fatal&lt;/code&gt;, &lt;code&gt;doen&lt;/code&gt; 모두가 false인 경우는 일시 정지한다. (일반적으로 Unhealthy 상태의 자원 시뮬레이션용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Plans&lt;/h3&gt;
&lt;p&gt;plan은 운영 과제들 (Tasks)의 개별적인 단계들 (Steps)을 파악하고 이런 업무적인 과제들을 Phases와 Steps 조합으로 구성한다. 각 Step은 실행할 과제들을 참조한다. Task를 Phases 와 Steps 로 정리하면 서비스의 복잡한 행동들을 파악할 수 있다.&lt;/p&gt;
&lt;p&gt;Plans 와 Phases 는 전략을 가지고 동작하며 순차적 (Serial)로 동작할지 병렬 (Parallel)로 동작할지를 나타낸다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
tasks:
  - name: deploy-master
    kind: Apply
    spec:
      resources:
        - master-service.yaml
        - master.yaml
  - name: deploy-agent
    kind: Apply
    spec:
      resources:
        - agent-service.yaml
        - agent.yaml
plans:
  deploy:
    strategy: parallel
    phases:
      - name: deploy-master
        strategy: serial
        steps:
          - name: deploy-master
            tasks:
              - deploy-master
      - name: deploy-agent
        strategy: serial
        steps:
          - name: deploy-agent
            tasks:
              - deploy-agent&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;KUDO Controller는 Operator에 정의된 Plans 를 배포하며, 한번에 하나의 plan만 배포할 수 있다. KUDO는 어떤 plan을 배포할지를 결정하는 접근 방식이 다르다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deploy and update plans&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기본적으로 KUDO는 어떤 plan도 배포되지 않은 상황이라면 deploy plan을 시작할 것이다. Instance를 갱신하는 상황이라면 KUDO는 upgrade 또는 update plan이 존재하는지를 검증하고 이를 시작하려고 한다. 만일 해당 plan이 모두 없다면 deploy plan을 시작한다.&lt;/p&gt;
&lt;p&gt;아래 예는 service.yaml에 정의된 서비스를 생성하는 deploy plan이다. 갱신 상황이라면 서비스의 캐시를 갱신해야 하는데 update-cache.yaml은 update plan의 일부로 필요한 리소스를 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
tasks:
  - name: app
    kind: Apply
    spec:
      resources:
        - service.yaml
  - name: update
    kind: Apply
    spec:
      resources:
        - service.yaml
        - update-cache.yaml
plans:
  deploy:
    strategy: serial
    phases:
      - name: deploy-service
        strategy: serial
        steps:
          - name: deploy
            tasks:
              - app
  update:
    strategy: serial
    phases:
      - name: update-service
        strategy: serial
        steps:
          - name: update
            tasks:
              - update&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cleanup plans&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Operator에 옵션이기는 하지만 cleanup plan이 존재한다면 이 plan은 Instance가 삭제될 때 KUDO Manager에 의해서 자동으로 실행되며 이 plan이 성공하던 실패하던 Instance는 삭제된다. 당연히 Instance의 정상적인 라이프사이클 동안에 cleanup plan을 트리거하면 오류가 발생하게 된다. 추가로 이 plan의 step은 실패할 것으로 예상해야 한다. 에를 들어 사용자가 deploy plan이 고착되어 instance를 삭제하길 원하는 상황일 수도 있으며 이런 상황이라면 cleanup plan은 리소스를 삭제하려고 시도하는데 해당 리소스가 클러스터에 존재하지 않을 수도 있기 떄문이다. 따라서 다른 plan이 동작하는 중이라도 cleanup plan은 시작된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
tasks:
  - name: database
    kind: Apply
    spec:
      resources:
        - database.yaml
  - name: cleanup
    kind: Apply
    spec:
      resources:
        - cleanup-job.yaml
spec:
  plans:
    deploy:
      strategy: serial
      phases:
        - name: deploy-database
          strategy: serial
          steps:
            - name: deploy
              tasks:
                - database
    cleanup:
      strategy: serial
      phases:
        - name: cleanup-databse
          strategy: serial
          steps:
            - name: cleanup
              tasks:
                - cleanup&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Instance가 삭제되면 instance에 속한 모든 리소스가 자동으로 삭제되므로 database.yaml은 삭제할 필요가 없다. 그러나 복잡한 애플리케이션의 경우는 가끔 Kubernetes 리소스로 포착되지 않는 상태를 만들기도 한다. 이런 상황이라면 Operator를 제거할 떄 이 상태를 제거할 수 있으므로 cleanup plan에서 이를 처리할 수 있다. 위의 예제에서는 연계된 Task의 cleanup-job.yaml 에서 필요한 작업을 번들로 묶어서 처리한다. 일반적으로 cleanup plan을 Instance가 삭제되기 전에 호출된다는 점을 제외하면 다른 모든 plan과 동일하다.&lt;/p&gt;
&lt;p&gt;cleanup plan은 &lt;a href=&quot;https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#finalizers&quot;&gt;Finalizers&lt;/a&gt;를 사용해서 구현되었고 Instance의 &lt;code&gt;metadata.finalizers&lt;/code&gt;에는 cleanup plain이 동작하는 동안 &amp;quot;kudo.dev.instance.cleanup&amp;quot; 값이 설정된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parameter Trigger&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;파라미터는 옵션으로 tigger 속성을 가지고 있고 트리거를 통해서 plan을 지정한다. 따라서 파라미터가 변경되면 지정된 plan이 실행된다.&lt;/p&gt;
&lt;p&gt;실행 중인 애플리케이션을 갱신하는데 필요한 작업은 어떤 파라미터가 변경되는지에 따라 달라질 수 있다. 예를 들어 BROKER_COUNT 가 변경되었다면 간단히 배포를 통해서 갱신되는 것이지만, APPLICATION_MEMORY 가 변경되는 경우는 canary 또는 blue/green 배포를 통한 새로운 버전을 Rollout 하는 갱신이 될 수도 있다는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
parameters:
- name: REPLICAS
  trigger: deploy
- name: APPLICATION_MEMORY
  trigger: canary&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Executing Plans&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KUDO Manager는 파라미터의 변경에 따라 자동적으로 연계된 plan을 실행하지만 가끔 수동으로 plan을 실행시켜야 할 때도 존재한다. 예를 들어 데이터 손상에 대비하기 위해서 주기적으로 백업 plan을 실행해야 하는 경우는 어떤 파라미터가 변경되는 것이 아니기 때문에 수동으로 처리를 해 줘야 한다. &lt;a href=&quot;https://github.com/kudobuilder/kudo/releases/tag/v0.11.0&quot;&gt;KUDO v0.11.0&lt;/a&gt; 에서는 이를 위해 수동으로 plan을 호출하는 명령을 제공하고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl kudo plan trigger --name deploy --instance my-instance&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 명령은 deploy plan을 my-instance라는 이름으로 지정된 instance에 대해서 실행하라는 의미다. 단순해 보이지만 실제 구동되는 상황을 잘 살펴봐야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plan Life Cycle&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;필요에 따라서 두 개의 plan이 실행될 수 있다면 (현재는 하나만 실행 가능) 동시에 plan이 동작할 때 어떤 문제가 생길지 생각해 봐야 한다. 만일 plan이 서비스 deploy plan과 monitoring pod를 배포하는 monitoring plan 이라면 서로 연관성이 없어서 병렬로 동작해도 별 문제가 없겠지만, 두 plan이 백업 / 복구 또는 배포 / 마이그레이션 과 같이 연관성이 있는 것이라면 서로 병렬로 동작할 수 없는 상황이며 데이터의 손상을 야기할 수도 있는 문제가 존재한다.&lt;/p&gt;
&lt;p&gt;현재는 Plan에 대한 선호도/반선호도, 재선정/비선정, 취소 등 많은 것들을 검토 중에 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Admission Controllers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;간단히 보면 Kubernetes admission controller는 클러스터가 어떻게 사용되어야 하는지를 제어하고 실행하는 플러그인으로 인증된 API 요청을 가로채는 GateKeeper로 생각할 수 있으며 요청 객체를 변경하거나 요청을 전면 거부할 수 있다. Kubernetes는 이미 사용자 권한 부여부터 네임스페이스의 수명주기까지 모든 것을 통제하는 등의 &lt;a href=&quot;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/&quot;&gt;사전 설치&lt;/a&gt;된 제품을 가지고 있다.&lt;/p&gt;
&lt;p&gt;KUDO Manager는 Instance의 변경을 관리하는 Instance admission controller를 사용해서 plan들이 간섭되지 않도록 하고 있다. 아래와 같이 요청은 처리되고 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://kudo.dev/images/admission-instance-update.png&quot; alt=&quot;KUDO Request Lifecycle&quot;&gt;&lt;/p&gt;
&lt;p&gt;Instance admission controller는 수동으로 실행된 plan 또는 파라미터가 변경되서 실행된 plan을 통해서 instance의 갱신을 모두 관리한다. 일반적으로 새로운 plan이 시작되기 전에 모든 계획은 성공 (COMPLETE)이던 실패 (FATAL_ERROR)이던 종료된 상태여야 한다. 단 하나의 plan만 다시 시작될 수 있고 모든 plan이 재 시작될 수 있을 것으로 간주한다. 요청이 거부되면 instance controller는 명확한 거부 사유를 오류로 반환한다.&lt;/p&gt;
&lt;p&gt;또한 파라미터의 변경으로 여러 개별적인 Plan 들이 동작하는 것을 거부한다. 물론 instance가 삭제될 떄 실행되는 cleanup plan은 예외가 된다. 단, cleanup plan은 수동으로 트리거를 할 수 없다.&lt;/p&gt;
&lt;p&gt;KUDO 0.11.0의 경우 Instance admission controller는 선택 사항이지만 향후는 의무가 될 예정이다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Operator Parameters&lt;/h3&gt;
&lt;p&gt;Operator Parameter는 Operator 개발자가 정의한 key-value 쌍으로 Operator 사용자는 배포할 떄 재정의해서 사용할 수 있으며, 이를 통해서 Operator의 동작을 커스터마이징할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kubectl kudo package list parameters (operatorname)&lt;/code&gt; 명령을 통해서 Operator에 연관된 모든 파라미터의 리스트를 확인할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Declaring parameters&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;개발자는 &lt;code&gt;params.yaml&lt;/code&gt; 파일에 파라미터들은 정의한다. 이 파일은 필수적이고 Operator의 root 디렉터리에 존재해야 한다. 파라미터가 필요없는 경우라면 빈 파일이라도 존재해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kudo.dev/v1beta1
parameters:
  - name: BACKUP_FILE
    description: &amp;quot;Filename to save the backups to.&amp;quot;
    default: &amp;quot;backup.sql&amp;quot;
    displayName: &amp;quot;BackupFile&amp;quot;
  - name: PASSWORD
    default: &amp;quot;password&amp;quot;
    trigger: backup
  - name: OPTIONAL_PARAM
    description: &amp;quot;This parameter is not required.&amp;quot;
    required: False
  - name: REQUIRED_PARAM
    description: &amp;quot;This parameter is required but does not have a default value.&amp;quot;
    required: True
  - name: ARRAY_PARAM
    description: &amp;quot;This parameter describes an array of values.&amp;quot;
    default:
      - user1
      - user2
    type: array
  - name: MAP_PARAM
    description: &amp;quot;This parameter describes a map of values.&amp;quot;
    default:
      label1: foo
      label2: bar
    type: map&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;파라미터는 아래와 같은 속성들로 구성된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;name&lt;/strong&gt; : 필수이며 파라미터 이름이다. 대문자로 단어 단위를 &amp;#39;_&amp;#39;로 연결하는 관례를 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;displayName&lt;/strong&gt; : 선택적이며 파라미터의 보여질 이름이다. 다른 곳에는 사용되지 않고 KUDO UI에서 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;description&lt;/strong&gt; : 선택적이며 파라미터의 목적에 대한 설명이다. 다른 곳에는 사용되지 않고 KUDO UI에서 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;type&lt;/strong&gt; : 선택적이며 파라미터 값의 형식을 지정한다. 기본 값은 string 이고, array, map 등도 가능하며 이 경우는 YAML 포맷에 맞도록 값을 지정해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;default&lt;/strong&gt; : 선택적이며 파라미터의 기본 값을 지정한다. 이 속성을 생략하면 해당 파라미터는 required로 설정된다. 이를 변경하려면 required 속성 값을 지정해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;required&lt;/strong&gt; : 선택적이며 설치 시에 파라미터가 반드시 제공되어야 하는지 여부로 true로 설정되어 있는데 설치 시에 제공되지 않으면 설치가 실패하게 된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tigger&lt;/strong&gt; : 선택적이며 초기 설치 이후에 파라미터의 변경에 따라서 트리거될 Plan을 설정한다. 만일 지정하지 않는다면 &lt;code&gt;update&lt;/code&gt;나 &lt;code&gt;deploy&lt;/code&gt; plan이 실행된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;immutable&lt;/strong&gt; : 선택적이며 기본 값은 false로 Instance에 대한 갱신할 떄 변경이 가능하지를 나타내는 것이다. 만일 true로 설정된다면 반드시 required 또는 default 값을 가져야 하며 Instance가 설치될 떄 파라마터 값을 설정해야 하며, 나중에는 변경할 수 없는 상태가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Overriding parameters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Operator 사용자가 CLI를 통해서 파라미터를 재 정의할 수 있다. &lt;/p&gt;
&lt;p&gt;&lt;code&gt;kubectl kudo&lt;/code&gt; 명령에서 &lt;code&gt;install, upgrade, update&lt;/code&gt; 등의 명령을 수행할 때 &lt;code&gt;--parameter 또는 -p&lt;/code&gt; 플래그를 사용해서 처리한다. 지정하는 파라미터는 &lt;code&gt;PARMATER_NAME=parameter_value&lt;/code&gt; 형식으로 처리하고 Array의 경우는 &lt;code&gt;PARAMTER_NAME=[array_value1, array_value2]&lt;/code&gt;로 처리하고 Map의 경우는 &lt;code&gt;PARAMTER_NAME={map_key1:map_value1, map_key2:map_value2}&lt;/code&gt; 와 같이 처리한다. &lt;/p&gt;
&lt;p&gt;이 플래그는 여러 파라미터를 지정할 수 있도록 여러 번 사용할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tiggers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Operator 개발자가 update때 파라미터가 변경되면 실행될 plan의 이름을 지정할 수 있다. 달리 말하면 이 plan이 파라미터의 변경된 값을 Kubernetes 애플리케이션에 적용한다는 의미가 된다. 이를 지정하지 않으면 기본적으로 update plan을 호출하고 이 plan이 없다면 deploy plan을 호출하게 된다. &lt;/p&gt;
&lt;p&gt;단, 현재 하나 이상의 plan을 트리거하도록 파라미터를 설정할 수는 없다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Immutability&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;Operator는 종종 설치 시점에 설정한 파라미터를 나중에 바꿀 수 없도록 하는 경우가 존재한다. 에를 들어 KUDO Cassandra Operator는 NUM_TOKENS라는 관리되는 클러스터의 각 노드들이 관리할 토큰의 갯수를 지정하는 파라미터를 가지고 있는데, 이 파라미터의 값은 설치할 때 적용되면 Cassandra Cluster의 라이프사이클 동안 변경할 수 없고, 이 값을 변경해야 하는 경우라면 다른 파리미터의 값으로 새로운 Cluster를 구성하고 데이터를 마이그레이션 해야 한다.&lt;/p&gt;
&lt;p&gt;이런 불변 파라미터는 기본 값을 가지거나 required로 설정되어야 한다. 즉 설치 시점에 반드시 값이 필요하다는 것이다. &lt;/p&gt;
&lt;p&gt;보다 자세한 사항은 &lt;a href=&quot;https://github.com/kudobuilder/kudo/blob/main/keps/0030-immutable-parameters.md&quot;&gt;KEP-30&lt;/a&gt;를 참고한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Templates&lt;/h3&gt;
&lt;p&gt;KUDO는 &lt;a href=&quot;https://golang.org/pkg/text/template/&quot;&gt;GO 템플릿 엔진&lt;/a&gt;을 사용하고 &lt;a href=&quot;http://masterminds.github.io/sprig/&quot;&gt;스플릭 템플릿 기능&lt;/a&gt;을 사용할 수도 있다. 이를 통해서 사용자가 파라미터 처리된 Operator 기능을 사용할 수 있도록 하며, Operator Package의 &lt;code&gt;templates&lt;/code&gt; 디렉터리에 있는 모든 파일에 적용되어 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vaiables&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;{{.Variable}}&lt;/code&gt; 형식으로 변수의 값을 적용할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;.Params&lt;/strong&gt; : &lt;code&gt;params.yaml&lt;/code&gt;에 정의된 instance parameter에 접근할 수 있다. 예를 들어 &lt;code&gt;REPLICAS&lt;/code&gt; 파라미터를 사용할 경우는 &lt;code&gt;{{.Params.REPLICAS}}&lt;/code&gt;와 같이 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.OperatorName&lt;/strong&gt; : Operator 명&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.Name&lt;/strong&gt; : 현재 Instance 명&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.Namespace&lt;/strong&gt; : Instance가 속한 namespace 명&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.Pipes&lt;/strong&gt; : Pipeline Parameter&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.PlanName&lt;/strong&gt; : Instance의 활성화된 Plan 명&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.PhaseName&lt;/strong&gt; : Instance의 활성화된 Plan의 활성화된 Phase 명&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.StepName&lt;/strong&gt; : Instance의 활성화된 Plan의 활성화된 Phase의 활성화된 Step 명&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.AppVersion&lt;/strong&gt; : 애플리케이션의 버전 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Functions&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;입력 값을 변수처럼 변환하고, Pipeline을 이용하면 다른 함수의 결과를 다른 함수의 입력으로 사용할 수도 있다. 이 함수 기능은 Go 템플릿과 Sprig 템플릿에서 제공된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Go Template&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compare&lt;/strong&gt; : &lt;code&gt;eq&lt;/code&gt;, &lt;code&gt;not&lt;/code&gt;, &lt;code&gt;ne&lt;/code&gt;, &lt;code&gt;lt&lt;/code&gt;, ... 등과 같이 Go Template의 비교 연산자 사용 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simple Calculate&lt;/strong&gt; : &lt;code&gt;len&lt;/code&gt;, &lt;code&gt;and&lt;/code&gt;, &lt;code&gt;or&lt;/code&gt;, &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;mul&lt;/code&gt;, ... 등과 같은 단순 연산 함수 사용 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text manipulation&lt;/strong&gt; : &lt;code&gt;html&lt;/code&gt;, &lt;code&gt;js&lt;/code&gt;, &lt;code&gt;trim&lt;/code&gt;, &lt;code&gt;wrap&lt;/code&gt;, ... 등과 같은 문자열 처리 함수 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sprig Template&lt;/strong&gt; : 환경 접근을 허용하는 Sprig 함수들은 비활성화된다. &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;expandenv&lt;/code&gt;, &lt;code&gt;base&lt;/code&gt;, &lt;code&gt;dir&lt;/code&gt;, &lt;code&gt;clean&lt;/code&gt;, &lt;code&gt;ext&lt;/code&gt;, &lt;code&gt;isAbs&lt;/code&gt;, ...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO Provide&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;toYAML&lt;/strong&gt; : 아규먼트들을 YAML로 반환하며, | trim | indent N 등과 같이 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Actions&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;Action은 템플릿에서 분기 또는 반복 처리에 사용한다. 아래의 내용은 몇 가지 예시로 보다 자세한 사항은 &lt;a href=&quot;https://golang.org/pkg/text/template/#hdr-Actions&quot;&gt;Go Template Actions&lt;/a&gt;를 참고한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Perform arithmetic using parameters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;모든 .Params는 문자열 형식이기 떄문에 처리하기 전에 형식 변환을 먼저해야 한다. 아래의 예는 파라미터에서 1을 뺴는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ sub (atoi .Params.NODE_COUNT) 1 }}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable or disable features using a feature parameter&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;.Params.ENCRYPTION 파라미터가 true/false인지에 따라서 HTTP/HTTPS로 설정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spec:
  ports:
    {{ if eq .Params.ENCRYPTION &amp;quot;true&amp;quot; }}
    - name: https
      port: 443
    {{ else }}
    - name: http
      port: 80
    {{ end }}
...&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create objects for a specific number&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;.Params.VOLUMES 에 따라서 persistent volume claims를 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ range $i, $v := until (int .Params.VOLUMES) }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: claim-{{ $i }}
...
{{ end }}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;일부 Kubernetes Object들은 템플릿으로 사용할 수 없다. 대표적인 예가 &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/job/&quot;&gt;Jobs&lt;/a&gt;로 &lt;code&gt;spec.template&lt;/code&gt; 필드는 변경할 수 없으며, Job이 적용된 후에는 이 필드롤 변경할 수 없다. spec.template를 템플릿화된 파라미터로 변경을 시도하면 아래와 같은 오류가 발생된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;... failed to execute patch: Job.batch &amp;quot;xxx&amp;quot; is invalid: spec.template: Invalid value: core.PodTemplateSpec{...} field is immutable&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이런 문제는 해당 Job을 삭제 (Delete Task 사용)하고 다시 생성해야 하며, 이 작업은 Operator Developer의 선택이다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CLI Usage&lt;/h2&gt;
&lt;h3&gt;Concepts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operator&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;High level description of a deployable service&lt;/li&gt;
&lt;li&gt;A deployable service can be anyting that you&amp;#39;d want to run on your cluster&lt;/li&gt;
&lt;li&gt;Represented as a CRD Object&lt;/li&gt;
&lt;li&gt;여러 개의 OperatorVersion 정보를 가질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OperatorVersion&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implementation of an Operator&lt;/li&gt;
&lt;li&gt;Specific version of a deployable application&lt;/li&gt;
&lt;li&gt;Contains parameters, objects, plans&lt;/li&gt;
&lt;li&gt;여러 개의 Instance 정보를 가질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instance&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tiles application instantiation to an OperatorVersion&lt;/li&gt;
&lt;li&gt;Once created, renders parameters in templates such as services, pods or StatefulSets&lt;/li&gt;
&lt;li&gt;Can create multiple instances of an OperatorVersion within your cluster&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Orchestarate tasks through phases and steps&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A structured &amp;#39;runbook&amp;#39; which can then be executed by software&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Typically define several plans:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deploy&lt;/li&gt;
&lt;li&gt;Backup&lt;/li&gt;
&lt;li&gt;Restore&lt;/li&gt;
&lt;li&gt;Upgrade&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Phases and steps can be run serial or parallel&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CLI extension to kubectl&lt;/li&gt;
&lt;li&gt;Can still use &amp;#39;vanilla&amp;#39; kubectl&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/orgs/kubobuilder/projects/2&quot;&gt;Roadmap&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic CRDs&lt;/strong&gt; : Manage the lifecycle of operator CRDs for the operator developers and users&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operator Dependencies&lt;/strong&gt; : Ability for KUDO to support a wide range of dependencies (from existing instances and connection strings to entirely new dependencies that are KUDO managed), and for tighter control of dependency specification by operator developers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operator Extensions&lt;/strong&gt; : Extend from other formats such as other KUDO operators, Helm charts, or CNAB bundles without forking and operator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Something other than YAML!&lt;/strong&gt; : Starlark or CUE like candidates&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pipe Tasks&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generation of content which can then be &amp;#39;piped&amp;#39; to another task&lt;/li&gt;
&lt;li&gt;e.g certicate generation / creation as part of bootstrap&lt;/li&gt;
&lt;li&gt;Just landed (&lt;a href=&quot;https://github.com/kudobuilder/kudo/pull/1105&quot;&gt;https://github.com/kudobuilder/kudo/pull/1105&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Helm chart&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Import and extend&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operator Development&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Skeleton Generator&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Also just landed (&lt;a href=&quot;https://github.com/kudobuilder/kudo/pull/1206&quot;&gt;https://github.com/kudobuilder/kudo/pull/1206&lt;/a&gt;, &lt;a href=&quot;https://github.com/kudobuilder/kudo/pull/1222&quot;&gt;https://github.com/kudobuilder/kudo/pull/1222&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Linter&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Snippet / extension library&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KUDO API&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operator Extensions&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;KUDO는 Dev/Ops 팀이 Operator 관리를 통해서 상태 저장 서비스를 포함해서 Kubernetes 환경에서 Day 2 Operating 관리를 할 수 있도록 설계&lt;/code&gt;되어 초기 배포 이상의 지원 (백업, 복구, 관찰, 업그레이드 등) 한다. 실제 발생하는 사례는 다음과 같다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/&quot;&gt;KUDO&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>Kubernetes</category>
      <category>kubernetes universal declarative operator</category>
      <category>Kudo</category>
      <category>kudobuilder</category>
      <category>operator</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/52</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-%EC%A0%95%EB%A6%AC-Kubernetes-Universal-Declarative-Operator-Kudobuilder#entry52comment</comments>
      <pubDate>Mon, 21 Dec 2020 17:06:21 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - Operator]  KUDO CLI 명령어 정리</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-CLI-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;h1&gt;How to use KUDO CLI Commands&lt;/h1&gt;
&lt;p&gt;KUDO 및 Package 관리를 위한 KUDO CLI를 정리한다.&lt;/p&gt;
&lt;h2&gt;Global Flags&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Global Flags&lt;/strong&gt; : 전체 명령들에 사용할 수 있는 전역 옵션들&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--home &amp;lt;path&amp;gt;&lt;/code&gt; : Kudo 설정이 있는 Home Directory 지정 (Default: &amp;quot;~/.kudo&amp;quot;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--kubeconfig &amp;lt;path&amp;gt;&lt;/code&gt; : Kubernetes 설정이 있는 Home Directory 지정 (Default: &amp;quot;~/.kube/config&amp;quot;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-n, --namespace &amp;lt;namespace&amp;gt;&lt;/code&gt; : 객체를 처리할 대상 Namespace (Default: &amp;quot;Default&amp;quot;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--request-timeout &amp;lt;seconds&amp;gt;&lt;/code&gt; : 요청 시간 제한을 초단위로 지정 (Default: 0 - 무제한)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v &amp;lt;line count&amp;gt;, --v&lt;/code&gt; : 상세 로그 수준을 라인단위로 지정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--validate-install&lt;/code&gt; : 명령을 실행하기 전에 KUDO 설치 여부 검증 (Default: true)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Semi-Global Flags&lt;/strong&gt; : 전체 명령들은 아니지만 많은 명령들에 사용할 수 있는 옵션들&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--help&lt;/code&gt; : 모든 명령에 지정 가능하며, 해당 명령의 상세 정보 출력&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o, --output&lt;/code&gt; : 많은 명령들이 지원하며, &amp;quot;Yaml&amp;quot; 또는 &amp;quot;Json&amp;quot; 방식으로 출력 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--dry-run&lt;/code&gt; : 실제 실행하지는 않고, 점검과 준비만 처리하며, &lt;code&gt;--output&lt;/code&gt; 옵션으로 대상 리소스에 대한 출력물 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;init&lt;/h2&gt;
&lt;p&gt;KUDO Server와 Client 컴포넌트의 초기화 및 업그레이드 처리&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo init [options]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;옵션들은 설치 및 업그레이드에 대한 변경을 위한 것으로 주로 Server를 대상으로 하며 저장되지 않기 때문에 매번 제공해야 한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;설치에 사용한 옵션들은 update / upgrade 등에도 동일하게 지정되어야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;초기화 관련 옵션&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;n, --namespace &amp;lt;namespace&amp;gt;&lt;/code&gt; : KUDO 초기화를 위한 것으로 Default: &amp;quot;kudo-system&amp;quot; 이며, 특정한 값을 지정하면 KUDO Controller가 해당 네임스페이스로 설치된다. &lt;code&gt;지정한 사용자 정의 네임스페이스는 생성되는 것이 아니기 때문에 미리 생성&lt;/code&gt;되어 있어야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-i, --kudo-image &amp;lt;image and/or version&amp;gt;&lt;/code&gt; : KUDO Controller image and/or Version을 지정한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--version &amp;lt;version&amp;gt;&lt;/code&gt; : KUDO Image의 버전을 지정한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--kudo-image-pull-policy &amp;lt;policy&amp;gt;&lt;/code&gt; : KUDO Controller Image pull 정책을 지정한다. (Default: &amp;quot;Always&amp;quot;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--service-account &amp;lt;account&amp;gt;&lt;/code&gt; : 기본 서비스 계정을 지정한다. 기본적으로 &amp;quot;kudo-manager&amp;quot; 계정이 cluster-admin 권한으로 생성된다. 따라서 &lt;code&gt;지정한 사용자 지정 계정도 이미 생성되어 있어야 하며, cluster-admin 권한을 가지고 있어야&lt;/code&gt; 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--unsafe-self-signed-webhook-ca&lt;/code&gt; : Webhook에 대한 테스트 목적의 자체 서명 번들을 사용하도록 지정한다. 기본적으로 KUDO는 &lt;a href=&quot;https://cert-manager.io/&quot;&gt;cert-manager&lt;/a&gt;가 존재하는 것으로 진행하며 설치되면 여러 버전을 지원한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다른 작업 옵션 &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--upgrade&lt;/code&gt; : 설치된 KUDO를 업그레이드 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--verify&lt;/code&gt; : 설치된 KUDO를 검증한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--client-only&lt;/code&gt; : ~/.kudo 디렉터리와 Repository 설정을 초기화하며, Server 관련 어떤 리소스도 처리하지 않는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-w --wait&lt;/code&gt; : KUDO Manager가 실행되고 요청을 받을 수 있는 상태일 때까지 대기한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--wait-timeout int&lt;/code&gt; : --wait 옵션과 같이 사용하며 지정한 시간 (Default: 300초) 동안 대기한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;get&lt;/h2&gt;
&lt;p&gt;설치된 사용자 Instances, Operators, OperatorVersions 객체를 조회한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo get [flags]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;instances&lt;/code&gt; : 설치된 모든 사용자 Instance 정보&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operators&lt;/code&gt; : 설치된 모든 사용자 Operators 정보&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operatorversions&lt;/code&gt; : 설치된 모든 사용자 OperatorVersions 정보&lt;/li&gt;
&lt;li&gt;&lt;code&gt;all&lt;/code&gt; : 설치된 모든 사용자 KUDO 객체들 정도 (Instances + Operators + OperatorVersions)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;--output yarml&lt;/code&gt; 옵션을 사용해서 출력을 저장할 수 있다.&lt;/p&gt;
&lt;h2&gt;search&lt;/h2&gt;
&lt;p&gt;Repository에서 지정한 검색어를 포함하는 항목을 검색한다. 기본적으로 설치된 Operator의 최신 버전만 나타나며 &lt;code&gt;--all-versions&lt;/code&gt;를 지정하면 설치된 모든 버전이 나타난다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo search &amp;lt;검색어&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--repo &amp;lt;repo name&amp;gt;&lt;/code&gt; : 지정한 Repository를 기준으로 검색을 수행한다. 기본 값은 KUDO Context 상의 기본 Respository (community)를 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-a, --all-versions&lt;/code&gt; : 지정한 검색어를 포함하는 모든 버전의 Operator들을 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;install&lt;/h2&gt;
&lt;p&gt;로컬 디렉터리, 공식 Repository 또는 클러스터 내의 리소스에서 KUDO Package를 설치한다.&lt;br&gt;Package name은 반드시 Repository 내의 Package 이름이거나 *.tgz 포맷의 Package를 나타내는 URL 또는 Path 이거나 &lt;code&gt;--in-cluster 옵션&lt;/code&gt;을 통해 이미 설치된 Operator의 이름 또는 압축 해제된 패키지 디렉터리를 지정해야 한다.&lt;br&gt;&lt;code&gt;로컬 경로의 Operator를 설치할 경우는 &amp;quot;./&amp;quot;, &amp;quot;../&amp;quot;, &amp;quot;/&amp;quot; 를 사용&lt;/code&gt;해야 한다. 그외는 모두 원격 Repository의 Operator name을 지정한 것으로 간주한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo install &amp;lt;operator name&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--app-version &amp;lt;app version&amp;gt;&lt;/code&gt; : App 버전을 지정한다. (Default: Official Github repo기준 최신 값)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--operator-version &amp;lt;operator version&amp;gt;&lt;/code&gt; : Operator 버전을 지정한다. (Default: Official Github repo기준 최신 값)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--repo &amp;lt;repo name&amp;gt;&lt;/code&gt; : 사용할 Repository를 지정한다. (Default: Context)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--instance string&lt;/code&gt; : 설치할 Instance 이름을 지정한다. (Default: Operator name + &amp;quot;-instance&amp;quot;, ex. first-operator =&amp;gt; first-operator-instance)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--skip-instance&lt;/code&gt; : Instance를 제외하고 Operator와 OperatorVersion만 설치 (Default: false)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--create-namespace&lt;/code&gt; : Namespace를 생성한다. 만일 이미 존재하는 경우라면 실패처리 된다. (Default: false)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-p, --parameter &amp;lt;parameter-name=parameter-value&amp;gt;&lt;/code&gt; : 파라미터의 이름과 값을 &amp;quot;=&amp;quot;를 이용해서 지정한다. ex. -p NODE_COUNT=3, ... 또한 배열이나 Map 형식의 경우는 YAML 포맷으로 지정이 가능하다. ex. 배열은 -p PORT=&amp;quot;[80, 443]&amp;quot; 으로 Map은 -p CUSTOM_LABELS=&amp;quot;{team: dev, owner: foo}&amp;quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-P, --parameter-file &amp;lt;parameter yaml format file path&amp;gt;&lt;/code&gt; : 파라미터를 지정한 YAML 포맷 파일 경로를 지정한다. 이 파일의 최상위 요소는 반드시 매핑 형식이어야 한다. 키는 파라미터 이름이고 값은 파라미터 값이다. 이 기능은 Instance의 파라미터 값을 버전 제어로 유지하려는 경우나 Command line에 지정하기 힘들게 긴 값이나 복잡한 값을 처리할 때 유용하다. 파라미터는 --paramter-file/-P 로 지정된 파일을 먼저 읽고 --parameter/-p 로 지정된 값을 읽어서 갱신하게 된다. 즉, 파라미터 파일에 정의된 파라미터 값을 지정한 파라미터 값으로 재 정의하는 경우에 유용하게 사용할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--wait&lt;/code&gt; : CLI가 설치가 완료될 때까지 대기해야 하는 경우 지정한다. (Default: false)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--wait-time &amp;lt;wait time seconds&amp;gt;&lt;/code&gt; : CLI가 최대 대기할 시간을 초 단위로 지정한다. (Default: 300초)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--in-cluster&lt;/code&gt; : 지정되면 이미 클러스터에 설치된 Operator version에서 검색한다. 이미 설치된 Operation version은 &lt;code&gt;get&lt;/code&gt; 명령을 통해서 확인할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;plan&lt;/h2&gt;
&lt;h3&gt;Status&lt;/h3&gt;
&lt;p&gt;지정한 Instance에 대한 Plan 상태를 나타낸다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo plan status --instance=&amp;lt;instance name&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--output yaml&lt;/code&gt; : 결과를 YAML 포맷 파일로 출력&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--wait&lt;/code&gt; : CLI가 모든 결과를 출력할 때까지 대기 (Default: false)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;History&lt;/h3&gt;
&lt;p&gt;지정한 Instance에 대한 각 Plan의 History를 나타낸다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo plan history --instance=&amp;lt;instance name&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;tigger&lt;/h3&gt;
&lt;p&gt;지정한 Instance에 대해 지정한 Plan을 실행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo plan trigger &amp;lt;plan name&amp;gt; --instance=&amp;lt;instance name&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--wait&lt;/code&gt; : CLI가 모든 결과를 출력할 때까지 대기 (Default: false)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--wait-time &amp;lt;wait time second&amp;gt;&lt;/code&gt; : Trigger된 Plan이 완료될 때까지 대기할 최대 시간을 초 단위로 지정 (Default: 300s)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;uninstall&lt;/h2&gt;
&lt;p&gt;설치된 KUDO Package의 Instance를 제거한다. 물론 연관된 Deployments, Pods 와 같은 리소스들을 제거한다. 단, OperatorVersion이나 Operator CRD는 제거하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo uninstall --instance=&amp;lt;instance name&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;주의&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이 명령은 Instance를 제거하는 것이기 때문에 데이터, 로그 파일 등의 손실이 발생하게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;update&lt;/h2&gt;
&lt;p&gt;설치된 KUDO Operator Instance를 새로운 파라미터 값으로 갱신한다. 파라미터의 갱신은 파라미터 정의할 때 지정한 특정한 Plan을 실행시키게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo update --instance=&amp;lt;instance name&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-p, --parameter &amp;lt;parameter-name=parameter-value&amp;gt;&lt;/code&gt; : 파라미터의 이름과 값을 &amp;quot;=&amp;quot;를 이용해서 지정한다. ex. -p NODE_COUNT=3, ... &lt;code&gt;파라미터 옵션은 여러 번 중복 지정이 가능하다&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-P, --parameter-file &amp;lt;parameter yaml format file path&amp;gt;&lt;/code&gt; : 파라미터를 지정한 YAML 포맷 파일 경로를 지정한다. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;--wait&lt;/code&gt; : CLI가 모든 결과를 출력할 때까지 대기 (Default: false)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--wait-time &amp;lt;wait time second&amp;gt;&lt;/code&gt; : Trigger된 Plan이 완료될 때까지 대기할 최대 시간을 초 단위로 지정 (Default: 300s)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;upgrade&lt;/h2&gt;
&lt;p&gt;현재 설치된 KUDO Operator Instance 버전을 새로운 버전으로 업그레이드 한다. 업그레이드하는 Operator의 이름은 Repository에 존재하는 패키지의 이름이거나 *.tgz 포맷의 경로 또는 URL 이거나 로컬의 압축 해제된 Opeartor 경로를 지정한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;로컬 경로의 Operator를 설치할 경우는 &amp;quot;./&amp;quot;, &amp;quot;../&amp;quot;, &amp;quot;/&amp;quot; 를 사용&lt;/code&gt;해야 한다. 그외는 모두 원격 Repository의 Operator name을 지정한 것으로 간주한다.&lt;/p&gt;
&lt;p&gt;새로운 Operator 버전에 따라서 특정한 파라미터를 지정해야 할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo upgrade &amp;lt;opeator name | path | url&amp;gt; --instance=&amp;lt;instance name&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--app-version &amp;lt;app version&amp;gt;&lt;/code&gt; : App 버전을 지정한다. (Default: Official Github repo기준 최신 값)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--operator-version &amp;lt;operator version&amp;gt;&lt;/code&gt; : Operator 버전을 지정한다. (Default: Official Github repo기준 최신 값)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--repo &amp;lt;repo name&amp;gt;&lt;/code&gt; : 사용할 Repository를 지정한다. (Default: Context)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p, --parameter &amp;lt;parameter-name=parameter-value&amp;gt;&lt;/code&gt; : 파라미터의 이름과 값을 &amp;quot;=&amp;quot;를 이용해서 지정한다. ex. -p NODE_COUNT=3, ... &lt;code&gt;파라미터 옵션은 여러 번 중복 지정이 가능하다&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-P, --parameter-file &amp;lt;parameter yaml format file path&amp;gt;&lt;/code&gt; : 파라미터를 지정한 YAML 포맷 파일 경로를 지정한다. &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;diagnostics&lt;/h2&gt;
&lt;p&gt;진단 기능은 진단 데이터를 수집하고 분석하는 기능을 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl kudo diagnostics collect --instance=&amp;lt;instance name&amp;gt; [options]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--log-since &amp;lt;timeDuration&amp;gt;&lt;/code&gt; : 현재 시점을 기준으로 지정한 5s, 2m, 3h 등과 같이 지정한 구간까지의 로그들을 대상으로 한다. (Default: all logs)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-O, --output-directory &amp;lt;path&amp;gt;&lt;/code&gt; : 수집된 리소스들을 저장할 디렉터리 경로 지정 (Default: &amp;quot;diag&amp;quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Development Commands&lt;/h2&gt;
&lt;p&gt;Operator 개발자들에 유용한 명령들이다.&lt;/p&gt;
&lt;h3&gt;Package&lt;/h3&gt;
&lt;p&gt;KUDO Package와 상호 작용하는 여러 개의 서브 명령들로 구성된다.&lt;/p&gt;
&lt;p&gt;Operator를 패키징하거나 검증하고 파라미터들을 나열하는데 사용한다. 파라미터들과 같이 작업할 때는 지정한 URL이나 이름과 버전등으로 원격 Repository의 파라미터 변수 목록을 제공할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt; : Operator Package 파일들에 새로운 컨텐츠를 추가한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create&lt;/code&gt; : 로컬에 구성한 KUDO Operator를 tarball 형식의 패키지로 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt; : Operator Package로 부터 Parameters, Plans, Tasks 왁 같은 정보들을 출력한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; : 상호 작용을 통해서 새로운 Operator를 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verify&lt;/code&gt; : Operator Package를 검증한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;좀 더 자세한 사항은 해당 명령에 대한 &lt;code&gt;--help&lt;/code&gt; 옵션을 지정해서 확인하면 된다.&lt;/p&gt;
&lt;h3&gt;Repository&lt;/h3&gt;
&lt;p&gt;KUDO Repositories와 상호 작용하는 여러 개의 서브 명령들로 구성된다.&lt;/p&gt;
&lt;p&gt;It can be used to add, remove, list, and index kudo repositories.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt; : Operator Repository를 추가한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context&lt;/code&gt; : KUDO Context에 관리되는 Repository들 중에 하나를 기본 값으로 설정한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index&lt;/code&gt; : 지정한 KUDO Operator Package들이 존재하는 디렉터리에 색인 파일 (Index.yaml)을 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt; : KUDO Context에서 관리되는 Repository 리스트를 출력한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remove&lt;/code&gt; : Remove an operator repository&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;좀 더 자세한 사항은 해당 명령에 대한 &lt;code&gt;--help&lt;/code&gt; 옵션을 지정해서 확인하면 된다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kudo.dev/docs/cli/commands.html&quot;&gt;KUDO CLI Commands&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>Commands</category>
      <category>Kubernetes</category>
      <category>Kudo</category>
      <category>kudo-cli</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/51</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Operator-KUDO-CLI-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%A0%95%EB%A6%AC#entry51comment</comments>
      <pubDate>Mon, 21 Dec 2020 16:48:52 +0900</pubDate>
    </item>
    <item>
      <title>[Container] Multi-Container Design Patterns 간략 정리</title>
      <link>https://ccambo.tistory.com/entry/Container-Multi-Container-Design-Patterns-%EA%B0%84%EB%9E%B5-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;h1&gt;Multi-Container Design Patterns 정리&lt;/h1&gt;
&lt;p&gt;지난 몇 년동안 컨테이너 기술은 코드를 패키징하고 배포하는 대중적인 기술로 발전했다. 여기서도 컨테이너를 통해 &lt;code&gt;분산 응용 프로그램을 구축&lt;/code&gt;하는 방법의 변화에 대해 주목할 필요가 있다.&lt;/p&gt;
&lt;p&gt;이번에는 마아크로 서비스 아키텍처에서 컨테이너늘 다루는데 사용할 수 있는 디자인 패턴 3가지에 대해서 정리해 본다.&lt;/p&gt;
&lt;h2&gt;Sidecar Pattern&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;font color=&quot;red&quot;&gt;&lt;strong&gt;하나의 컨테이너는 하나의 책임만 가지고 있어야 한다.&lt;/strong&gt;&lt;/font&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;예를 들면 웹 서버와 로그 수집기가 같이 존재하는 컨테이너가 있다고 가정했을 때 다음과 같은 고민을 하게 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;웹 서버를 수정할 때 로그 수집기의 종속성을 고려해야 한다.&lt;/li&gt;
&lt;li&gt;직접적인 관련은 없더라도 오류가 발생했을 때 어디서 문제가 발생했는지 빠르게 파악하지 못할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 같이 상관이 없음에도 고려할 부분이 생길 수 있기 때문에 서로 다른 역할을 하는 서비스는 각각의 컨테이너로 분리해 주는 것이 좋으며, 이런 상황에서 활용할 수 있는 패턴이 &lt;code&gt;Sidecar Pattern&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?export=view&amp;id=16ifJN7HnSGaYZ3XP7U8sW9WbTm8X0HNN&quot; alt=&quot;Sidecar Pattern in Pod&quot;&gt;&lt;/p&gt;
&lt;p&gt;Pod 내의 메인 컨테이너 (위의 그림에서는 Web Server Container)를 확장하고 향상 및 개선하는 역할을 컨테이너를 &lt;code&gt;Sidecar Container&lt;/code&gt;라고 하고, 이렇게 적용하는 패턴을 &lt;code&gt;Sidecar Pattern&lt;/code&gt; 이라고 한다.&lt;/p&gt;
&lt;p&gt;서로 다른 컨테이너이며 단지 Pod내에 묶여 있는 것이기 때문에 서로 간에 종속성이나 간섭의 문제가 생기지 않는다. 당연히 Web Server를 교체하더라도 로그 수집기 컨테이너는 변경이 없고 특정 기능을 활용하도록 재 사용성이 증가하는 효과를 가져오게 된다.&lt;/p&gt;
&lt;h2&gt;Ambassador Pattern&lt;/h2&gt;
&lt;p&gt;메인 컨테이너의 네트워크 연결을 전담하는 프록시 컨테이너를 두는 패턴이다. &lt;br&gt;Sidecar Pattern 이 이미 완성되고 동작하는 애플리케이션에 기능 추가를 위한 변경이 없이 보조 기능으로 추가해서 활용하는 측면을 강조한 것이라면 Ambassador Pattern은 개별 기능만을 중점적으로 처리하는 기능 모듈과 같은 패턴이다.&lt;/p&gt;
&lt;p&gt;따라서 Sidecar Pattern 과는 달리 사용할 기능을 가진 모듈에 종속성이 존재하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?export=view&amp;id=1BKMBOzRsLnX2I5IaxziQJRvsD1wxnXPe&quot; alt=&quot;Ambassador Pattern in Pod&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 그림에서와 같이 Pod 내에서 메인 어플리케이션 컨테이너는 자체 기능에 집중하고 네트워크와 관련된 부분은 Proxy Ambassador 컨테이너가 전담하는 방식이다.&lt;/p&gt;
&lt;h2&gt;Adapter Pattern&lt;/h2&gt;
&lt;p&gt;Adapter Pattern은 메인 애플리케이션의 기능을 표준화해서 다양한 기능들을 표준화에 맞춰서 연결하는 패턴을 의미한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?export=view&amp;id=1sKdNWTLU2xmMYU6gK_YrgeUZ1aM2ImmU&quot; alt=&quot;Adapter Pattern in Pod&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 그림과 같이 동작하고 있는 애플리케이션에 대한 모니터링 정보를 수집하는 경우에 직접 구현하는 것이 아니라 표준화된 모니터링 데이터 I/O 를 정의해 놓고 실제 기능은 Adapter 에처 처리하는 방식으로 원하는 기능을 표준화해서 다양한 유형으로 연계하는 방식이다.&lt;/p&gt;
&lt;h2&gt;3가지 패턴의 차이점&lt;/h2&gt;
&lt;p&gt;3가지 패턴은 딱 구별하기 힘들다. 어떤 패턴을 쓰더라도 원하는 기능은 만족시킬 수가 있다. 따라서 &lt;font color=&quot;red&quot;&gt;&lt;code&gt;패턴의 차이점은 목적이 무엇인지를 기준&lt;/code&gt;&lt;/font&gt;으로 차이점을 볼 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sidecar Pattern : 메인 컨테이너의 변경없이 기능을 확장 또는 향상 시키는 목적&lt;/li&gt;
&lt;li&gt;Ambassador Pattern : 메인 컨테이너의 특정 기능을 전담하는 종속성을 특화 시키는 목적 (물론 다른 애플리케이션에도 사용할 수 있도록 독립적이어야 한다)&lt;/li&gt;
&lt;li&gt;Adapter Pattern : 메인 컨테이너의 입/출력 표준화를 통한 다양한 연계 구성을 교체가능하도록 하는 목적 (ex. Databases, Files, ...)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gruuuuu.github.io/cloud/architecture-microservice/&quot;&gt;마이크로 서비스 아키텍처란?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/blog/2015/06/the-distributed-system-toolkit-patterns/&quot;&gt;Kubernetes/The Distributed System Toolkit: Patterns for Composite Containers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Container</category>
      <category>adapter</category>
      <category>ambassadar</category>
      <category>Container</category>
      <category>Design Pattern</category>
      <category>multi-container</category>
      <category>side-car</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/50</guid>
      <comments>https://ccambo.tistory.com/entry/Container-Multi-Container-Design-Patterns-%EA%B0%84%EB%9E%B5-%EC%A0%95%EB%A6%AC#entry50comment</comments>
      <pubDate>Sun, 20 Dec 2020 22:29:01 +0900</pubDate>
    </item>
    <item>
      <title>[Golang] Golang으로 MongoDB 사용하기</title>
      <link>https://ccambo.tistory.com/entry/Golang-Golang%EC%9C%BC%EB%A1%9C-MongoDB-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to use MongoDB with Golang&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;이 문서에서는 아주 간단하게 Docker-compose를 이용해서 MongoDB를 구동하고 Golang을 통해서 연결하는 부분에 대해서 정리하고 있습니다.&lt;br&gt;환경은 맥북, VSCode, Golang, Docker 등이 이미 설치되어 있는 것을 기준으로 합니다. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;간단한 MongoDB 정리&lt;/h2&gt;
&lt;h3&gt;RDBMS와의 비교&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;RDBMS&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;MongoDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Database&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Table&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Tuple/Row&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Column&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Key/Field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Table Join&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Embedded Documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Primary Key&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Primary Key (_id)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;특징과 장/단점&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;주요 특징&lt;/strong&gt;들은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document-Oriented Storage&lt;/strong&gt; : Database &amp;gt; Collections &amp;gt; Documents 구조로 Document는 key-value 형태의 &lt;a href=&quot;http://bsonspec.org/&quot;&gt;BSON (Binary JSON)&lt;/a&gt;으로 되어 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Full Index Support&lt;/strong&gt; : 다양한 인덱싱을 제공한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single Field Indexes&lt;/strong&gt; : 기본적인 인덱스 타입&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compound Indexes&lt;/strong&gt; : RDBMS의 복합 인덱스 타입&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multikey Indexes&lt;/strong&gt; : Array에 매칭되는 값이 하나라도 있으면 인덱스에 추가하는 인덱스 타입&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Geospatial Indexes and Queries&lt;/strong&gt; : 위치기반 인덱스와 쿼리 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text Indexes&lt;/strong&gt; : String에 대한 인덱스 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hashed Indexes&lt;/strong&gt; : Btree 인덱스가 아닌 Hash 타입의 인덱스도 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replication &amp;amp; High Availablity&lt;/strong&gt; : 간단한 설정을 통해서 데이터 복제를 지원하므로 가용성이 향상된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto-Sharding&lt;/strong&gt; : 자동으로 데이터를 분산해서 저장하며, 하나의 컬랙션처럼 사용할 수 있도록 수평적 확장 기능을 제공한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Querying (Document based query)&lt;/strong&gt; : 필터링, 수집, 정렬, 정규표현식 등의 다양한 쿼리문 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fast In-Place Updates&lt;/strong&gt; : 고성능 atomic operation 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Map/Reduce&lt;/strong&gt; : 맵리듀스 지원 (map과 reduce 함수의 조합을 통해서 분산/병렬 시스템 운용 지원, 하둡처럼 MR전용 시스템에 비해서는 성능이 떨어진다)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GridFS&lt;/strong&gt; : 분산 파일 저장을 지원하기 때문에 실제 파일이 어디 저장되는지를 알 필요가 없으며 복구도 자동으로 지원된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Commerical Support&lt;/strong&gt; : 10gen에서 관리&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;장점&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;데이터를 쌓아놓고 삭제가 필요없는 경우가 가장 적합하다. (ex. 로그 데이터 등)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flexibility&lt;/strong&gt; : Schema-less (or Schema-Free) 라서 어떤 형태의 데이터라도 저장이 가능하다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt; : Read &amp;amp; Write 성능이 뛰어나다.캐싱이나 많은 트래픽을 감당할때 사용해도 좋다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt; : 기본적으로 Scale-out 구조를 채택해서 쉽게 운용이 가능하다. Auto-Sharding 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deep Query Ability&lt;/strong&gt; : 문서 지향적 Query Language를 사용해서 SQL만큼 강력한 Query성능을 제공한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conversion / Mapping&lt;/strong&gt; : JSON 형태로 저장 (실제는 BSON) 되기 때문에 직관적이고 개발이 편리하다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;단점&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;정합성이 요구되어 트랜잭션 관리가 필요한 경우는 부적합하다. (ex. 금융, 회계, 회원정보 등)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Join이 없다&lt;/strong&gt; : JOIN이 필요없도록 데이터의 구조를 잡아야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File 기반이다&lt;/strong&gt; : Memroy mapped file 기반의 파일 엔진 DB이며 메모리 관리를 OS에 의존한다. 따라서 메모리 의존성이 있으며 메모리 크기가 성능을 좌우한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SQL을 완전히 이관할 수 없다&lt;/strong&gt; : SQL을 그대로 이전할 수 없으며 맞도록 변환해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;BTree 성능 이슈가 있다&lt;/strong&gt; : 인덱스를 BTree 기반으로 하고 있으므로 크기가 커질수록 새로운 데이터를 입력하거나 삭제할 때 성능이 저하된다. 따라서 데이터를 넣어두고 조회만 하는데 적합하다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;메모리 관련 이슈&lt;/h3&gt;
&lt;p&gt;데이터를 저장할 때 논리적으로 메모리에 먼저 저장하고 일정 주기에 따라서 메모리 블럭들을 디스크로 출력하는데 이 부분을 OS의 의존하고 있다. 실제 메모리가 작아도 OS의 가상메모리 운영 방식에 따라서 운영된다.&lt;/p&gt;
&lt;p&gt;이런 운영 구조 때문에 메모리에서 데이터 블럭을 참조할 때 없다면 &amp;quot;Page Fault&amp;quot; 오류가 발생하고, 이 상황에서 디스크에서 해당 블록을 찾아서 메모리에 로드하여 처리하게 된다. 이 과정에서 모자라는 메모리 때문에 다른 블럭을 디스크에 쓰고 제거한 후 필요한 블럭을 메모리로 올리는 작업이 처리되기 때문에 디스크 I/O가 발생하므로 성능 저하가 발생할 수 밖에는 없다.&lt;/p&gt;
&lt;p&gt;따라서 메모리 크기가 성능을 좌우한다는 것은 Page Fault 오류의 발생 반도에 근거하고 있다고 보면 된다. 따라서 데이터 설계를 할 때 자주 사용되는 데이터가 메모리에 상주할 수 있도록 key 설계를 하는 것이 매우 중요하다. 또한 테이블을 풀 스캔하는 작업은 무조건 Page Fault를 발생시키게 되므로 이런 경우는 Index Table (Summary Table) 등을 만들어서 운영하는 것이 성능을 위한 방법이라고 할 수 있다.&lt;/p&gt;
&lt;h3&gt;주요 용어&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Document&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RDBMS의 Tuple / Row와 대응되는 개념으로 Key-Value 쌍으로 구성되며, Value에는 또 다른 document가 설정될 수도 있다. 동적 스키마를 가지고 있기 때문에 같은 Collection (Table) 안에 있는 Document끼리도 다른 스키마를 가질 수 있다. (Free Schema)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{ 
  &amp;quot;_id&amp;quot;: ObjectId(&amp;quot;5099803df3f4948bd2f98391&amp;quot;), 
  &amp;quot;username&amp;quot;: &amp;quot;Morris&amp;quot;, 
  &amp;quot;language&amp;quot;: { 
    Nuxt: 3,
    Go: 3,
  } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Primary Key&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RDBMS의 Primary Key와 대응되는 개념으로 ObjectId는 12bytes의 16진수 값으로 각 Document의 유일성을 보장하는 역할을 담당한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4bytes : Timestamp Data&lt;/li&gt;
&lt;li&gt;3bytes : Machine id Data&lt;/li&gt;
&lt;li&gt;2bytes : MongoDB 서버의 Process id Data&lt;/li&gt;
&lt;li&gt;3bytes : Sequenctial number Data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;개념적으로는 RDBMS의 auto increment와 비슷한 개념으로 생각하면 될 듯 하다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MongoDB에서 Collection에 저장된 각 Document들은 반드시 기본 키 역할을 담당하는 &lt;strong&gt;&amp;quot;_id&amp;quot;&lt;/strong&gt; 라는 필드를 가져야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Collection&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RDBMS의 Table에 대응되는 개념으로 Document의 그룹이며, Document들이 Collection 내부에 위치한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RDBMS의 Database에 대응되는 개념으로 Collection들의 물리적인 컨테이너다. 따라서 각 Database는 물리적인 파일 시스템에 여러 개의 파일로 저장된다.&lt;/p&gt;
&lt;h2&gt;MongoDB 실행하기 (docker-compose)&lt;/h2&gt;
&lt;p&gt;MongoDB 자체를 실행하는 것은 그렇게 어렵지 않다. 아래와 같이 아주 단순한 docker-compose.yml을 구성하면 바로 구동된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &amp;quot;3.3&amp;quot;
services:
  mongodb:
    image: mongo:latest     # 사용할 Docker Image
    container_name: mongdb  # Docker Container 식별 명
    restart: always
    environment:
      MONGO_INITDB_DATABASE: &amp;quot;사용할 데이터베이스 명 설정&amp;quot;    # 정보만 존재하고 실제 데이터가 처리될 때 생성됨.
      MONGO_INITDB_ROOT_USERNAME: &amp;quot;root 사용자 설정&amp;quot;          # 최초 실행되서 DB 구성할 때 사용자 생성됨.
      MONGO_INITDB_ROOT_PASSWORD: &amp;quot;root 사용바 비밀번호 설정&amp;quot; # 최초 실행되서 DB 구성할 때 사용자 생성됨.
    volumes:
      - ./data/mongodb/:/data/db  # 로컬 경로를 컨테이너의 볼륨으로 연계
    ports:
      - &amp;quot;27017:27017&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 구성하고 docker-compose.yml 파일이 존재하는 경로에서 아래의 명령으로 실행하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ docker-compose up

# Background로 실행하는 방법
$ docker-compose up -d

# 로그 확인하는 방법
$ docker-compose logs&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로그를 확인해 보면 중간에 root 계정을 생성하는 것을 확인할 수 있으며, 인증 모드로 동작하고 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
mongdb     | 2020-01-07T10:58:38.416+0000 I  INDEX    [conn2] index build: done building index user_1_db_1 on ns admin.system.users
mongdb     | Successfully added user: {
mongdb     |    &amp;quot;user&amp;quot; : &amp;quot;root&amp;quot;,
mongdb     |    &amp;quot;roles&amp;quot; : [
mongdb     |            {
mongdb     |                    &amp;quot;role&amp;quot; : &amp;quot;root&amp;quot;,
mongdb     |                    &amp;quot;db&amp;quot; : &amp;quot;admin&amp;quot;
mongdb     |            }
mongdb     |    ]
mongdb     | }
...
mongdb     | 2020-01-07T10:58:48.466+0000 I  CONTROL  [initandlisten] MongoDB starting : pid=1 port=27017 dbpath=/data/db 64-bit host=7531e03b8054
mongdb     | 2020-01-07T10:58:48.466+0000 I  CONTROL  [initandlisten] db version v4.2.2
mongdb     | 2020-01-07T10:58:48.466+0000 I  CONTROL  [initandlisten] git version: a0bbbff6ada159e19298d37946ac8dc4b497eadf
mongdb     | 2020-01-07T10:58:48.466+0000 I  CONTROL  [initandlisten] OpenSSL version: OpenSSL 1.1.1  11 Sep 2018
mongdb     | 2020-01-07T10:58:48.467+0000 I  CONTROL  [initandlisten] allocator: tcmalloc
mongdb     | 2020-01-07T10:58:48.467+0000 I  CONTROL  [initandlisten] modules: none
mongdb     | 2020-01-07T10:58:48.467+0000 I  CONTROL  [initandlisten] build environment:
mongdb     | 2020-01-07T10:58:48.468+0000 I  CONTROL  [initandlisten]     distmod: ubuntu1804
mongdb     | 2020-01-07T10:58:48.468+0000 I  CONTROL  [initandlisten]     distarch: x86_64
mongdb     | 2020-01-07T10:58:48.468+0000 I  CONTROL  [initandlisten]     target_arch: x86_64
mongdb     | 2020-01-07T10:58:48.468+0000 I  CONTROL  [initandlisten] options: { net: { bindIp: &amp;quot;*&amp;quot; }, security: { authorization: &amp;quot;enabled&amp;quot; } }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;좀 더 많은 구성 옵션들과 실행과 관련된 스크립트들 (예를 들어 일반 사용자 추가 등)을 더 설정할 수 있지만, 여기서는 이 정도만 구성해도 충분하다.&lt;/p&gt;
&lt;h2&gt;Golang으로 연결하기&lt;/h2&gt;
&lt;p&gt;처음 Golang으로 연결하면서 여러 가지 정보들을 확인해 봤지만 Golang 버전에 따라서 Mongo Driver 들에 따라서 다양한 글들과 방법들이 나오지만 이런저런 오류들이 발생하면서 오히려 헷갈리는 상황들이 존재한다.&lt;/p&gt;
&lt;p&gt;이 문서에는 Golang 버전의 MongoDB Official 격이라고 판단되는 &lt;a href=&quot;https://github.com/mongodb/mongo-go-driver&quot;&gt;mongo-go-driver&lt;/a&gt;를 기준으로 한다.&lt;/p&gt;
&lt;h3&gt;import 구성&lt;/h3&gt;
&lt;p&gt;아래의 코드는 mongodb driver의 go 라이브러리를 import 하는 것이다. 두 가지 방법 중에 무엇을 사용해도 상관없지만 코드 구성 후에 자동 import 처리되는 것을 확인해 보니 &lt;font color=&quot;red&quot;&gt;go.mongodb.org&lt;/font&gt;로 사용되기 때문에 이를 기준으로 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;...
import (
  ...
  // 다른 소스들은 아래와 같이 처리하는 것도 많다.
  &amp;quot;github.com/mongodb/mongo-go-driver/bson&amp;quot;
  &amp;quot;github.com/mongodb/mongo-go-driver/bson/primitive&amp;quot;
  &amp;quot;github.com/mongodb/mongo-go-driver/mongo&amp;quot;
  &amp;quot;github.com/mongodb/mongo-go-driver/mongo/options&amp;quot;
  ...
  // 실제 코드는 CDN 격인 go.mongodb.org를 사용해서 처리했다.
  &amp;quot;go.mongodb.org/mongo-driver/bson&amp;quot;
  &amp;quot;go.mongodb.org/mongo-driver/bson/primitive&amp;quot;
  &amp;quot;go.mongodb.org/mongo-driver/mongo&amp;quot;
  &amp;quot;go.mongodb.org/mongo-driver/mongo/options&amp;quot;
)
...&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;클라이언트 연결과 검증 코드&lt;/h3&gt;
&lt;p&gt;구동 중인 mongodb가 authentication mode로 동작하고 있고, root 사용자만 만들어 놓은 상태기 때문에 이를 아래의 코드를 통해서 연결과 검증을 하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;...
// timeout 기반의 Context 생성
ctx, _ := context.WithTimeout(context.Background(), conf.Timeout)

// Authetication 처리를 위한 Client Option 구성 (docker-compose.yml에 구성한 port 기준)
clientOptions := options.Client().ApplyURI(&amp;quot;mongodb://localhost:27017)
  .SetAuth(options.Credential{
    AuthSource: &amp;quot;&amp;quot;,   // 지금은 필요없음
    Username: &amp;quot;docker-compose.yml에 지정한 사용자&amp;quot;,
    Password: &amp;quot;docker-compose.yml에 지정한 사용자 비밀번호&amp;quot;,
  })

// mongodb 연결
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
  return nil, err
}

// 연결 검증
err = client.Ping(context.Background(), nil)
if err != nil {
  return nil, err
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;클라이언트 옵션에 더 많은 구성들이 있지만 이 부분들은 mongodb 매뉴얼등을 검토해 보면서 적용하면 된다.&lt;/p&gt;
&lt;h2&gt;발생했던 문제점들&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;ApplyURI에 &amp;quot;mongodb://ID:PW@localhost:27017&amp;quot; 방식으로 구성할 때 오류&lt;/strong&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;un-escaped character @ in user info 오류&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위의 같은 오류 메시지는 ID나 PW에 &lt;strong&gt;@&lt;/strong&gt; 문자가 존재하는 경우에 직접 전달되면 발생하게 된다. 이를 해결하기 위해서는 &lt;code&gt;&amp;quot;net/url&amp;quot;&lt;/code&gt; 패키지를 import 하고 아래와 같이 escape 처리를 해 줘야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;option=&amp;quot;linenos=inline,hl_lines=4 10&amp;quot; %}}
...
import (
  ...
  &amp;quot;net/url&amp;quot;
  ...
)
...
// getConnectionURI - Returns the connection URI from the configuration
func getConnectionURI(conf *Config) string {
    return &amp;quot;mongodb://&amp;quot; + url.QueryEscape(conf.UserName) + &amp;quot;:&amp;quot; + url.QueryEscape(conf.Password) + &amp;quot;@&amp;quot; + conf.Host + &amp;quot;:&amp;quot; + conf.Port + &amp;quot;/&amp;quot; + conf.DatabaseName
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;font color=&quot;red&quot;&gt;url.QueryEscape&lt;/font&gt; 함수를 이용해서 데이터에 존재하는 특수문자를 안전하게 인식될 수 있도록 변환해 주면 된다.&lt;/p&gt;
&lt;h3&gt;데이터 처리할 때 &lt;strong&gt;&amp;quot;(Unauthorized) command insert requires authentication&amp;quot; 오류&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;위에 언급했던 Authentication Mode로 구동되고 있는 mongodb에 인증을 처리하지 않고 Connection을 연결한 후 실제 데이터를 처리할 때 인증되지 않은 사용자로 인해서 발생하는 오류다. 이 경우는 위의 코드에서 보여진 것과 같이 인증을 한 Connection을 사용하면 오류가 해결된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;font color=&quot;red&quot;&gt;Authentication 처리를 구성한 Client Option 사용한 연결&lt;/font&gt; 처리가 필요하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;연결할 때 &lt;strong&gt;Authentication mechanism SCRAM-SHA-1 오류&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;이 오류는 mongodb의 인증 방식에 대한 문제로 위에서 보여준 &lt;strong&gt;연결 문자열을 사용해서 처리&lt;/strong&gt;할 떄 발생하는 오류로 인증 방식이 맞지 않아서 발생하게 된다. 이 부분에 대해서는 &lt;a href=&quot;http://www.mongoing.com/docs/core/authentication-mechanisms.html&quot;&gt;MongoDB Manual - Authentication Mechanisms&lt;/a&gt;를 참고하면 된다.&lt;/p&gt;
&lt;p&gt;제공되는 Authentication Mechanism은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SCRAM&lt;/li&gt;
&lt;li&gt;X.509 Certificate Authentication&lt;/li&gt;
&lt;li&gt;LDAP Proxy Authentication (Enterprise)&lt;/li&gt;
&lt;li&gt;Kerberos Authentication (Enterprise)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;표준 URI Connection Schema는 아래와 같다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;mongodb://[username:password@]host1[:port1][...hostN[:portN]][/[database][?options]]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위에서 보이는 것과 같이 여러 개의 host 연결 지정이 가능하고, 사용할 데이터베이스와 연결에 사용할 옵션들 지정이 가능하다.&lt;/p&gt;
&lt;p&gt;옵션을 통해서 Authentication Mechanism을 지정할 수 있다. 자세한 내용은 &lt;a href=&quot;https://docs.mongodb.com/manual/reference/connection-string/#connections-connection-options&quot;&gt;Connection Options&lt;/a&gt;을 참고한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Connection String을 기준으로 각종 옵션을 적용해서 처리하는 것도 방법이지만 여러 가지 설정을 Configuration으로 처리하고 운영하기에는 Authenticated Client 를 사용하는 방식이 더 효율적인 것으로 판단된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;참고 문서&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://bcho.tistory.com/746&quot;&gt;MongoDB Physical 데이터 저장 구조&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.mongodb.com/manual/&quot;&gt;MongDB - Document&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://junhobaik.github.io/mongodb-basic-crud/&quot;&gt;MongoDB 기초 - CRUD 명령어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://siyoon210.tistory.com/130&quot;&gt;SQL vs NoSQL (MySQL vs. MongoDB)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Golang</category>
      <category>docker-compose</category>
      <category>Golang</category>
      <category>MongoDB</category>
      <category>nosql</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/49</guid>
      <comments>https://ccambo.tistory.com/entry/Golang-Golang%EC%9C%BC%EB%A1%9C-MongoDB-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0#entry49comment</comments>
      <pubDate>Sun, 20 Dec 2020 22:16:57 +0900</pubDate>
    </item>
    <item>
      <title>[Yarn] Yarn 과 Npm 비교</title>
      <link>https://ccambo.tistory.com/entry/Yarn-Yarn-%EA%B3%BC-Npm-%EB%B9%84%EA%B5%90</link>
      <description>&lt;h1&gt;Yarn vs NPM 비교&lt;/h1&gt;
&lt;p&gt;node + npm이 기본이었는데, 몇 가지 npm의 문제점을 해결하기 위해 &lt;a href=&quot;https://yarnpkg.com/en/&quot;&gt;yarn&lt;/a&gt;이 발표되었다.  &lt;/p&gt;
&lt;p&gt;기존 NPM은 배포가 쉽고, 종속성을 쉽게 해결할 수 있지만 &lt;strong&gt;패키지가 중복으로 설치&lt;/strong&gt;될 수 있고, 파일이 많은 경우에 문제가 될 수 있다. 페이스북에서는 이런 문제점들을 해결하기 위해서 yarn을 발표했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;npm3 보다 패키지 설치 속도가 빠르다.&lt;/li&gt;
&lt;li&gt;json 포맷을 사용하지 않는다.&lt;/li&gt;
&lt;li&gt;offline 모드가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;YARN 설치&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://yarnpkg.com/en/docs/getting-started&quot;&gt;설치페이지&lt;/a&gt;를 통해서 직접 설치가 가능하다.&lt;/li&gt;
&lt;li&gt;맥북이라면 brew를 이용해서 설치가 가능하다.&lt;/li&gt;
&lt;li&gt;npm을 통해서도 설치가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;터미널에서의 설치는 다음의 명령으로 처리하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ npm install -g yarn   # npm 사용
$ brew install yarn     # 맥북&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;명령 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;npm 명령&lt;/th&gt;
&lt;th&gt;yarn 명령&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;npm init&lt;/td&gt;
&lt;td&gt;yarn init&lt;/td&gt;
&lt;td&gt;프로젝트 초기화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm install&lt;/td&gt;
&lt;td&gt;yarn or yarn install&lt;/td&gt;
&lt;td&gt;package.json 의 패키지 설치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm install --save [package name]&lt;/td&gt;
&lt;td&gt;yarn add [package name ]&lt;/td&gt;
&lt;td&gt;패키지를 프로젝트 의존성 수준으로 추가 (dependencies)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm install --save-dev [package name]&lt;/td&gt;
&lt;td&gt;yarn add -D[or --dev] [package name]&lt;/td&gt;
&lt;td&gt;패키지를 프로젝트 개발 의존성 수준으로 추가 (Devdependencies)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm install --global [package name]&lt;/td&gt;
&lt;td&gt;yarn global add [package name]&lt;/td&gt;
&lt;td&gt;패키지를 전역 수준으로 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm update --save&lt;/td&gt;
&lt;td&gt;yarn upgrade&lt;/td&gt;
&lt;td&gt;프로젝트의 패키지 업데이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm run [script name]&lt;/td&gt;
&lt;td&gt;yarn [script name]&lt;/td&gt;
&lt;td&gt;package.json의 scripts에 지정된 명령 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm uninstall --save [package name]&lt;/td&gt;
&lt;td&gt;yarn remove [package name]&lt;/td&gt;
&lt;td&gt;패키지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm cache clean&lt;/td&gt;
&lt;td&gt;yarn cache clean&lt;/td&gt;
&lt;td&gt;캐시 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://happygrammer.tistory.com/154&quot;&gt;yarn 특징과 성능&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>NPM</category>
      <category>yarn</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/48</guid>
      <comments>https://ccambo.tistory.com/entry/Yarn-Yarn-%EA%B3%BC-Npm-%EB%B9%84%EA%B5%90#entry48comment</comments>
      <pubDate>Sun, 20 Dec 2020 22:00:31 +0900</pubDate>
    </item>
    <item>
      <title>[Opencensus - Trace] Opencensus를 이용한 gRPC Global Tracing</title>
      <link>https://ccambo.tistory.com/entry/Opencensus-Opencensus%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-gRPC-Global-Tracing</link>
      <description>&lt;h1&gt;Opencensus를 이용한 gRPC Global Tracing&lt;/h1&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;Opencensus를 사용해서 시스템의 추적 및 매트릭을 수집하고 선택한 백엔드로 내보내 분산 시스템에 대한 관찰성을 제공할 수 있다.&lt;/p&gt;
&lt;p&gt;간단하게 gRPC 상에서 추적을 검증하기 위한 용도이므로 단순히 바이트를 포함하는 페이로드를 받아서 대문자 처리를 하는 서비스를 대상으로 검토해 보도록 한다.&lt;/p&gt;
&lt;h2&gt;Requirements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;go : &lt;a href=&quot;https://golang.org/doc/install&quot;&gt;Download and install - The Go Programming Language&lt;/a&gt; 를 통해 설치&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;gRPC&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ go get -u google.golang.org/grpc
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Protocol Buffer v3 : &lt;a href=&quot;https://grpc.io/docs/languages/go/quickstart/#install-protocol-buffers-v3&quot;&gt;Quick start – gRPC&lt;/a&gt; 문서의 내용에 따라서 &lt;a href=&quot;https://github.com/protocolbuffers/protobuf/releases&quot;&gt;Releases · protocolbuffers/protobuf · GitHub&lt;/a&gt; 에서 바이너리를 받을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# linux apt or apt-get
$ apt install -y protobuf-compiler
$ protoc --verion # 버전 3+ 확인

# Mac Homebrew
$ brew install protobuf
$ protoc --version # 버전 3+ 확인&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go 용 protoc 플러그인&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ go get -u github.com/golang/protobuf/protoc-gen-go&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Creating our gRPC Service&lt;/h2&gt;
&lt;p&gt;추적을 활성화하기 위해서는 다음과 같은 패키지가 필요하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Opencensus gRPC 지원 : &lt;code&gt;go.opencensus.io/plugin/ocgrpc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Opencensus Trace 지원 : &lt;code&gt;go.opencensus.io/trace&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;매트릭을 활성화하기 위해서는 다음과 같은 패키지가 필요하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서버 핸들러 : &lt;code&gt;go.opencensus.io/plugin/ocgrpc.SserverHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;클라이언트 핸들러 : &lt;code&gt;go.opencensus.io/plugin/ocgrpc.ClientHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;서버 gRPC 측정 항목 보기 : &lt;a href=&quot;https://godoc.org/go.opencensus.io/plugin/ocgrpc#DefaultServerViews&quot;&gt;https://godoc.org/go.opencensus.io/plugin/ocgrpc#DefaultServerViews&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;클라이언트 gRPC 측정 항목 보기 : &lt;a href=&quot;https://godoc.org/go.opencensus.io/plugin/ocgrpc#DefaultClientViews&quot;&gt;https://godoc.org/go.opencensus.io/plugin/ocgrpc#DefaultClientViews&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Protobuf 정의&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;rpc&lt;/code&gt; 폴더를 생성하고 &lt;code&gt;defs.proto&lt;/code&gt; 파일에 아래와 같이 정의를 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syntax = &amp;quot;proto3&amp;quot;;

package rpc;

message Payload {
    int32 id = 1;
    bytes data = 2;
}

service Fetch {
    rpc Capitalize(Payload) returns (Payload) {}
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;gRPC 서비스 생성&lt;/h3&gt;
&lt;p&gt;정의한 파일을 protoc로 컴파일 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# protoc 
$ protoc -I rpc rpc/defs.proto --go_out=plugins=grpc:rpc&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;정상적으로 수행되면 &lt;code&gt;defs.pb.go&lt;/code&gt; 파일이 생성된다.&lt;/p&gt;
&lt;h3&gt;서버 구현&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;server.go&lt;/code&gt;  파일을 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &amp;quot;bytes&amp;quot;
    &amp;quot;context&amp;quot;
    &amp;quot;log&amp;quot;
    &amp;quot;net&amp;quot;
    &amp;quot;google.golang.org/grpc&amp;quot;
    &amp;quot;oc.tutorials/ocgrpc/rpc&amp;quot;
)

type fetchIt int

// Compile time assertion that fetchIt implements FetchServer.
var _ rpc.FetchServer = (*fetchIt)(nil)

func (fi *fetchIt) Capitalize(ctx context.Context, in *rpc.Payload) (*rpc.Payload, error) {
    out :=  &amp;amp;rpc.Payload{
        Data: bytes.ToUpper(in.Data),
    }

    return out, nil
}

func main() {
    addr := &amp;quot;:9988&amp;quot;
    ln, err := net.Listen(&amp;quot;tcp&amp;quot;, addr)
    if nil != err {
        log.Fatalf(&amp;quot;gRPC server: failed to listen: %v&amp;quot;, err)
    }

    srv := grpc.NewServer()
    rpc.RegisterFetchServer(srv, new(fetchIt))

    log.Printf(&amp;quot;fetchIt gRPC server serving at %q&amp;quot;, addr)
    if err := srv.Serve(ln); nil != err {
        log.Fatalf(&amp;quot;gRPC server: error serving: %v&amp;quot;, err)
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제 실행해서 클라이언트 호출에 대기한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ go run server.go&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;클라이언트 구현&lt;/h3&gt;
&lt;p&gt;클라이언트는 gRPC 채널을 통해서 서버와 통신하며 바이트 단위로 전송하고 대문자 처리된 결과를 받게 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;client.go&lt;/code&gt; 파일을 생성하고 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &amp;quot;bufio&amp;quot;
    &amp;quot;context&amp;quot;
    &amp;quot;fmt&amp;quot;
    &amp;quot;log&amp;quot;
    &amp;quot;os&amp;quot;

    &amp;quot;google.golang.org/grpc&amp;quot;
    &amp;quot;oc.tutorials/ocgrpc/rpc&amp;quot;
)

func main() {
    serverAddr := &amp;quot;:9988&amp;quot;
    cc, err := grpc.Dial(serverAddr, grpc.WithInsecure())
    if nil != err {
        log.Fatalf(&amp;quot;fetchIt gRPC client failed to dial to server: %v&amp;quot;, err)
    }

    fc := rpc.NewFetchClient(cc)

    fIn := bufio.NewReader(os.Stdin)
    for {
        fmt.Print(&amp;quot;&amp;gt; &amp;quot;)
        line, _, err := fIn.ReadLine()
        if nil != err {
            log.Printf(&amp;quot;Failed to read a line in: %v&amp;quot;, err)
            return
        }

        ctx := context.Background()
        out, err := fc.Capitialize(ctx, &amp;amp;rpc.Payload{Data: line})
        if nil != err {
            log.Printf(&amp;quot;fetchIt gRPC client got error from server: %v&amp;quot;, err)
            continue
        }
        fmt.Printf(&amp;quot;&amp;lt; %s\n\n&amp;quot;, out.Data)
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제 실행해서 입력한 결과를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ go run client.go
&amp;gt; foo
&amp;lt; FOO

&amp;gt; bar
&amp;lt; BAR&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Instrumentation&lt;/h2&gt;
&lt;p&gt;위의 예제에 추적과 매트릭에 대한 계측을 추가한다.&lt;/p&gt;
&lt;h3&gt;서버 계측&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;추적&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;추적을 측정하기 위해서는 grpc.StatsHandler에 ServerHandler를 등록해서 gRPC 측정항목을 추출하고 추적할 수 있도록 추가해 줘야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
  ...
  &amp;quot;go.opencensus.io/trace&amp;quot;
)

func (fi *fetchIt) Capitalize(ctx context.Context, in *rpc.Payload) (*rpc.Payload, error) {
  ctx, span := trace.StartSpan(ctx, &amp;quot;oc.tutorials.grpc.Capitalize&amp;quot;)
  defer span.End()

  ...
}

func main() {
  ...
  srv := grpc.NewServer(grpc.StatsHandler(&amp;amp;ocgrpc.ServerHandler{}))
  ...
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;매트릭&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;매트릭을 측정하기 위해서는 필요한 View 정보를 추가해 줘야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
  ...
  &amp;quot;go.opencensus.io/plugin/ocgrpc&amp;quot;
  &amp;quot;go.opencensus.io/stats/view&amp;quot;
)

func main() {
  ...
  if err := view.Register(ocgrpc.DefaultServerViews...); nil != err {
    log.Fatalf(&amp;quot;Failed to register ocgrpc server views: %v&amp;quot;, err)
  }

  srv := grpc.NewServer(grpc.StatsHandler(&amp;amp;ocgrpc.ServerHandler{}))
  ...
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;완성된 최종 서버 코드&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;추출 및 수집된 데이터를 확인하기 위해서 &lt;code&gt;stackdriver&lt;/code&gt;를 Exporter로 등록하는 정보를 추가해 줘야 한다. (Exporter는 다양하므로 선호하는 것을 활용해도 된다)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
  &amp;quot;bytes&amp;quot;
  &amp;quot;context&amp;quot;
  &amp;quot;log&amp;quot;
  &amp;quot;net&amp;quot;

  &amp;quot;go.opencensus.io/plugin/ocgrpc&amp;quot;
  &amp;quot;go.opencensus.io/stats/view&amp;quot;
  &amp;quot;go.opencensus.io/trace&amp;quot;
  &amp;quot;google.golang.org/grpc&amp;quot;

  &amp;quot;oc.tutorials/ocgrpc/rpc&amp;quot;

  // The exporter to extract our metrics and traces
  &amp;quot;contrib.go.opencensus.io/exporter/stackdriver&amp;quot;
)

type fetchIt int

// Compile time assertion that fetchIt implements FetchServer.
var _ rpc.FetchServer = (*fetchIt)(nil)

func (fi *fetchIt) Capitalize(ctx context.Context, in *rpc.Payload) (*rpc.Payload, error) {
  ctx, span := trace.StartSpan(ctx, &amp;quot;oc.tutorials.grpc.Capitalize&amp;quot;)
  defer span.End()

  out := &amp;amp;rpc.Payload{
    Data: bytes.ToUpper(in.Data),
  }
  return out, nil
}

func main() {
  if err := view.Register(ocgrpc.DefaultServerViews...); nil != err {
    log.Fatalf(&amp;quot;Failed to register ocgrpc server views: %v&amp;quot;, err)
  }

  // Create and register the exporter
  sd, err := stackdriver.NewExporter(stackdriver.Options{
    ProjectID:    &amp;quot;census-demos&amp;quot;, // Insert your projectID here
    MetricPrefix: &amp;quot;ocgrpctutorial&amp;quot;,
  })
  if nil != err {
    log.Fatalf(&amp;quot;Failed to create Stackdriver exporter: %v&amp;quot;, err)
  }
  defer sd.Flush()
  trace.RegisterExporter(sd)
  sd.StartMetricsExporter()
  defer sd.StopMetricsExporter()
  // For demo purposes let&amp;#39;s always sample
  trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})

  addr := &amp;quot;:9988&amp;quot;
  ln, err := net.Listen(&amp;quot;tcp&amp;quot;, addr)
  if nil != err {
    log.Fatalf(&amp;quot;gRPC server: failed to listen: %v&amp;quot;, err)
  }
  srv := grpc.NewServer(grpc.StatsHandler(&amp;amp;ocgrpc.ServerHandler{}))
  rpc.RegisterFetchServer(srv, new(fetchIt))
  log.Printf(&amp;quot;fetchIt gRPC server serving at %q&amp;quot;, addr)
  if err := srv.Serve(ln); nil != err {
    log.Fatalf(&amp;quot;gRPC server: error serving: %v&amp;quot;, err)
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;클라이언트 계측&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;추적&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;추적을 측정하기 위해서는 grpc.StatsHandler에 ClientHandler를 등록해서 gRPC 측정항목을 추출하고 추적할 수 있도록 추가해 줘야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
  ...
  &amp;quot;go.opencensus.io/trace&amp;quot;
)

func main() {
  ...
  cc, err := grpc.Dial(serverAddr, grpc.WithInsecure(), grpc.WithStatsHandler(new(ocgrpc.ClientHander)))
  ...
  ctx, span := trace.StartSpan(context.Background(), &amp;quot;oc.tutorials.grpc.ClientCapitalize&amp;quot;)
  out, err := fc.Capitalize(ctx, &amp;amp;rpc.Payload{Data: line})
  if nil != err {
    span.SetStatus(trace.Status{Code: trace.StatusCodeInternal, Message: err.Error()})
    log.Printf(&amp;quot;fetchIt gRPC client got error from server: %v&amp;quot;, err)
  } else {
    fmt.Printf(&amp;quot;&amp;lt; %v\n\n&amp;quot;, out.Data)
  }
  span.End()
  ...
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;매트릭&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;매트릭을 측정하기 위해서는 필요한 View 정보를 추가해 줘야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
  ...
  &amp;quot;go.opencensus.io/plugin/ocgrpc&amp;quot;
  &amp;quot;go.opencensus.io/stats/view&amp;quot;
  &amp;quot;go.opencensus.io/trace&amp;quot;
)

func main() {
  ...
  if err := view.Register(ocgrpc.DefaultClientViews...); nil != err {
    log.Fatalf(&amp;quot;Failed to register ocgrpc client views: %v&amp;quot;, err)
  }
  cc, err := grpc.Dial(serverAddr, grpc.WithInsecure(), grpc.WithStatsHandler(new(ocgrpc.ClientHander)))
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;완성된 클라이언트 코드&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;추출 및 수집된 데이터를 확인하기 위해서 &lt;code&gt;stackdriver&lt;/code&gt;를 Exporter로 등록하는 정보를 추가해 줘야 한다. (Exporter는 다양하므로 선호하는 것을 활용해도 된다)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
  &amp;quot;bufio&amp;quot;
  &amp;quot;context&amp;quot;
  &amp;quot;fmt&amp;quot;
  &amp;quot;log&amp;quot;
  &amp;quot;os&amp;quot;

  &amp;quot;go.opencensus.io/plugin/ocgrpc&amp;quot;
  &amp;quot;go.opencensus.io/stats/view&amp;quot;
  &amp;quot;go.opencensus.io/trace&amp;quot;
  &amp;quot;google.golang.org/grpc&amp;quot;

  &amp;quot;oc.tutorials/ocgrpc/rpc&amp;quot;

  // The exporter to extract our metrics and traces
  &amp;quot;contrib.go.opencensus.io/exporter/stackdriver&amp;quot;
)

func main() {
  if err := view.Register(ocgrpc.DefaultClientViews...); nil != err {
    log.Fatalf(&amp;quot;Failed to register ocgrpc client views: %v&amp;quot;, err)
  }

  // Create and register the exporter
  sd, err := stackdriver.NewExporter(stackdriver.Options{
    ProjectID:    &amp;quot;census-demos&amp;quot;, // Insert your projectID here
    MetricPrefix: &amp;quot;ocgrpctutorial&amp;quot;,
  })
  if nil != err {
    log.Fatalf(&amp;quot;Failed to create Stackdriver exporter: %v&amp;quot;, err)
  }
  defer sd.Flush()
  trace.RegisterExporter(sd)
  sd.StartMetricsExporter()
  defer sd.StopMetricsExporter()
  // For demo purposes let&amp;#39;s always sample
  trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})

  serverAddr := &amp;quot;:9988&amp;quot;
  cc, err := grpc.Dial(serverAddr, grpc.WithInsecure(), grpc.WithStatsHandler(new(ocgrpc.ClientHandler)))
  if nil != err {
    log.Fatalf(&amp;quot;fetchIt gRPC client failed to dial to server: %v&amp;quot;, err)
  }
  fc := rpc.NewFetchClient(cc)

  fIn := bufio.NewReader(os.Stdin)
  for {
    fmt.Print(&amp;quot;&amp;gt; &amp;quot;)
    line, _, err := fIn.ReadLine()
    if nil != err {
      log.Printf(&amp;quot;Failed to read a line in: %v&amp;quot;, err)
      return
    }

    ctx, span := trace.StartSpan(context.Background(), &amp;quot;oc.tutorials.grpc.ClientCapitalize&amp;quot;)
    out, err := fc.Capitalize(ctx, &amp;amp;rpc.Payload{Data: line})
    if nil != err {
      span.SetStatus(trace.Status{Code: trace.StatusCodeInternal, Message: err.Error()})
      log.Printf(&amp;quot;fetchIt gRPC client got error from server: %v&amp;quot;, err)
    } else {
      fmt.Printf(&amp;quot;&amp;lt; %s\n\n&amp;quot;, out.Data)
    }
    span.End()
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Example Traces&lt;/h2&gt;
&lt;p&gt;GCP와 연계를 했고 stackdriver를 선택해서 추적 정보를 Export 했다면 &lt;a href=&quot;https://console.cloud.google.com/traces/traces&quot;&gt;Goolgle Cloud Platform Trace 정보&lt;/a&gt; 를 통해서 처리된 작업을 확인할 수 있다.&lt;/p&gt;
&lt;h2&gt;Example Metrics&lt;/h2&gt;
&lt;p&gt;GCP와 연계를 했고 stackdriver를 선택해서 매트릭 정보를 Export 했다면 &lt;a href=&quot;https://console.cloud.google.com/monitoring&quot;&gt;Google Cloud Platform Metrics 정보&lt;/a&gt; 를 통해서 처리된 작업을 확인할 수 있다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://opencensus.io/guides/grpc/go/&quot;&gt;OpenCensus&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>gRPC</category>
      <category>Metrics</category>
      <category>opencensus</category>
      <category>Trace</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/47</guid>
      <comments>https://ccambo.tistory.com/entry/Opencensus-Opencensus%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-gRPC-Global-Tracing#entry47comment</comments>
      <pubDate>Sun, 20 Dec 2020 01:07:45 +0900</pubDate>
    </item>
    <item>
      <title>[Opencensus - Trace] Opencesus를 활용한 Trace 정리</title>
      <link>https://ccambo.tistory.com/entry/Opencensus-Trace-Opencesus%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Trace-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;h1&gt;Trace 정리&lt;/h1&gt;
&lt;h2&gt;Tracing (이하 트레이싱)&lt;/h2&gt;
&lt;p&gt;추적은 응용 프로그램을 구성하는 다른 서비스에 의해 처리되는 단일 사용자 요청의 진행과정을 추적하는 것이다.&lt;/p&gt;
&lt;p&gt;진행 과정의 각 작업은 &lt;code&gt;Span&lt;/code&gt; 으로 부르며, 각 스팬에는 단계에 소요된 시간 (대기 시간), 상태, 시간 이벤트, 속성, 링크를 포함하여 작업을 나타낼 수 있는 메타 데이터가 포함된다. 이 추적 정보를 활용해서 애플리케이션의 오류 및 대기 시간 문제를 디버깅 할 수 있다.&lt;/p&gt;
&lt;h2&gt;Trace (이하 추적)&lt;/h2&gt;
&lt;p&gt;추적은 스팬 트리로 구성된다. 즉, 작업의 흐름 경로를 보여주는 관찰 가능한 신호의 집합이다. 추적 자체는 고유한 16바이트 시퀀스로 표현되는 &lt;code&gt;TraceID&lt;/code&gt;로 식별된다. 트레이싱을 통해서 수집된 추적 정보는 다음과 같이 표현된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?export=view&amp;id=1Esl-jKFEp7_27sMnJOV1Xwb-ykKgQHGd&quot; alt=&quot;Trace&quot;&gt;&lt;/p&gt;
&lt;p&gt;위의 그림에서와 같이 여러 단계의 작업(스팬)이 호출된 것을 확인할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;auth: 사용자 인증 여부 검사&lt;/li&gt;
&lt;li&gt;cache.Get: 캐시 검증&lt;/li&gt;
&lt;li&gt;mysql.Query: 캐시에 존재하지 않아 DB 조회&lt;/li&gt;
&lt;li&gt;cache.Put: DB 조회결과 캐시 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Exporting (이하 내보내기)&lt;/h2&gt;
&lt;p&gt;수집된 추적 정보를 다양한 분석을 위해서 여러 가지 백엔드들에 대한 내보내기를 구성해서 처리할 수 있다.&lt;br&gt;다양한 내보내기 목록등은 &lt;a href=&quot;https://opencensus.io/exporters/&quot;&gt;Opencensus&lt;/a&gt; 를 참고하면 된다.&lt;/p&gt;
&lt;h2&gt;Span (이하 스팬)&lt;/h2&gt;
&lt;p&gt;스팬은 추적의 단일 작업을 나타낸다. 주로 HTTP 요청, RPC (원격 프로시저 호출: Remote Procedure Call), 데이터베이스 쿼리 또는 코드가 사용자 코드에서 사용하는 경로 등을 나타낸다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스팬은 트리 구조를 이루기 때문에 상위 스팬의 존재여부에 따라서 상위 스팬과 하위 스팬으로 나뉜다.&lt;/li&gt;
&lt;li&gt;각 스팬은 &lt;code&gt;SpanID&lt;/code&gt;로 식별된다.&lt;/li&gt;
&lt;li&gt;SpanID 와 옵션 바이트들을 합쳐서 &lt;code&gt;Span Context&lt;/code&gt;라고 한다.&lt;/li&gt;
&lt;li&gt;동일 프로세스 내에서 스팬 컨텍스트는 컨텍스트 객체로 전달된다. 프로세스 경계를 넘어서면 프로토콜 헤더로 직렬화 된다. 따라서 수신 측은 스팬 컨텍스트를 읽고 하위 스팬을 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스팬은 다음과 같은 필드 정보로 구성된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;작업을 설명할 수 있는 문자열, 일반적으로 통계적으로 의미가 있을 수 있는 이름을 사용하는 것이 좋다. 백엔드 및 분석 도구가 이 이름을 기준으로 보고서를 생성한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;스팬 생성자에서 지정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cox, span := trace.StartSpan(ctx, &amp;quot;cache.Get&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SpanID&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스팬 식별자&lt;/li&gt;
&lt;li&gt;무작위로 생성된 8바이트 구성이며 전역적으로 고유한 값이어야 한다.&lt;/li&gt;
&lt;li&gt;하위 스팬이 존재할 경우 &lt;code&gt;ParentSpanID&lt;/code&gt;로 지정된다. ParentSpanID 가 null인 경우는 최상위 스팬으로 판단한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TraceID&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;추적 식별자&lt;/li&gt;
&lt;li&gt;무작위로 생성된 16바이트 구성이며 전역적으로 고유한 값이어야 한다.&lt;/li&gt;
&lt;li&gt;모든 프로세스에서 동일한 추적에 속하는 모든 스팬들을 그룹화하는 기준이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ParentSpanID&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;현재 작업 단위 스팬의 상위 스팬 여부를 의미한다.&lt;/li&gt;
&lt;li&gt;null이거나 SpanID 값이 존재할 수 있다.&lt;/li&gt;
&lt;li&gt;동일한 ParentSpanID를 가지는 하위 스팬들이 존재할 수 있지만, 하나의 스팬은 하나의 ParentSpanID만 가질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;StartTime / EndTime&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;StartTime은 작업이 시작된 시간을 기록하는 타임스탬프 값이다.&lt;/li&gt;
&lt;li&gt;EndTime은 작업이 종료된 시간을 기록하는 타임스탬프 값이다.&lt;/li&gt;
&lt;li&gt;처리(지연) 시간은 EndTime과 StartTime 의 차이를 의미한다.&lt;/li&gt;
&lt;li&gt;스팬의 수명은 스팬 객체에 시작 시간과 종료 시간을 기록하는 프로세스를 나타낸다.&lt;ul&gt;
&lt;li&gt;시작 시간은 스팬이 생성될 떄 기록되며, 시작 시간이 기록된 경우만 살아있다.&lt;/li&gt;
&lt;li&gt;종료 시간은 작업의 종료 시간으로 기록하며, 추적이 종료된 후에 스팬을 종료하는 것이 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Status&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;특정 시점에 스팬의 필터링 가능한 상태를 나타내는 논리적 모델을 정의한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int32&lt;/code&gt; 코드로 구성된다.&lt;/li&gt;
&lt;li&gt;상태 코드 매핑된 정보는 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;STATE&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;CODE&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;DESCRIPTION&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;HTTP STATUS CODE EQUIVALENT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Not an error, returned on success&lt;/td&gt;
&lt;td&gt;200 and 2XX HTTP statuses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CANCELLED&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;The operation was cancelled, typically by the caller&lt;/td&gt;
&lt;td&gt;499&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNKNOWN&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;An unknown error raised by APIs that don’t return enough error information&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INVALID_ARGUMENT&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The client specified an invalid argument&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEADLINE_EXCEEDED&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;The deadline expired before the operation could succeed&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOT_FOUND&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Content was not found or request was denied for an entire class of users&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALREADY_EXISTS&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;The entity attempted to be created already exists&lt;/td&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PERMISSION_DENIED&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;The caller doesn’t have permission to execute the specified operation&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RESOURCE_EXHAUSTED&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;The resource has been exhausted e.g. per-user quota exhausted, file system out of space&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FAILED_PRECONDITION&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;The client shouldn’t retry until the system state has been explicitly handled&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ABORTED&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;The operation was aborted&lt;/td&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OUT_OF_RANGE&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;The operation was attempted past the valid range e.g. seeking past the end of a file&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNIMPLEMENTED&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;The operation is not implemented or is not supported/enabled for this operation&lt;/td&gt;
&lt;td&gt;501&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INTERNAL&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Some invariants expected by the underlying system have been broken. This code is reserved for serious errors&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNAVAILABLE&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;The service is currently available e.g. as a transient condition&lt;/td&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATA_LOSS&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Unrecoverable data loss or corruption&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNAUTHENTICATED&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;The requester doesn’t have valid authentication credentials for the operation&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;span.SetStatus(trace.Status{Code: int32(trace.StatusCodeNotFound), Message: &amp;quot;Cache miss&amp;quot;})&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time events&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;일정 기간 동안 특정 시점에 발생한 이벤트를 설명한다. 아래와 같은 필드 중에 하나를 사용할 수 있지만 모두 사용할 수는 없다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;주석 : 일정 기간 동안 발생한 이벤트에 대해 텍스트로 설명하는 스토리지 제공&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Description: 이벤트를 설명하는 사용자 메시지&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Attributes: 주석을 표현하는데 필요한 속성들&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import &amp;quot;go.opencensus.io/trace&amp;quot;
    ...
    span.Annotation([]trace.Attribute{
        trace.StringAttribute(&amp;quot;store&amp;quot;, &amp;quot;memcache&amp;quot;),
        trace.BoolAttribute(&amp;quot;cache_miss&amp;quot;, true),
        trace.Int64Attribute(&amp;quot;age_ns&amp;quot;, 13488999),
    }, &amp;quot;Cache miss durtin GC&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;메시지 이벤트 : 스팬 간에 주고 받은 메시지 설명&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;유형 (Type)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;유형&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;값&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;기술&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;SEND&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;이 메시지가 전송되었음을 나타냅니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RECEIVE&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;이 메시지가 수신되었음을 나타냅니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNKNOWN&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;알 수없는 이벤트 유형 또는 기본값&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;신분증명 (ID) : SEND/RECEIVE 간의 메시지 이벤트 상관 관계를 위한 메시지 ID로 예를 들어 프로토콜 핸드 셰이크 또는 스트리밍 RPC간에 시퀀스/상태 번호를 일치시킬 떄 유용할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;압축되지 않은 크기 (Uncompressed Size) : 송/수신 상에 압축되지 않은 바이트 수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;압축된 크기 (Compressed Size) : 송/신 상에 압축된 바이트 수, 0 이면 압축되지 않은 바이트 수와 동일한 것으로 간주&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// On the client
span.AddMessageReceiveEvent(seqNumber, 1024, 512)
// On the server
span.AddMessageSendEvent(seqNumber, 1024, 512)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;시간 이벤트의 모음 이지만, 삭제 된 주석 수와 삭제된 메시지 이벤트 수에 대한 정보도 관리한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;시간 이벤트 모음&lt;/li&gt;
&lt;li&gt;삭제된 주석 수&lt;/li&gt;
&lt;li&gt;삭제된 메시지 이벤트 수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Link&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;동일하거나 다른 추적에 속하는 스팬 간의 상호 관계를 설명한다. 예를 들어 서로 다른 추적 또는 서로 다른 프로세스로 구성된 일괄 작업이 수행된 경우에 한 스팬에서 다른 스팬으로의 링크는 관련 스팬들을 상호 연결하는데 도움이 될 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다음과 같은 필드로 구성된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TraceID&lt;/li&gt;
&lt;li&gt;SpanID&lt;/li&gt;
&lt;li&gt;유형&lt;ul&gt;
&lt;li&gt;CHILD&lt;/li&gt;
&lt;li&gt;PARENT&lt;/li&gt;
&lt;li&gt;UNKNOWN&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Attributes&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# SpanA : 신뢰할 수 없는 경계 (예, 클라우드에 대한 클라이언트 요청)에서 시작됨
# SpanB : 신뢰할 수 있는 경계 (예, 서비스/클라우드 프런트엔드 서버)에서 시작됨

_, spanA := trace.StartSpan(context.Background(), &amp;quot;SpanA&amp;quot;)
spanASC := spanA.SpanContext()

_, spanB := trace.StartSpan(context.Background(), &amp;quot;SpanB&amp;quot;)
spanB.AddLink(trace.Link{
  TraceID: spanASC.TraceID,
  SpanID: spanASC.SpanID,
  Type: trace.LinkTypeChild,
  Attributes: map[string]interface{}{
      &amp;quot;reason&amp;quot;: &amp;quot;client-RPC unverified source&amp;quot;,
  }
})&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SpanKind&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스팬 간의 상/하위 관계 뿐만 아니라 추가적인 관계를 설명하는데 사용한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;유형&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;값&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;의미&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;SERVER&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;범위는 RPC의 서버 측 처리를 다룹니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLIENT&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;범위는 RPC의 클라이언트 측 처리를 포함합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNSPECIFIED&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;지정되지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Started on the client
ctx, cSpan := trace.StartSpan(ctx, &amp;quot;SpanStarted&amp;quot;, trace.WithSpanKind(trace.SpanKindClient))
// Received from the server
ctx, sSpan := trace.StartSpan(ctx, &amp;quot;SpanStarted&amp;quot;, trace.WithSpanKind(trace.SpanKindServer))&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TraceOptions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TraceOptions는 각 Opencensus 스팬의 바이트로 마지막 비트는 스팬이 샘플링된 경우에 설정된다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;16 진수 상태&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;바이너리 상태&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;의미&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;00000000&lt;/td&gt;
&lt;td&gt;스팬이 샘플링되지 않았습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x01&lt;/td&gt;
&lt;td&gt;00000001&lt;/td&gt;
&lt;td&gt;스팬이 샘플링 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tracestate&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여러 분산 추적 그래프에서 위치 / 순서에 대한 정보를 설정하기 위한 것으로 최대 32개의 정렬된 목록 사용이 가능한 Key-Value 쌍이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;필드&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;기술&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;제한&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;키&lt;/td&gt;
&lt;td&gt;캐릭터 모음&lt;/td&gt;
&lt;td&gt;소문자로 시작해야하며 소문자 영숫자, 대시, 별표 및 슬래시를 포함 &amp;amp;할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;값&lt;/td&gt;
&lt;td&gt;캐릭터 모음&lt;/td&gt;
&lt;td&gt;인쇄 가능한 ASCII 문자 만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sampling (이하 샘플링)&lt;/h2&gt;
&lt;h3&gt;견본 추출&lt;/h3&gt;
&lt;p&gt;추적 데이터는 대량으로 생성되는 경우가 많기 때문에 수집 및 저장 비용뿐만 아니라 전송 비용도 많이 소비된다. 따라서 관찰 가능성과 비용 간의 균형을 맞춰서 추적을 샘플링하는 것이 필요하다. 즉, 전체 범위 중에 추적 대상을 정하는 범위와 내보낼지 여부를 결정하는 프로세스라고 생각하면 된다.&lt;/p&gt;
&lt;h3&gt;샘플러&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Always : 모든 샘플링 결정에 true를 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; import &amp;quot;go.opencensus.io/traces&amp;quot;

 _ = trace.AlwaysSample()&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Never : 모든 샘플링 결정에 false를 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import &amp;quot;go.opencensus.io/traces&amp;quot;

  _ = trace.NeverSample()&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Probabilistic : 동전 던지기와 같이 샘플링 결정에 true/false를 확률적으로 반환한다. (기본적으로 확률적 샘플링은 10,000분의 1이다)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import &amp;quot;go.opencensus.io/traces&amp;quot;

  theSampler = trace.ProbabilitySampler(1/1000.0)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RateLimiting : 초당 0.1개의 추적 샘플링을 시도한다. 만일 상위 스팬이 샘플링되었다면 하위 스팬은 샘플링을 유지한다. 이와 같은 샘플링을 사용하는 이유는 아래와 같은 문제들을 해결하기 위한 용도로 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;QPS 기반 샘플링 얻기&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;실제 샘플링 확률 제공&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;최소한의 오버헤드&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 속도 제한을 달성하기 위해서 마지막 QPS 기반 샘플링 결정을 내린 시간이 원자 변수 (Atomic Variable)에 저장되고 마지막 확률적 결정을 내린 이후의 시간도 저장된다. 그리고 원하는 샘플링 QPS를 얻기 위한 확률 함수를 사용한다.

P(Z) = min(Z * X, 1)
// X : 원하는 QPS, Z : 마지막 샘플링 이후 경과된 시간 (초)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;샘플러의 결정은 샘플러가 각각 true 또는 false를 반환하는 경우에 설정하거나 지워 &lt;code&gt;Span.TraceOptions의 샘플링 비트&lt;/code&gt; 에 영향을 주게 된다.&lt;/p&gt;
&lt;h3&gt;샘플러 설정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;글로벌 샘플러 : TraceConfig 설정을 통해서 전역으로 적용되는 샘플러를 말한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  package main

  import &amp;quot;go.opencensus.io/traces&amp;quot;

  func main() {
      // Having already created your sampler &amp;quot;theSampler&amp;quot;
      trace.ApplyConfig(trace.Config{DefaultSampler: theSampler})
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;스팬별 샘플러 (스팬 범위) : 스팬을 생성할 때 지정해서 스팬에 적용되는 샘플러를 말한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  import &amp;quot;go.opencensus.io/traces&amp;quot;

  func doWork() {
      // Having already created your sampler &amp;quot;theSampler&amp;quot; and &amp;quot;ctx&amp;quot;
      ctx, span := trace.StartSpan(ctx, &amp;quot;DoWork&amp;quot;, trace.WithSampler(theSampler))
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;규칙&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;ParentSpan의 샘플링 결정은 추적 구성과 관련 없이 항상 모든 항목에 상속된다. 이를 통해서 추적의 연속성이 보장된다. 예를 들어 부모가 샘플링 되었는데 자식이 샘플링이 되지 않거나, 자식이 샘플링이 되었는데 부모가 샘플링이 되지 않는 경우들이 발생하는 것을 방지하기 위한 것이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://opencensus.io/tracing/&quot;&gt;OpenCensus&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>opencensus</category>
      <category>ratelimit</category>
      <category>Sampling</category>
      <category>span</category>
      <category>Trace</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/46</guid>
      <comments>https://ccambo.tistory.com/entry/Opencensus-Trace-Opencesus%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Trace-%EC%A0%95%EB%A6%AC#entry46comment</comments>
      <pubDate>Sat, 19 Dec 2020 21:12:15 +0900</pubDate>
    </item>
    <item>
      <title>[macOS - SSH] macOS에서 SSH Key를 이용해서 원격 Ubuntu 서버에 접속하기</title>
      <link>https://ccambo.tistory.com/entry/macOS-SSH-macOS%EC%97%90%EC%84%9C-SSH-Key%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%9B%90%EA%B2%A9-Ubuntu-%EC%84%9C%EB%B2%84%EC%97%90-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;Access to remote server using ssh key with iterm2&lt;/h1&gt;
&lt;h2&gt;환경&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Mac OS&lt;/li&gt;
&lt;li&gt;ssh-keygen (SSH에 사용할 키 발급)&lt;/li&gt;
&lt;li&gt;ssh-copy-id (발급된 공개키를 서버에 전송)&lt;/li&gt;
&lt;li&gt;원격 서버 (Ubuntu 16.04) 및 사용자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;서버에 사용자가 추가되어 있다고 가정한다. 사용자 추가에 대한 부분은 많은 자료들이 존재하므로 참고하도록 한다.&lt;/p&gt;
&lt;h2&gt;ssh-copy-id 설치&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ssh-copy-id&lt;/code&gt;는 로컬에서 발급된 공개키를 서버로 전송할 때 사용하는 패키지다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ brew install ssh-copy-id&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SSH Key 발급&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;~/.ssh&lt;/code&gt; 폴더를 기준으로 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ssh-keygen [-t] [rsa] [-b] [2048]
# -t : dsa, ecdsa, ed25519, rsa, rsa1 중에 어떤 암호화 알고리즘을 선택할지 지정 (기본 값, rsa)
# -b : key를 생성할 때 bit 수 지정 (기본 값, 2048)
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/&amp;lt;현재 사용자&amp;gt;/.ssh/id_rsa): &amp;lt;&amp;lt;&amp;lt; 여기에 저장될 파일 경로 또는 파일명 입력
Enter passphrase (empty for no passphrase): &amp;lt;&amp;lt;&amp;lt; 비밀번호로 사용할 값
Enter same passphrase again: &amp;lt;&amp;lt;&amp;lt; 다시 입력
Your identification has been saved in &amp;lt;파일명&amp;gt;
Your public key has been saved in &amp;lt;파일명&amp;gt;.pub.&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;키 파일에 대한 권한 문제&lt;/strong&gt;&lt;br&gt;ssh-key을 통해서 처리한 경우는 문제가 되지 않을 수 있지만 수동으로 처리한 경우는 다음과 같이 파일 권한을 설정해 줘야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공개 키 : 0644 (-rw-r--r--)&lt;/li&gt;
&lt;li&gt;개인 키 : 0600 (-rw-------)&lt;br&gt;위와 같이 권한을 설정해 주지 않으면 원격 서버에 연결했을 떄 오류가 발생하며 접속이 허용되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;발급된 공개 키 서버로 전송&lt;/h3&gt;
&lt;p&gt;별다른 입력이 없이 기본으로 처리했다면 &lt;code&gt;~/.ssh&lt;/code&gt; 폴더에 private key와 public key 파일이 생성된다.&lt;br&gt;파일명을 &lt;code&gt;test&lt;/code&gt;라고 지정했다면 &amp;#39;test&amp;#39;, &amp;#39;test.pub&amp;#39; 파일이 존재할 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ssh-copy-id -i [key path or name] [username in server]@[server ip]
$ ssh-copy-id -i test.pub ccambo@192.168.10.44
/usr/local/bin/ssh-copy-id: INFO: Source of key(s) to be installed: &amp;quot;test.pub&amp;quot;
/usr/local/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/local/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
test@[IP Address]&amp;#39;s password:

Number of key(s) added:        1

Now try logging into the machine, with:   &amp;quot;ssh &amp;#39;test@[IP Address]&amp;#39;&amp;quot;
and check to make sure that only the key(s) you wanted were added.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실체 키 추가 여부는 지정한 서버의 사용자로 로그인을 해서 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; 파일에 지정한 키가 제대로 추가되었는지 확인하면 된다.&lt;/p&gt;
&lt;h2&gt;SSH로 접속하기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ssh -i [private key name] [username in server]@[server id] -p [port number]
$ ssh -i test ccambo@192.168.10.44&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SSH 접속 설정하기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;~/.ssh/config&lt;/code&gt; 파일을 통해서 접속을 쉽게 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;config 파일이 경로에 없다면 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ cd ~/.ssh
$ touch config&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;config 파일을 열고 아래와 같이 설정해 주면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host ccamboServer
    HostName [IP Address]
    User [Username]
    Port [Port Number]
    IdentityFile ~/.ssh/[private key name]

Host Name2
    HostName [IP Address]
    User [Username]
    Port [Port Number]
    IdentityFile ~/.ssh/[private key name]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;설정을 통해서 SSH 접속은 다음과 같이 지정한 Host 이름을 기준으로 하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ssh [host name in config file]
$ ssh ccamboServer&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://twpower.github.io/31-add-user-in-ubuntu&quot;&gt;Ubuntu 서버에 사용자 추가하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>macos</category>
      <category>ssh</category>
      <category>ssh-copy-id</category>
      <category>ssh-keygen</category>
      <category>ubuntu</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/45</guid>
      <comments>https://ccambo.tistory.com/entry/macOS-SSH-macOS%EC%97%90%EC%84%9C-SSH-Key%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%9B%90%EA%B2%A9-Ubuntu-%EC%84%9C%EB%B2%84%EC%97%90-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0#entry45comment</comments>
      <pubDate>Sat, 19 Dec 2020 18:25:46 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes] Kubernetes Dashboard 설치 및 NodePort 접근설정</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-Kubernetes-Dashboard-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%A0%91%EA%B7%BC%EC%84%A4%EC%A0%95</link>
      <description>&lt;h1&gt;How to install and access Kubernetes Dashboard&lt;/h1&gt;
&lt;p&gt;대시보드는 웹 기반 Kubernetes 사용자 인터페이스로 컨테이너화된 애플리케이션을 배포하고 문제를 해결하고, 클러스터 리소스들을 관리한다. 또한 클러스터의 쿠버네티스 리소스 상태 및 발생했을 수 있는 오류에 대한 정보도 확인할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deployments&lt;/li&gt;
&lt;li&gt;StatefulSets&lt;/li&gt;
&lt;li&gt;DaemonSets&lt;/li&gt;
&lt;li&gt;Jobs&lt;/li&gt;
&lt;li&gt;Services&lt;/li&gt;
&lt;li&gt;Ingress Deployments&lt;/li&gt;
&lt;li&gt;Scaling&lt;/li&gt;
&lt;li&gt;Rolling Update&lt;/li&gt;
&lt;li&gt;Pod Management&lt;/li&gt;
&lt;li&gt;Persistent Volume / Claim&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;배포 및 액세스&lt;/h2&gt;
&lt;p&gt;기본적으로 쿠버네티스에 포함되어있지 않기 때문에 별도 배포해 줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml

namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;kubectl 명령줄 도구를 사용해서 대시보드에 접근할 수 있도록 프록시 처리를 해 준다. (8001 포트)&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;kubectl proxy&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;kubectl은 &lt;code&gt;http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/&lt;/code&gt;로 대시보드에 접근할 수 있도록 구성한다.&lt;/p&gt;
&lt;h2&gt;Using NodePort&lt;/h2&gt;
&lt;p&gt;권장 사항을 배포하는 것이므로 수정을 해야하는 경우라면 다음과 같이 다운로드해서 처리해 줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;sudo dnf -y install wget

wget https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended.yaml

mv recommended.yaml kubernetes-dashboard-deployment.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 파일의 &lt;code&gt;kind: Service&lt;/code&gt; 부분을 보면 아래와 같이 되어있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  ports:
    - port: 443
      targetPort: 8443
  selector:
    k8s-app: kubernetes-dashboard&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 부분의 &lt;code&gt;spec&lt;/code&gt; 에 &lt;code&gt;type: NodePort&lt;/code&gt; 를 지정하고 저장한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  ports:
    - port: 443
      targetPort: 8443
  selector:
    k8s-app: kubernetes-dashboard
  type: NodePort&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;&lt;code&gt;NodePort&lt;/code&gt;는 각 노드에 정적 포트를 서비스로 노출하며 NodePort 서비스가 라우팅하는 ClusterIP 서비스는 자동으로 생성된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이제 변경된 대시보드를 아래와 같이 배포한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;kubectl apply -f kubernetes-dashboard-deployment.yml

namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;배포된 상태는 아래의 명령으로 확인이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl get deployments -n kubernetes-dashboard

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
dashboard-metrics-scraper   1/1     1            1           21s
kubernetes-dashboard        0/1     1            0           21s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 두 개의 배포가 생성된 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl get pod -n kubernetes-dashboard

NAME                                         READY   STATUS             RESTARTS   AGE
dashboard-metrics-scraper-79c5968bdc-fz4cl   1/1     Running            0          41s
kubernetes-dashboard-665f4c5ff-q22qg         0/1     ImagePullBackOff   0          41s&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;주의&lt;/b&gt;&lt;br /&gt;위와 같이 &lt;code&gt;ImagePullBackOff&lt;/code&gt; 상태가 나타날 수 있다. 일반적으로 도커에서 이미지 처리를 하는 과정 중에 발생할 수 있으므로 시간이 좀 지난 뒤에 확인해 보면 된다. 그러나 시간이 지나도 변화가 없이 똑같은 상태라면 오류가 발생했을 수 있으므로 아래의 명령으로 확인해 봐야 한다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;# kubectl describe pod {POD_ID} -n {namespace}

...
Events:
 Type     Reason     Age                    From               Message
 ----     ------     ----                   ----               -------
 Normal   Scheduled  5m42s                  default-scheduler  Successfully assigned kubernetes-dashboard/kubernetes-dashboard-665f4c5ff-q22qg to node-2
 Warning  Failed     5m24s                  kubelet            Failed to pull image &quot;kubernetesui/dashboard:v2.0.4&quot;: rpc error: code = Unknown desc = Error response from daemon: Get https://registry-1.docker.io/v2/kubernetesui/dashboard/manifests/v2.0.4: Get https://auth.docker.io/token?scope=repository%3Akubernetesui%2Fdashboard%3Apull&amp;amp;service=registry.docker.io: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
 Warning  Failed     5m24s                  kubelet            Error: ErrImagePull
 Normal   BackOff    5m23s                  kubelet            Back-off pulling image &quot;kubernetesui/dashboard:v2.0.4&quot;
 Warning  Failed     5m23s                  kubelet            Error: ImagePullBackOff
 Normal   Pulling    5m11s (x2 over 5m41s)  kubelet            Pulling image &quot;kubernetesui/dashboard:v2.0.4&quot;
 Normal   Pulled     4m24s                  kubelet            Successfully pulled image &quot;kubernetesui/dashboard:v2.0.4&quot; in 46.672759653s
 Normal   Created    4m23s                  kubelet            Created container kubernetes-dashboard
 Normal   Started    4m22s                  kubelet            Started container kubernetes-dashboard&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 정보에서 실제 이미지를 제대로 받지 못해서 오류가 발생했었으나 재 시도해서 성공한 것을 확인할 수 있다.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;참고로 도커의 유료화 정책에 따라서 IP 당 또는 Docker ID 당 Rate Limit 제한이 걸린다. 한번 걸리면 1시간 이상 지나야 풀리므로 Describe를 통해서 오류 원인이 뭔지 확인해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl get pod -n kubernetes-dashboard

NAME                                         READY   STATUS    RESTARTS   AGE
dashboard-metrics-scraper-79c5968bdc-fz4cl   1/1     Running   0          8m25s
kubernetes-dashboard-665f4c5ff-q22qg         1/1     Running   0          8m25s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 두 개의 파드가 생성된 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get service -n kubernetes-dashboard

NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)         AGE
dashboard-metrics-scraper   ClusterIP   10.96.147.203    &amp;lt;none&amp;gt;        8000/TCP        8m55s
kubernetes-dashboard        NodePort    10.108.179.191   &amp;lt;none&amp;gt;        443:30568/TCP   8m56s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 NodePort를 적용산 서비스가 생성된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;이제 &lt;code&gt;https://localhost:30568&lt;/code&gt; 로 접근이 가능하다.&lt;/p&gt;
&lt;h2&gt;사용자 (관리자) 생성&lt;/h2&gt;
&lt;p&gt;클러스터 데이터를 보호하기 위해서 기본적으로 최소한의 RBAC 구성으로 배포되며 현재는 Bearer Token으로 로그인만 지원한다.&lt;/p&gt;
&lt;h3&gt;관리용 서비스 계정 만들기&lt;/h3&gt;
&lt;p&gt;서비스 계정 생성을 위해서 매니페스트부터 구성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# admin-sa.yml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ccambo-admin        ## 생성할 계정이름
  namespace: kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;구성한 매니페스트 정보를 적용해서 쿠버네티스 클러스터에 객체를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;kubectl apply -f admin-sa.yml

serviceaccount/ccambo-admin created&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;클러스터 역할 바인딩 생성&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cluster-admin&lt;/code&gt;의 클러스터 역할 바인딩을 생성한 서비스 계정에 할당하기 위해 아래와 같이 매니페스트 파일을 구성한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# admin-rbac.yml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ccambo-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: ccambo-admin
    namespace: kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;구성한 매니페스트 정보를 적용해서 역할을 할당한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;kubectl apply -f admin-rbac.yml

clusterrolebinding.rbac.authorization.k8s.io/ccambo-admin created&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;관리자 토큰 확인&lt;/h3&gt;
&lt;p&gt;생성된 서비스 계정으로 대시보드에 접근하기 위해서는 토큰 (Bearer Token)을 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 변수에 사용사 설정
SA_NAME = &quot;ccambo-admin&quot;

# 토큰 확인
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep ${SA_NAME} | awk '{print $1}')

Name:         ccambo-admin-token-xdvfq
Namespace:    kube-system
Labels:       &amp;lt;none&amp;gt;
Annotations:  kubernetes.io/service-account.name: ccambo-admin
              kubernetes.io/service-account.uid: 1c84b3e8-caf7-4da4-8bac-bca612b7a274

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1066 bytes
namespace:  11 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6Imo0Zzc5MWoyRzE2ZUxHQk5KMExDSlEyR1I4MnhaQlRxLXQ3Sm8waVlUTjQifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW...
9MmJgFMXIrBVQtoLZ17a9nyM4oAd1Mqv6K3Gro-npeepSLO9d6hVWHdqsXjJkvhMGdYrCYtXSTf7vrDUKuRGRU-1Dv5Q&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 결과에서 토큰 부분을 복사한다.&lt;/p&gt;
&lt;h3&gt;대시보드 액세스&lt;/h3&gt;
&lt;p&gt;만일 대시보드를 NodePort로 설정한 경우는 아래의 명령으로 포트 번호를 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get services -n &amp;lt;namespace&amp;gt; | grep dashbaord

NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
kubernetes-dashboard   NodePort    10.111.76.69    &amp;lt;none&amp;gt;        443:32254/TCP   414d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 주소로 접근하면 액세스할 유형을 선택하는 화면이 나타나고 여기서 토큰 방식을 선택한 후 복사한 토큰을 입력하면 된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;Chrome등의 브라우저에서는 보안의 문제로 HTTPS 관련 인증서 문제가 발생하면 보안되지 않은 사이트로 처리되고 접근을 허용하지 않는다. 따라서 이런 상황이라면 Firefox를 설치해서 접근하면 보안 경고는 나오지만 고급 메뉴에 따라서 진입할 수는 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://computingforgeeks.com/create-admin-user-to-access-kubernetes-dashboard/&quot;&gt;How To Create Admin User to Access Kubernetes Dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kubernetes/dashboard/blob/master/docs/user/access-control/creating-sample-user.md&quot;&gt;Creating sample user&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/&quot;&gt;Web UI (Dashbaord)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://computingforgeeks.com/how-to-install-kubernetes-dashboard-with-nodeport/&quot;&gt;How to install Kubernetes Dashboard with NodePort&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>access token</category>
      <category>Dashboard</category>
      <category>Kubernetes</category>
      <category>NodePort</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/44</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-Kubernetes-Dashboard-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%A0%91%EA%B7%BC%EC%84%A4%EC%A0%95#entry44comment</comments>
      <pubDate>Sat, 19 Dec 2020 18:16:11 +0900</pubDate>
    </item>
    <item>
      <title>[macOS] oh-my-zsh 설치 및 Item2 적용</title>
      <link>https://ccambo.tistory.com/entry/macOS-oh-my-zsh-%EC%84%A4%EC%B9%98-%EB%B0%8F-Item2-%EC%A0%81%EC%9A%A9</link>
      <description>&lt;h1&gt;Set up Item2 terminal with oh-my-zsh on Mac&lt;/h1&gt;
&lt;p&gt;별다른 수정이 없었다면 기본 터미널 쉘은 `bash(Bourne Again Shell) 일 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ echo $0
-bash&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아마 맥북 최신 OS 상태라면 &lt;code&gt;zsh&lt;/code&gt;로 나올 것이다. (macOS 10.15 Catalina 부터 GPL v3 라이센스 제한)&lt;/p&gt;
&lt;h2&gt;필수 준비 항목들&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://brew.sh/index_ko&quot;&gt;Homebrew&lt;/a&gt; 설치 : Mac OS 용 패키지 관리자&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ /bin/bash -c &amp;quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)&amp;quot;
또는
$ /usr/bin/ruby -e &amp;quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 후에는 다음과 같이 패키지 명을 지정해서 다른 패키지들을 설치할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ brew install [package-name]&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;zsh : bash를 확장한 쉘로 Mac은 bash 기반으로 zsh도 같이 설치되어 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ brew install zsh
$ which zsh
/usr/local/bin/zsh
$ chsh -s $(which zsh)&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://www.iterm2.com/&quot;&gt;Item2&lt;/a&gt; : Mac OS의 Terminal emulator, 다운로드해서 설치&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Oh-my-zsh 설치 및 설정&lt;/h2&gt;
&lt;p&gt;터미널을 쉽게 사용할 수 있도록 &lt;code&gt;ZSH&lt;/code&gt;를 확장하는 오프소스 프로그램으로 커뮤니티가 활발하게 형성되어 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# curl을 이용한 설치
$ sh -c &amp;quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&amp;quot;

# wget을 이용한 설치
$ brew install wget
$ sh -c &amp;quot;$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&amp;quot;

# fetch를 이용한 설치
$ sh -c &amp;quot;$(fetch -o - https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 중에 zsh를 기본 쉘로 사용할 것인지를 묻는데 &lt;code&gt;Y&lt;/code&gt;를 입력하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ohmyzsh/ohmyzsh/wiki/External-themes&quot;&gt;테마&lt;/a&gt;로 이동해서 마음에 드는 테마를 선택해서 설치한다. 주로 많이 사용되는 테마는 &lt;code&gt;agnoster 테마&lt;/code&gt;, &lt;code&gt;powerlevel10k 테마&lt;/code&gt; 이며 여기서는 &lt;code&gt;powerlevel10k 테마&lt;/code&gt;를 사용하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git clone https://github.com/romkatv/powerlevel10k.git $ZSH/custom/themes/powerlevel10k&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;git 클론될 폴더는 원하는 곳으로 설정하면 된다. custom/themes를 주로 사용하고 themes 폴더도 존재한다.&lt;/p&gt;
&lt;p&gt;이제 ZSH에서 테마를 인식할 수 있도록 &lt;code&gt;$HOME/.zshrc&lt;/code&gt; 파일을 수정해 줘야 한다. (파일 편집은 편한 에디터를 사용하면 된다.)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;아래와 같이 테마를 설정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;...
ZSH_THEME=&amp;quot;powerlevel10k/powerlevel10k&amp;quot;
...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;빈 공간에 아래와 같이 기본적인 사용자 정보를 설정한다. (기존에 사용자 정보가 출력되는 것을 방지하기 위한 것이다.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;...
DEFAULT_USER=&amp;quot;morris&amp;quot;
...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위와 같이 설정하고 저장한 후에 터미널을 종료하고 다시 시작하면 된다. powerlevel10k 테마의 경우는 여러 옵션 선택하는 창이 나타나므로 원하는 선택을 하면 된다.&lt;/p&gt;
&lt;p&gt;문제가 있거나 다시 설정할 경우는 아래의 명령으로 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ $ZSH/custom/themes/powerlevel10k/config&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Iterm2 설정&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://iterm2colorschemes.com/&quot;&gt;Iterm2 Color Schemes&lt;/a&gt; 에서 컬러를 다운로드해서 설치한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Item2 Preferences 변경&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;Profiles&amp;quot; Tab &amp;gt; Default 선택 &amp;gt; Colors Tab &amp;gt; Color Presets... 콤보 선택해서 다운로드한 컬러 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/powerline/fonts&quot;&gt;powerline fonts&lt;/a&gt; 에서 사용할 폰트를 다운로드해서 설치한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&amp;quot;Profiles&amp;quot; Tab &amp;gt; Default 선택 &amp;gt; Text Tab &amp;gt; Font 에서 사용할 폰트 선택&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;D2Coding 폰트인 경우는 Powerlevel10k에서 깨지므로 &lt;code&gt;Use a different font for non-ASCII text&lt;/code&gt; 체크를 해서 &amp;quot;MesloLGS NF&amp;quot; 로 추가 선택한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.outsider.ne.kr/1490&quot;&gt;Powerlevel10k로 zsh 설정하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://brew.sh/index_ko&quot;&gt;Homebrew&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.iterm2.com/&quot;&gt;item2 download&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ohmyzsh/ohmyzsh&quot;&gt;oh-my-zsh&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>iterm2</category>
      <category>macos</category>
      <category>oh-my-zsh</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/43</guid>
      <comments>https://ccambo.tistory.com/entry/macOS-oh-my-zsh-%EC%84%A4%EC%B9%98-%EB%B0%8F-Item2-%EC%A0%81%EC%9A%A9#entry43comment</comments>
      <pubDate>Sat, 19 Dec 2020 17:58:02 +0900</pubDate>
    </item>
    <item>
      <title>[Golang] 의존성 없이 웹으로 서비스할 정적 파일들을 Golang 바이너리에 추가하기</title>
      <link>https://ccambo.tistory.com/entry/Golang-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%97%86%EC%9D%B4-%EC%9B%B9%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%ED%95%A0-%EC%A0%95%EC%A0%81-%ED%8C%8C%EC%9D%BC%EB%93%A4%EC%9D%84-Golang-%EB%B0%94%EC%9D%B4%EB%84%88%EB%A6%AC%EC%97%90-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;[Golang] How to add static files (for Web serving) to the golang binary without dependencies&lt;/h1&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeertJohan/go.rice&quot;&gt;go.rice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jteeuwen/go-bindata&quot;&gt;go-bindata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gobuffalo/packr&quot;&gt;packr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mjibson/esc&quot;&gt;esc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;과연 이런 작업이 필요한 것일까?&lt;/h2&gt;
&lt;p&gt;특히 고객에게 전달되어야 하는 최종 산출물일 경우라면 이런 접근 방법에 대한 명확한 이유를 설명할 수 있어야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;장점&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&amp;quot;안정성&amp;quot;&lt;/strong&gt; : 바이너리에 추가된 파일은 다시 추출하기 어렵기 때문에 안정성이 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;quot;편리&amp;quot;&lt;/strong&gt; : 배포 또는 소스를 전달할 떄 웹 서비스를 위한 추가적인 폴더들을 제공할 필요가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;단점&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&amp;quot;크기&amp;quot;&lt;/strong&gt; : 추가된 만큼 바이너리 자체의 크기가 증가한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;quot;성능&amp;quot;&lt;/strong&gt; : 그다지 큰 이슈가 될 것 같지는 않지만, Admin Web과 같이 정적 파일을 운영할 경우는 별도로 웹을 구성해서 서비스하는 것보다는 바이너리내의 파일을 서비스하는 개념이기 때문에 아주 약간의 성능 상 이슈가 있을 수 있다. 틱 단위의 서비스가 중요한 서비스가 아니라 내부 관리자 용이라면 이런 걱정은 하지 않아도 될만한 차이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;가능한 원리는 무엇일까?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;프로그램으로 프로그램 코드를 작성하는 &amp;quot;제너레이터&amp;quot;를 이용&lt;/code&gt;하는 것이다. (이전에는 &amp;quot;golang.org/x/tools/cmd/goyacc&amp;quot;를 이용했었다) &lt;/p&gt;
&lt;p&gt;&lt;font-color color=&quot;darkblue&quot;&gt;golang 1.4 부터는 이런 작업을 편하게 처리할 수 있도록 &amp;quot;go generate&amp;quot; 명령이 추가&lt;/font-color&gt;되었다. 이를 통하면 소스 상에 특수한 주석으로 명기된 명령을 검색해서 처리해주는 방식으로 운영할 수 있다.&lt;/p&gt;
&lt;h2&gt;구현해 보자.&lt;/h2&gt;
&lt;p&gt;이제 아주 간단한 실제 동작하는 샘플을 통해서 검증을 해 보자.&lt;/p&gt;
&lt;h3&gt;모듈 구성&lt;/h3&gt;
&lt;p&gt;go module을 사용하는 프로젝트 구성&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;# go mod init &amp;lt;project module path&amp;gt;

$ go mod init gitlab.com/ccambo/samples/golang/embedded&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 명령으로 프로젝트 폴더에 &amp;quot;go.mod&amp;quot; 파일이 생성되었을 것이다.&lt;/p&gt;
&lt;h3&gt;빌드 구성 (makefile)&lt;/h3&gt;
&lt;p&gt;이제 makefile을 구성해 보자. (./makefile)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-script&quot;&gt;.PHONY: generate

generate:
    @go generate ./...
    @echo &amp;quot;[OK] 정적 파일들이 추가되었습니다!&amp;quot;

security:
    @gosec ./...
    @echo &amp;quot;[OK] 보안 검증이 완료되었습니다!&amp;quot;

build: generate security
    @go build -o ./build/server ./cmd/app/*.go
    @echo &amp;quot;[OK] 어플리케이션 바이너리가 생성되었습니다.!&amp;quot;

run:
    @./build/server&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;tip 팁!&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;코드에 &lt;a href=&quot;https://github.com/securego/gosec&quot;&gt;GoSecurityChecker&lt;/a&gt; 사용을 권장한다. 이를 통해서 코드의 보안문제를 사전에 검증할 수 있다.  &lt;/p&gt;
&lt;p&gt;사용을 위한 설치는 다음과 같이 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ go get github.com/securego/gosec/v2/cmd/gosec&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;아직 동작하지 않는 상태이므로 아래의 나머지를 구성한 후에 테스트를 진행할 때 확인해 보도록 한다.&lt;/p&gt;
&lt;h3&gt;적용할 코드 구성&lt;/h3&gt;
&lt;p&gt;아래의 코드는 실제 Generate할 때 사용될 메서드들을 지정하는 것이다. 실제 사용할 코드가 아니라 정적 파일들을 추가하고 관리하는데 사용할 것이기 때문에 Box라고 패키지를 구성했다. (./internal/box.go)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;//go:generate go run generator.go

// Package box - Embedded resource helper
package box

// ===== [ Constants and Variables ] =====

var (
    // 기본 Embedding Box 인스턴스 생성
    box = newEmbedBox()
)

// ===== [ Types ] =====

type (
    // Embedding 대상 파일들의 정보를 관리하기 위한 저장소 구조
    embedBox struct {
        storage map[string][]byte
    }
)

// ===== [ Implementations ] =====

// Add - Embedded Box 정보에 지정한 경로와 byte 정보 추가
func (e *embedBox) Add(file string, content []byte) {
    e.storage[file] = content
}

// Get - Embedded Box 정보에서 지정한 파일을 byte로 반환
func (e *embedBox) Get(file string) []byte {
    if f, ok := e.storage[file]; ok {
        return f
    }
    return nil
}

// Has - Embedded Box 정보내에 지정한 파일이 존재하는지 검증
func (e *embedBox) Has(file string) bool {
    if _, ok := e.storage[file]; ok {
        return true
    }
    return false
}

// ===== [ Private Functions ] =====

// newEmbedBox - Embedding할 파일들을 관라하기 위한 인스턴스 생성
func newEmbedBox() *embedBox {
    return &amp;amp;embedBox{storage: make(map[string][]byte)}
}

// ===== [ Public Functions ] =====

// Add - Embedded Box 정보에 지정한 경로와 byte 정보 추가
func Add(file string, content []byte) {
    box.Add(file, content)
}

// Get - Embedded Box 정보에서 지정한 파일을 byte로 반환
func Get(file string) []byte {
    return box.Get(file)
}

// Has - Embedded Box 정보내에 지정한 파일이 존재하는지 검증
func Has(file string) bool {
    return box.Has(file)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드에서 1번째 줄을 보면 &lt;font-color color=&quot;red&quot; weight=&quot;bold&quot;&gt;&amp;quot;//go:generate go run generator.go&amp;quot;&lt;/font-color&gt; 주석이 있는 것이 보일 것이다.&lt;br&gt;이 주석을 통해서 다른 코드를 생성하는데 이 파일의 메서드가 사용될 수 있도록 처리하는 것이다.&lt;/p&gt;
&lt;p&gt;즉, &lt;code&gt;go build&lt;/code&gt;를 통해서 빌드 작업을 하는 중에 이 주석을 만나게 되면 &lt;code&gt;go run generate.go&lt;/code&gt;가 동작하게 되며 현재 파일 (box.go)의 메서드들이 호출되어 사용된다.&lt;/p&gt;
&lt;h3&gt;BLOB 파일 템플릿 구성&lt;/h3&gt;
&lt;p&gt;이제 실제 Generate 작업을 수행하는 코드를 구성해 보자. Generating 동작이 수행될 떄의 Entry point가 된다. (./internal/generator.go)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;//+build ignore

// Package main - Generator Entry point
package main

import (
    &amp;quot;bytes&amp;quot;
    &amp;quot;fmt&amp;quot;
    &amp;quot;go/format&amp;quot;
    &amp;quot;io/ioutil&amp;quot;
    &amp;quot;log&amp;quot;
    &amp;quot;os&amp;quot;
    &amp;quot;path/filepath&amp;quot;
    &amp;quot;strings&amp;quot;
    &amp;quot;text/template&amp;quot;
)

// ===== [ Constants and Variables ] =====

const (
    // 생성될 파일 명
    blobFileName string = &amp;quot;blob.go&amp;quot;
    // 처리 대상 정적 파일 경로
    embedFolder string = &amp;quot;../static&amp;quot;
)

var (
    // 템플릿 처리에서 호출할 함수 맵
    conv = map[string]interface{}{&amp;quot;conv&amp;quot;: fmtByteSlice}

    // 코드 구성을 위한 템플릿
    tmpl = template.Must(template.New(&amp;quot;&amp;quot;).Funcs(conv).Parse(`package box
// Code generated by go generate; DO NOT EDIT.
func init() {
    {{- range $name, $file := . }}
        box.Add(&amp;quot;{{ $name }}&amp;quot;, []byte{ {{ conv $file }} })
    {{- end }}
}`),
    )
)

// ===== [ Types ] =====
// ===== [ Implementations ] =====
// ===== [ Private Functions ] =====

// fmtByteSlice - 지정한 []byte를 문자로 반환
func fmtByteSlice(s []byte) string {
    builder := strings.Builder{}

    for _, v := range s {
        builder.WriteString(fmt.Sprintf(&amp;quot;%d,&amp;quot;, int(v)))
    }

    return builder.String()
}

func main() {
    // 정적 파일 경로 확인
    if _, err := os.Stat(embedFolder); os.IsNotExist(err) {
        log.Fatal(&amp;quot;지정한 정적 경로가 존재하지 않습니다!&amp;quot;)
    }

    // 정적 파일들의 파일명과 파일 내용 관리용 맵 생성
    configs := make(map[string][]byte)

    // 정적 파일 경로를 기준으로 파일 검증 및 처리
    err := filepath.Walk(embedFolder, func(path string, info os.FileInfo, err error) error {
        // 축약 경로 구성
        relativePath := filepath.ToSlash(strings.TrimPrefix(path, embedFolder))

        // 폴더 여부 검증
        if info.IsDir() {
            // 현재는 폴더는 생략
            log.Println(path, &amp;quot;는 디렉토리이므로 현재 지원되지 않습니다.&amp;quot;)
            return nil
        } else {
            // 파일만 처리
            log.Println(path, &amp;quot;는 파일이므로 추가합니다.&amp;quot;)

            b, err := ioutil.ReadFile(path)
            if err != nil {
                // 파일 내용 오류
                log.Printf(&amp;quot;Error reading %s: %s&amp;quot;, path, err)
                return err
            }

            // 경로와 내용을 맵에 추가
            configs[relativePath] = b
        }

        return nil
    })
    if err != nil {
        log.Fatal(&amp;quot;정적 폴더를 처리하는 중에 오류 발생:&amp;quot;, err)
    }

    // 출력할 대상 파일 생성
    f, err := os.Create(blobFileName)
    if err != nil {
        log.Fatal(&amp;quot;처리 대상 파일 생성 중에 오류 발생:&amp;quot;, err)
    }
    defer f.Close()

    // 버퍼 구성
    builder := &amp;amp;bytes.Buffer{}

    // 정적 파일 정보를 기준으로 템플릿 처리
    if err = tmpl.Execute(builder, configs); err != nil {
        log.Fatal(&amp;quot;템플릿 실행 중에 오류 발생&amp;quot;, err)
    }

    // 생성된 코드 포맷 처리
    data, err := format.Source(builder.Bytes())
    if err != nil {
        log.Fatal(&amp;quot;생성된 코드의 포맷 처리 중에 오류 발생&amp;quot;, err)
    }

    // 생성된 코드 출력
    if err = ioutil.WriteFile(blobFileName, data, os.ModePerm); err != nil {
        log.Fatal(&amp;quot;생성된 코드를 파일로 저장 중에 오류 발생&amp;quot;, err)
    }
}

// ===== [ Public Functions ] =====&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드에도 &lt;font-color color=&quot;red&quot; weight=&quot;bold&quot;&gt;&amp;quot;//+build ignore&amp;quot;&lt;/font-color&gt; 주석이 있는 것을 확인할 수 있다. 이 주석은 모듈 프로젝트의 빌드 작업이 진행될 떄 처리할 대상이 아니라는 것을 지정한 것이다. &lt;code&gt;즉, 다른 코드를 생성하는 처리에만 필요하고 기본 빌드에는 포함될 필요가 없다는 의미다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;generator 코드를 통해서 생성된 파일을 &lt;code&gt;blob.go&lt;/code&gt; 파일이고, 이 파일은 실제 빌드과정에 포함되어 빌드되게 된다. 나중에 테스트를 통해서 확인할 수 있지만, 생성된 파일에는 (../../static) 폴더내의 모든 파일들이 바이트 조각으로 포함되게 된다.&lt;/p&gt;
&lt;h2&gt;검증 대상 만들기 (Static Files)&lt;/h2&gt;
&lt;p&gt;이제 빌드 과정을 통해서 바이너리로 포함될 정적 파일을 구현하도록 한다. 실제 바이너리에 포함될 파일이다. (./static/index.html)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;ko&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;meta http-equiv=&amp;quot;X-UA-Compatible&amp;quot; content=&amp;quot;ie=edge&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;{{.Title}}&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;{{.Heading}}&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{{.Description}}&amp;lt;/p&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드는 단순하게 정보를 출력하는 HTML 파일로 위의 코드에서 볼 수 있는 것과 같이 &lt;code&gt;Go Template&lt;/code&gt;을 사용했다.&lt;/p&gt;
&lt;h2&gt;테스트 및 검증&lt;/h2&gt;
&lt;p&gt;이제 테스트를 위한 어플리케이션 진입점을 만들도록 한다. (./cmd/app/main.go)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Package main - Entry point of application
package main

import (
    &amp;quot;log&amp;quot;
    &amp;quot;net/http&amp;quot;
    &amp;quot;text/template&amp;quot;

    box &amp;quot;gitlab.com/ccambo/samples/golang/embedded/internal&amp;quot;
)

// ===== [ Constants and Variables ] =====
// ===== [ Types ] =====

type (
    // PageData - 정적 파일에 전달할 데이터 구조
    PageData struct {
        Title       string
        Heading     string
        Description string
    }
)

// ===== [ Implementations ] =====
// ===== [ Private Functions ] =====

func main() {
    // 바이너리로 추가했던 정적 페이지 호출 (바이너리로 포함된 축약 경로)
    index := string(box.Get(&amp;quot;/index.html&amp;quot;))

    // 템플릿 처리
    tmpl := template.Must(template.New(&amp;quot;&amp;quot;).Parse(index))

    // 웹 액세스 핸들러 설정
    http.HandleFunc(&amp;quot;/&amp;quot;, func(rw http.ResponseWriter, req *http.Request) {
        data := PageData{
            Title:       &amp;quot;외부 의존성 없이 정적 파일을 바이너리에 포함시키기&amp;quot;,
            Heading:     &amp;quot;샘플 코드&amp;quot;,
            Description: &amp;quot;블로그의 샘플을 잘 보고 활용하면, 더 많은 작업들을 할 수 있다&amp;quot;,
        }

        // 템플릿 실행
        if err := tmpl.Execute(rw, data); err != nil {
            log.Fatal(err)
        }
    })

    // 웹 서버 실행
    if err := http.ListenAndServe(&amp;quot;:8080&amp;quot;, nil); err != nil {
        log.Fatal(err)
    }
}

// ===== [ Public Functions ] =====&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 간단하지만 실행 가능한 모든 소스들을 구성했으므로 빌드 작업을 진행해 보도록 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ make build
2020/09/22 16:20:00 ../static 는 디렉토리이므로 현재 지원되지 않습니다.
2020/09/22 16:20:00 ../static/index.html 는 파일이므로 추가합니다.
[OK] 정적 파일들이 추가되었습니다!
[gosec] 2020/09/22 16:20:00 Including rules: default
[gosec] 2020/09/22 16:20:00 Excluding rules: default
[gosec] 2020/09/22 16:20:00 Import directory: /Users/morris/Workspaces/Samples/Golang/modules/embedded/cmd/app
[gosec] 2020/09/22 16:20:00 Checking package: main
[gosec] 2020/09/22 16:20:00 Checking file: /Users/morris/Workspaces/Samples/Golang/modules/embedded/cmd/app/main.go
[gosec] 2020/09/22 16:20:00 Import directory: /Users/morris/Workspaces/Samples/Golang/modules/embedded/internal
[gosec] 2020/09/22 16:20:00 Checking package: box
[gosec] 2020/09/22 16:20:00 Checking file: /Users/morris/Workspaces/Samples/Golang/modules/embedded/internal/blob.go
[gosec] 2020/09/22 16:20:00 Checking file: /Users/morris/Workspaces/Samples/Golang/modules/embedded/internal/box.go
Results:


Summary:
   Files: 3
   Lines: 127
   Nosec: 0
  Issues: 0

[OK] 보안 검증이 완료되었습니다!
[OK] 어플리케이션 바이너리가 생성되었습니다.!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 정적 파일 생성, 보안 검사, 바이너리 생성 등의 작업이 진행된 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;이제 생성된 정적 파일을 포함해서 생성된 소스 파일 (./internal/blob.go)을 확인해 보도록 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package box

// Code generated by go generate; DO NOT EDIT.
func init() {
    box.Add(&amp;quot;/index.html&amp;quot;, []byte{60, 33, 68, 79, 67, 84, 89, 80, 69, 32, 104, 116, 109, 108, 62, 10, 60, 104, 116, 109, 108, 32, 108, 97, 110, 103, 61, 34, 107, 111, 34, 62, 10, 10, 60, 104, 101, 97, 100, 62, 10, 32, 32, 32, 32, 60, 109, 101, 116, 97, 32, 99, 104, 97, 114, 115, 101, 116, 61, 34, 85, 84, 70, 45, 56, 34, 32, 47, 62, 10, 32, 32, 32, 32, 60, 109, 101, 116, 97, 32, 110, 97, 109, 101, 61, 34, 118, 105, 101, 119, 112, 111, 114, 116, 34, 32, 99, 111, 110, 116, 101, 110, 116, 61, 34, 119, 105, 100, 116, 104, 61, 100, 101, 118, 105, 99, 101, 45, 119, 105, 100, 116, 104, 44, 32, 105, 110, 105, 116, 105, 97, 108, 45, 115, 99, 97, 108, 101, 61, 49, 46, 48, 34, 32, 47, 62, 10, 32, 32, 32, 32, 60, 109, 101, 116, 97, 32, 104, 116, 116, 112, 45, 101, 113, 117, 105, 118, 61, 34, 88, 45, 85, 65, 45, 67, 111, 109, 112, 97, 116, 105, 98, 108, 101, 34, 32, 99, 111, 110, 116, 101, 110, 116, 61, 34, 105, 101, 61, 101, 100, 103, 101, 34, 32, 47, 62, 10, 32, 32, 32, 32, 60, 116, 105, 116, 108, 101, 62, 123, 123, 46, 84, 105, 116, 108, 101, 125, 125, 60, 47, 116, 105, 116, 108, 101, 62, 10, 60, 47, 104, 101, 97, 100, 62, 10, 10, 60, 98, 111, 100, 121, 62, 10, 32, 32, 32, 32, 60, 104, 49, 62, 123, 123, 46, 72, 101, 97, 100, 105, 110, 103, 125, 125, 60, 47, 104, 49, 62, 10, 32, 32, 32, 32, 60, 112, 62, 123, 123, 46, 68, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 125, 125, 60, 47, 112, 62, 10, 60, 47, 98, 111, 100, 121, 62, 10, 10, 60, 47, 104, 116, 109, 108, 62})
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;제대로 처리가 되었는지는 아래의 명령으로 실행을 검증해 보면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ make run&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정상적이라면 위와 같이 HTTP 서비스 상태가 된다. 로컬의 브라우저를 열어서 &amp;quot;&lt;a href=&quot;http://localhost:8080&amp;quot;&quot;&gt;http://localhost:8080&amp;quot;&lt;/a&gt; 으로 출력을 확인해 보면 된다.&lt;/p&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;아주 간단하게 단순 파일을 바이너리에 추가하고, 이를 서비스하는 방법에 대해 정리해 보았다. &lt;/p&gt;
&lt;p&gt;아직 실제 사용하기에는 많은 부분이 부족하다. 하지만 최소한 어떻게 접근하면 되는지에 대한 방법을 찾았기 때문에 아래의 참조 정보들을 검토해서 원하는 결과물을 생성하면 된다.&lt;/p&gt;</description>
      <category>개발/기타언어</category>
      <category>binary</category>
      <category>embed</category>
      <category>generator</category>
      <category>Go</category>
      <category>Golang</category>
      <category>static files</category>
      <category>template</category>
      <category>WEB</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/42</guid>
      <comments>https://ccambo.tistory.com/entry/Golang-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%97%86%EC%9D%B4-%EC%9B%B9%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%ED%95%A0-%EC%A0%95%EC%A0%81-%ED%8C%8C%EC%9D%BC%EB%93%A4%EC%9D%84-Golang-%EB%B0%94%EC%9D%B4%EB%84%88%EB%A6%AC%EC%97%90-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0#entry42comment</comments>
      <pubDate>Fri, 18 Dec 2020 14:11:11 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 컨테이너 내부에서 외부의 HTTPS 호출할때 X509 오류 해결하기</title>
      <link>https://ccambo.tistory.com/entry/Docker-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C-%EC%99%B8%EB%B6%80%EC%9D%98-HTTPS-%ED%98%B8%EC%B6%9C%ED%95%A0%EB%95%8C-X509-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to resolve X509 errors when invoking external HTTPS from inside the container&lt;/h1&gt;
&lt;h2&gt;오류 정보&lt;/h2&gt;
&lt;p&gt;로컬 PC에서 개발할 때는 HTTP Client를 이용해서 HTTPS 사이트에 접속해서 결과를 받아오는데 문제가 없지만, Docker Image를 구성하고 컨테이너로 동작을 시키면 &lt;code&gt;certificate signed by unknown authority&lt;/code&gt; 오류 발생&lt;/p&gt;
&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;TLS (이전에는 SSL) 사이트는 서버 인증을 통해서 보안이 유지되는 연결을 구성하기 위한 것이기 때문에 클라이언트가 접속하게 되면 서버 인증서를 클라이언트로 보내고 클라이언트는 인증서를 검증해서 제대로 구성된 CA 목록에 존재하는지를 확인한 후에 서버에 응답하면 통신 중에 정한 대칭키를 이용해서 보안 통신을 하게 된다.&lt;/p&gt;
&lt;p&gt;이 과정에서 문제가 발생하여 클라이언트와 서버 간에 인증 과정을 제대로 처리하지 못하는 상태가 되면 이와 관련된 오류가 발생하게 된다. 오류 메시지의 내용을 보면 서버에서 보내온 인증서의 서명을 확인하지 못하는 것으로 판단이 된다. (물론 다른 이유도 여러 가지 있을 수 있다)&lt;/p&gt;
&lt;h2&gt;해결 방법&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Docker Image를 만들 때 어떤 베이스 이미지를 사용했는지에 따라서 인증 관련 패키지가 설치되지 않았을 경우가 많다. 따라서 Image를 구성할 때 인증서 처리를 위한 패키지를 추가적으로 설치 또는 이미 존재하는 Cert 파일 복사&lt;/code&gt;를 해 주면 이런 문제를 해결할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;단일 Stage 방법으로 Image를 생성하는 경우 (CA-Certificates 파일이 존재하는 경우)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM scrtch

ADD ca-certificates.crt /etc/ssl/certs/     # 이미 존재하는 경우 복사
ADD main /

CMD [&amp;quot;/main&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi Stage 방법으로 Image를 생성하는 경우 (CA-Certificates 패키지 설치하는 경우)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM golang:alpine as build

RUN apk --no-cache add ca-certificates      # CA-Certificates 패키지 설치

WORKDIR /go/src/app

COPY . .

RUN CGO_ENABLED=0 go-wrapper install -ldflags &amp;#39;-extldflags &amp;quot;-static&amp;#39;

FROM scratch

COPY --from-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/        ## CA-Certificates 인증서 복사
COPY --from-build /go/bin/app /app

ENTRYPOINT [&amp;quot;/app&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이미지를 만들 때 이미 존재하는 CA-Certificates 인증서 파일을 이미지에 특정 경로 (&lt;code&gt;/etc/ssl/certs&lt;/code&gt;)에 복사&lt;/li&gt;
&lt;li&gt;Alpine 이미지를 사용하는 경우는 CA-Certificates 패키지 설치 및 최종 이미지의 특정 경로(&lt;code&gt;/etc/ssl/certs&lt;/code&gt;)에 복사&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Container</category>
      <category>Container</category>
      <category>docker</category>
      <category>error</category>
      <category>external https</category>
      <category>https</category>
      <category>x509</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/41</guid>
      <comments>https://ccambo.tistory.com/entry/Docker-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%82%B4%EB%B6%80%EC%97%90%EC%84%9C-%EC%99%B8%EB%B6%80%EC%9D%98-HTTPS-%ED%98%B8%EC%B6%9C%ED%95%A0%EB%95%8C-X509-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0#entry41comment</comments>
      <pubDate>Fri, 18 Dec 2020 13:40:22 +0900</pubDate>
    </item>
    <item>
      <title>[macOS, GIT] Mac Book에 Git 설치</title>
      <link>https://ccambo.tistory.com/entry/macOS-GIT-Mac-Book%EC%97%90-Git-%EC%84%A4%EC%B9%98</link>
      <description>&lt;h1&gt;Mac북에 Git 설치하기&lt;/h1&gt;
&lt;h2&gt;패키지를 이용한 설치&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;[https://git-scm.com/download/mac](https://git-scm.com/download/mac)&lt;/code&gt; 접속&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;git-x.x.x-xxx.dmg 다운로드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다운로드 받은 파일 실행&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Homebrew를 이용한 설치&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://zetawiki.com/wiki/Homebrew&quot;&gt;Home-brew&lt;/a&gt; 가 미 설치 상태라면 참고해서 설치를 하도록 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$ brew install git&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;설치 확인&lt;/h2&gt;
&lt;p&gt;설치 여부는 아래의 명령을 통해서 확인 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 설치 버전 확인
$ git --version
git version 1.7.10.3

# 설치 경로 확인
$ which git
/usr/local/git/bin/git&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;두 가지 모두 설치는 가능하지만 맥북이라면 나중에 설치를 삭제하거나 업그레이드 등의 관리가 편하므로 Homebrew를 이용해서 설치하는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;참고 정보&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zetawiki.com/wiki/%EB%A7%A5OS_Git_%EC%84%A4%EC%B9%98&quot;&gt;맥OS Git 설치 - 제타위키&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zetawiki.com/wiki/Homebrew&quot;&gt;Homebrew - 제타위키&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/기타공통</category>
      <category>GIT</category>
      <category>install</category>
      <category>MAC</category>
      <category>macos</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/40</guid>
      <comments>https://ccambo.tistory.com/entry/macOS-GIT-Mac-Book%EC%97%90-Git-%EC%84%A4%EC%B9%98#entry40comment</comments>
      <pubDate>Thu, 17 Dec 2020 10:39:08 +0900</pubDate>
    </item>
    <item>
      <title>[CentOS 8] Python 2 / 3 설치하기</title>
      <link>https://ccambo.tistory.com/entry/CentOS-8-Python-2-3-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;How to install Python2 and Python3 to CentOS 8&lt;/h1&gt;
&lt;p&gt;Python은 셰계에서 가장 인기있는 프로그래밍 언어 중의 하나로 간단히 배우기 쉬운 구문을 가진기 때문에 초보자나 숙달된 개발자들에게 인기가 많다.&lt;/p&gt;
&lt;p&gt;다른 Linux 배포판들과는 달리 &lt;code&gt;CentOS 8에는 기본적으로 설치되저 있지 않기 때문에 추가 설치&lt;/code&gt;를 해줘야 하며, Python2 / Python3 로 양분된 버전이 존재한다. 이중에 Python 2는 2020년에 지원이 종료될 것이기 때문에 앞으로는 Python 3를 사용하는 것이 좋다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RHEL (Red Hat Enterprise Linux) 와 CentOS 8에는 사용자를 특정 버전 Python으로 한정되지 않도록 하는 unversioned system-wide python Command가 없다.&lt;/code&gt; 대신에 사용자가 특정한 Python 버전을 설치 및 구성과 실행할 수 있는 옵션을 제공한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;CentOS 8에서는 &lt;code&gt;yum 패키지 관리자&lt;/code&gt; 대신 &lt;code&gt;dnf 패키지 관리자&lt;/code&gt;가 탑재되어 있다. 그러나 현재는 두 개 모두 사용이 가능하다. 단, 이 문서에는 dnf 를 사용하는 것으로 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Install Python 3 on CentOS 8&lt;/h2&gt;
&lt;p&gt;아래의 명령으로 Python 3 버전을 설치한다. (pip 도 같이 설치가 된다)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo dnf install -y python3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령으로 설치된 버전을 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ python3 --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;python은 &lt;code&gt;python3&lt;/code&gt;로, pip는 &lt;code&gt;pip3&lt;/code&gt; 로 명시적인 버전을 지정해야 한다. 그리고 pip는 가상환경 내에서만 사용해야 한다. &lt;code&gt;Python Virtual Environment를 사용하면 Python 모듈을 글로벌하게 설치하는 것이 아니라 특정한 프로젝트의 격리된 위치에 설치&lt;/code&gt;할 수 있게 된다. 이를 통해 다른 Python 프로젝트 간의 영향을 걱정할 필요가 없다.&lt;/p&gt;
&lt;h2&gt;Install Python 2 on CentOS 8&lt;/h2&gt;
&lt;p&gt;아래의 명령으로 Python 2 버전을 설치한다. (pip 도 같이 설치가 된다)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo dnf install -y python2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래의 명령으로 설치된 버전을 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ python2 --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;python은 &lt;code&gt;python2&lt;/code&gt;로, pip는 &lt;code&gt;pip2&lt;/code&gt; 로 명시적인 버전을 지정해야 한다. &lt;/p&gt;
&lt;h2&gt;Set Default Python Version (Unversioned Python Command)&lt;/h2&gt;
&lt;p&gt;시스템 경로에서 python 명령을 찾을 것으로 예상되는 애플리케이션을 가지고 작업을 해야 하는 경우는 Unversioned system-wide python command를 생성하고 기본 버전을 지정해 줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;alternatives&lt;/code&gt; 유틸리티를 이용해서 다음의 명령으로 Python3를 지정할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo alternatives --set python /usr/bin/python3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python2를 지정할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo alternatives --set python /usr/bin/python2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;alternatives 명령은 지정한 python 버전을 가르키는 &lt;a href=&quot;https://linuxize.com/post/how-to-create-symbolic-links-in-linux-using-the-ln-command/&quot;&gt;symlink&lt;/a&gt;인 python을 생성한다.&lt;/p&gt;
&lt;p&gt;따라서 &lt;code&gt;python --version&lt;/code&gt; 명령을 사용하면 지정된 실제 Pythone 버전을 출력하게 된다.&lt;/p&gt;
&lt;p&gt;다른 버전을 사용해야 할 경우는 위의 명령과 같이 다시 한번 실행하면 되고, 만일 이를 삭제할 경우는 아래의 명령으로 python symlink를 삭제하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo alternatives --auto python&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발/기타공통</category>
      <category>CentOS</category>
      <category>centos8</category>
      <category>python</category>
      <category>python2</category>
      <category>python3</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/39</guid>
      <comments>https://ccambo.tistory.com/entry/CentOS-8-Python-2-3-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0#entry39comment</comments>
      <pubDate>Wed, 16 Dec 2020 19:26:37 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes - KREW] KREW란 무엇일까?</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-KREW-KREW%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
      <description>&lt;h1&gt;What is the KREW&lt;/h1&gt;
&lt;p&gt;&lt;font color=&quot;seablue&quot; size=&quot;3&quot;&gt;KREW는 kubectl을 확장하기 위한 플러그인 매니저&lt;/font&gt;로 &lt;a href=&quot;https://krew.sigs.k8s.io/&quot;&gt;Kubernetes SIG&lt;/a&gt;로 개발이 진행되고 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/&quot;&gt;kubectl 플러그인&lt;/a&gt; 검색&lt;/li&gt;
&lt;li&gt;플러그인 설치&lt;/li&gt;
&lt;li&gt;설치된 플러그인 최신 상태 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;kubectl v1.12 이상 버전에만 호환된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;현재 Krew에는 127개의 kubectl 플러그인이 등록되어 있으며, &lt;code&gt;macOS&lt;/code&gt;, &lt;code&gt;linux&lt;/code&gt;, &lt;code&gt;windows&lt;/code&gt; 등의 모든 플랫폼에서 작동된다.&lt;/p&gt;
&lt;p&gt;KREW를 사용하면 플러그인을 개발하고 여러 플랫폼에 쉽게 배포하고 중앙 집중식 플러그인 저장소를 통해 검색 및 설치 및 관리가 가능하다.&lt;/p&gt;
&lt;h2&gt;Install&lt;/h2&gt;
&lt;h3&gt;macOS / Linux&lt;/h3&gt;
&lt;blockquote&gt;
&lt;h2&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Linux에서 shell 확인하는 방법&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;grep &amp;lt;user name&amp;gt; /etc/passwd&lt;/code&gt; 를 실행하면 해당 사용자 정보 및 사용하는 쉘 정보가 출력된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat /etc/shells&lt;/code&gt; 를 실행하면 현재 설치되어 있는 쉘 리스트를 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;git 설치하는 방법&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CentOS 8에서는 &lt;code&gt;sudo yum -y install git&lt;/code&gt; 또는 &lt;code&gt;sudo dnf -y install git&lt;/code&gt; 를 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git --version&lt;/code&gt; 명령으로 설치 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;bash 또는 zsh 쉘&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;git&lt;/code&gt;가 설치되어 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;krew&lt;/code&gt; 다운로드 및 설치&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;(
  set -x; cd &amp;quot;$(mktemp -d)&amp;quot; &amp;amp;&amp;amp;
  curl -fsSLO &amp;quot;https://github.com/kubernetes-sigs/krew/releases/latest/download/krew.tar.gz&amp;quot; &amp;amp;&amp;amp;
  tar zxvf krew.tar.gz &amp;amp;&amp;amp;
  KREW=./krew-&amp;quot;$(uname | tr &amp;#39;[:upper:]&amp;#39; &amp;#39;[:lower:]&amp;#39;)_$(uname -m | sed -e &amp;#39;s/x86_64/amd64/&amp;#39; -e &amp;#39;s/arm.*$/arm/&amp;#39;)&amp;quot; &amp;amp;&amp;amp;
  &amp;quot;$KREW&amp;quot; install krew
)&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$HOME/.krew/bin&lt;/code&gt; 디렉터리를 PATH에 추가하고 아래와 같은 명령을 &lt;code&gt;.bashrc&lt;/code&gt; 또는 &lt;code&gt;.zshrc&lt;/code&gt; 파일에 추가하고 쉘을 재 시작한다.&lt;pre&gt;&lt;code&gt;export PATH=&amp;quot;${KREW_ROOT:-$HOME/.krew}/bin:$PATH&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl krew&lt;/code&gt; 명령을 통해 정상 동작여부 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;fish 쉘&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;git&lt;/code&gt;가 설치되어 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;krew&lt;/code&gt; 다운로드 및 설치&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;begin
  set -x; set temp_dir (mktemp -d); cd &amp;quot;$temp_dir&amp;quot; &amp;amp;&amp;amp;
  curl -fsSLO &amp;quot;https://github.com/kubernetes-sigs/krew/releases/latest/download/krew.tar.gz&amp;quot; &amp;amp;&amp;amp;
  tar zxvf krew.tar.gz &amp;amp;&amp;amp;
  set KREWNAME krew-(uname | tr &amp;#39;[:upper:]&amp;#39; &amp;#39;[:lower:]&amp;#39;)_(uname -m | sed -e &amp;#39;s/x86_64/amd64/&amp;#39; -e &amp;#39;s/arm.*$/arm/&amp;#39;) &amp;amp;&amp;amp;
  ./$KREWNAME install krew &amp;amp;&amp;amp;
  set -e KREWNAME; set -e temp_dir
end&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$HOME/.krew/bin&lt;/code&gt; 디렉터리를 PATH에 추가하고 아래와 같은 명령을 &lt;code&gt;config.fish&lt;/code&gt; 파일에 추가하고 쉘을 재 시작한다.&lt;pre&gt;&lt;code&gt;set -gx PATH $PATH $HOME/.krew/bin&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl krew&lt;/code&gt; 명령을 통해 정상 동작여부 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;git&lt;/code&gt;가 설치되어 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kubernetes-sigs/krew/releases&quot;&gt;Release Page&lt;/a&gt;에서 &lt;code&gt;krew.exe&lt;/code&gt; 다운로드&lt;/li&gt;
&lt;li&gt;심볼릭 링크를 사용하므로 관리자 권한으로 cmd.exe를 실행하고 다운로드한 디렉토리로 이동&lt;/li&gt;
&lt;li&gt;krew 설치 명령을 실행&lt;pre&gt;&lt;code&gt;krew install krew&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%USERPROFILE%\.krew\bin&lt;/code&gt; 디렉터리를 &lt;code&gt;PATH 환경변수&lt;/code&gt;에 추가&lt;/li&gt;
&lt;li&gt;새로운 cmd.exe를 실행하고 &lt;code&gt;kubectl krew&lt;/code&gt; 명령을 통해 정상 동작여부 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;사용 방법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;플러그인 리스트 다운로드및 검색&lt;pre&gt;&lt;code&gt;$ kubectl krew update   # 최신 플러그인 리스트 다운로드
$ kubectl krew search [plugin name]  # 플러그인 검색, plugin name을 생략하면 가능한 모든 플러그인 리스트 출력&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;플러그인 설치&lt;pre&gt;&lt;code&gt;$ kubectl krew install access-matrix&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;설치된 플러그인 사용&lt;pre&gt;&lt;code&gt;$ kubectl &amp;lt;installed plugin name&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;설치된 플러그인 갱신&lt;pre&gt;&lt;code&gt;$ kubectl krew upgrade&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;설치된 플러그인 삭제&lt;pre&gt;&lt;code&gt;$ kubectl krew uninstall &amp;lt;plugin name&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>krew</category>
      <category>KUBECTL</category>
      <category>Kubernetes</category>
      <category>plugin</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/38</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-KREW-KREW%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C#entry38comment</comments>
      <pubDate>Wed, 16 Dec 2020 13:52:30 +0900</pubDate>
    </item>
    <item>
      <title>[OpenStack] CentOS 8에 OpenStack Client 설치 (PIP)</title>
      <link>https://ccambo.tistory.com/entry/CentOS-8%EC%97%90-OpenStack-Client-%EC%84%A4%EC%B9%98-PIP</link>
      <description>&lt;h1&gt;How to install Open Stack Client on CentOS 8 (PIP Installation)&lt;/h1&gt;
&lt;p&gt;OpenStack Client는 명령줄 도구로 네트워크를 통해 OpenStack 구성 요소, 즉 Compute, Identity, Image, Object Storage 및 Block Storage API를 함께 관리할 수 있다.&lt;/p&gt;
&lt;p&gt;이 문서에서는 CentOS 8 버전에 OpenStack Client를 설치하고 구성하는 방법을 정리한다.&lt;/p&gt;
&lt;h2&gt;환경 검증&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ cat /etc/redhat-release
CentOS Linux release 8.2.2004 (Core) &lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;STEP 0. 환경 갱신&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ sudo yum update
# or
$ sudo dnf update&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;STEP 1. Python2 PIP 설치 (옵션)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ sudo yum -y install python2-pip
# or
$ sudo dnf -y install python2-pip&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;STEP 2. Python3 PIP 설치 (권장)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ sudo yum -y install python3-pip
# or
$ sudo dnf -y install python3-pip&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 2. OpenStack Client 설치&lt;/h2&gt;
&lt;p&gt;아래 명령을 실행해서 OpenStack Client를 설치한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo pip3 install -y python-openstackclient&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;br&gt;&lt;code&gt;sudo&lt;/code&gt;나 &lt;code&gt;root&lt;/code&gt; 사용자로 설치를 하면 권장하지 않는다면 경고메시지가 나온다.&lt;br&gt;이 경고 메시지는 권한이 있는 경로에 설치하지 말고, 현재 사용자로 한정된 구역으로 설치하라는 의미가 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WARNING: Running pip install with root privileges is generally not a good idea. Try&lt;/code&gt;pip3 install --user&lt;code&gt;instead.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;다만 위의 경고대로 설치를 하면 &lt;code&gt;/usr/local/bin&lt;/code&gt;에 설치가 되는 것이 아니라 &lt;code&gt;~/.local/bin&lt;/code&gt;으로 설치가 된다. 따라서 환경 변수의 경로부분에 추가를 해 줘야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Step 3. 확인&lt;/h2&gt;
&lt;p&gt;아래 명령을 실행해서 OpenStack CLI 설치를 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ which openstack
/usr/local/bin/openstack

$ openstack --version
openstack 5.4.0&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;환경 설정하기&lt;/h2&gt;
&lt;p&gt;OpenStack에 대한 세세한 제어를 위해서는 OpenStack CLI를 사용해야 하며, 이 명령을 사용하기 위해 아래와 같이 변수를 정의한 &lt;code&gt;~/keystonrc&lt;/code&gt; 파일을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export OS_PROJECT_DOMAIN_NAME=Default
export OS_USER_DOMAIN_NAME=Default
export OS_PROJECT_NAME=admin
export OS_USERNAME=&amp;lt;user name&amp;gt;
export OS_PASSWORD=&amp;lt;user password&amp;gt;
export OS_AUTH_URL=&amp;lt;openstack url&amp;gt;
export OS_IDENTITY_API_VERSION=3
export OS_IMAGE_API_VERSION=2&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;생성한 파일의 환경 변수로 적용하고 권한을 &lt;code&gt;600&lt;/code&gt;으로 변경한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ source ~/keystonerc

$ chmod 600 ~/keystonerc&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로그인 할 때 적용되도록 &lt;code&gt;~/.profile 또는 셸 프로파일 (.bash_profile, .zsh_profile, ...)&lt;/code&gt; 파일에 아래와 같이 설정을 추가하도록 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
# OpenStack rc file
if [ -f ~/keystonerc ]; then
  source ~/keystonerc
fi
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;정상적인 동작을 확인하기 위해 아래와 같이 명령이 실행되는지를 검증한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ openstack user list&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Commands&lt;/h2&gt;
&lt;p&gt;OpenStack Web Console로 대 부분 처리가 가능하지만 Client로 처리를 해야하는 것들도 존재한다. 따라서 Command관련된 내용은 작업을 진행하면서 수시로 갱신될 수 있다.&lt;/p&gt;
&lt;h3&gt;Network&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Network 정보 확인&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ openstack network list&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Floating IP&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Floating IP 검색&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ openstack floating ip list&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Floating IP 정보&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ openstack floating ip show &amp;lt;floating_ip&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Floating IP 생성&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ openstack floating ip create --floating-ip-address &amp;lt;floating-ip&amp;gt; &amp;lt;network pool&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Floating IP 삭제&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ openstack floating ip delete &amp;lt;floating-ip&amp;gt; [&amp;lt;floating-ip&amp;gt; ...]&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jackerlab.com/pip-command/&quot;&gt;Python pip 사용법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linuxcnf.com/2019/11/how-to-install-open-stack-client-on.html&quot;&gt;How to Install Open Stack Client on CentOS 8 (pip Installation)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://m.blog.naver.com/PostView.nhn?blogId=sunguru&amp;amp;logNo=221322122409&amp;amp;categoryNo=66&amp;amp;proxyReferer=https:%2F%2Fwww.google.com%2F&quot;&gt;OpenStackClient 명령을 위한 변수 설정&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/OpenStack</category>
      <category>CentOS 8</category>
      <category>client</category>
      <category>openstack</category>
      <category>pip</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/37</guid>
      <comments>https://ccambo.tistory.com/entry/CentOS-8%EC%97%90-OpenStack-Client-%EC%84%A4%EC%B9%98-PIP#entry37comment</comments>
      <pubDate>Mon, 14 Dec 2020 16:06:17 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes] kubectl 활용팁</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-kubectl-%ED%99%9C%EC%9A%A9%ED%8C%81</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
  &lt;h1&gt;kubectl 활용 팁&lt;/h1&gt;&lt;p&gt;Kubernetes 관련 정보들을 검색하던 중에 &lt;a href=&quot;https://kubernetes.io/docs/user-guide/kubectl&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;&lt;code&gt;kubectl&lt;/code&gt;&lt;/a&gt; 툴을 좀 더 활용할 수 있는 팁 정보가 있어서 정리해 놓는다.&lt;/p&gt;
  &lt;p&gt;kubectl 은 쿠버네티스를 운영하기 위한 CLI 도구로 상당히 많은 기능들을 제공하기 때문에 기능들을 다 파악하는 것은 어렵지만 강력한 도구로 활용이 가능하다.&lt;/p&gt;&lt;p&gt;기본적인 사용법에 관련된 것은 &lt;a href=&quot;https://kubernetes.io/docs/user-guide/kubectl-cheatsheet/&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;Cheatsheet&lt;/a&gt; 를 참고하면 된다.&lt;/p&gt;
  &lt;h2 id=&quot;kubectl-with-shell-completion&quot;&gt;
    &lt;a class=&quot;header-anchor&quot; href=&quot;#kubectl-with-shell-completion&quot;/&gt;kubectl with Shell Completion&lt;/h2&gt;&lt;p&gt;kubectl 은 bash 및 zsh가 내장된 쉘 완성 기능을 제공하기 때문에 명령, 플래그 및 객체를 네임스페이스 또는 파드 이름과 같이 자동 완성으로 사용하는 것이 훨씬 쉽다.&lt;/p&gt;&lt;p&gt;아래의 그림은 실제 자동완성 기능을 제공하도록 설정한 후의 사용법을 보여주는 것이다.&lt;/p&gt;&lt;p&gt;
    &lt;img src=&quot;https://s3cr3t.net/completion.gif&quot; alt=&quot;shell completion&quot;&gt;
    &lt;/p&gt;&lt;p&gt;
      &lt;a href=&quot;https://kubernetes.io/docs/tasks/tools/install-kubectl/&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;원문&lt;/a&gt; 에는 kubectl 바이너리 설치부터 설명이 되어 있지만 대부분은 kubernetes 설치환경일 것이므로 자동 완성만 처리하면 된다.&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;
        &lt;strong&gt;Notes&lt;/strong&gt;
      &lt;/p&gt;&lt;p&gt;자동 완성 스크립트는 kubectl에 의해서 생성되므로 프로파일에 설정해서 사용하면 된다. 관련된 정보는 &lt;code&gt;kubectl completion -h&lt;/code&gt; 를 확인하면 된다.&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;h2 id=&quot;on-linux-usnig-bash&quot;&gt;
      &lt;a class=&quot;header-anchor&quot; href=&quot;#on-linux-usnig-bash&quot;/&gt;On linux, usnig bash&lt;/h2&gt;&lt;p&gt;리눅스 bash 환경에서 자동완성 스크립트를 Shell 로 로드 처리는 아래의 명령을 사용하면 된다.&lt;/p&gt;
    &lt;pre&gt;
      $ source &amp;lt;(kubectl completion bash)
    &lt;/pre&gt;
    &lt;p&gt;프로파일에 적용하려면 아래의 명령으로 프로파일에 적용해 주면 된다.&lt;/p&gt;
    &lt;pre&gt;
      $ echo &quot;source &amp;lt;(kubectl completion bash)&quot; &amp;gt;&amp;gt; ~/.bashrc
    &lt;/pre&gt;
    &lt;h2 id=&quot;on-macos-using-bash&quot;&gt;
      &lt;a class=&quot;header-anchor&quot; href=&quot;#on-macos-using-bash&quot;/&gt;On MacOS, using bash&lt;/h2&gt;&lt;p&gt;맥에서 bash 자동 완성을 수행하려면 아래의 명령으로 자동완성 지원 기능을 먼저 설치해야 한다.&lt;/p&gt;
    &lt;pre&gt;
      $ brew install bash-completion
    &lt;/pre&gt;&lt;p&gt;Shell에 자동완성 스크립트 로드 처리는 아래의 명령을 사용하면 된다.&lt;/p&gt;

    &lt;pre&gt;
      $ source $(brew --prefix)/etc/bash_completion
      $ source &amp;lt;(source &amp;lt;(kubectl completion bash)
    &lt;/pre&gt;
    &lt;p&gt;프로파일에 적용하려면 아래의 명령으로 프로파일에 적용해 주면 된다.&lt;/p&gt;

    &lt;pre&gt;
      $ echo &quot;source $(brew --prefix)/etc/bash_completion&quot; &amp;gt;&amp;gt; ~/.bash_profile
      $ echo &quot;source &amp;lt;(kubectl completion bash)&quot; &amp;gt;&amp;gt; ~/.bash_profile
    &lt;/pre&gt;

    &lt;h1&gt;Kubernetes 구성 병합&lt;/h1&gt;&lt;p&gt;여러 쿠버네티스 클러스들과 상호 작용을 하는 경우라면 쿠버네티스 설정을 병합하는 것이 일반적인 패턴이며 병하해서 사용할 떄 특정 클러스터를 사용하도록 하는 파라미터들을 서술하기 위해 context 개념을 사용한다.&lt;/p&gt;&lt;p&gt;제대로 운영하기에는 복잡하기 때문에 &lt;a href=&quot;https://kubernetes.io/docs/tasks/access-application-cluster/authenticate-across-clusters-kubeconfig/&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt; &lt;code&gt;KUBECONFIG&lt;/code&gt;      &lt;/a&gt; 환경 변수를 사용해서 구성 파일을 지정하도록 병합할 수 있다.&lt;/p&gt;&lt;p&gt;만일 병합할 2개의 쿠버네티스 클러스터 (cluster1, cluster2) 가 있다면 각 클러스터별로 정보를 가지는 설정 파일이 다음과 같을 것이므로 이를 개별 파일로 구성하도록 한다.&lt;/p&gt;&lt;p&gt;클러스터 #1 설정&lt;/p&gt;

    &lt;pre&gt;
      $ kubectl config view --minify &amp;gt; cluster1-config
      apiVersion: v1
      clusters:
      - cluster:
      certificate-authority: cluster1_ca.crt
      server: https://cluster1
      name: cluster1
      contexts:
      - context:
      cluster: cluster1
      user: cluster1
      name: cluster1
      current-context: cluster1
      kind: Config
      preferences: &amp;#123;&amp;#125;
      users:
      - name: cluster1
      user:
      client-certificate: cluster1_apiserver.crt
      client-key: cluster1_apiserver.key
    &lt;/pre&gt;

    &lt;p&gt;클러스터 #2 설정 (#1 에서와 같이 처리해서 사용할 장소에 복사하면 된다)&lt;/p&gt;

    &lt;pre&gt;
      $ cat cluster2-config
      apiVersion: v1
      clusters:
      - cluster:
      certificate-authority: cluster2_ca.crt
      server: https://cluster2
      name: cluster2
      contexts:
      - context:
      cluster: cluster2
      user: cluster2
      name: cluster2
      current-context: cluster2
      kind: Config
      preferences: &amp;#123;&amp;#125;
      users:
      - name: cluster2
      user:
      client-certificate: cluster2_apiserver.crt
      client-key: cluster2_apiserver.key
    &lt;/pre&gt;

    &lt;p&gt;위의 2개 설정을 &lt;code&gt;KUBECONFIG&lt;/code&gt; 환경 변수를 통해서 접근할 수 있도록 병합하고 context 지정을 통해서 2개의 클러스터를 동적으로 사용할 수 있다. context 는 클러스터를 인증 및 상호작용하도록 구성을 참조할 수 있는 클러스터, 사용자 및 이름을 설명하는 맴 정보라고 생각하면 된다.&lt;/p&gt;&lt;p&gt;이제 &lt;code&gt;--kubeconfig&lt;/code&gt; 플래그를 이용해서 각 파일의 context 정보를 확인할 수 있다.&lt;/p&gt;

    &lt;pre&gt;
      $ kubectl --kubeconfig=cluster1-config config get-contexts
      CURRENT NAME CLUSTER AUTHINFO NAMESPACE
      * cluster1 cluster1 cluster1
      $ kubectl --kubeconfig=cluster2-config config get-contexts
      CURRENT NAME CLUSTER AUTHINFO NAMESPACE
      * cluster2 cluster2 cluster2
    &lt;/pre&gt;
    &lt;p&gt;각각의 설정 파일은 하나의 context를 가지기 때문에 서로 충돌하지 않으며, 두 파일을 실제 병합하면 두 개의 Context 가 모두 표시된다.&lt;/p&gt;&lt;p&gt;병합을 위해서 &lt;code&gt;cluster-merge&lt;/code&gt; 라는 새로운 파일을 만들고 아래와 같이 처리를 하면 된다.&lt;/p&gt;

    &lt;pre&gt;
      $ export KUBECONFIG=cluster-merge:cluster-config:cluster2-config
      $ kubectl config get-contexts
      CURRENT NAME CLUSTER AUTHINFO NAMESPACE
      * cluster1 cluster1 cluster1
      cluster2 cluster2 cluster2
    &lt;/pre&gt;
    &lt;p&gt;병합되는 순서는 지정된 순서대로 처리되고 current-context 는 첫번째 구성 파일에서 지정한 context 가 되기 떄문에 액티브 표시 (&lt;code&gt;*&lt;/code&gt;) 가 cluster1 context 를 가르키고 있는 것을 확인할 수 있다.&lt;/p&gt;&lt;p&gt;context 를 2번쨰로 변경하면 정보를 아래와 같이 보여지게 된다.&lt;/p&gt;

    &lt;pre&gt;
      $ kubectl config get-contexts
      CURRENT NAME CLUSTER AUTHINFO NAMESPACE
      * cluster1 cluster1 cluster1
      cluster2 cluster2 cluster2

      $ kubectl config use-context cluster2
      Switched to context &quot;cluster2&quot;.

      $ kubectl config get-contexts
      CURRENT NAME CLUSTER AUTHINFO NAMESPACE
      cluster1 cluster1 cluster1
      * cluster2 cluster2 cluster2
    &lt;/pre&gt;
    &lt;p&gt;병합에 사용했던 파일을 확인해 보면 아래와 같이 변경된 것을 확인할 수 있다.&lt;/p&gt;

    &lt;pre&gt;
      $ cat cluster-merge
      apiVersion: v1
      clusters: []
      contexts: []
      current-context: cluster2
      kind: Config
      preferences: &amp;#123;&amp;#125;
      users: []
    &lt;/pre&gt;
    &lt;p&gt;실제 사용되는 클러스터는 항상 &lt;code&gt;current-context&lt;/code&gt; 정보로 확인할 수 있다.&lt;/p&gt;&lt;p&gt;Context는 강력하고 다양한 방법으로 활용 및 병합이 가능하다. 예를 들어 모든 kubectl 명령에 적용될 수 있도록 네임 스페이스를 지정하는 Context 를 만들 수도 있다. (사실 매번 네임스페이스 지정하는 것도 귀찮은 일이다)&lt;/p&gt;

    &lt;pre&gt;
      $ kubectl config set-context cluster1_kube-system --cluster=cluster1 --namespace=kube-system --user=cluster1
      Context &quot;cluster1_kube-system&quot; set.

      $ cat cluster-merge
      apiVersion: v1
      clusters: []
      contexts:
      - context:
      cluster: cluster1
      namespace: kube-system
      user: cluster1
      name: cluster1_kube-system
      current-context: cluster2
      kind: Config
      preferences: &amp;#123;&amp;#125;
      users: []
    &lt;/pre&gt;
    &lt;p&gt;위의 예는 cluster1 에 &lt;code&gt;kube-system&lt;/code&gt; 이라는 네임스페이스를 모든 명령에 사용할 수 있도록 context를 구성한 것으로 아래와 같이 context를 지정해서 사용할 수 있다.&lt;/p&gt;

    &lt;pre&gt;
      $ kubectl config use-context cluster_kube-system
      Switched to context &quot;cluster1_kube-system&quot;.

      $ kubectl get pods
      NAME READY STATUS RESTARTS AGE
      default-http-backend-fwx3g 1/1 Running 0 28m
      kube-addon-manager-cluster 1/1 Running 0 28m
      kube-dns-268032401-snq3h 3/3 Running 0 28m
      kubernetes-dashboard-b0thj 1/1 Running 0 28m
      nginx-ingress-controller-b15xz 1/1 Running 0 28m
    &lt;/pre&gt;
    &lt;p&gt;context 에 네임스페이스가 지정되어 있기 때문에 각 명령 단위로 추가로 지정할 필요가 없다.&lt;/p&gt;
    &lt;h2 id=&quot;kubernetes-api-에-대한-정보-확인&quot;&gt;
      &lt;a class=&quot;header-anchor&quot; href=&quot;#kubernetes-api-에-대한-정보-확인&quot;/&gt;Kubernetes API 에 대한 정보 확인&lt;/h2&gt;&lt;p&gt;Kubernetes는 API 정보 제공을 위해서 &lt;code&gt;swagger UI&lt;/code&gt; 가 통합되어 있다. 따라서 아래의 명령으로 json 문서를 얻을 수도 있다.&lt;/p&gt;
    &lt;pre&gt;
      $ kubectl proxy
      $ curl -O 127.0.0.1:8001/swagger.json
    &lt;/pre&gt;
    &lt;p&gt;아니면 직접 &lt;a href=&quot;http://localhost:8001/api/&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;http://localhost:8001/api/&lt;/a&gt; 페이지를 통해 정보를 확인할 수도 있다.&lt;/p&gt;&lt;p&gt;만일 json 문서를 활용한다면 &lt;a href=&quot;https://stedolan.github.io/jq/&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;
        &lt;code&gt;jq&lt;/code&gt;
      &lt;/a&gt; 툴을 이용해서 활용할 수도 있다.&lt;/p&gt;

    &lt;pre&gt;
      $ cat swagger.json | jq '.paths | keys[]'      
        &quot;/api/&quot;
        &quot;/api/v1/&quot;
        &quot;/api/v1/configmaps&quot;
        &quot;/api/v1/endpoints&quot;
        &quot;/api/v1/events&quot;
        &quot;/api/v1/namespaces&quot;
        &quot;/api/v1/nodes&quot;
        &quot;/api/v1/persistentvolumeclaims&quot;
        &quot;/api/v1/persistentvolumes&quot;
        &quot;/api/v1/pods&quot;
        &quot;/api/v1/podtemplates&quot;
        &quot;/api/v1/replicationcontrollers&quot;
        &quot;/api/v1/resourcequotas&quot;
        &quot;/api/v1/secrets&quot;
        &quot;/api/v1/serviceaccounts&quot;
        &quot;/api/v1/services&quot;
        &quot;/apis/&quot;
        &quot;/apis/apps/&quot;
        &quot;/apis/apps/v1beta1/&quot;
        &quot;/apis/apps/v1beta1/statefulsets&quot;
        &quot;/apis/autoscaling/&quot;
        &quot;/apis/batch/&quot;
        &quot;/apis/certificates.k8s.io/&quot;
        &quot;/apis/extensions/&quot;
        &quot;/apis/extensions/v1beta1/&quot;
        &quot;/apis/extensions/v1beta1/daemonsets&quot;
        &quot;/apis/extensions/v1beta1/deployments&quot;
        &quot;/apis/extensions/v1beta1/horizontalpodautoscalers&quot;
        &quot;/apis/extensions/v1beta1/ingresses&quot;
        &quot;/apis/extensions/v1beta1/jobs&quot;
        &quot;/apis/extensions/v1beta1/networkpolicies&quot;
        &quot;/apis/extensions/v1beta1/replicasets&quot;
        &quot;/apis/extensions/v1beta1/thirdpartyresources&quot;
        &quot;/apis/policy/&quot;
        &quot;/apis/policy/v1beta1/poddisruptionbudgets&quot;
        &quot;/apis/rbac.authorization.k8s.io/&quot;
        &quot;/apis/storage.k8s.io/&quot;
        &quot;/logs/&quot;
        &quot;/version/&quot;
      
    &lt;/pre&gt;
    &lt;h2 id=&quot;기타-명령&quot;&gt;
      &lt;a class=&quot;header-anchor&quot; href=&quot;#기타-명령&quot;/&gt;기타 명령&lt;/h2&gt;&lt;p&gt;
      &lt;code&gt;api-versions&lt;/code&gt; 명령은 관리자로 실행되며, RBAC가 활성화된 경우라면 다른 API 세트 정보가 표시될 수 있다.&lt;/p&gt;

    &lt;pre&gt;
      $ kubectl api-versions
      apps/v1beta1
      authentication.k8s.io/v1beta1
      authorization.k8s.io/v1beta1
      autoscaling/v1
      batch/v1
      batch/v2alpha1
      certificates.k8s.io/v1alpha1
      coreos.com/v1
      etcd.coreos.com/v1beta1
      extensions/v1beta1
      oidc.coreos.com/v1
      policy/v1beta1
      rbac.authorization.k8s.io/v1alpha1
      storage.k8s.io/v1beta1
      v1
    &lt;/pre&gt;
    &lt;p&gt;
      &lt;code&gt;explain&lt;/code&gt; 명령은 각 파트에 대한 이해를 위한 기능적인 도움을 제공한다.&lt;/p&gt;

    &lt;pre&gt;
      $ kubectl explain
      You must specify the type of resource to explain. Valid resource types include:
      * all
      * certificatesigningrequests (aka 'csr')
      * clusters (valid only &lt;span class=&quot;keyword&quot;&gt;for federation apiservers)
      * clusterrolebindings
      * clusterroles
      * componentstatuses (aka 'cs')
      * configmaps (aka 'cm')
      * daemonsets (aka 'ds')
      * deployments (aka 'deploy')
      * endpoints (aka 'ep')
      * events (aka 'ev')
      * horizontalpodautoscalers (aka 'hpa')
      * ingresses (aka 'ing')
      * jobs
      * limitranges (aka 'limits')
      * namespaces (aka 'ns')
      * networkpolicies
      * nodes (aka 'no')
      * persistentvolumeclaims (aka 'pvc')
      * persistentvolumes (aka 'pv')
      * pods (aka 'po')
      * poddisruptionbudgets (aka 'pdb')
      * podsecuritypolicies (aka 'psp')
      * podtemplates
      * replicasets (aka 'rs')
      * replicationcontrollers (aka 'rc')
      * resourcequotas (aka 'quota')
      * rolebindings
      * roles
      * secrets
      * serviceaccounts (aka 'sa')
      * services (aka 'svc')
      * statefulsets
      * storageclasses
      * thirdpartyresources
      error: Required resource not specified.
      See 'kubectl explain -h'
        &lt;span class=&quot;keyword&quot;&gt;for
        help and examples.
    &lt;/pre&gt;
    &lt;p&gt;예를 들어 &lt;code&gt;explain deploy&lt;/code&gt; 명령을 실행해 보고, deploy 의 좀더 세분화된 객체를 지정해서 정보를 확인할 수도 있다.&lt;/p&gt;

    &lt;pre&gt;
      $ kubectl explain deploy.spec.template.spec.containers.livenessProbe.exec
      RESOURCE: exec &amp;lt;Object&amp;gt;
      DESCRIPTION:
      One and only one of the following should be specified. Exec specifies the
      action to take.
      ExecAction describes a &quot;run in container&quot; action.
      FIELDS:
        command &amp;lt;[]string&amp;gt;
      Command is the command line to execute inside the container, the working
      directory &lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt; the command is root ('/') &lt;span class=&quot;keyword&quot;&gt;in&lt;/span&gt; the container's filesystem. The
      
      command is simply exec'd, it is not run inside a shell, so traditional shell
      instructions ('|', etc) won't work. To use a shell, you need to explicitly
      
      call out to that shell. Exit status of 0 is treated as live/healthy and
      non-zero is unhealthy.
    &lt;/pre&gt;
    &lt;h2 id=&quot;conclusion&quot;&gt;
      &lt;a class=&quot;header-anchor&quot; href=&quot;#conclusion&quot;/&gt;Conclusion&lt;/h2&gt;&lt;p&gt;다양한 기능들을 제공하기 때문에 모든 것을 알지도 못하고 정리하는 것도 쉽지는 않지만 다양한 자료들을 확인하면서 유용하다고 생각되는 기능들은 비록 발 번역 수준에 오타, 다른 이해가 있더라도 정리를 해 나갈 예정이다.&lt;/p&gt;&lt;p&gt;현재는 프로젝트를 쉬고 있는 중이지만, 향후 실제 기업에서 운영되는 어플리케이션들을 Containerize 처리와 Kubernetes Cluster에 배포 운영하는 프로젝트를 진행할 예정이기 때문에 좀 더 현실적이고 구체적인 사례들을 정리할 수 있을 것 같다.&lt;/p&gt;
    &lt;hr&gt;
      &lt;p&gt;References&lt;/p&gt;
      &lt;ul&gt;
        &lt;li&gt;          &lt;a href=&quot;https://coreos.com/blog/kubectl-tips-and-tricks&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;https://coreos.com/blog/kubectl-tips-and-tricks&lt;/a&gt;
        &lt;/li&gt;
        &lt;li&gt;          &lt;a href=&quot;https://kubernetes.io/docs/tasks/tools/install-kubectl/&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;https://kubernetes.io/docs/tasks/tools/install-kubectl/&lt;/a&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    
     CHEAT SHEET, CONFIGURAITON MERGE, KUBECTL, KUBERNETES, SHELL COMPLETION, 쿠버네티스</description>
      <category>개발/Kubernetes 이해</category>
      <category>cheat sheet</category>
      <category>CONFIGURAITON MERGE</category>
      <category>KUBECTL</category>
      <category>Kubernetes</category>
      <category>SHELL COMPLETION</category>
      <category>쿠버네티스</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/35</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-kubectl-%ED%99%9C%EC%9A%A9%ED%8C%81#entry35comment</comments>
      <pubDate>Wed, 17 May 2017 21:50:52 +0900</pubDate>
    </item>
    <item>
      <title>부업 - 주식으로 용돈벌기 도전</title>
      <link>https://ccambo.tistory.com/entry/%EB%B6%80%EC%97%85-%EC%A3%BC%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%9A%A9%EB%8F%88%EB%B2%8C%EA%B8%B0-%EB%8F%84%EC%A0%84</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
  &lt;h1&gt;부업으로 삼을 수 있을지 주식에 도전한다.&lt;/h1&gt;&lt;p&gt;하고 싶은 것을 다 하면서 살 수 있으면 좋겠지만, 흙수저 출신인 나에게는 꿈과 같은 이야기일 뿐이고, 그렇다고 남달리 능력이 좋은 것도 아니고, 경력관리를 잘해서 주 분야가 있는 것도 아닌 일반 단순 개발자라서 시장에 부름을 받기는 힘든 상황이니 인생 후반기를 대비해야 할 무엇인가를 늦었지만 빨리 찾아야 할 상황이다.&lt;/p&gt;&lt;p&gt;딱히 기술이 있는 것도 아니고, 금손도 아니고, 대인 관계가 그렇게 활발한 것도 아니기 때문에 여러 가지를 고려해 볼 때 주식 투자가 적합해 보인다. 그러나 시드 머니도 없는 상태고, 관련된 지식도 없는 상태라서 아주 조금씩이라도 부업으로 가능성이 있는지를 확인하는 차원에서 용돈이라도 벌어보자는 도전을 해 보려고 한다. 물론 쉽지 않은 도전이지만 최소한 투자한 시간 대비 최저 임금 수준만 유지하면 용돈으로 쓰기에는 만족할 수준일 것 같다. 향후에는 용돈을 넘어 생활비 벌기에 도전할 수 있도록 열심히 해야할 듯 하다.&lt;/p&gt;
  &lt;h2 id=&quot;아무-것도-하지-않으면-아무-일도-일어나지-않는다&quot;&gt;
    &lt;a class=&quot;header-anchor&quot; href=&quot;#아무-것도-하지-않으면-아무-일도-일어나지-않는다&quot;&gt;&lt;/a&gt;
    &lt;code&gt;&amp;quot;아무 것도 하지 않으면, 아무 일도 일어나지 않는다.&amp;quot;&lt;/code&gt;
  &lt;/h2&gt;&lt;p&gt;가능성이 있는 것일지, 성공일지, 실패일지를 미리 고민할 필요는 없다. 돈을 잃는다고 해도 그만큼의 경험을 살 수 있다면 나름대로 성공한 것일거다. 물론 계속된 실패를 반복하지만 않는다면 말이다.&lt;/p&gt;
  &lt;h2 id=&quot;초기-설정&quot;&gt;
    &lt;a class=&quot;header-anchor&quot; href=&quot;#초기-설정&quot;&gt;&lt;/a&gt;초기 설정&lt;/h2&gt;&lt;p&gt;주식에 관해서는 아는 것이 별로 없다. 그냥 상식적인 수준에서 단어만 몇개 알고 있을 뿐이다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;매수/매도&lt;/li&gt;
    &lt;li&gt;챠트&lt;/li&gt;
    &lt;li&gt;유/무상 증자&lt;/li&gt;
    &lt;li&gt;배당&lt;/li&gt;
    &lt;li&gt;시간외 거래&lt;/li&gt;
    &lt;li&gt;상/하한 30%&lt;/li&gt;
    &lt;li&gt;매매 수수료, 거래세&lt;/li&gt;
  &lt;/ul&gt;&lt;p&gt;시작은 작게 그러나 향후의 꿈은 크게 하는 것이 좋을 듯 하다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;투자금액 : 100만원&lt;/li&gt;
    &lt;li&gt;목표비율 : 최저임금 수준으로 일별 +3% 내외&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;20170518-보유-관심종목&quot;&gt;
    &lt;a class=&quot;header-anchor&quot; href=&quot;#20170518-보유-관심종목&quot;&gt;&lt;/a&gt;20170518 보유/관심종목&lt;/h2&gt;
    &lt;p&gt;주식시장에 등록된 주식수가 2,000 종목 내외라고 한다. 망할 회사가 아니라면 주가의 흐름 상으로 언젠가는 복구가 될 수 밖에는 없는 것으로 판단이 되고, 업종 / 섹터 등의 구분이 필요하지만 지금은 아무것도 모르는 상태이 그냥 여러 가지 정보 중에서 가능성이 있다고 판단되는 것을 기준으로 선별해 본다.&lt;/p&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;종목&lt;/th&gt;
        &lt;th&gt;관련 종목&lt;/th&gt;
        &lt;th&gt;정보&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;필룩스&lt;/td&gt;
        &lt;td/&gt;
        &lt;td&gt;장 시작 30분 이내에 전일 거래량의 100%를 넘겼다. 대형 신규사업 발주 예정 소문&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;에스디생명과학&lt;/td&gt;
        &lt;td/&gt;
        &lt;td&gt;공모가 이하 진행 중, 중국 한한령 풀리면 화장품/합작드라마 가능성 있음&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;에이디칩스&lt;/td&gt;
        &lt;td&gt;디에스티로봇, 우리기술,디피시,셀바스AI&lt;/td&gt;
        &lt;td&gt;인공지능, 로봇, 사물인터넷 핵심 초경량 CPU 코어 개발 성공, 5월 중 상용화 가능성, 삼성전자와의 연계&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;한신기계&lt;/td&gt;
        &lt;td/&gt;
        &lt;td&gt;적대적 M&amp;amp;A 가능성&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;&lt;p&gt;아직은 뭐가 좋고 나쁜지에 대한 판단을 제대로 하지는 못하지만, 가능성 있는 종목들을 고르다 보면 좋은 결과가 있을지도 모르겠다.&lt;/p&gt;&lt;p&gt;매일은 아닐지 몰라도 관련된 종목과 정보들을 정리하다 보면 연관성과 시장을 바라보는 경험이 쌓일 것이라고 생각한다.&lt;/p&gt;
  &lt;h2 id=&quot;매매-종목&quot;&gt;
    &lt;a class=&quot;header-anchor&quot; href=&quot;#매매-종목&quot;&gt;&lt;/a&gt;매매 종목&lt;/h2&gt;
  &lt;ul&gt;
    &lt;li&gt;삼부토건 : 입찰 기대감에 대한 끝물이라고 판단해서 익절&lt;/li&gt;
    &lt;li&gt;에이디칩스 : 가능성 때문에 분할로 모아가기 및 읿부 익절&lt;/li&gt;
    &lt;li&gt;한강인터트레이드 : 상한가, 일부 익절&lt;/li&gt;
    &lt;li&gt;캠시스 : 익절&lt;/li&gt;
    &lt;li&gt;에스에프씨 : 일부 익절&lt;/li&gt;
  &lt;/ul&gt;&lt;p&gt;첫날인 오늘은 다행하게도 목표인 +3%에 턱걸이를 했지만 내일은 또 어떨지 벌써 궁금해지고 있다. 내일도 웃을 수 있을까? ^^&lt;/p&gt;
  &lt;blockquote&gt;
    &lt;p&gt;
      &lt;strong&gt;Importants&lt;/strong&gt;
    &lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;매수할 때 목표 수익률과 손절률 정하고 지키기&lt;/li&gt;
      &lt;li&gt;지속 가능성이 적다면 당일로 매도해서 이익 보전 또는 손해 줄이기&lt;/li&gt;
      &lt;li&gt;장기적인 가능성이 있다면 소량씩 분할로 모아가기&lt;/li&gt;
      &lt;li&gt;10원을 벌어도 잃지 않으면 손해가 아니다&lt;/li&gt;
      &lt;li&gt;절대 욕심내지 말고 이익 또는 본절 지키기&lt;/li&gt;
      &lt;li&gt;손절은 생명을 구하는 것과 같다&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/blockquote&gt;&lt;p&gt;느리고 답답하더라도 하나씩 자신만의 규칙을 정해가면 좋은 결과가 있을 듯 하다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>주식</category>
      <category>관심종목</category>
      <category>에스디생명과학</category>
      <category>에이디칩스</category>
      <category>주식</category>
      <category>필룩스</category>
      <category>한신기계</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/34</guid>
      <comments>https://ccambo.tistory.com/entry/%EB%B6%80%EC%97%85-%EC%A3%BC%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%9A%A9%EB%8F%88%EB%B2%8C%EA%B8%B0-%EB%8F%84%EC%A0%84#entry34comment</comments>
      <pubDate>Wed, 17 May 2017 21:26:14 +0900</pubDate>
    </item>
    <item>
      <title>[취미] 20170511 - 하이런 6점 기록</title>
      <link>https://ccambo.tistory.com/entry/%EC%B7%A8%EB%AF%B8-20170511-%ED%95%98%EC%9D%B4%EB%9F%B0-6%EC%A0%90-%EA%B8%B0%EB%A1%9D</link>
      <description>&lt;div class=&quot;article-entry&quot; itemprop=&quot;articleBody&quot;&gt;
  &lt;p&gt;취미로 활동하던 당구 동호회 (세븐당구동호회)에서 나름대로 열심히 연습을 하면서 몇 가지 궁금한 것이 있었다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;정말 올바른 자세를 취하고 있는 것인가?&lt;/li&gt;
    &lt;li&gt;내 플레이를 제 3자의 입장에 보면 어떻게 보일까?&lt;/li&gt;
    &lt;li&gt;샷이 정상적으로 처리되고 있는 것일까?&lt;/li&gt;
    &lt;li&gt;항상 만나는 사람들이 아닌 전혀 다른 사람들과의 실력 차이는 얼마나 될까?&lt;/li&gt;
    &lt;li&gt;정말 20점 수지에 맞는 실력을 가지고 있는 것일까?&lt;/li&gt;
  &lt;/ul&gt;&lt;p&gt;취미지만 이왕이면 제대로 확인을 해 보고 싶어서 여러 가지를 검색해 보던 중에 큐스코 시스템을 사용하는 구장들이 있다는 것을 알았다.&lt;/p&gt;
&lt;p&gt;집에서 가장 가까운 곳을 검색해 보니 신대방 SBS 당구장이 나왔다. 유튜브에서 하이런등의 경기 영상을 많이 보던 곳인데 의외로 가까운 곳에 위치해 있어 용기(?) 내서 다녀왔다.&lt;/p&gt;
&lt;p&gt;정액제 동호회가 아니기 때문에 전투적으로 임해야 하는 살벌한 구장이다. 대대 (10분 2,000원) 경기도 많이 해보지 않아서 많이 힘들었지만 중대에서 치는 것과는 다른 기분이다. 일단 무지하게 넓고 힘을 많이 써야할 듯 하다.&lt;/p&gt;
&lt;p&gt;2일 동안 4게임을 진행하면서 하이런 4점과 6점을 기록했고, 20, 21, 22 점과의 대결에서 모두 승리했다. 물론 재수(뽀록)가 많이 도움이 되었지만 그래도 상대방과 비교해서 터무니없이 못치는 수준은 아니라는 것을 확인했다. 당연히 기본적인 실력은 내가 모자라다는 것을 느끼지만…&lt;/p&gt;
&lt;p&gt;역시나 동영상으로 촬영된 내 플레이 영상을 보니 상당히 많은 문제점들이 존재했다. ㅠㅠ&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;라스트 샷 순간에 너무 빠르고 힘차게(?) 큐를 내미는 문제&lt;/li&gt;
    &lt;li&gt;큐선과 시선이 자주 불일치해서 두께를 제대로 유지하지 못하는 문제&lt;/li&gt;
    &lt;li&gt;어깨 부상 때문에 업샷이 자주 나오는 문제&lt;/li&gt;
    &lt;li&gt;회전을 많이 줄 때 샷 트릭이 무의식적으로 많이 들어가는 문제 (삑사리 과다)&lt;/li&gt;
  &lt;/ul&gt;&lt;p&gt;기타 등등의 문제가 너무나 많다는 사실과 이런데 어떻게 이겼지?? 라는 의문이 든다. 쩝~&lt;/p&gt;
&lt;p&gt;큐스코 구장을 사용했을 때의 좋은 점은 여러 가지가 있다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;제 3자의 시각에서 볼 수 있는 동영상 촬영이 기본으로 된다.&lt;/li&gt;
    &lt;li&gt;앱을 이용해서 언제든지 리플레이와 다운로드가 가능하다.&lt;/li&gt;
    &lt;li&gt;전적 기록과 엡버리지 등의 관리가 가능하다.&lt;/li&gt;
    &lt;li&gt;다양한 고수들과 대결을 할 수 있으며, 촬영된 영상을 통해서 공 선택과 자세, 샷 등의 정보를 얻을 수 있다.&lt;/li&gt;
    &lt;li&gt;엉망인 내 문제들을 확인할 수 있다.&lt;/li&gt;
  &lt;/ul&gt;&lt;p&gt;
    &lt;em&gt;2017년 5월 11일에 기록한 하이런 6점 영상&lt;/em&gt;
  &lt;/p&gt;
  &lt;div class=&quot;video-container&quot;&gt;
    &lt;iframe src=&quot;//www.youtube.com/embed/CpAw1b29SfQ&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot; style=&quot;width:100%;height:280px;&quot;&gt;
&lt;/iframe&gt;
  &lt;/div&gt;

&lt;p&gt;음, 영상을 보면서 또 하나 느낀 것은 관리되지 않은 얼굴과 몸매가 그대로 드러나는 단점이다. -_- 취미로 운동(헬쓰와 스쿼시)을 다시 시작해야할 것 같다는 불안감이다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>잡다한 것들</category>
      <category>billiard</category>
      <category>HIGHRUN</category>
      <category>당구</category>
      <category>세븐당구동호회</category>
      <category>신대방SBS</category>
      <category>큐스코</category>
      <category>하이런</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/33</guid>
      <comments>https://ccambo.tistory.com/entry/%EC%B7%A8%EB%AF%B8-20170511-%ED%95%98%EC%9D%B4%EB%9F%B0-6%EC%A0%90-%EA%B8%B0%EB%A1%9D#entry33comment</comments>
      <pubDate>Fri, 12 May 2017 17:14:53 +0900</pubDate>
    </item>
    <item>
      <title>[OS] Windows에서 MSVCR90.dll 오류 대처하기</title>
      <link>https://ccambo.tistory.com/entry/OS-Windows%EC%97%90%EC%84%9C-MSVCR90dll-%EC%98%A4%EB%A5%98-%EB%8C%80%EC%B2%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
  &lt;h1&gt;MSVCR 관련 오류&lt;/h1&gt;&lt;p&gt;“MSVCR (Microsoft Visual C++ Runtime)” 관련 오류는 어플리케이션을 직접 개발해서 배포하거나 아니면 다른 어플리케이션을 설치해서 사용할 때 흔히 발생하는 오류 중에 하나기 때문에 큰 문제는 없다. 그러나 조치를 취했음에도 불구하고 동일한 오류가 계속 반복되는 경우라면 상황을 잘 파악해 봐야 한다.&lt;/p&gt;
  &lt;h2 id=&quot;일반적인-발생-원인&quot;&gt;
    &lt;a class=&quot;header-anchor&quot; href=&quot;#일반적인-발생-원인&quot;&gt;&lt;/a&gt;일반적인 발생 원인&lt;/h2&gt;&lt;p&gt;거의 대부분은 MSVCR 라이브러리가 설치되지 않았기 떄문에 발생한다. 주로 볼 수 있는 오류 상황은 다음과 같다.&lt;/p&gt;
  &lt;img src=&quot;http://ccambo.gitlab.io/2017/05/11/OS-윈도우에서-MSVCR90-dll-오류/msvcr90-notfound.png&quot; alt=&quot;라이브러리를 찾지 못하는 경우&quot; title=&quot;라이브러리를 찾지 못하는 경우&quot;&gt;
    &lt;p&gt;설치가 되지 않은 상황이거나 아니면 버전 또는 아키텍처와 관련되어 라이브러리를 로드할 수 없는 경우도 존재한다.&lt;/p&gt;
    &lt;img src=&quot;http://ccambo.gitlab.io/2017/05/11/OS-윈도우에서-MSVCR90-dll-오류/msvcr90-load-fail.png&quot; alt=&quot;라이브러리를 로드하지 못하는 경우&quot; title=&quot;라이브러리를 로드하지 못하는 경우&quot;&gt;
      &lt;p&gt;오늘 이 게시글을 작성하는 이유는 설치도 로드도 모두 잘 되었지만 사용 중에 오류가 발생하면서 어플리케이션이 종료가 되는 경우도 있기 때문이다. 이 경우는 아마도 C++ 코드 작성에서 메모리 처리를 하면서 OS 특성을 타는 부분일 것으로 예상된다.&lt;/p&gt;&lt;p&gt;발생한 오류는 예외 코드 &lt;code&gt;0xc0000417&lt;/code&gt; 이며 발생한 모듈은 &lt;code&gt;MSVCR90.DLL&lt;/code&gt; 이다. 좀 더 구체적인 상항은 덤프된 정보를 분석해 봐야하지만 일반적인 해결 방법을 적용해 보도록 한다.&lt;/p&gt;
      &lt;h2 id=&quot;해결-방법&quot;&gt;
        &lt;a class=&quot;header-anchor&quot; href=&quot;#해결-방법&quot;&gt;&lt;/a&gt;해결 방법&lt;/h2&gt;&lt;p&gt;해결 방법은 구체적인 오류 발생 정보가 없는 경우라면 아래와 같이 단순하게 적용해 볼 수 있다.&lt;/p&gt;
      &lt;h3 id=&quot;설치가-안되었거나-로드할-수-없는-경우&quot;&gt;
        &lt;a class=&quot;header-anchor&quot; href=&quot;#설치가-안되었거나-로드할-수-없는-경우&quot;&gt;&lt;/a&gt;설치가 안되었거나 로드할 수 없는 경우&lt;/h3&gt;&lt;p&gt;설치가 안된 상태이거나 설치에 문제가 있거나 아니면 다른 버전과의 문제가 있다면 &lt;code&gt;프로그램 및 기능&lt;/code&gt; 을 통해서 기존에 설치되어 있는 것을 제거한 후에 재 부팅해서 다시 설치하는 방식을 사용하면 된다.&lt;/p&gt;
      &lt;img src=&quot;http://ccambo.gitlab.io/2017/05/11/OS-윈도우에서-MSVCR90-dll-오류/program-installed-lists.png&quot; alt=&quot;프로그램 설치 리스트&quot; title=&quot;프로그램 설치 리스트&quot;&gt;
        &lt;p&gt;설치를 해서 필요로 하는 버전과 아키텍처가 맞는지를 확인해 봐야 한다. 필요하다면 다른 버전의 배포판도 검토해야 한다.&lt;/p&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;https://www.microsoft.com/ko-KR/download/details.aspx?id=5582&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;Microsft Visual C++ 2008 SP1 재배포 가능 패키지 - 32bit (x86)&lt;/a&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://www.microsoft.com/ko-KR/download/details.aspx?id=2092&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;Microsft Visual C++ 2008 SP1 재배포 가능 패키지 - 64bit (x64)&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
        &lt;h3 id=&quot;설치-및-로드-후-문제가-있는-경우&quot;&gt;
          &lt;a class=&quot;header-anchor&quot; href=&quot;#설치-및-로드-후-문제가-있는-경우&quot;&gt;&lt;/a&gt;설치 및 로드 후 문제가 있는 경우&lt;/h3&gt;&lt;p&gt;설치도 모두 되어 있고, 정상적으로 어플리케이션이 실행도 되지만 특정한 상황에서 오류로 종료되는 경우라면 작성한 개발 코드가 OS 아키텍처나 특성 상의 문제가 내포하고 있는 경우가 대 부분이다.&lt;/p&gt;&lt;p&gt;따라서 이런 경우는 어플리케이션에서 문제를 해결한 새 버전을 배포하지 않는 한은 어플리케이션 호환성을 설정해 주고 실행해서 문제가 없는 상황으로 어플리케이션을 실행시키는 방법을 사용해야 한다.&lt;/p&gt;&lt;p&gt;문제가 되는 어플리케이션의 &amp;quot;속성 창&amp;quot;을 열고 &amp;quot;호환성 탭&amp;quot;을 선택한다.&lt;/p&gt;
        &lt;img src=&quot;http://ccambo.gitlab.io/2017/05/11/OS-윈도우에서-MSVCR90-dll-오류/application-comportable-test.png&quot; alt=&quot;어플리케이션 호환성 설정&quot; title=&quot;어플리케이션 호환성 설정&quot;&gt;
          &lt;p&gt;그림에서 빨간 박스로 처리한 “호환성 문제 해결사 실행” 을 누르면 &amp;quot;호환성 마법사&amp;quot;가 동작을 하는 것이고, 익히 겪어본 상황(?) 이라면 파란 박스로 처리한 부분을 직접 설정해서 사용하면 된다.&lt;/p&gt;&lt;p&gt;호환성 문제 해결사는 관련된 문제 요소를 검증해서 자체적인 규칙으로 호환성을 설정한 후에 선택할 수 있도록 제공한다.&lt;/p&gt;
          &lt;img src=&quot;http://ccambo.gitlab.io/2017/05/11/OS-윈도우에서-MSVCR90-dll-오류/application-compatable-recommand.png&quot; alt=&quot;호환성 권장 설정&quot; title=&quot;호환성 권장 설정&quot;&gt;
            &lt;p&gt;권장 설정을 선택하면 해당 프로그램을 테스트 할 수 있도록 제공하므로 반드시 “프로그램 테스트…” 를 눌러서 호환성 환경에서 어플리케이션의 문제가 발생하지 않는지를 확인해야 한다.&lt;/p&gt;
            &lt;img src=&quot;http://ccambo.gitlab.io/2017/05/11/OS-윈도우에서-MSVCR90-dll-오류/application-compatable-test-save.png&quot; alt=&quot;호환성 테스트 실행 및 설정 반영&quot; title=&quot;호환성 테스트 실행 및 설정 반영&quot;&gt;
              &lt;p&gt;위의 호환성 테스트 실행에 문제가 없는 것을 확인하면 &lt;code&gt;다음&lt;/code&gt; 버튼을 눌러서 호환성 설정을 어플리케이션에 반영해야 한다. 반영된 설정으로 다음 실행부터 계속 적용이 된다.&lt;/p&gt;
              &lt;blockquote&gt;
                &lt;p&gt;
                  &lt;strong&gt;Importants&lt;/strong&gt;
                &lt;/p&gt;&lt;p&gt;설정이 저장되어 잘 실행된다고 해도 &lt;code&gt;Windows Update&lt;/code&gt; 등과 같은 처리가 진행되면서 기존의 호환성 설정 정보가 적용되지 않는 경우도 심심치 않게 발생하므로 문제가 발생할 경우는 다시 호환성 설정을 처리해 줘야 한다.&lt;/p&gt;
              &lt;/blockquote&gt;
              &lt;h2 id=&quot;결론&quot;&gt;
                &lt;a class=&quot;header-anchor&quot; href=&quot;#결론&quot;&gt;&lt;/a&gt;결론&lt;/h2&gt;&lt;p&gt;개발자들이 흔히 간과하는 것이 개발 환경과 실행 환경이 판이하게 다를 수 있다는 점이다. 개발 환경에서 모든 설정을 해 놓고 어플리케이션이 잘 돌아간다고 하는 것은 전혀 다른 사용자의 환경을 고려하지 않았다는 반증이다. 따라서 개발이 완료된 상태라면 블라인드 테스트와 배포 테스트를 통해서 어플리케이션의 실행에 필요한 라이브러리나 설정이 빠진 것이 없는지를 모두 검토해 봐야 한다.&lt;/p&gt;
              &lt;blockquote&gt;
                &lt;p&gt;
                  &lt;strong&gt;Notice&lt;/strong&gt;
                &lt;/p&gt;&lt;p&gt;간혹 msvcr90.dll 파일을 직접 복사해야할 경우가 생길 수 있는데 다음의 경로를 검토해 보면 된다.&lt;/p&gt;
                &lt;ul&gt;
                  &lt;li&gt;32 bit인 경우 : C:\Windows\System32&lt;/li&gt;
                  &lt;li&gt;64 bit인 경우 : C:\Windows\SysWOW64&lt;/li&gt;
                &lt;/ul&gt;
              &lt;/blockquote&gt;
              &lt;hr&gt;
                &lt;p&gt;References&lt;/p&gt;
                &lt;ul&gt;
                  &lt;li&gt;&lt;a href=&quot;https://answers.microsoft.com/ko-kr/windows/forum/windows_7-windows_programs/msvcr90dll-%EC%98%A4%EB%A5%98%EB%AC%B8%EC%A0%9C/938f0439-a667-40df-b246-7319bc6e58bc&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;https://answers.microsoft.com/ko-kr/windows/forum/windows_7-windows_programs/msvcr90dll-오류문제/938f0439-a667-40df-b246-7319bc6e58bc&lt;/a&gt;
                  &lt;/li&gt;
                  &lt;li&gt;&lt;a href=&quot;http://rgy0409.tistory.com/1561&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;http://rgy0409.tistory.com/1561&lt;/a&gt;
                  &lt;/li&gt;
                &lt;/ul&gt;
              &lt;/div&gt;</description>
      <category>개발/오류처리</category>
      <category>0xc0000417</category>
      <category>msvcr90.dll</category>
      <category>windows</category>
      <category>오류</category>
      <category>호환성테스트</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/32</guid>
      <comments>https://ccambo.tistory.com/entry/OS-Windows%EC%97%90%EC%84%9C-MSVCR90dll-%EC%98%A4%EB%A5%98-%EB%8C%80%EC%B2%98%ED%95%98%EA%B8%B0#entry32comment</comments>
      <pubDate>Fri, 12 May 2017 12:11:21 +0900</pubDate>
    </item>
    <item>
      <title>[취미] 새로운(?) 당구 시스템?</title>
      <link>https://ccambo.tistory.com/entry/%EC%B7%A8%EB%AF%B8-%EC%83%88%EB%A1%9C%EC%9A%B4-%EB%8B%B9%EA%B5%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
  &lt;p&gt;몇 가지 안되는 취미생활 중에 요즘 빠져있는 것이 당구다. 물론 영화 보는 것도 좋아하지만, 너무 활동량이 없어서 운동 겸해서 동호회 당구 (정액제)를 하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;작년 1월부터 다시 시작을 했고, 3구 기준으로 이제 20점이다. 물론 아직은 제대로 원리를 이해하지는 못하는 수준이지만 개발도 맨땅에 헤딩하면서 배웠듯이 당구도 지금 열심히 헤딩 중이다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;이런 저런 이론적인 것을 동호회나 카페, 유튜브 등에서 읽어보고 휴일에 당구장에서 살 듯이 연습을 해 보지만 역시나 이론과 실제는 너무나 차이가 많다는 것을 체감할 뿐이다. ㅠㅠ&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;예전에는 거의 4구 경기만 했었기 때문에 오늘 소개하는 것도 역시 4구에 대한 이론 (주로 세리)을 개인적으로 정립하신 정필규님의 자료다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;div class=&quot;tt-youtube-plugin&quot; style=&quot;text-align: center&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/utIeJYqMnNs?rel=0&quot; width=&quot;820&quot; height=&quot;615&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;겨냥점이라는 것은 어려운 것이 아니다. 수구로 제 1 목적구를 맞춘 후에 제 2 목적구를 맞추기 위해서 어떤 곳을 향해서 쳐야 하는지를 정리한 것이라고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;여기서 당구의 이론과 각종 시스템을 논의하는 것도 아니고, 내가 좋아하는 취미고, 4구 이론을 3구에도 적용할 수 있을 것 같아서 소개하는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;아직은 형편없는 수준이지만 몇 가지만 기억하면서 연습하면 될 듯 하다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;타격, 힘, 과다한 회전 등은 공의 성질을 변화시킨다.&lt;/li&gt;
    &lt;li&gt;최대한 공의 자연스러운 방향과 힘을 유지할 수 있도록 해야 한다.&lt;/li&gt;
    &lt;li&gt;과다한 스트록은 오히려 독이 된다. 상황에 맞는 스트록을 적용해야 한다.&lt;/li&gt;
    &lt;li&gt;자신의 회전량을 알아야 한다.&lt;/li&gt;
    &lt;li&gt;기준이 되는 공의 흐름 또는 라인을 정하고 이에 맞춰서 스트록, 회전 등에 따른 변화를 몸에 익혀야 한다.&lt;/li&gt;
  &lt;/ul&gt;&lt;p&gt;당구라는 것이 워낙 다양한 변수들이 조합되는 것이라서, 딱히 정답이라는 것은 없을 수 밖에 없다. 단지 고점자들이 다양한 경험을 기준으로 대략적인 방법을 만들어 놓은 것 (시스템)을 제시하는 것일 뿐이다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;몇 번 시스템을 검토해 보고 따라해 봤지만, 나는 역시 감으로 하는 당구 (무식한 감이 아니라 어느 정도 확률이 높은 나만의 라인을 찾아가고 있다)가 맞는 듯 해서 지금도 열심히 감을 키우고 있다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;목표는 2년 안에 30점을 만드는 것이다. 듣기로는 40점 이상부터 프로 선수 수준이라고 하고 대 부분 1년에 1점씩 그것도 많이 연습해서 올릴 수 있다고 하니 너무 높은 목표인 듯 하지만 그래야 열심히 할 듯 하다. 물론 항상 좌절을 느끼겠지만…&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;혹시 저와 같이 당구가 취미이신 분은 댓글 달아 주세요. 지역/시간 맞으면 같이 즐겨요~ ^^&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;지역 : 대방 (휴일) / 보라매 (휴일) / 구로디지털 (평일)&lt;/li&gt;
  &lt;/ul&gt;
  &lt;hr&gt;
    &lt;p&gt;References&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;
        &lt;a href=&quot;https://www.youtube.com/channel/UCz64nBJbmuGajVHNmlG3DyA&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;https://www.youtube.com/channel/UCz64nBJbmuGajVHNmlG3DyA&lt;/a&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;a href=&quot;http://cafe.naver.com/8440566&quot; target=&quot;_blank&quot; rel=&quot;external&quot;&gt;http://cafe.naver.com/8440566&lt;/a&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;</description>
      <category>잡다한 것들</category>
      <category>3구</category>
      <category>4구</category>
      <category>겨냥점시스템</category>
      <category>당구</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/31</guid>
      <comments>https://ccambo.tistory.com/entry/%EC%B7%A8%EB%AF%B8-%EC%83%88%EB%A1%9C%EC%9A%B4-%EB%8B%B9%EA%B5%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C#entry31comment</comments>
      <pubDate>Mon, 8 May 2017 18:08:25 +0900</pubDate>
    </item>
    <item>
      <title>Hexo Plugin Series - hexo-related-popular-posts 설정하기</title>
      <link>https://ccambo.tistory.com/entry/Hexo-Plugin-Series-hexorelatedpopularposts-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
  &lt;h1 id=&quot;hexo-plugin-series-hexo-related-popular-posts&quot;&gt;Hexo Plugin Series - hexo-related-popular-posts&lt;/h1&gt;
  &lt;p&gt;이 시리즈는 &lt;code&gt;Hexo&lt;/code&gt; 블로그를 &lt;code&gt;Gitlab&lt;/code&gt; 페이지로 운영하면서 다양한 플러그인들을 테스트해 보고, 나름대로 설정하는 방법을 정리하는 것이다.&lt;/p&gt;
  &lt;h2 id=&quot;overview&quot;&gt;Overview&lt;/h2&gt;
  &lt;p&gt;태그를 기반으로 관련 포스트들에 대한 링크 목록을 만들고, &lt;code&gt;Google Analytics&lt;/code&gt; 의 페이지 뷰를 기준으로 인기 있는 (많은 페이지 뷰를 기록하는) 포스트들을 정렬된 링크 목록을 만드는
    기능을 제공하는 플러그인이다.&lt;/p&gt;
  &lt;p&gt;또한 포스트의 내용에 따라서 관련된 포스트들에 대한 링크 목록을 생성할 수 있고, 다양한 Styles, Thumbnails 등을 지원하고, 다양한 부분에 사용자 정의가 가능하며, 성능과 관련해서 캐싱 기능도 포함하고
    있다.
  &lt;/p&gt;
  &lt;blockquote&gt;
    &lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;
    &lt;p&gt;이 글에서는 _config.yml 파일에 설정을 다양하게 지정한다. 따라서 설정을 추가/변경하는 경우는 기존에 실행 중이던 Hexo Server를 재 실행해 줘야 하며 다음과 같이 처리하는 것을 권장한다.&lt;/p&gt;
    &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh [서버 재 실행 명령] hljs axapta&quot;&gt; hexo clean            &lt;span class=&quot;hljs-preprocessor&quot;&gt;# 기존 캐시 및 Static content 삭제 (db.json)&lt;/span&gt;
 hexo g                &lt;span class=&quot;hljs-preprocessor&quot;&gt;# 신규 Static content 생성 (/public)&lt;/span&gt;
 hexo &lt;span class=&quot;hljs-keyword&quot;&gt;server&lt;/span&gt; --draft   &lt;span class=&quot;hljs-preprocessor&quot;&gt;# Draft 버전도 동일하게 서비스&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;/blockquote&gt;
  &lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;
  &lt;p&gt;이 플러그인은 다음과 같은 기능을 제공한다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;연관된 포스트에 대한 링크 리스트 생성 (태그 연관성과 내용 연관성)&lt;/li&gt;
    &lt;li&gt;인기 있는 포스트에 대한 링크 리스트 생성 (페이지 뷰 수 기준 정렬)&lt;/li&gt;
    &lt;li&gt;포스트에 대한 방문자 수 (페이지 뷰) 표시&lt;/li&gt;
  &lt;/ul&gt;
  &lt;blockquote&gt;
    &lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;이 플러그인의 인기있는 게시글 및 페이지 뷰 처리는 &lt;code&gt;Google Analytics&lt;/code&gt; 연계를 통해서 처리되며, 설정이 쉽지않고(?) 관련된 구글 계정이 구성되어 있어야 하므로 아래의 설정 부분을 검토하고 이 플러그인을 사용할지 여부를 결정하는 것이 좋다.&lt;/p&gt;
  &lt;/blockquote&gt;
  &lt;h2 id=&quot;settings&quot;&gt;Settings&lt;/h2&gt;

  &lt;p&gt;설치는 아래의 명령을 통해서 Hexo 블러그에 추가하면 된다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs lasso&quot;&gt;$ npm install hexo&lt;span class=&quot;hljs-attribute&quot;&gt;-related&lt;/span&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;-popular&lt;/span&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;-posts&lt;/span&gt; &lt;span class=&quot;hljs-subst&quot;&gt;--&lt;/span&gt;save&lt;/code&gt;&lt;/pre&gt;
  &lt;h3 id=&quot;기본-적용&quot;&gt;기본 적용&lt;/h3&gt;
  &lt;p&gt;기본적인 적용 방법은 블로그에서 사용하는 테마 폴더에서 &lt;code&gt;article.ejs&lt;/code&gt; 파일에 아래와 같은 내용을 추가하면 된다. &lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-ejs hljs mel&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-variable&quot;&gt;%-&lt;/span&gt; popular_posts () &lt;span class=&quot;hljs-variable&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;이제 Hexo 를 재 처리해서 결과를 확인해 보면 된다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs ruby&quot;&gt;&lt;span class=&quot;hljs-variable&quot;&gt;$ &lt;/span&gt;hexo clean
&lt;span class=&quot;hljs-variable&quot;&gt;$ &lt;/span&gt;hexo server --draft&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;위의 처리는 해당 포스트 뷰에 태그 기준으로 연관된 다른 포스트에 대한 링크를 보여주는 것으로 실행되면 아래와 같이 생성된 결과를 볼 수 있다.&lt;/p&gt;
  
  &lt;p&gt;태그 연관 목록 생성 샘플&lt;/p&gt;
  &lt;img src=&quot;http://ccambo.gitlab.io/2017/04/19/Plugins-hexo-related-popular-posts-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/related-posts-sample.png&quot;&gt;
  
  &lt;p&gt;참고로 샘플에서는 &lt;code&gt;article-footer&lt;/code&gt; 바로 위 부분에 삽입한 것이다.&lt;/p&gt;
  &lt;p&gt;여기까지는 외부의 정보 및 기타 설정 (_config.yml)이 없이 플러그인의 기본 기능을 사용하는 것이다. 따라서 어느 위치에 연관 목록을 만들지만 고민해서 적용하면 된다.&lt;/p&gt;
  &lt;h3 id=&quot;확장-적용&quot;&gt;확장 적용&lt;/h3&gt;
  &lt;p&gt;확장 기능은 다음과 같다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;a href=&quot;#options-of-helper&quot;&gt;Options of helper&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#popular-posts&quot;&gt;Popular posts&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#avanced-related-posts-morphological-analysis&quot;&gt;Avanced Related posts (Morphological Analysis)&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#cache-improve-generation-speed&quot;&gt;Cache (Improve generation speed)&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#log&quot;&gt;Log&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#customize-html&quot;&gt;Customize HTML&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#visitor-counter&quot;&gt;Visitor Counter&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#ranking-sheet&quot;&gt;Ranking Sheet&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p&gt;확장 적용을 위해서는 아래와 같이 블로그 설정 파일 (블로그 루트에 있는 _config.yml) 에 설정을 추가해 줘야 한다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-yaml hljs avrasm&quot;&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;# More detailed settings&lt;/span&gt;
&lt;span class=&quot;hljs-label&quot;&gt;popularPosts:&lt;/span&gt;
  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) Popular posts options&lt;/span&gt;
  googleAnalyticsAPI:
    clientId: ******&lt;span class=&quot;hljs-preprocessor&quot;&gt;.apps&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.googleusercontent&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;
    serviceEmail: *****@developer&lt;span class=&quot;hljs-preprocessor&quot;&gt;.gserviceaccount&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;
    key: /hexo-project-root/path/to/google-services&lt;span class=&quot;hljs-preprocessor&quot;&gt;.pem&lt;/span&gt;
    viewId: &lt;span class=&quot;hljs-number&quot;&gt;12345678&lt;/span&gt;
    dateRange: &lt;span class=&quot;hljs-number&quot;&gt;30&lt;/span&gt;
    expiresDate: &lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;
    pvMeasurementsStartDate: &lt;span class=&quot;hljs-number&quot;&gt;2015&lt;/span&gt;/&lt;span class=&quot;hljs-number&quot;&gt;11&lt;/span&gt;/&lt;span class=&quot;hljs-number&quot;&gt;01&lt;/span&gt;
    rankingSheet: rankingSheet&lt;span class=&quot;hljs-preprocessor&quot;&gt;.txt&lt;/span&gt;
    &lt;span class=&quot;hljs-preprocessor&quot;&gt;# cache:            # (Deprecated) This options is Deprecated &amp;gt; v0.1.3&lt;/span&gt;
    &lt;span class=&quot;hljs-preprocessor&quot;&gt;#  path: hexo-related-popular-posts-ga-cached.json  # (Deprecated) This options is Deprecated &amp;gt; v0.1.3&lt;/span&gt;
    &lt;span class=&quot;hljs-preprocessor&quot;&gt;#  expiresDate: 10  # (Deprecated) This options is Deprecated &amp;gt; v0.1.3&lt;/span&gt;
  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) Advanced Related posts options&lt;/span&gt;
  morphologicalAnalysis: 
    negativeKeywordsList: pluginSettings/hexo-rpp-negativewords&lt;span class=&quot;hljs-preprocessor&quot;&gt;.txt&lt;/span&gt;
    limit: &lt;span class=&quot;hljs-number&quot;&gt;300&lt;/span&gt;
  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) Related post's weight options&lt;/span&gt;
  weight:
    tagRelevancy: &lt;span class=&quot;hljs-number&quot;&gt;1.0&lt;/span&gt;
    contentsRelevancy: &lt;span class=&quot;hljs-number&quot;&gt;1.0&lt;/span&gt;
  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) Cache options (Improve generation speed.)&lt;/span&gt;
  cache:
    path: cache/hexo-popular-related-posts-ga-cached&lt;span class=&quot;hljs-preprocessor&quot;&gt;.json&lt;/span&gt;
  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) Log options&lt;/span&gt;
  log: true&lt;/code&gt;&lt;/pre&gt;
  &lt;blockquote&gt;
    &lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;&lt;/p&gt;
    &lt;p&gt;설정 항목이 모두 &lt;strong&gt;(optional)&lt;/strong&gt; 처리가 되어 있다. 그러나 &lt;strong&gt;googleAnalyticsAPI&lt;/strong&gt; 설정은 설정할꺼면 전부 올바른 값을 설정해야 한다. 그렇지 않으면 오류가 발생한다.&lt;/p&gt;
  &lt;/blockquote&gt;
  &lt;p&gt;우선 각 기능에 대해서 확인을 먼저하고 나머지 설정에 대한 부분을 확인해 보도록 한다.&lt;/p&gt;
  &lt;h4 id=&quot;options-of-helper&quot;&gt;Options of helper&lt;/h4&gt;
  &lt;p&gt;위에서 사용한 &lt;code&gt;popular_posts()&lt;/code&gt; 에 추가 옵션을 설정하는 것으로 다음과 같은 옵션들을 지정할 수 있다.&lt;/p&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;option&lt;/th&gt;
        &lt;th&gt;description&lt;/th&gt;
        &lt;th&gt;default&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;maxCount&lt;/td&gt;
        &lt;td&gt;목록으로 구성할 최대 포스트 수&lt;/td&gt;
        &lt;td&gt;5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;ulClass&lt;/td&gt;
        &lt;td&gt;목록에 사용할 CSS 클래스 명&lt;/td&gt;
        &lt;td&gt;‘popular-posts’&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;PPMixingRate&lt;/td&gt;
        &lt;td&gt;목록에 인기 포스트와 연관 포스트의 구성 비율&lt;/td&gt;
        &lt;td&gt;0.0 (= 연관 포스트만 처리)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;isDate&lt;/td&gt;
        &lt;td&gt;날짜 표시 여부&lt;/td&gt;
        &lt;td&gt;false&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;isImage&lt;/td&gt;
        &lt;td&gt;이미지 표시 여부&lt;/td&gt;
        &lt;td&gt;false&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;isExcerpt&lt;/td&gt;
        &lt;td&gt;발췌 표시 여부&lt;/td&gt;
        &lt;td&gt;false&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;PPCategoryFilter&lt;/td&gt;
        &lt;td&gt;인기 포스트 목록에 카테고리 설정 옵션&lt;/td&gt;
        &lt;td&gt;미 정의&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;p&gt;옵션 설정의 예제는 다음과 같다.&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;연관 포스트 목록에 5개를 표시하고 이미지를 구성하는 경우&lt;/strong&gt;&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-plain hljs ruby&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-string&quot;&gt;%- popular_posts_json({ maxCount: 5 , ulClass: 'popular-&lt;/span&gt;posts&lt;span class=&quot;hljs-string&quot;&gt;' , PPMixingRate: 0.0 , isImage: true}) %&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;&lt;strong&gt;인기 포스트 목록에 10개를 생성하는 경우 (단, Google Analytics API 설정 필요)&lt;/strong&gt;&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-plain hljs ruby&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-string&quot;&gt;%- popular_posts_json({ maxCount: 10 , ulClass: 'popular-&lt;/span&gt;posts&lt;span class=&quot;hljs-string&quot;&gt;' , PPMixingRate: 1.0 }) %&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;기타 옵션들은 직접 테스트를 해보는 것으로 하고, 화면에 표시되는 HTML 을 설정하고 싶은 경우는 아래의 &lt;a href=&quot;#customize-html&quot;&gt;Customize HTML&lt;/a&gt; 을 참고하면 된다.&lt;/p&gt;
  &lt;h4 id=&quot;popular-posts&quot;&gt;Popular posts&lt;/h4&gt;
  &lt;p&gt;여기는 &lt;a href=&quot;https://www.npmjs.com/package/ga-analytics&quot;&gt;&lt;code&gt;Google Analytics 설정&lt;/code&gt;&lt;/a&gt; 의 페이지 뷰 데이터를 기준으로 동작하는 것이기 때문에 Google Analytics API 호출 설정이 필요하다.&lt;/p&gt;
  &lt;p&gt;Google Analytics 설정이 되었다는 전제하에 아래와 같은 정보를 블로그 루트 폴더에 있는 &lt;code&gt;_config.yml&lt;/code&gt; 파일에 설정해 줘야 한다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-yaml hljs avrasm&quot;&gt;&lt;span class=&quot;hljs-label&quot;&gt;popularPosts:&lt;/span&gt;
  googleAnalyticsAPI:
    clientId: ******&lt;span class=&quot;hljs-preprocessor&quot;&gt;.apps&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.googleusercontent&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;
    serviceEmail: *****@developer&lt;span class=&quot;hljs-preprocessor&quot;&gt;.gserviceaccount&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;
    key: /hexo-project-root/path/to/google-services&lt;span class=&quot;hljs-preprocessor&quot;&gt;.pem&lt;/span&gt;
    viewId: &lt;span class=&quot;hljs-number&quot;&gt;12345678&lt;/span&gt;
    dateRange: &lt;span class=&quot;hljs-number&quot;&gt;30&lt;/span&gt;       &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (Optional) The period you want to get by Google Analytics page view. Default = 30&lt;/span&gt;
    expiresDate: &lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;     &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) Expiration date of cache file. Default = 10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;Google Analytics 설정 페이지를 잘 확인해서 구성한 후에 필요한 정보를 위의 설정에서 바꿔주면 된다.&lt;/p&gt;
  &lt;p&gt;우선 아래와 같은 사항에 대해서 구성할 때 검토가 필요하다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;clientId&lt;/strong&gt;: 는 사용자 인증 정보의 &lt;code&gt;OAuth 2.0 클라이언트 ID&lt;/code&gt; 값을 의미한다.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;serviceEmail&lt;/strong&gt;: 은 사용자 인증 정보가 아닌 &lt;code&gt;Service Account&lt;/code&gt; 를 구성하면 생성되는 &lt;code&gt;서비스 계정 ID&lt;/code&gt; 를 의미한다. 작성자가 데이터가 이메일 포맷이라서 용어를 그렇게 사용한 것 같다. 이 것 때문에 엄청 헤맸다. -_-&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;key&lt;/strong&gt;: 는 기본 제공되는 것이 아니기 때문에 &lt;a href=&quot;https://console.developers.google.com/iam-admin/serviceaccounts&quot;&gt;&lt;code&gt;Service Account 관리&lt;/code&gt;&lt;/a&gt;      페이지에서 프로젝트를 선택하고 키를 생성해 줘야 한다. 이 부분에서도 json 과 p12 포맷이 존재하는데 json 을 이용하는 방식을 플러그인에서 제공하지 않으므로 &lt;code&gt;반드시 p12&lt;/code&gt; 방식으로 선택해서 키를 생성하고 다운로드하도록 한다. 키 변환 명령은 아래에서 다시 설명한다.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;viewId&lt;/strong&gt;: 는 &lt;a href=&quot;https://ga-dev-tools.appspot.com/account-explorer/&quot;&gt;&lt;code&gt;Account Expolorer&lt;/code&gt;&lt;/a&gt;      를 실행해서 표시되는 사이트 정보 밑에 Account 와 같이 표시되는 &lt;code&gt;View&lt;/code&gt; 항목에 설정된 값을 사용하면 된다.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p&gt;원문에 Google Analytics 설정 관련 &lt;a href=&quot;https://www.npmjs.com/package/ga-analytics&quot;&gt;참고자료&lt;/a&gt; 가 있기는 하지만 나중에 별도의 포스트를 작성해야 할지도 모르겠다.
  &lt;/p&gt;
  &lt;p&gt;위의 _config.yml 파일에 설정한 정보 중에서 몇가지는 환경변수로 처리가 가능하기 때문에 아래와 같이 환경변수와 _config.yml 혼용으로 사용해도 된다.&lt;/p&gt;

  &lt;strong&gt;환경변수 설정&lt;/strong&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh [환경변수 설정] hljs bash&quot;&gt;$ &lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; GOOGLEAPI_CLIENTID=&lt;span class=&quot;hljs-string&quot;&gt;&quot;******.apps.googleusercontent.com&quot;&lt;/span&gt;
$ &lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; GOOGLEAPI_EMAIL=&lt;span class=&quot;hljs-string&quot;&gt;&quot;*****@developer.gserviceaccount.com&quot;&lt;/span&gt;
$ &lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; GOOGLEAPI_KEY=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/path/to/google-services.pem&quot;&lt;/span&gt;
$ &lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; GOOGLEAPI_ANALYTICS_TABLE=&lt;span class=&quot;hljs-string&quot;&gt;&quot;ga:12345678&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;strong&gt;_config.yml 설정 변경&lt;/strong&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-yaml [yaml _config 설정] hljs avrasm&quot;&gt;&lt;span class=&quot;hljs-label&quot;&gt;popularPosts:&lt;/span&gt;
  googleAnalyticsAPI:
    dateRange: &lt;span class=&quot;hljs-number&quot;&gt;60&lt;/span&gt;
    expiresDate: &lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;h4 id=&quot;avanced-related-posts-morphological-analysis&quot;&gt;Avanced Related posts (Morphological Analysis)&lt;/h4&gt;
  &lt;p&gt;기본적인 연관 포스트 목록은 태그 값을 기준으로 하지만, 내부의 링크나 포스트의 키워드들을 기준으로 &lt;code&gt;형태소 분석(Morphological Analysis)&lt;/code&gt; 을 기준으로 설정할 수도 있다.&lt;/p&gt;
  &lt;p&gt;현재 지원되는 언어는 일어와 영어 딱 2개만 지원되고 있다. 개발자가 일본 사람인 듯하니 어쩔 수 없을 듯하며, 엄청 활성화되기 전에는 한글 지원은 요원해 보인다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;ja&lt;/li&gt;
    &lt;li&gt;en&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p&gt;이를 사용하기 위해서는 _config.yml 에 관련 옵션을 설정해 줘야 한다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-yaml [_config.yml - 형태소 옵션 설정] hljs avrasm&quot;&gt;&lt;span class=&quot;hljs-label&quot;&gt;popularPosts:&lt;/span&gt;
  morphologicalAnalysis: 
    negativeKeywordsList: hexo-rpp-negativewords&lt;span class=&quot;hljs-preprocessor&quot;&gt;.txt&lt;/span&gt;  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) 분석 대상에서 제외할 키워드들을 지정한 파일 경로 지정&lt;/span&gt;
    limit: &lt;span class=&quot;hljs-number&quot;&gt;300&lt;/span&gt;              &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) 분석에 사용할 키워드의 갯수 제한&lt;/span&gt;
  weight:                   &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional)&lt;/span&gt;
    tagRelevancy: &lt;span class=&quot;hljs-number&quot;&gt;1.0&lt;/span&gt;       &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) 태그 연관성 가중치 설정. 기본 값 = 1.0&lt;/span&gt;
    contentsRelevancy: &lt;span class=&quot;hljs-number&quot;&gt;1.0&lt;/span&gt;  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) 내용의 연관성 가중치 설정. 기본 값 = 1.0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;제외 키워드 설정 파일은 한 라인에 하나씩 정규식으로 설정할 수 있다.&lt;/p&gt;

  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-plain [키워드 제외 설정 파일 예시] hljs r&quot;&gt;^.$
^[&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;-&lt;span class=&quot;hljs-number&quot;&gt;9&lt;/span&gt;]+$
^String to exclude from related keywords$
^関連キーワードから除外しておきたい文字列を正規表現で指定する$
^要从相关关键字排除的字符串$
&lt;span class=&quot;hljs-keyword&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;한국어가 지원되지 않으므로 굳이 이 옵션을 사용할 이유가 없다. 그냥 태그만으로 구성해도 충분하다.&lt;/p&gt;
  &lt;blockquote&gt;
    &lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;
    &lt;p&gt;예전에 Solr 검색엔진 관련해서 프로젝트를 수행할 때 한글 형태소 분석 엔진을 설정해서 사용한 적이 있다. 나중에 시간이 좀 되면 형태소 엔진을 Hexo 에 연결해서 활용할 수 있는지 검토해 볼 필요가 있을
      듯 하다.&lt;/p&gt;
  &lt;/blockquote&gt;
  &lt;h4 id=&quot;cache-improve-generation-speed&quot;&gt;Cache (Improve generation speed)&lt;/h4&gt;
  &lt;p&gt;Hexo 에서 블로그 제너레이션을 할 때 성능 향상을 위해서 캐시 설정을 할 수 있다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-yaml [_config.yml - 캐시 옵션 설정] hljs lasso&quot;&gt;popularPosts:
  &lt;span class=&quot;hljs-keyword&quot;&gt;cache&lt;/span&gt;:
    path: hexo&lt;span class=&quot;hljs-attribute&quot;&gt;-popular&lt;/span&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;-related&lt;/span&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;-posts&lt;/span&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;-cached&lt;/span&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;.&lt;/span&gt;json&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;캐시로 사용할 파일을 지정해 주면 된다.&lt;/p&gt;
  &lt;h4 id=&quot;log&quot;&gt;Log&lt;/h4&gt;
  &lt;p&gt;로그 정보 표시 여부를 설정할 수 있다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-yaml [_config.yml - 로그 옵션 설정] hljs rust&quot;&gt;popularPosts:
  &lt;span class=&quot;hljs-keyword&quot;&gt;log&lt;/span&gt;: &lt;span class=&quot;hljs-keyword&quot;&gt;true&lt;/span&gt;  # (Optional) &lt;span class=&quot;hljs-keyword&quot;&gt;true&lt;/span&gt; 가 되면 로그가 표시된다. 기본 값 = &lt;span class=&quot;hljs-keyword&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;h4 id=&quot;customize-html&quot;&gt;Customize HTML&lt;/h4&gt;
  &lt;p&gt;직접 설정해 보면 기본으로 지정되는 HTML 이 단순 링크기 때문에 목록 리스트에 대한 HTML 변경을 할 수 밖에는 없으며 &lt;code&gt;popular_posts_json()&lt;/code&gt; 헬퍼와 &lt;code&gt;htmlGenerator&lt;/code&gt;    레지스터를 사용해야 한다.&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;아티클 템플릿 파일&lt;/li&gt;
  &lt;/ol&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-ejs [article.ejs 에 htmlGenerator 설정 추가] hljs ruby&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-string&quot;&gt;%-
    htmlGenerator( 
        popular_posts_json({ maxCount: 5 , ulClass: 'popular-&lt;/span&gt;posts&lt;span class=&quot;hljs-string&quot;&gt;' , PPMixingRate: 0.0 , isDate: true , isImage: true , isExcerpt: true})
    )
%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;ol&gt;
    &lt;li&gt;테마에 처리를 위한 스크립트 파일을 구성해 줘야 한다.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-js [Html 처리 함수 추가] hljs &quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Examples of helper&lt;/span&gt;
hexo.extend.helper.register(&lt;span class=&quot;hljs-string&quot;&gt;'htmlGenerator'&lt;/span&gt;, &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;hljs-params&quot;&gt;(args)&lt;/span&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt;(!args || !args.json || args.json.length == &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)&lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;;
  &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; returnHTML = &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;;
  &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;generateHTML&lt;/span&gt;&lt;span class=&quot;hljs-params&quot;&gt;(list)&lt;/span&gt;{&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; ret = &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;;
    ret += &lt;span class=&quot;hljs-string&quot;&gt;&quot;&amp;lt;li class=\&quot;&quot;&lt;/span&gt; + args.class + &lt;span class=&quot;hljs-string&quot;&gt;&quot;-item\&quot;&amp;gt;&quot;&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt;(list.date &amp;amp;&amp;amp; list.date != &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;){
        ret += &lt;span class=&quot;hljs-string&quot;&gt;'&amp;lt;div class=&quot;'&lt;/span&gt;+args.class+&lt;span class=&quot;hljs-string&quot;&gt;'-date&quot;&amp;gt;'&lt;/span&gt; + list.date + &lt;span class=&quot;hljs-string&quot;&gt;&quot;&amp;lt;/div&amp;gt;&quot;&lt;/span&gt;;
    }
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt;(list.img &amp;amp;&amp;amp; list.img != &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;){
        ret += &lt;span class=&quot;hljs-string&quot;&gt;'&amp;lt;div class=&quot;'&lt;/span&gt;+args.class+&lt;span class=&quot;hljs-string&quot;&gt;'-img&quot;&amp;gt;'&lt;/span&gt; + &lt;span class=&quot;hljs-string&quot;&gt;'&amp;lt;img src=&quot;'&lt;/span&gt;+list.img+&lt;span class=&quot;hljs-string&quot;&gt;'&quot; /&amp;gt;'&lt;/span&gt; + &lt;span class=&quot;hljs-string&quot;&gt;&quot;&amp;lt;/div&amp;gt;&quot;&lt;/span&gt;;
    }
    ret += &lt;span class=&quot;hljs-string&quot;&gt;'&amp;lt;div class=&quot;'&lt;/span&gt;+args.class+&lt;span class=&quot;hljs-string&quot;&gt;'-title&quot;&amp;gt;&amp;lt;h3&amp;gt;&amp;lt;a href=&quot;'&lt;/span&gt; + list.path + &lt;span class=&quot;hljs-string&quot;&gt;'&quot; title=&quot;'&lt;/span&gt;+ list.title +&lt;span class=&quot;hljs-string&quot;&gt;'&quot; rel=&quot;bookmark&quot;&amp;gt;'&lt;/span&gt;+ list.title + &lt;span class=&quot;hljs-string&quot;&gt;&quot;&amp;lt;/a&amp;gt;&amp;lt;/h3&amp;gt;&amp;lt;/div&amp;gt;&quot;&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt;(list.excerpt &amp;amp;&amp;amp;  list.excerpt != &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;){
        ret += &lt;span class=&quot;hljs-string&quot;&gt;'&amp;lt;div class=&quot;'&lt;/span&gt;+args.class+&lt;span class=&quot;hljs-string&quot;&gt;'-excerpt&quot;&amp;gt;&amp;lt;p&amp;gt;'&lt;/span&gt; + list.excerpt + &lt;span class=&quot;hljs-string&quot;&gt;&quot;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&quot;&lt;/span&gt;;
    }
    ret +=  &lt;span class=&quot;hljs-string&quot;&gt;&quot;&amp;lt;/li&amp;gt;&quot;&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; ret;
  }
  &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; i=&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i&amp;lt;args.json.length; i++){
      returnHTML += generateHTML(args.json[i]);
  }
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt;(returnHTML != &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;)returnHTML = &lt;span class=&quot;hljs-string&quot;&gt;&quot;&amp;lt;ul class=\&quot;&quot;&lt;/span&gt; + args.class + &lt;span class=&quot;hljs-string&quot;&gt;&quot;\&quot;&amp;gt;&quot;&lt;/span&gt; + returnHTML + &lt;span class=&quot;hljs-string&quot;&gt;&quot;&amp;lt;/ul&amp;gt;&quot;&lt;/span&gt;;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; returnHTML;
});&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;위의 스크립트는 기본 제공되는 HTML 에 날짜, 이미지, 타이틀, 발췌 정보 등을 살짝 추가하는 정도기 떄문에 이 스크립트를 자신이 원하는 HTML 구성으로 모두 변경해 주면 된다.&lt;/p&gt;
  &lt;blockquote&gt;
    &lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;&lt;/p&gt;
    &lt;p&gt;스크립트가 반영될 수 있도록 실행 중인 Hexo Server를 중지하고 Clean &amp;amp; Generate 를 해줘야 반영된다.&lt;/p&gt;
  &lt;/blockquote&gt;
  &lt;p&gt;위의 샘플을 응용해서 관련 포스트는 포스트 본문의 위/아래에 배치하고, 인기 포스트는 사이드바의 가장 위에 위치하도록 적용하였다.&lt;/p&gt;
  &lt;p&gt;연관/인기 포스트 생성 샘플&lt;/p&gt;
  &lt;img src=&quot;http://ccambo.gitlab.io/2017/04/19/Plugins-hexo-related-popular-posts-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/popular-posts-sample.png&quot;&gt;
  &lt;h4 id=&quot;visitor-counter&quot;&gt;Visitor Counter&lt;/h4&gt;
  &lt;p&gt;&lt;code&gt;pvMeasurementsStartDate&lt;/code&gt; 옵션을 추가로 설정하면 포스트에 페이지 뷰 카운트를 설정할 수 있다. 이 부분도 역시 &lt;a href=&quot;#popular-posts&quot;&gt;&lt;code&gt;Google Analytics 설정&lt;/code&gt;&lt;/a&gt;    를 사용한다. 따라서 아래와 같이 기존에 설정해 놓은 부분에 추가해 주면 된다.&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;페이지 뷰 측정 기준일자 설정 추가&lt;/li&gt;

  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-yaml [Google Analytics 설정에 Visitor Counter 기준일자 설정] hljs avrasm&quot;&gt;&lt;span class=&quot;hljs-label&quot;&gt;popularPosts:&lt;/span&gt;
  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) Popular posts options&lt;/span&gt;
  googleAnalyticsAPI:
    clientId: ******&lt;span class=&quot;hljs-preprocessor&quot;&gt;.apps&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.googleusercontent&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;
    serviceEmail: *****@developer&lt;span class=&quot;hljs-preprocessor&quot;&gt;.gserviceaccount&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;
    key: /hexo-project-root/path/to/google-services&lt;span class=&quot;hljs-preprocessor&quot;&gt;.pem&lt;/span&gt;
    viewId: &lt;span class=&quot;hljs-number&quot;&gt;12345678&lt;/span&gt;
    dateRange: &lt;span class=&quot;hljs-number&quot;&gt;30&lt;/span&gt;
    expiresDate: &lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;
    pvMeasurementsStartDate: &lt;span class=&quot;hljs-number&quot;&gt;2017&lt;/span&gt;/&lt;span class=&quot;hljs-number&quot;&gt;04&lt;/span&gt;/&lt;span class=&quot;hljs-number&quot;&gt;01&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
    &lt;li&gt;페이지 뷰 렌더링을 위한 템플릿 파일&lt;/li&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-ejs [템플릿 파일에 페이지 뷰 렌더링 추가] hljs vbnet&quot;&gt;This post&lt;span class=&quot;hljs-comment&quot;&gt;'s Visitor Counts is &lt;span class=&quot;hljs-xmlDocTag&quot;&gt;&amp;lt;%- popular_posts_pv() %&amp;gt;&lt;/span&gt; views.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;/ol&gt;
  &lt;p&gt;위의 샘플을 응용해서 아래와 같이 포스트 타이틀과 작성일자 및 태그 정보 바로 아래에 위치하도록 조정했다.&lt;/p&gt;
  &lt;p&gt;포스트 페이지 뷰 카운터 샘플&lt;/p&gt;
  &lt;img src=&quot;http://ccambo.gitlab.io/2017/04/19/Plugins-hexo-related-popular-posts-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/page-view-sample.png&quot;&gt;
  &lt;h4 id=&quot;ranking-sheet&quot;&gt;Ranking Sheet&lt;/h4&gt;
  &lt;p&gt;&lt;code&gt;rangkingSheet&lt;/code&gt; 옵션을 추가로 설정하면 페이지 뷰의 랭킹 정보를 생성할 수 있다.이 부분도 역시 &lt;a href=&quot;#popular-posts&quot;&gt;&lt;code&gt;Google Analytics 설정&lt;/code&gt;&lt;/a&gt;    를 사용한다. 따라서 아래와 같이 기존에 설정해 놓은 부분에 추가해 주면 된다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-yaml [Google Analytics 설정에 Ranking Sheet 파일 설정] hljs avrasm&quot;&gt;&lt;span class=&quot;hljs-label&quot;&gt;popularPosts:&lt;/span&gt;
  &lt;span class=&quot;hljs-preprocessor&quot;&gt;# (optional) Popular posts options&lt;/span&gt;
  googleAnalyticsAPI:
    clientId: ******&lt;span class=&quot;hljs-preprocessor&quot;&gt;.apps&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.googleusercontent&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;
    serviceEmail: *****@developer&lt;span class=&quot;hljs-preprocessor&quot;&gt;.gserviceaccount&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;
    key: /hexo-project-root/path/to/google-services&lt;span class=&quot;hljs-preprocessor&quot;&gt;.pem&lt;/span&gt;
    viewId: &lt;span class=&quot;hljs-number&quot;&gt;12345678&lt;/span&gt;
    dateRange: &lt;span class=&quot;hljs-number&quot;&gt;30&lt;/span&gt;
    expiresDate: &lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;
    pvMeasurementsStartDate: &lt;span class=&quot;hljs-number&quot;&gt;2017&lt;/span&gt;/&lt;span class=&quot;hljs-number&quot;&gt;04&lt;/span&gt;/&lt;span class=&quot;hljs-number&quot;&gt;01&lt;/span&gt;
    rankingSheet: rankingSheet&lt;span class=&quot;hljs-preprocessor&quot;&gt;.txt&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;h2 id=&quot;trouble-shootings&quot;&gt;Trouble Shootings&lt;/h2&gt;
  &lt;h3 id=&quot;플러그인에-utc-시간-처리에-문제가-있어서-교체-함-20170421&quot;&gt;플러그인에 UTC 시간 처리에 문제가 있어서 교체 함. (2017.04.21)&lt;/h3&gt;
  &lt;p&gt;플러그인 원본 소스에 UTC 시간 처리 문제 소스를 아래와 같이 변경해야 한다.&lt;/p&gt;
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-js list-json.js hljs &quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 원본 코드&lt;/span&gt;
...
&lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt;(options.isDate &amp;amp;&amp;amp; list.date != &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;){
    ret.date =  moment(list.date._i).format(config.date_format || &lt;span class=&quot;hljs-string&quot;&gt;&quot;YYYY-MM-DD&quot;&lt;/span&gt;);
...
&lt;span class=&quot;hljs-comment&quot;&gt;// 수정 코드&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt;(options.isDate &amp;amp;&amp;amp; list.date != &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;){
    ret.date = moment(list.date).format(&lt;span class=&quot;hljs-string&quot;&gt;'YYYY-MM-DD'&lt;/span&gt;);
...&lt;/code&gt;&lt;/pre&gt;
  &lt;p&gt;위의 원본에서는 moment에 &lt;code&gt;._i&lt;/code&gt; 값을 기준으로 처리했는데, 이 처리는 moment가 최초 생성되는 시점의 정보를 반영하는 문제가 있는 것으로 보인다. 결과적으로 모든 일자가 동일하게 호출
    일자로 반환되는 문제가 생긴다. &lt;/p&gt;
  &lt;p&gt;따라서 시간을 제외한 일자 정보만 처리하는 경우는 수정된 코드를 사용해야 게시된 일자을 정확하게 표시할 수 있다.&lt;/p&gt;
  &lt;h2 id=&quot;conclustion&quot;&gt;Conclustion&lt;/h2&gt;
  &lt;p&gt;정리해서 노출시킨 글들이 어떻게 읽혀지고, 누가 읽고 있었는지 등이 궁금해서 시작을 했지만, 상업적인 솔루션이나 회사 또는 물건을 광고하는 것이 아니기 때문에 솔직히 &lt;code&gt;Google Analytics&lt;/code&gt;    의 모든 기능을 사용하는 것은 아니다. &lt;/p&gt;
  &lt;p&gt;단지, 글들에 대한 액세스등이 궁금해서 플러그인을 설정하기는 했지만, 아직도 필요성 대비 설정이 너무 많고 모르는 것이 넘치기 때문에 이렇게까지 설정해서 사용해야 하는지에 대한 의문점이 든다.&lt;/p&gt;
  &lt;p&gt;아마도 아직은 단순 무식해서 정말 그 효과를 몰라서 그럴지도… ㅠㅠ&lt;/p&gt;
  &lt;hr&gt;
  &lt;p&gt;References &lt;br /&gt; - &lt;a href=&quot;https://github.com/tea3/hexo-related-popular-posts&quot;&gt;Github - hexo-related-popular-posts&lt;/a&gt;    &lt;br /&gt; - &lt;a href=&quot;https://hexo.io/plugins/&quot;&gt;Hexo 다양한 플러그인&lt;/a&gt; &lt;br /&gt; - &lt;a href=&quot;https://www.npmjs.com/package/ga-analytics&quot;&gt;Google Analytics 설정&lt;/a&gt;&lt;/p&gt;
  &lt;div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <category>개발/기타공통</category>
      <category>Google Analytics</category>
      <category>HEXO</category>
      <category>HEXO-RELATED-POPULAR-POSTS</category>
      <category>Plugins</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/30</guid>
      <comments>https://ccambo.tistory.com/entry/Hexo-Plugin-Series-hexorelatedpopularposts-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0#entry30comment</comments>
      <pubDate>Fri, 28 Apr 2017 16:20:22 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes] 설치 시점에 특정 버전 지정하기</title>
      <link>https://ccambo.tistory.com/entry/Kubernetes-%EC%84%A4%EC%B9%98-%EC%8B%9C%EC%A0%90%EC%97%90-%ED%8A%B9%EC%A0%95-%EB%B2%84%EC%A0%84-%EC%A7%80%EC%A0%95%ED%95%98%EA%B8%B0</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;
  
  &lt;p&gt;이 문서는 Ubuntu 에 Kubernetes 를 설치할 때 특정 버전의 바이너리를 지정해서 처리하는 방법을 설명하는 것이기 때문에 전체 설치 과정을 다루고 있지 않습니다.&lt;/p&gt;
  
  &lt;p&gt;설치에 관련된 문서는 &lt;code&gt;iamartin&lt;/code&gt; 님 블로그에 &lt;a href=&quot;http://blog.iamartin.com/kubernetes-install/&quot;&gt;kubeadm을 이용해서 아주 쉽게 Kubernetes 설치하기&lt;/a&gt; 에 상세하게 기술되어 있으므로 이 부분을 참고하시면 됩니다. (향후 변경된 사항이나 요청이 있다면 정리된 버전을 추가로 올릴 수도 있습니다)&lt;/p&gt;
  
  &lt;p&gt;참고로 Kubernetes 를 설치하는 도구들은 상당히 많이 존재하며 향후 &lt;code&gt;kubeadm&lt;/code&gt; 으로 통합될 것으로 개인적인 예상을 하고 있기 때문에 다른 설치 도구에 대한 부분은 따로 정리하지 않고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;h1 id=&quot;kubernetes-특정-버전으로-구성하기&quot;&gt;Kubernetes 특정 버전으로 구성하기&lt;/h1&gt;

&lt;p&gt;이 문서를 정리한 이유는 Kubernetes가 버전 향상 작업을 수시로 진행하고 있고, 각 종 설치 도구는 &lt;code&gt;latest 버전&lt;/code&gt;을 대상으로 운영되고 있기 때문에 솔루션 개발 중이거나 또는 다른 이유로 버전을 고정해야 할 경우가 있을 수 있기 때문입니다.&lt;/p&gt;



&lt;h2 id=&quot;기본-설치-관련-정보&quot;&gt;기본 설치 관련 정보&lt;/h2&gt;

&lt;p&gt;ubuntu 에서 설치는 초기에 아래와 같이 4 단계의 작업을 거치게 됩니다. 물론 &lt;a href=&quot;https://kubernetes.io/docs/admin/kubeadm/&quot;&gt;&lt;code&gt;kubeadm&lt;/code&gt;&lt;/a&gt;을 사용하는 경우는 다른 환경에서도 거의 유사하게 사용됩니다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Apt Key 생성&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs avrasm&quot;&gt;$ curl http://packages&lt;span class=&quot;hljs-preprocessor&quot;&gt;.cloud&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.google&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;/apt/doc/apt-key&lt;span class=&quot;hljs-preprocessor&quot;&gt;.gpg&lt;/span&gt; | apt-key &lt;span class=&quot;hljs-keyword&quot;&gt;add&lt;/span&gt; -&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubernetes Source Repository 생성 (데비안 패키지)&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs avrasm&quot;&gt;cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/apt/sources&lt;span class=&quot;hljs-preprocessor&quot;&gt;.list&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.d&lt;/span&gt;/kubernetes&lt;span class=&quot;hljs-preprocessor&quot;&gt;.list&lt;/span&gt;
deb http://apt&lt;span class=&quot;hljs-preprocessor&quot;&gt;.kubernetes&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.io&lt;/span&gt;/ kubernetes-xenial main
EOF&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apt Repository 갱신&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs cs&quot;&gt;apt-&lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt; update&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;필수 프로그램 설치&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs lasso&quot;&gt;apt&lt;span class=&quot;hljs-attribute&quot;&gt;-get&lt;/span&gt; install &lt;span class=&quot;hljs-attribute&quot;&gt;-y&lt;/span&gt; docker&lt;span class=&quot;hljs-built_in&quot;&gt;.&lt;/span&gt;io
apt&lt;span class=&quot;hljs-attribute&quot;&gt;-get&lt;/span&gt; install &lt;span class=&quot;hljs-attribute&quot;&gt;-y&lt;/span&gt; kubelet kubeadm kubectl kubernetes&lt;span class=&quot;hljs-attribute&quot;&gt;-cni&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;위의 단계를 거치고 나면 Kubernetes 설치에 필요한 기본 패키지들이 준비된 것입니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt; &lt;br /&gt;
  다운로드한 바이너리들에 대한 특정 버전을 지정하는 방법은 아래의 예와 같이 각 Component 별로 버전 식별 정보를 지정하면 됩니다.&lt;/p&gt;
  
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs lasso&quot;&gt; $ sudo apt&lt;span class=&quot;hljs-attribute&quot;&gt;-get&lt;/span&gt; &lt;span class=&quot;hljs-attribute&quot;&gt;-y&lt;/span&gt; install kubectl&lt;span class=&quot;hljs-subst&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.3&lt;/span&gt;&lt;span class=&quot;hljs-subst&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt; kubelet&lt;span class=&quot;hljs-subst&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.3&lt;/span&gt;&lt;span class=&quot;hljs-subst&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt; kubernetes&lt;span class=&quot;hljs-attribute&quot;&gt;-cni&lt;/span&gt;&lt;span class=&quot;hljs-subst&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.0&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.1&lt;/span&gt;&lt;span class=&quot;hljs-subst&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;07&lt;/span&gt;a8a2&lt;span class=&quot;hljs-subst&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;



&lt;h2 id=&quot;버전-정보-확인하기&quot;&gt;버전 정보 확인하기&lt;/h2&gt;

&lt;p&gt;재미있는 부분은 위의 설치 2번째 작업에서 처리한 데비안 패키지의 내용을 아래의 경로에서 확인을 해 보면 최근에 릴리즈된 정보가 다 구성되어 있다는 것으로 위의 소스 리파지토리 설정 정보를 이용해서 아래와 같은 URL 로 패키지 정보 검증이 가능합니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt; &lt;br /&gt;
  지금까지 발표된 모든 Release가 아니고 최근에 발표된 버전이 기준입니다. 따라서 1.5 이전 버전을 사용해야 하는 상황이라면 이 문서에 정리된 내용은 맞지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs ruby&quot;&gt;&lt;span class=&quot;hljs-variable&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;hljs-symbol&quot;&gt;https:&lt;/span&gt;/&lt;span class=&quot;hljs-regexp&quot;&gt;/packages.cloud.google.com/apt&lt;/span&gt;&lt;span class=&quot;hljs-regexp&quot;&gt;/dists/kubernetes&lt;/span&gt;-xenial/main/binary-amd64/&lt;span class=&quot;hljs-constant&quot;&gt;Packages&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;아래의 그림은 위의 경로에서 확인한 패키지 정보를 일부 발췌한 내용입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ccambo.gitlab.io/2017/04/14/%EC%84%A4%EC%B9%98-%EC%8B%9C%EC%A0%90%EC%97%90-%ED%8A%B9%EC%A0%95-%EB%B2%84%EC%A0%84-%EC%A7%80%EC%A0%95%ED%95%98%EA%B8%B0/kubernetes-xenial-main.png&quot; alt=&quot;Kubernetes xenial main 패키지 일부&quot; title=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;위의 그림에서 박스로 표시한 것처럼 &lt;code&gt;kubelet&lt;/code&gt; 이 1.5.1 ~ 1.6.1 까지 구성 (위의 빨간 박스) 되어 있는 것을 볼 수 있으며, 각 구성 요소에 의존적인 다른 패키지의 버전과 관련된 Depends (위의 파란 박스) 정보가 구성된 것을 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;따라서 처음에는 특정 릴리즈 버전의 정보만을 가져오는 것에 대해 검토를 했지만, 위의 정보를 확인하고 나서는 다운로드된 바이너리들을 어떻게 버전을 지정해서 처리하는지에 대한 것만 결정하고 설정하면 됩니다.&lt;/p&gt;



&lt;h2 id=&quot;kubeadm-초기화할-때-버전-지정&quot;&gt;kubeadm 초기화할 때 버전 지정&lt;/h2&gt;

&lt;p&gt;Kubernetes Master Node 의 초기화는 아래와 같은 명령을 기본으로 사용합니다.&lt;/p&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs &quot;&gt;kubeadm init&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위의 초기화 명령에 사용할 수 있는 다양한 옵션들 중에 Kuberenetes 의 바이너리 구성 버전을 지정할 수 있습니다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--kubernetes-version&lt;/code&gt;: 기본 값은 &lt;code&gt;lastest&lt;/code&gt; 로 설정되어 있으며 원하는 Kubernetes 버전을 지정하면 됩니다. 단, 이때 지정할 버전은 &lt;a href=&quot;https://github.com/kubernetes/kubernetes/releases&quot;&gt;릴리즈 페이지&lt;/a&gt; 를 참고하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2 id=&quot;kubeadm-자체-버전-문제&quot;&gt;Kubeadm 자체 버전 문제&lt;/h2&gt;

&lt;p&gt;위의 &lt;code&gt;Kubernetes 패키지 (xenial main) 에는 kubeadm 1.6.1 만 존재&lt;/code&gt;하기 때문에 Kubernetes 1.6 버전만 처리가 가능합니다. 정확하게는 Depends 로 kubelet &amp;gt;= 1.6.0, kubectl &amp;gt;= 1.6.0 제한이 걸려 있기 때문에 1.6.0 또는 1.6.1 로만 구성이 가능합니다.&lt;/p&gt;

&lt;p&gt;따라서 현재 상황에서 1.5.1 ~ 1.5.6 까지의 Kubernetes 버전을 구성할 수 없는 상태입니다.&lt;/p&gt;



&lt;h2 id=&quot;kubeadm-특정-버전-처리하기&quot;&gt;Kubeadm 특정 버전 처리하기&lt;/h2&gt;

&lt;p&gt;위에서 Kubernetes의 버전 문제라기 보다는 Kubeadm 버전 문제 떄문에 Kubernetes 1.6.0 이전 설정이 안되는 문제를 확인했기 때문에 이번에는 Kubeadm 의 버전을 처리하는 방법으로 설정해야 합니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt; &lt;br /&gt;
  Kubernetest 1.6.0 을 설정하는 경우는 이 부분을 처리하지 않아도 처리가 가능합니다. Kubernetes 버전 지정으로 진행하시면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;아래의 스크립트는 현재 버전 1.6.1 기준으로 특정한 버전 (ex. 1.5.3) 을 설정하기 위해서 kubeadm 을 재 설정하는 것입니다.&lt;/p&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs avrasm&quot;&gt;$ apt-get update &amp;amp;&amp;amp; apt-get install -&lt;span class=&quot;hljs-built_in&quot;&gt;y&lt;/span&gt; apt-transport-https
$ curl -s https://packages&lt;span class=&quot;hljs-preprocessor&quot;&gt;.cloud&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.google&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.com&lt;/span&gt;/apt/doc/apt-key&lt;span class=&quot;hljs-preprocessor&quot;&gt;.gpg&lt;/span&gt; | apt-key &lt;span class=&quot;hljs-keyword&quot;&gt;add&lt;/span&gt; -
$ cat &amp;lt;&amp;lt;EOF &amp;gt;/etc/apt/sources&lt;span class=&quot;hljs-preprocessor&quot;&gt;.list&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.d&lt;/span&gt;/kubernetes&lt;span class=&quot;hljs-preprocessor&quot;&gt;.list&lt;/span&gt;
deb http://apt&lt;span class=&quot;hljs-preprocessor&quot;&gt;.kubernetes&lt;/span&gt;&lt;span class=&quot;hljs-preprocessor&quot;&gt;.io&lt;/span&gt;/ kubernetes-xenial main
EOF
$ apt-get update

&lt;span class=&quot;hljs-preprocessor&quot;&gt;# 위의 처리까지는 기존과 크게 다르지 않습니다. 아래부터는 Kubernetes 의 특정한 버전을 지정하는 부분입니다.&lt;/span&gt;
$ sudo apt-get -&lt;span class=&quot;hljs-built_in&quot;&gt;y&lt;/span&gt; install kubectl=&lt;span class=&quot;hljs-number&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.3&lt;/span&gt;-&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt; kubelet=&lt;span class=&quot;hljs-number&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.3&lt;/span&gt;-&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt; kubernetes-cni=&lt;span class=&quot;hljs-number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.0&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.1&lt;/span&gt;-&lt;span class=&quot;hljs-number&quot;&gt;07&lt;/span&gt;a8a2-&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt;

&lt;span class=&quot;hljs-preprocessor&quot;&gt;# 아래는 Kubernetes 1.5 버전을 처리하기 위해서 kubeadm 을 설정하는 부분입니다.&lt;/span&gt;
$ curl -Lo /tmp/old-kubeadm&lt;span class=&quot;hljs-preprocessor&quot;&gt;.deb&lt;/span&gt; https://apt&lt;span class=&quot;hljs-preprocessor&quot;&gt;.k&lt;/span&gt;8s&lt;span class=&quot;hljs-preprocessor&quot;&gt;.io&lt;/span&gt;/pool/kubeadm_1&lt;span class=&quot;hljs-number&quot;&gt;.6&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.0&lt;/span&gt;-alpha&lt;span class=&quot;hljs-number&quot;&gt;.0&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.2074&lt;/span&gt;-a092d8e0f95f52-&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt;_amd64_0206dba536f698b5777c7d210444a8ace18f48e045ab78687327631c6c694f42&lt;span class=&quot;hljs-preprocessor&quot;&gt;.deb&lt;/span&gt;
$ sudo dpkg -i /tmp/old-kubeadm&lt;span class=&quot;hljs-preprocessor&quot;&gt;.deb&lt;/span&gt;
$ sudo apt-get install -f

&lt;span class=&quot;hljs-preprocessor&quot;&gt;# 아래는 업그레이드되는 것을 방지하기 위해서 버전을 유지하기 위한 것입니다.&lt;/span&gt;
sudo apt-mark hold kubeadm kubectl kubelet kubernetes-cni&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위의 같이 기본적인 설정 및 바이너리 다운로드는 동일하지만 kubeadm 을 이전 버전으로 다운로드하여 설정하고 버전을 고정하는 방식으로 처리하면 됩니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; &lt;br /&gt;
  현재 프로젝트를 마무리하고 나온 상태라서 위의 정리된 내용을 실제 검증한 것은 아니기 때문에 다소 내용과 실제가 맞지 않을 수 있습니다. 따라서 환경이 구성된 후에 위의 정리된 내용과 다른 부분은 갱신을 할 예정입니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;

&lt;p&gt;References &lt;br /&gt;
- &lt;a href=&quot;https://gist.github.com/jbeda/5e8a4ce0a23711da146e51900859c037&quot;&gt;https://gist.github.com/jbeda/5e8a4ce0a23711da146e51900859c037&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;blockquote&gt;
  &lt;p&gt;Written by Morris (&lt;a href=&quot;mailto:ccambo@gmail.com&quot;&gt;ccambo@gmail.com&lt;/a&gt; - MSFL)&lt;/p&gt;
&lt;/blockquote&gt;&lt;/div&gt;</description>
      <category>개발/Kubernetes 이해</category>
      <category>deb</category>
      <category>Installation</category>
      <category>k8s</category>
      <category>kubeadm</category>
      <category>Kubernetes</category>
      <category>specific version</category>
      <category>ubuntu</category>
      <category>데비안패키지</category>
      <category>쿠버네티스</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/29</guid>
      <comments>https://ccambo.tistory.com/entry/Kubernetes-%EC%84%A4%EC%B9%98-%EC%8B%9C%EC%A0%90%EC%97%90-%ED%8A%B9%EC%A0%95-%EB%B2%84%EC%A0%84-%EC%A7%80%EC%A0%95%ED%95%98%EA%B8%B0#entry29comment</comments>
      <pubDate>Mon, 17 Apr 2017 22:20:46 +0900</pubDate>
    </item>
    <item>
      <title>[VHD] 부트 시점에 VHD 자동 마운트 처리 2 - 스크립트 버전</title>
      <link>https://ccambo.tistory.com/entry/%EB%B6%80%ED%8A%B8-%EC%8B%9C%EC%A0%90%EC%97%90-VHD-%EC%9E%90%EB%8F%99-%EB%A7%88%EC%9A%B4%ED%8A%B8-2-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%B2%84%EC%A0%84</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&lt;h1 id=&quot;스크립트-방식으로-마운트-및-해제-조정하기&quot;&gt;스크립트 방식으로 마운트 및 해제 조정하기&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;http://ccambo.tistory.com/entry/VHD-%EC%9E%90%EB%8F%99-%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%B2%98%EB%A6%AC&quot;&gt;[이전 게시글]&lt;/a&gt; 에서 &lt;code&gt;diskpart&lt;/code&gt; 를 이용한 작업 스케줄러 처리로 부트 시점에 자동 마운트 처리를 정리 했었다.&lt;/p&gt;

&lt;p&gt;막상 실제 사용하는데 몇 가지 불만 사항이 생겨서 스크립트를 사용하는 방식으로 변경해 본다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;여러 개의 VHD 를 마운트 시킬 경우에 Task 를 여러 개 등록하는 문제 (한번에 여러 개 처리하는 방법을 못 찾음. ㅠㅠ)&lt;/li&gt;
&lt;li&gt;특정 사용자인 경우만 처리하는 경우 (작업 스케줄러로도 처리는 가능하지만 두가지 방식 혼용이 싫음)&lt;/li&gt;
&lt;li&gt;쉽게 추가/변경해서 재 시작 없이도 처리하고 싶음.&lt;/li&gt;
&lt;li&gt;그냥 작업 스케줄러가 귀찮음.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;위의 같은 이유로 아래와 같이 스크립트 처리를 추가했다. 기본은 diskpart를 이용하는 방식이다.&lt;/p&gt;

&lt;p&gt;윈도우에서 스크립트를 처리할 때는 항상 관리자 권한 여부가 상당히 껄끄럽다. 이런 저런 방법들도 많다. 대략적으로는 아마 아래와 같은 형식들이 될 듯 하다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;레지스트리를 조정해서 항상 Cmd.exe 가 관리자 권한으로 실행되도록 한다.&lt;/li&gt;
&lt;li&gt;UAC 조정 또는 로그인 사용자를 관리자인 것으로 처리한다.&lt;/li&gt;
&lt;li&gt;사용자를 Administrator 로만 사용한다.&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그런데 이런 저런 시스템 설정을 하기는 싫고 순수하게 스크립트에서만 이를 해결할 수는 없을까?&lt;/p&gt;



&lt;h2 id=&quot;실행되는-스크립트를-관리자-모드인-것으로-처리하기&quot;&gt;실행되는 스크립트를 관리자 모드인 것으로 처리하기&lt;/h2&gt;

&lt;p&gt;이런 저런 자료를 검토하고 나름대로 관리자 모드로 실행 부분을 생각해 보니 의외로 쉽게 할 수 있는 방법이 있다. 그것도 시스템을 건들이지 않고 스크립트 만으로 조정해서 가능하다.&lt;/p&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs mel&quot;&gt;&lt;span class=&quot;hljs-variable&quot;&gt;@echo&lt;/span&gt; 관리자 권한 검증 &amp;gt;&lt;span class=&quot;hljs-variable&quot;&gt;%windir&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;%\&lt;/span&gt;admin.confirm&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위의 명령은 &lt;code&gt;%windir%&lt;/code&gt; (보통 C:\Windows) 경로에 “관리자 권한 검증” 이란 문자열을 출력해서 admin.confirm 이라는 파일을 생성하는 것이다. 당연히 관리자 권한이 없으면 &lt;code&gt;액세스가 거부되었습니다.&lt;/code&gt; 라는 오류 메시지를 보게 된다.&lt;/p&gt;

&lt;p&gt;이를 이용해서 아래와 같이 관리자 권한을 탈취(?)하는 코드를 생성하면 된다.&lt;/p&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs perl&quot;&gt;&lt;span class=&quot;hljs-variable&quot;&gt;@echo&lt;/span&gt; 관리자 권한 검증 &amp;gt;&lt;span class=&quot;hljs-variable&quot;&gt;%windir&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;%\&lt;/span&gt;admin.confirm || &lt;span class=&quot;hljs-variable&quot;&gt;@(&lt;/span&gt;
echo Set UAC = CreateObject^(&lt;span class=&quot;hljs-string&quot;&gt;&quot;Shell.Application&quot;&lt;/span&gt;^) &amp;gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;&lt;span class=&quot;hljs-variable&quot;&gt;%temp&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;%\&lt;/span&gt;admin.vbs&quot;&lt;/span&gt;
echo UAC.ShellExecute &lt;span class=&quot;hljs-string&quot;&gt;&quot;&lt;span class=&quot;hljs-variable&quot;&gt;%~&lt;/span&gt;0&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;runas&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class=&quot;hljs-variable&quot;&gt;%temp&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;%\&lt;/span&gt;admin.vbs&lt;span class=&quot;hljs-string&quot;&gt;&quot;
wscript.exe &quot;&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;%temp&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;%\&lt;/span&gt;admin.vbs&lt;span class=&quot;hljs-string&quot;&gt;&quot; &amp;amp; del &quot;&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;%temp&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;%\&lt;/span&gt;admin.vbs&lt;span class=&quot;hljs-string&quot;&gt;&quot; &amp;amp; exit
)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위의 명령은 관리자 권한이 없는 경우에 &lt;code&gt;%temp%&lt;/code&gt; (보통 C:\Users\\appdata\local\temp) 에 admin.vbs 라는 파일을 만들고 아래와 같이 UAC 객체를 얻어서 실행하는 명령을 구성해서 &lt;code&gt;wscript.exe&lt;/code&gt; 로 관리자 권한으로 실행하도록 하는 것이다.&lt;/p&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs avrasm&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;Set&lt;/span&gt; UAC = CreateObject(&lt;span class=&quot;hljs-string&quot;&gt;&quot;Shell.Application&quot;&lt;/span&gt;)
UAC&lt;span class=&quot;hljs-preprocessor&quot;&gt;.ShellExecute&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;cmd_test&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;runas&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;따라서 관리자 권한을 탈취(?)하고 다시 실행되는 경우에 테스트를 위해서 만들었던 파일들을 삭제하고 나머지를 계속 수행하면 된다.&lt;/p&gt;

&lt;p&gt;아래는 실제 작업을 진행한 코드를 보여주는 것이다. (multi_vhd_mount.bat)&lt;/p&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs dos&quot;&gt;@&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; off

@title VHD Mount.
@&lt;span class=&quot;hljs-keyword&quot;&gt;setlocal&lt;/span&gt; enabledelayedexpansion

@&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; 관리자 권한 확인용 &amp;gt;&lt;span class=&quot;hljs-envvar&quot;&gt;%windir%&lt;/span&gt;\admin.confirm || @(
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;Set&lt;/span&gt; UAC = CreateObject^(&quot;Shell.Application&quot;^) &amp;gt; &quot;&lt;span class=&quot;hljs-envvar&quot;&gt;%temp%&lt;/span&gt;\admin.vbs&quot;
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; UAC.ShellExecute &quot;%~&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;&quot;, &quot;&quot;, &quot;&quot;, &quot;runas&quot;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class=&quot;hljs-envvar&quot;&gt;%temp%&lt;/span&gt;\admin.vbs&quot;
wscript.exe &quot;&lt;span class=&quot;hljs-envvar&quot;&gt;%temp%&lt;/span&gt;\admin.vbs&quot; &amp;amp; &lt;span class=&quot;hljs-winutils&quot;&gt;del&lt;/span&gt; &quot;&lt;span class=&quot;hljs-envvar&quot;&gt;%temp%&lt;/span&gt;\admin.vbs&quot; &amp;amp; &lt;span class=&quot;hljs-flow&quot;&gt;exit&lt;/span&gt;
)
@&lt;span class=&quot;hljs-winutils&quot;&gt;del&lt;/span&gt; &lt;span class=&quot;hljs-envvar&quot;&gt;%windir%&lt;/span&gt;\admin.confirm

&lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt; list=D:&quot;C:\VMs\Workspace.vhd&quot; E:&quot;S:\VMs\Tools.vhd&quot;

&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Start mounting.

&lt;span class=&quot;hljs-flow&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;hljs-envvar&quot;&gt;%%y&lt;/span&gt; &lt;span class=&quot;hljs-flow&quot;&gt;in&lt;/span&gt; (&lt;span class=&quot;hljs-envvar&quot;&gt;%list%&lt;/span&gt;) &lt;span class=&quot;hljs-flow&quot;&gt;do&lt;/span&gt; (
&lt;span class=&quot;hljs-comment&quot;&gt;rem  echo Loop's Data: %%y&lt;/span&gt;
  &lt;span class=&quot;hljs-flow&quot;&gt;for&lt;/span&gt; /F &quot;tokens=&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;,* delims=: usebackq&quot; &lt;span class=&quot;hljs-envvar&quot;&gt;%%a&lt;/span&gt; &lt;span class=&quot;hljs-flow&quot;&gt;in&lt;/span&gt; ('&lt;span class=&quot;hljs-envvar&quot;&gt;%%y&lt;/span&gt;') &lt;span class=&quot;hljs-flow&quot;&gt;do&lt;/span&gt; (
&lt;span class=&quot;hljs-comment&quot;&gt;rem  echo Checking for file %%b to assign letter %%a:&lt;/span&gt;
    &lt;span class=&quot;hljs-flow&quot;&gt;call&lt;/span&gt; %~dp0\mount_vhd &lt;span class=&quot;hljs-envvar&quot;&gt;%%b&lt;/span&gt; &lt;span class=&quot;hljs-envvar&quot;&gt;%%a&lt;/span&gt;
  )
)

&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Mounting complete.

@&lt;span class=&quot;hljs-keyword&quot;&gt;endlocal&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;list&lt;/strong&gt;: “Leter:VHDPath” 포맷으로 여러 개를 지정하면 처리 함.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;for %%y in (%list%) do&lt;/strong&gt;: list 에 설정된 값 (’ ’ 값으로 분리됨 예제는 2개) 을 하나씩 %%y 변수로 할당한 루프 처리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;for /F “token=1,* delims=: usebackq” %%a in (‘%%y’) do&lt;/strong&gt;: %%y 값을 문자열 처리하는 double quote(“) 를 제거 (usebackq 옵션) 하고 분리 문자로 “:” 사용해서 분리 (delims=:) 한 결과들 중에서 첫번째와 나머지 (token=1,*) 인 두개로 반환반환하기 때문에 %%a 는 첫 번째 결과가 %%b는 선언하지 않아도 나머지가 배정된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;당연한 말이지만 위의 &lt;code&gt;call mount_vhd %%b %%a&lt;/code&gt; 를 &lt;code&gt;call unmount_vhd %%b&lt;/code&gt; 로 하면 해제 처리 가능하다.&lt;/p&gt;

&lt;p&gt;이제 관리자 권한을 탈취해서 실행되는 스크립트를 만들었으니 내부에서 호출되는 스크립트들을 확인해 보도록 하자.&lt;/p&gt;



&lt;h2 id=&quot;마운트-스크립트-mountvhdcmd&quot;&gt;마운트 스크립트 (mount_vhd.cmd)&lt;/h2&gt;

&lt;p&gt;아래 스크립트는 VHD 를 diskpart 명령을 사용해서 처리하는 스크립트다. 중간 중간에 테스트를 위해 사용한 echo 명령은 제거해도 된다. 물론 diskpart 스크립트 파일 생성을 위한 부분은 남겨둬야 한다.&lt;/p&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs dos&quot;&gt;@&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; off

&lt;span class=&quot;hljs-flow&quot;&gt;if&lt;/span&gt; {%&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;}=={} (
    &lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Usage: %~nx0 [vhd full path] [letter]
    &lt;span class=&quot;hljs-flow&quot;&gt;exit&lt;/span&gt; /b &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
)
&lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt; vhdPath=%~dpnx1
&lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt; driveLetter=%&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;

&lt;span class=&quot;hljs-flow&quot;&gt;if&lt;/span&gt; {&lt;span class=&quot;hljs-envvar&quot;&gt;%driveLetter%&lt;/span&gt;}=={} (
    &lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Mounting &lt;span class=&quot;hljs-envvar&quot;&gt;%vhdPath%&lt;/span&gt;
) &lt;span class=&quot;hljs-flow&quot;&gt;else&lt;/span&gt; (
    &lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Mounting &lt;span class=&quot;hljs-envvar&quot;&gt;%vhdPath%&lt;/span&gt; to &lt;span class=&quot;hljs-envvar&quot;&gt;%driveLetter%&lt;/span&gt;:
)

&lt;span class=&quot;hljs-comment&quot;&gt;REM&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;REM create dispart script&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;REM&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt; diskPartScript=%~nx0.diskpart
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; sel vdisk file=&quot;&lt;span class=&quot;hljs-envvar&quot;&gt;%vhdPath%&lt;/span&gt;&quot;&amp;gt;&lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; attach vdisk&amp;gt;&amp;gt;&lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;REM assign the drive letter if requested&lt;/span&gt;
&lt;span class=&quot;hljs-flow&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;hljs-flow&quot;&gt;not&lt;/span&gt; {&lt;span class=&quot;hljs-envvar&quot;&gt;%driveLetter%&lt;/span&gt;}=={} (
    &lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; select partition &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &amp;gt;&amp;gt;&lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; assign letter=&lt;span class=&quot;hljs-envvar&quot;&gt;%driveLetter%&lt;/span&gt;&amp;gt;&amp;gt;&lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;
)

&lt;span class=&quot;hljs-comment&quot;&gt;REM Show script&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt;.
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Running diskpart script:
type &lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;REM&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;REM diskpart&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;REM&lt;/span&gt;
diskpart /s &lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;
&lt;span class=&quot;hljs-winutils&quot;&gt;del&lt;/span&gt; /q &lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Done!&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;%1&lt;/strong&gt;: 첫 번째 파라미터, 없으면 사용방법 출력&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;%2&lt;/strong&gt;: 두 번째 파라미터&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~nx0&lt;/strong&gt;: 실행하고 있는 cmd 파일 명&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;%~dpnx1&lt;/strong&gt;: 첫 번째 파라미터의 경로를 포함한 값&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;%..%&lt;/strong&gt;: set 으로 설정된 것 포함 환경변수 값&lt;/li&gt;
&lt;/ul&gt;



&lt;h2 id=&quot;해제-스크립트-unmountvhdcmd&quot;&gt;해제 스크립트 (unmount_vhd.cmd)&lt;/h2&gt;

&lt;p&gt;아래 스크립트는 마운트해서 사용한 VHD 를 연결 해제하는 스크립트다.&lt;/p&gt;



&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs dos&quot;&gt;@&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; off

&lt;span class=&quot;hljs-flow&quot;&gt;if&lt;/span&gt; {%&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;}=={} (
    &lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Usage: %~nx0 [vhd full path]
    &lt;span class=&quot;hljs-flow&quot;&gt;exit&lt;/span&gt; /b &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
)
&lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt; vhdPath=%~dpnx1

&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Unmounting &lt;span class=&quot;hljs-envvar&quot;&gt;%vhdPath%&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;REM&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;REM create dispart script&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;REM&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt; diskPartScript=%~nx0.diskpart
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; sel vdisk file=&quot;&lt;span class=&quot;hljs-envvar&quot;&gt;%vhdPath%&lt;/span&gt;&quot;&amp;gt;&lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; detach vdisk&amp;gt;&amp;gt;&lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;REM&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;REM diskpart&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;REM&lt;/span&gt;

diskpart /s &lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;
&lt;span class=&quot;hljs-winutils&quot;&gt;del&lt;/span&gt; /q &lt;span class=&quot;hljs-envvar&quot;&gt;%diskPartScript%&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; Done!&lt;/code&gt;&lt;/pre&gt;



&lt;h2 id=&quot;자동-실행-등록&quot;&gt;자동 실행 등록&lt;/h2&gt;

&lt;p&gt;수동으로 실행을 해도 되지만, 작업 스케줄러를 이용해도 되고, 아래에서 확인해 볼 것처럼 시작 프로그램으로 등록해 놓고 사용해도 된다.&lt;/p&gt;



&lt;h3 id=&quot;시작-프로그램-폴더-사용&quot;&gt;시작 프로그램 폴더 사용&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;개별 사용자&lt;/strong&gt;: &lt;code&gt;%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모든 사용자&lt;/strong&gt;: &lt;code&gt;%ProgramData%\Microsoft\Windows\Start Menu\Programs\Startup&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;위의 폴더 중에서 원하는 위치에 작성한 스크립트를 복사해 놓으면 된다. (단, 내부에서 사용한 다른 호출 스크립트 경로를 조정해 줘야 한다)&lt;/p&gt;



&lt;h3 id=&quot;레지스트리-기준-시작-프로그램-등록&quot;&gt;레지스트리 기준 시작 프로그램 등록&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;레지스트리 편집기 (regedit)&lt;/code&gt;를 열고 아래의 키 들 중에 원하는 위치로 이동한다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;개별 사용자&lt;/strong&gt;: &lt;code&gt;HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모든 사용자&lt;/strong&gt;: &lt;code&gt;HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;해당 키 하위로 &lt;code&gt;새로운 문자열 키&lt;/code&gt; 를 생성하고 이름은 원하는데로 설정하고, 데이터는 실행할 배치 파일을 전체 경로를 설정하면 된다.&lt;/p&gt;



&lt;h2 id=&quot;conclustion&quot;&gt;Conclustion&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;
  
  &lt;p&gt;배치는 잠시지만 명령 창이 나타났다 사라진다. 차라리 길게 보이면 그럭저럭 넘길텐데, 후다닥 지나가면 뭔가 오류가 발생한 느낌이 나기 때문에 심하게 싫다.&lt;/p&gt;
  
  &lt;p&gt;이런 경우라면 아래와 같이 VBS 처리를 해주면 보이지 않게 된다. 아래 코드에서 &lt;code&gt;&amp;lt;실행할 Command 파일&amp;gt;&lt;/code&gt; 를 위에서 만든 스크립트 파일 전체 경로로 바꿔야 한다.&lt;/p&gt;
  
  &lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-sh hljs vbscript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;Set&lt;/span&gt; WinScriptHost = &lt;span class=&quot;hljs-built_in&quot;&gt;CreateObject&lt;/span&gt;( &lt;span class=&quot;hljs-string&quot;&gt;&quot;WScript.shell&quot;&lt;/span&gt; )
WinScriptHost.Run &lt;span class=&quot;hljs-built_in&quot;&gt;Chr&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;34&lt;/span&gt;) &amp;amp; &amp;lt;실행할 Command 파일&amp;gt; &amp;amp; &lt;span class=&quot;hljs-built_in&quot;&gt;Chr&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;34&lt;/span&gt;), &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;Set&lt;/span&gt; WinScriptHost = &lt;span class=&quot;hljs-literal&quot;&gt;Nothing&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  
  &lt;p&gt;위와 같이 처리를 해서 이 VBS 파일을 위에 자동 실행 등록으로 처리하면 정신적으로 위로(?)가 된다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;역시 뭔가 자꾸 바꾸고 테스트하는 것은 스크립트를 활용하는 것이 좋다. 물론 한번 설정하고 안 바꿀 거라면 작업 스케줄러도 나쁘지는 않다. 단, 적응이 안되니 그냥 뭔가 귀찮고 싫을 뿐이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;배치 파일, VHD 관련 정보는 고수님들이 친절하게 설명한 글이 무수하게 많으니 꼭 찾아서 읽어보는 것이 좋다.&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;blockquote&gt;
  &lt;p&gt;Written by Morris (&lt;a href=&quot;mailto:ccambo@gmail.com&quot;&gt;ccambo@gmail.com&lt;/a&gt; - MSFL)&lt;/p&gt;
&lt;/blockquote&gt;&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>개발/기타공통</category>
      <category>Mount</category>
      <category>script</category>
      <category>Unmount</category>
      <category>vhd</category>
      <category>관리자권한</category>
      <category>마운트</category>
      <category>스크립트</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/28</guid>
      <comments>https://ccambo.tistory.com/entry/%EB%B6%80%ED%8A%B8-%EC%8B%9C%EC%A0%90%EC%97%90-VHD-%EC%9E%90%EB%8F%99-%EB%A7%88%EC%9A%B4%ED%8A%B8-2-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%B2%84%EC%A0%84#entry28comment</comments>
      <pubDate>Wed, 12 Apr 2017 14:34:39 +0900</pubDate>
    </item>
    <item>
      <title>[VHD] 부트 시점에 VHD 자동 마운트 처리 1 - Task Scheduler 버전</title>
      <link>https://ccambo.tistory.com/entry/VHD-%EC%9E%90%EB%8F%99-%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%B2%98%EB%A6%AC</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1 id=&quot;how-to-auto-mount-a-vhd-at-startup-in-windows-8-and-10&quot;&gt;How to Auto-mount a VHD at startup in Windows 8 and 10&lt;/h1&gt;
&lt;p&gt;데이터 백업과 관리는 항상 중요하다. 프로젝트의 경우는 더욱 더 중요하다. 소스관리는 Git 등을 이용해서 관리를 하면 되지만, Open source 검토나 개인적인 문서 등의 자료는 딱히 편한 방법을 찾지 못해서 Sync 툴들을 사용하던 방법을 VHD 파일 단위로 구별해서 관리하고 주기적으로 VHD 파일을 백업하는 방법을 적용해 보려하니 매 부팅마다 마운트해 주는 것이 또 답답하다.&lt;/p&gt;
&lt;p&gt;이런 저런 관런 자료를 찾다보니 괜찮은 (쉬운?) 방법이 있어서 정리해 놓도록 한다.&lt;/p&gt;
&lt;h2 id=&quot;general-steps&quot;&gt;General Steps&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Project 등의 구별로 VHD(x) 파일을 생성한다.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;마운트 처리할 &lt;code&gt;Diskpart 스크립트 파일&lt;/code&gt;을 생성한다.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;작업 스케줄러에 &lt;code&gt;부트 타임 스케줄&lt;/code&gt;을 구성한다.&lt;br /&gt;
3.1. 보안 옵션에서 &lt;code&gt;로그인과 상관없이&lt;/code&gt; 실행 선택&lt;br /&gt;
3.2. 트리거 생성 후 동작 옵션을 &lt;code&gt;프로그램 시작&lt;/code&gt;으로 설정&lt;br /&gt;
3.3. &lt;code&gt;작업 지연 시간 1분&lt;/code&gt;으로 설정&lt;br /&gt;
3.4. 동작 새로 만들기 후 프로그램/스크립트에 &lt;code&gt;diskaprt&lt;/code&gt; 설정&lt;br /&gt;
3.5. 인수 추가 옵션에 2.번에서 만든 &lt;code&gt;/s 스크립트 파일&lt;/code&gt; 지정&lt;br /&gt;
3.6. 전원옵션에 &lt;code&gt;AC 전원이 켜져있는 경우만 작업 시작 옵션&lt;/code&gt; 해제&lt;br /&gt;
3.7. 사용자 보안 정보 입력&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PC 재 부팅으로 정상 동작 여부를 검증한다.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;detail-view&quot;&gt;Detail View&lt;/h2&gt;
&lt;p&gt;diskpart에 옵션으로 지정할 스크립트 파일이므로 확장자는 상관없이 지정해도 된다.&lt;/p&gt;
&lt;pre class=&quot;editor-colors lang-txt&quot;&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;mpe-syntax--text mpe-syntax--plain mpe-syntax--null-grammar&quot;&gt;&lt;span&gt;select&amp;nbsp;vdisk&amp;nbsp;file=&quot;C:\test.vhd&quot;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;mpe-syntax--text mpe-syntax--plain mpe-syntax--null-grammar&quot;&gt;&lt;span&gt;attach&amp;nbsp;vdisk&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;mpe-syntax--text mpe-syntax--plain mpe-syntax--null-grammar&quot;&gt;&lt;span&gt;assign&amp;nbsp;letter=D&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;select vdisk&lt;/strong&gt;: 실제 마운트할 VHD 파일 경로를 문자열로 지정한다.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;assign&lt;/strong&gt;: 연결할 드라이브 레터를 지정한다.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 스크립트를 저장한다. 이 샘플에서는 &lt;code&gt;S:\VMs\Scripts\AutoMount.txt&lt;/code&gt; 이름으로 저장했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;윈도우키 + R&lt;/code&gt; 을 눌러서 &lt;code&gt;작업 스케줄러&lt;/code&gt;를 실행한다. (윈도우 10 이라면 Windows 검색 창에서 &lt;code&gt;작업&lt;/code&gt;으로 검색 가능)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?id=0BxB40ItyDj9ZRFRFREVPSnVVejg&quot; alt=&quot;실행&quot;&gt;&lt;/p&gt;
&lt;p&gt;작업 스케줄러에서 아래와 같이 &lt;code&gt;작업 만들기&lt;/code&gt;를 실행해서 기본 정보를 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?id=0BxB40ItyDj9ZQmMyTDNjMWF0aTQ&quot; alt=&quot;새작업&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;사용자 로그인과 상관없이 무조건 처리할 것으로 설정하면 된다.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;트리거 탭으로 이동해서 작업 시간 설정을 한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?id=0BxB40ItyDj9ZMlRQd1pIZFJvRjA&quot; alt=&quot;트리거&quot;&gt;&lt;/p&gt;
&lt;p&gt;작업 탭으로 이동해서 실제 구동될 프로그램 (diskpart) 과 위에서 저장했던 스크립트 파일을 지정한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?id=0BxB40ItyDj9ZUjFzQjVWbkh4cmc&quot; alt=&quot;동작&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;만일 스크립트를 저장한 경로에 ' ' 문자가 있는 경우라면 반드시 &quot;&quot;로 문자열 처리를 해줘야 한다.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;조건 탭으로 이동해서 기본 설정되어 있는 AC 전원 관련 옵션을 해제한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?id=0BxB40ItyDj9ZdW41SkQzSk9qQmM&quot; alt=&quot;실행조건&quot;&gt;&lt;/p&gt;
&lt;p&gt;마지막으로 &quot;확인&quot; 버튼을 누르면 사용자 계정 확인을 하게 되므로 정보를 입력한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://drive.google.com/uc?id=0BxB40ItyDj9ZelZrZUdsQUxQQXc&quot; alt=&quot;보안설정&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;이제 부팅 시점에 VHD 를 마운트하는 작업이 등록되었으므로 실제 부팅을 통해서 작업이 잘 처리되는지를 확인하면 된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;단, 만일 보안 상의 이유로 &lt;code&gt;Bitlocker&lt;/code&gt;가 걸려 있거나 또는 디스크가 &lt;code&gt;압축 설정&lt;/code&gt;이 되어 있는 경우는 VHD 를 마운트할 수 없으므로 관련된 상황을 확인해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;References&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://www.eightforums.com/tutorials/20342-vhd-auto-mount-startup-windows-8-a.html&quot;&gt;https://www.eightforums.com/tutorials/20342-vhd-auto-mount-startup-windows-8-a.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;Written by Morris (&lt;a href=&quot;file:///D:\DevWorks\Docs\Etc\Blogging\mailto:ccambo@gmail.com&quot;&gt;ccambo@gmail.com&lt;/a&gt; - MSFL)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;</description>
      <category>개발/기타공통</category>
      <category>diskpart</category>
      <category>Task Scheduler</category>
      <category>taskschd.msc</category>
      <category>vhd</category>
      <category>자동 마운트</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/27</guid>
      <comments>https://ccambo.tistory.com/entry/VHD-%EC%9E%90%EB%8F%99-%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%B2%98%EB%A6%AC#entry27comment</comments>
      <pubDate>Tue, 11 Apr 2017 12:36:23 +0900</pubDate>
    </item>
    <item>
      <title>Console 인코딩 변경하기</title>
      <link>https://ccambo.tistory.com/entry/Console-%EC%9D%B8%EC%BD%94%EB%94%A9-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;Console/Cmd 에서 인코딩 변경하기&lt;/h1&gt;
&lt;p&gt;Markdown 문서를 HTML 문서로 변경을 하는데 Universal Document Converter 인 &lt;a href=&quot;http://pandoc.org/&quot;&gt;Pandoc&lt;/a&gt; 을 사용해서 작업 중인데 Command Console 에서 뜬금없이 모든 한글이 깨져서 나오는 증상이 발생했다.&lt;/p&gt;
&lt;h2&gt;발생 원인&lt;/h2&gt;
&lt;p&gt;문제의 발생은 모든 Markdown 문서와 코드들을 &lt;code&gt;UTF-8&lt;/code&gt; 기준으로 작업을 했지만 Command Console 은 Windows 기본 인코딩인 &lt;code&gt;949&lt;/code&gt; 를 사용하기 때문에 변환했을 때 문자열이 깨진다.&lt;/p&gt;
&lt;h2&gt;해결 방법&lt;/h2&gt;
&lt;p&gt;문제의 해결은 은근 간단하다.&lt;/p&gt;
&lt;pre&gt;# 영문 모드로 변경
&amp;gt; chcp 437
# 한글 모드로 변경
&amp;gt; chcp 949
# UTF-8 모드로 변경
&amp;gt; chcp 65001
&lt;/pre&gt;
&lt;h2&gt;예외 사항&lt;/h2&gt;
&lt;p&gt;만일 위의 명령을 실행했을 때 &lt;code&gt;잘못된 코드 페이지&lt;/code&gt; 라는 메시지가 출력될 경우는 윈도우 10 버전일 경우는 &lt;code&gt;제어판 &amp;gt;&amp;nbsp;국가 또는 지역 &amp;gt; 관리자 옵션 탭&lt;/code&gt; 에서 &lt;code&gt;유니코드를 지원하지 않는 프로그램용 언어&lt;/code&gt; 부분에서 &lt;code&gt;시스템 로캘 변경&lt;/code&gt; 버튼을 눌러 언어를 선택하면 된다. 만일 다른 윈도우 버전이라면 위와 유사한 메뉴가 있을 것이므로 찾아보고 처리하면 될 듯 하다.&lt;/p&gt;
&lt;h2&gt;참고 사항&lt;/h2&gt;
&lt;ul&gt;&lt;li&gt;
Console 창의 타이틀 메뉴에서 속성을 통해서 폰트와 여러 가지 속성을 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;
폰트는 다른 방식으로  &lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows&amp;nbsp;NT\CurrentVersion\Console\TrueTypeFont&lt;/code&gt; 에서 코드 페이지 값에 맞는 키 (ex. 949, REG_SZ, 돋움)를 생성해서 사용할 폰트를 설정해도 된다.&lt;/li&gt;
&lt;li&gt;
Unix 또는 Linux 에서는 &lt;code&gt;/etc/my.conf&lt;/code&gt; 파일에 &lt;code&gt;default-character-set=utf8&lt;/code&gt;을 설정해서 인코딩을 변경하면 된다고 하는데 사용 환경이 아니라서 검증은 해 보지 못했다.&lt;/li&gt;
&lt;li&gt;
MYSQL 을 사용하는 경우라면 MYSQL command 창에서 &lt;code&gt;set&amp;nbsp;character&amp;nbsp;set&amp;nbsp;euckr;&lt;/code&gt; 명령으로 클라이언트 인코딩을 변경해서 설정할 수 있다. (기본 값은 utf-8)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;&lt;p&gt;Written by Morris (&lt;a href=&quot;mailto:ccambo@gmail.com&quot; target=&quot;_blank&quot;&gt;ccambo@gmail.com&lt;/a&gt; - MSFL)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;</description>
      <category>개발/기타공통</category>
      <category>65001</category>
      <category>949</category>
      <category>charset</category>
      <category>cmd</category>
      <category>Console</category>
      <category>Encoding</category>
      <category>euckr</category>
      <category>my.conf</category>
      <category>mysql</category>
      <category>utf-8</category>
      <category>인코딩</category>
      <category>캐릭터셋</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/26</guid>
      <comments>https://ccambo.tistory.com/entry/Console-%EC%9D%B8%EC%BD%94%EB%94%A9-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0#entry26comment</comments>
      <pubDate>Thu, 6 Apr 2017 11:15:25 +0900</pubDate>
    </item>
    <item>
      <title>[Markdown] 특수문자들을 정리해 보자!</title>
      <link>https://ccambo.tistory.com/entry/Markdown-%ED%8A%B9%EC%88%98%EB%AC%B8%EC%9E%90%EB%93%A4%EC%9D%84-%EC%A0%95%EB%A6%AC%ED%95%B4-%EB%B3%B4%EC%9E%90</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p&gt;마크다운으로 문서를 정리할 때 가끔 또는 자주 예약된 문자 처리 때문에 난처한 경우가 발생한다.&lt;/p&gt;
&lt;p&gt;이 내용을 정리하게 된 것도 마크다운 테이블을 구성하는데 예약어 (&lt;code&gt;'|'&lt;/code&gt;) 문자 때문이다. 일반적으로 &lt;code&gt;'pipe'&lt;/code&gt; 또는 &lt;code&gt;'vertialcal bar'&lt;/code&gt; 라고 표현하는 문자인데 테이블을 구성하는 예약어다.&lt;/p&gt;
&lt;p&gt;마크다운 문법에서 설명하는 것과 같이 여러 가지 방법을 적용해 봤지만 적용되지 않는다.&lt;/p&gt;
&lt;p&gt;해결 방법은 의외로 쉽다. 마크다운이 결국 렌더링되서 HTML 로 표현되는 것이기 때문에 아예 &lt;code&gt;HTML 문자를 사용&lt;/code&gt;하면 된다.&lt;/p&gt;
&lt;p&gt;아래의 표는 참조한 사이트에서 필요한 몇 가지만 발췌한 것이므로 참고 사이트에서 전체 내용을 확인하면 된다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Symbol&lt;/th&gt;&lt;th&gt;HTML Number&lt;/th&gt;&lt;th&gt;HTML Name&lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;!&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#33;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;exclamation point&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#34;&lt;/td&gt;&lt;td&gt;&amp;amp;quot;&lt;/td&gt;&lt;td&gt;double quotes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;#&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#35;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;number sign&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#36;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;dollar sign&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#37;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;percent sign&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#38;&lt;/td&gt;&lt;td&gt;&amp;amp;amp;&lt;/td&gt;&lt;td&gt;ampersand&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;'&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#39;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;single quote&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;(&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#40;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;opening parenthesis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#41;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;closing parenthesis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#42;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;asterisk&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#43;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;plus sign&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;,&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#44;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;comma&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#45;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;minus sign - hyphen&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#46;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;period&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#47;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;slash&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;:&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#58;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;colon&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#59;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;semicolon&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#60;&lt;/td&gt;&lt;td&gt;&amp;amp;lt;&lt;/td&gt;&lt;td&gt;less than sign&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#61;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;equal sign&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#62;&lt;/td&gt;&lt;td&gt;&amp;amp;gt;&lt;/td&gt;&lt;td&gt;greater than sign&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#63;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;question mark&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#64;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;at symbol&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#91;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;opening bracket&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;\&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#92;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;back slash&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#93;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;closing bracket&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#94;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;caret - circumflex&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;_&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#95;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;underscore&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;`&lt;/td&gt;&lt;td&gt;&amp;amp;#96;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;grave accent&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;{&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#123;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;opening brace&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;|&lt;/td&gt;&lt;td&gt;&amp;amp;#124;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;vertical bar - pipe&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#125;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;closing brace&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#126;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;equivalency sign - tilde&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&amp;amp;#8211;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;en dash&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;관련된 문자를 찾고 싶어도 용어를 몰라서 못 찾는 것은 이제 그만!!!&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;References&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;http://www.ascii.cl/htmlcodes.htm&quot;&gt;http://www.ascii.cl/htmlcodes.htm&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;Written by Morris (&lt;a href=&quot;file:///D:\DevWorks\Docs\markdown\mailto:ccambo@gmail.com&quot;&gt;ccambo@gmail.com&lt;/a&gt; - MSFL)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;</description>
      <category>개발/기타언어</category>
      <category>HTML Codes</category>
      <category>markdown</category>
      <category>Special Characters</category>
      <category>마크다운</category>
      <category>특수문자</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/25</guid>
      <comments>https://ccambo.tistory.com/entry/Markdown-%ED%8A%B9%EC%88%98%EB%AC%B8%EC%9E%90%EB%93%A4%EC%9D%84-%EC%A0%95%EB%A6%AC%ED%95%B4-%EB%B3%B4%EC%9E%90#entry25comment</comments>
      <pubDate>Wed, 5 Apr 2017 12:11:05 +0900</pubDate>
    </item>
    <item>
      <title>Node.js 관련 패키지들 정리 (갱신중)</title>
      <link>https://ccambo.tistory.com/entry/Nodejs-%EA%B4%80%EB%A0%A8-%ED%8C%A8%ED%82%A4%EC%A7%80%EB%93%A4-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p&gt;다른 작업을 진행하다가 정말로 오랜만에 다시 Node.js 관련 오픈 소스 분석 작업을 하는데 참조하고 있는 패키지들이 많기도 하다. 아는 것보다는 모르는 것이 더 많아서 뭐가 뭔지 하나도 모르겠다.&lt;/p&gt;
&lt;p&gt;앞으로 작업을 하면서 좀 더 다양한 패키지들을 자세하게 알아야 하지만, 당장은 제목과 기능이라도 알아야 이해를 할 수 있을 듯 하다. ㅠㅠ&lt;/p&gt;
&lt;h1 data-breakpage&gt;Packages for NodeJS&lt;/h1&gt;
&lt;h2&gt;bluebird&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Full featured promise library with unmatched performance&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
npm install bluebird
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
var Promise = require(&amp;quot;bluebird&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='http://bluebirdjs.com/docs/getting-started.html' target='_blank' &gt;http://bluebirdjs.com/docs/getting-started.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href='https://github.com/petkaantonov/bluebird' target='_blank' &gt;https://github.com/petkaantonov/bluebird&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;bunyan&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Simple and fast JSON Logging Module for node.js services&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install bunyan
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
var bunyan = require(&amp;#39;bunyan&amp;#39;);
var log = bunyan.createLogger({name: &amp;quot;myapp&amp;quot;});
log.info(&amp;quot;hi&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/trentm/node-bunyan' target='_blank' &gt;https://github.com/trentm/node-bunyan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;camelcase&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Convert a dash/dot/underscore/space separated string to camelCase: foo-bar -&amp;gt; fooBar&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install --save camelcase
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
const camelCase = require(&amp;#39;camelcase&amp;#39;);

camelCase(&amp;#39;foo-bar&amp;#39;);       //=&amp;gt; &amp;#39;fooBar&amp;#39;
camelCase(&amp;#39;foo_bar&amp;#39;);       //=&amp;gt; &amp;#39;fooBar&amp;#39;
camelCase(&amp;#39;Foo-Bar&amp;#39;);       //=&amp;gt; &amp;#39;fooBar&amp;#39;
camelCase(&amp;#39;--foo.bar&amp;#39;);     //=&amp;gt; &amp;#39;fooBar&amp;#39;
camelCase(&amp;#39;__foo__bar__&amp;#39;);  //=&amp;gt; &amp;#39;fooBar&amp;#39;
camelCase(&amp;#39;foo bar&amp;#39;);       //=&amp;gt; &amp;#39;fooBar&amp;#39;
camelCase(&amp;#39;foo&amp;#39;, &amp;#39;bar&amp;#39;);        //=&amp;gt; &amp;#39;fooBar&amp;#39;
camelCase(&amp;#39;__foo__&amp;#39;, &amp;#39;--bar&amp;#39;);  //=&amp;gt; &amp;#39;fooBar&amp;#39;

console.log(process.argv[3]);   //=&amp;gt; &amp;#39;--foo-bar&amp;#39;
camelCase(process.argv[3]);     //=&amp;gt; &amp;#39;fooBar&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/sindresorhus/camelcase' target='_blank' &gt;https://github.com/sindresorhus/camelcase&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;chai&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;BDD (Behavior-Driven Development) / TDD (Test-Driven Development) Assersion Library for Node&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install bunyan
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
// Should
chai.should();

foo.should.be.a(&amp;#39;string&amp;#39;);
foo.should.equal(&amp;#39;bar&amp;#39;);
foo.should.have.lengthOf(3);
tea.should.have.property(&amp;#39;flavors&amp;#39;).with.lengthOf(3);

// Expect
var expect = chai.expect;

expect(foo).to.be.a(&amp;#39;string&amp;#39;);
expect(foo).to.equal(&amp;#39;bar&amp;#39;);
expect(foo).to.have.lengthOf(3);
expect(tea).to.have.property(&amp;#39;flavors&amp;#39;).with.lengthOf(3);

// Assert
var assert = chai.assert;

assert.typeOf(foo, &amp;#39;string&amp;#39;);
assert.equal(foo, &amp;#39;bar&amp;#39;);
assert.lengthOf(foo, 3)
assert.property(tea, &amp;#39;flavors&amp;#39;);
assert.lengthOf(tea.flavors, 3);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='http://chaijs.com/' target='_blank' &gt;http://chaijs.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;chalk&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Terminal String Styling done right&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install chalk
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
const chalk = require(&amp;#39;chalk&amp;#39;);
console.log(chalk.blue(&amp;#39;Hello world!&amp;#39;));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/chalk/chalk' target='_blank' &gt;https://github.com/chalk/chalk&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;chokidar&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A neat wrapper around node.js fs.watch / fs.watchFile / fsevents.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install chokidar --save
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
var chokidar = require(&amp;#39;chokidar&amp;#39;);

// One-liner for current directory, ignores .dotfiles
chokidar.watch(&amp;#39;.&amp;#39;, {ignored: /(^|[\/\\])\../}).on(&amp;#39;all&amp;#39;, (event, path) =&amp;gt; {
  console.log(event, path);
});
// Example of a more typical implementation structure:

// Initialize watcher.
var watcher = chokidar.watch(&amp;#39;file, dir, glob, or array&amp;#39;, {
  ignored: /(^|[\/\\])\../,
  persistent: true
});

// Something to use when events are received.
var log = console.log.bind(console);
// Add event listeners.
watcher
  .on(&amp;#39;add&amp;#39;, path =&amp;gt; log(`File ${path} has been added`))
  .on(&amp;#39;change&amp;#39;, path =&amp;gt; log(`File ${path} has been changed`))
  .on(&amp;#39;unlink&amp;#39;, path =&amp;gt; log(`File ${path} has been removed`));

// More possible events.
watcher
  .on(&amp;#39;addDir&amp;#39;, path =&amp;gt; log(`Directory ${path} has been added`))
  .on(&amp;#39;unlinkDir&amp;#39;, path =&amp;gt; log(`Directory ${path} has been removed`))
  .on(&amp;#39;error&amp;#39;, error =&amp;gt; log(`Watcher error: ${error}`))
  .on(&amp;#39;ready&amp;#39;, () =&amp;gt; log(&amp;#39;Initial scan complete. Ready for changes&amp;#39;))
  .on(&amp;#39;raw&amp;#39;, (event, path, details) =&amp;gt; {
    log(&amp;#39;Raw event info:&amp;#39;, event, path, details);
  });

// &amp;#39;add&amp;#39;, &amp;#39;addDir&amp;#39; and &amp;#39;change&amp;#39; events also receive stat() results as second
// argument when available: http://nodejs.org/api/fs.html#fs_class_fs_stats
watcher.on(&amp;#39;change&amp;#39;, (path, stats) =&amp;gt; {
  if (stats) console.log(`File ${path} changed size to ${stats.size}`);
});

// Watch new files.
watcher.add(&amp;#39;new-file&amp;#39;);
watcher.add([&amp;#39;new-file-2&amp;#39;, &amp;#39;new-file-3&amp;#39;, &amp;#39;**/other-file*&amp;#39;]);

// Get list of actual paths being watched on the filesystem
var watchedPaths = watcher.getWatched();

// Un-watch some files.
watcher.unwatch(&amp;#39;new-file*&amp;#39;);

// Stop watching.
watcher.close();

// Full list of options. See below for descriptions. (do not use this example)
chokidar.watch(&amp;#39;file&amp;#39;, {
  persistent: true,

  ignored: &amp;#39;*.txt&amp;#39;,
  ignoreInitial: false,
  followSymlinks: true,
  cwd: &amp;#39;.&amp;#39;,

  usePolling: true,
  interval: 100,
  binaryInterval: 300,
  alwaysStat: false,
  depth: 99,
  awaitWriteFinish: {
    stabilityThreshold: 2000,
    pollInterval: 100
  },

  ignorePermissionErrors: false,
  atomic: true // or a custom &amp;#39;atomicity delay&amp;#39;, in milliseconds (default 100)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/paulmillr/chokidar' target='_blank' &gt;https://github.com/paulmillr/chokidar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;escape-string-regexp&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Escape RegExp Special Characters&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install --save escape-string-regexp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
const escapeStringRegexp = require(&amp;#39;escape-string-regexp&amp;#39;);

const escapedString = escapeStringRegexp(&amp;#39;how much $ for a unicorn?&amp;#39;);
//=&amp;gt; &amp;#39;how much \$ for a unicorn\?&amp;#39;

new RegExp(escapedString);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/sindresorhus/escape-string-regexp' target='_blank' &gt;https://github.com/sindresorhus/escape-string-regexp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;eslint&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Pluggable Linting utility for JavaScript and JSX&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install eslint
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
Rule 정의 파일 구성
$ npm run eslint .
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='http://eslint.org/' target='_blank' &gt;http://eslint.org/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;graceful-fs&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;drop-in replacement for the fs module, making various improvements.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install graceful-fs
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
// use just like fs
var fs = require(&amp;#39;graceful-fs&amp;#39;)

// now go and do stuff with it...
fs.readFileSync(&amp;#39;some-file-or-whatever&amp;#39;)

//
// Global Patching
//

// Make sure to read the caveat below.
var realFs = require(&amp;#39;fs&amp;#39;)
var gracefulFs = require(&amp;#39;graceful-fs&amp;#39;)
gracefulFs.gracefulify(realFs)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://www.npmjs.com/package/graceful-fs' target='_blank' &gt;https://www.npmjs.com/package/graceful-fs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;istanbul&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;JS Code Coverage tool written in JS&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install -g istanbul
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ istanbul cover test.js
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/gotwarlost/istanbul' target='_blank' &gt;https://github.com/gotwarlost/istanbul&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;jscs&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Javascript Code Style Checker&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install jscs  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ jscs file.js --preset=airbnb
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='http://jscs.info/' target='_blank' &gt;http://jscs.info/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;minimist&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;guts of optimist&amp;#39;s argument parser without all the fanciful decoration&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install minimist
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
var argv = require(&amp;#39;minimist&amp;#39;)(process.argv.slice(2));
console.dir(argv);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/substack/minimist' target='_blank' &gt;https://github.com/substack/minimist&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;mocha&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Fun, Simple, Flexible Javascript Test Framework&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install mocha
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
Assert 파일 작성
$ mocha test/index.js
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://mochajs.org/' target='_blank' &gt;https://mochajs.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href='https://github.com/mochajs/mocha' target='_blank' &gt;https://github.com/mochajs/mocha&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;nyc&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Istanbul command line interface&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm i nyc --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;package.json script 설정&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-json' lang='json'&gt;
{
  &amp;quot;script&amp;quot;: {
    &amp;quot;test&amp;quot;: &amp;quot;nyc tap ./test/*.js&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;실행&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ nyc npm test
$ nyc --reporter=lcov --reporter=text-lcov npm test
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/istanbuljs/nyc' target='_blank' &gt;https://github.com/istanbuljs/nyc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href='https://istanbul.js.org/' target='_blank' &gt;https://istanbul.js.org/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;require-dir&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Node.js 지원을 위해 지정한 디렉터리 단위로 존재하는 파일들을 일괄 require() 처리&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install require-dir
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
var requireDir = require(&amp;#39;require-dir&amp;#39;);
var dir = requireDir(&amp;#39;./path/to/dir&amp;#39;);
var dir = requireDir(&amp;#39;./path/to/dir&amp;#39;, {recurse: true});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/aseemk/requireDir' target='_blank' &gt;https://github.com/aseemk/requireDir&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;rewire&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Easy monkey-patching for node.js unit tests&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install rewire
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
자세한 활용법은 아래 사이트 설명 참조
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/jhnns/rewire' target='_blank' &gt;https://github.com/jhnns/rewire&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;shelljs, shelljs/shx&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;shelljs: Portable Unix shell commands for Node.js&lt;/strong&gt;
&lt;strong&gt;shelljs/shx: Portable Shell Commands for Node.js&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install shelljs
$ npm install shelljs/shx   # for command line
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법 (shelljs)&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
var shell = require(&amp;#39;shelljs&amp;#39;);

if (!shell.which(&amp;#39;git&amp;#39;)) {
  shell.echo(&amp;#39;Sorry, this script requires git&amp;#39;);
  shell.exit(1);
}

// Copy files to release dir
shell.rm(&amp;#39;-rf&amp;#39;, &amp;#39;out/Release&amp;#39;);
shell.cp(&amp;#39;-R&amp;#39;, &amp;#39;stuff/&amp;#39;, &amp;#39;out/Release&amp;#39;);

// Replace macros in each .js file
shell.cd(&amp;#39;lib&amp;#39;);
shell.ls(&amp;#39;*.js&amp;#39;).forEach(function (file) {
  shell.sed(&amp;#39;-i&amp;#39;, &amp;#39;BUILD_VERSION&amp;#39;, &amp;#39;v0.1.2&amp;#39;, file);
  shell.sed(&amp;#39;-i&amp;#39;, /^.*REMOVE_THIS_LINE.*$/, &amp;#39;&amp;#39;, file);
  shell.sed(&amp;#39;-i&amp;#39;, /.*REPLACE_LINE_WITH_MACRO.*\n/, shell.cat(&amp;#39;macro.js&amp;#39;), file);
});
shell.cd(&amp;#39;..&amp;#39;);

// Run external tool synchronously
if (shell.exec(&amp;#39;git commit -am &amp;quot;Auto-commit&amp;quot;&amp;#39;).code !== 0) {
  shell.echo(&amp;#39;Error: Git commit failed&amp;#39;);
  shell.exit(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법 (shelljs/shx)&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
# used in command line using shx
$ shx mkdir -p foo
$ shx touch foo/bar.txt
$ shx rm -rf foo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/shelljs/shelljs' target='_blank' &gt;https://github.com/shelljs/shelljs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href='http://shelljs.org/' target='_blank' &gt;http://shelljs.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href='https://github.com/shelljs/shx' target='_blank' &gt;https://github.com/shelljs/shx&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;sinon&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Standalone test spies, stubs and mocks for Javascript. work with any unit testing framework.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install sinon
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
자세한 활용법은 아래 사이트 설명 참조
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='http://sinonjs.org/' target='_blank' &gt;http://sinonjs.org/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;standard&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Javascript 표준 스타일 가이드 with Linter &amp;amp; Automatic code fixer&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install standard --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ standard
Error: Use JavaScript Standard Style
  lib/torrent.js:950:11: Expected &amp;#39;===&amp;#39; and instead saw &amp;#39;==&amp;#39;.
$ standard &amp;quot;src/util/**/*.js&amp;quot; &amp;quot;test/**/*.js&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/feross/standard' target='_blank' &gt;https://github.com/feross/standard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href='https://standardjs.com/' target='_blank' &gt;https://standardjs.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;tildify&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Convert an absolute path to the tilde path: /Users/sindresorhus/dev to ~/dev&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install --save tildify
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-js' lang='js'&gt;
const tildify = require(&amp;#39;tildify&amp;#39;);

tildify(&amp;#39;/Users/sindresorhus/dev&amp;#39;);
//=&amp;gt; &amp;#39;~/dev&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='https://github.com/sindresorhus/tildify' target='_blank' &gt;https://github.com/sindresorhus/tildify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;yargs&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Yargs helps you build interactive command line tools by parsing arguments and generating an elegant user interface&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;p&gt;설치&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
$ npm install yargs
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;활용법&lt;/p&gt;
&lt;pre&gt;&lt;code class='language-sh' lang='sh'&gt;
#!/usr/bin/env node

require(&amp;#39;yargs&amp;#39;)
  .usage(&amp;#39;$0 &amp;lt;cmd&amp;gt; [args]&amp;#39;)
  .command(&amp;#39;hello [name]&amp;#39;, &amp;#39;welcome ter yargs!&amp;#39;, {
    name: {
      default: &amp;#39;default name&amp;#39;
    }
  }, function (argv) {
    console.log(&amp;#39;hello&amp;#39;, argv.name, &amp;#39;welcome to yargs!&amp;#39;)
  })
  .help()
  .argv

자세한 활용법은 아래 사이트 설명 참조
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href='http://yargs.js.org/' target='_blank' &gt;http://yargs.js.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href='https://github.com/yargs/yargs' target='_blank' &gt;https://github.com/yargs/yargs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;
&lt;blockquote&gt;&lt;p&gt;Written by Morris (&lt;a href='mailto:ccambo@gmail.com' target='_blank' &gt;ccambo@gmail.com&lt;/a&gt; - MSFL)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;/div&gt;</description>
      <category>개발/기타언어</category>
      <category>bluebird</category>
      <category>bunyan</category>
      <category>chai</category>
      <category>chalk</category>
      <category>eslint</category>
      <category>graceful-fs</category>
      <category>Istanbul</category>
      <category>minimist</category>
      <category>Mocha</category>
      <category>Node</category>
      <category>node.js</category>
      <category>Packages</category>
      <category>require-dir</category>
      <category>Standard</category>
      <category>tildify</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/24</guid>
      <comments>https://ccambo.tistory.com/entry/Nodejs-%EA%B4%80%EB%A0%A8-%ED%8C%A8%ED%82%A4%EC%A7%80%EB%93%A4-%EC%A0%95%EB%A6%AC#entry24comment</comments>
      <pubDate>Tue, 4 Apr 2017 19:45:37 +0900</pubDate>
    </item>
    <item>
      <title>Divide by Zero === Exception?? Really??</title>
      <link>https://ccambo.tistory.com/entry/Divide-by-Zero-Exception-Really</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1 id=&quot;divide-by-zero-exception-really-&quot;&gt;Divide by Zero === Exception?? Really?&lt;/h1&gt;
&lt;p&gt;흔히 산술연산을 처리할 때 0으로 나누면 Divide by Zero Exception 이 발생한다고 알고 있다.
실제 연산 샘플을 구성해 봐도 실제 Exception 이 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-java&quot;&gt;public static void main (String[] args) throws java.lang.Exception
{
    int aa = 100 / 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드는 &lt;code&gt;Exception in thread &quot;main&quot; java.lang.ArithmeticException: / by zero&lt;/code&gt; 예외가 발생한다.&lt;/p&gt;
&lt;p&gt;정말 모든 산술 연산이 이렇게 적용될까??? Really??&lt;/p&gt;
&lt;p&gt;책이나 관련된 정보를 찾아보면 보통 &lt;code&gt;정수를 0으로 나누면&lt;/code&gt; 이라는 전제 조건이 있다.&lt;/p&gt;
&lt;p&gt;그럼 정수가 아닌 경우는 어떻게 될까?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0 이 아닌 부동 소수점 값을 0 으로 나누면 &lt;code&gt;부호 있는 Infinity&lt;/code&gt; 가 되고 예외는 발생하지 않는다.&lt;ul&gt;
&lt;li&gt;1.0 / 0.0 은 Positive-Infinity 가 된다.&lt;/li&gt;
&lt;li&gt;-1.0 / 0.0 은 Negative-Infinity 가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;0 이 아닌 정수를 정수 0 으로 나누면 ArithmeticException 이 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;음!! 위와 같이 정리할 수 있다.&lt;/p&gt;
&lt;p&gt;그런데 &lt;code&gt;0.0 / 0.0&lt;/code&gt; 은 뭐가 나올까?&lt;/p&gt;
&lt;p&gt;결론은 isNaN 상태가 된다. 즉, 연산 불가라고 봐야 한다.&lt;/p&gt;
&lt;p&gt;이런 상황은 실제 변수나 VO 등에 Assign 할 경우에는 예외가 없으니 큰 문제가 없지만 JSON 으로 변환하는 등의 작업을 진행하면 뜬금없이 &quot;Infinity&quot;, &quot;isNaN&quot; 과 같은 값을 만나게 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어디서 저런 값이 나오는지 모르겠다.&lt;/li&gt;
&lt;li&gt;내가 만든 코드에서는 문자열 처리한 적이 없다.&lt;/li&gt;
&lt;li&gt;문자열이 Float, Double,.. 등의 자료형에 입력되면 오류가 발생하기 때문에 내가 만든 코드 문제가 아니다.&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대략 이런 반응들이 나올 듯 하다. ㅠㅠ&lt;/p&gt;
&lt;p&gt;그럼 아래과 같은 코드는 어떤 결과를 보여줄까? 예외는 발생할까?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-java&quot;&gt;public static void main (String[] args) throws java.lang.Exception
{
    Float aa = 100.0F / 0F;
    Double bb = 100.0D / 0D;
    int cc = (int)(100.0F / 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과를 한번씩 확인해 보면 위에서 정리한 내용을 알 수 있고 결과 값을 보면 왜???.....
또 다른 미지의 세계로 검색을 떠나게 될 것이다. ^^&lt;/p&gt;
&lt;p&gt;이를 기반으로 다양한 상황에 적용해서 오류 없는 코드를 만들어 보도록 하자.!!!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;References&lt;/p&gt;
&lt;h2 id=&quot;-http-www-w3resource-com-java-tutorial-java-arithmetic-operators-php&quot;&gt;- &lt;a href=&quot;http://www.w3resource.com/java-tutorial/java-arithmetic-operators.php&quot;&gt;http://www.w3resource.com/java-tutorial/java-arithmetic-operators.php&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Written by Morris (&lt;a href=&quot;mailto:ccambo@gmail.com&quot;&gt;ccambo@gmail.com&lt;/a&gt; - MSFL)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;</description>
      <category>개발/자바</category>
      <category>Divide by Zero Exception</category>
      <category>Double</category>
      <category>float</category>
      <category>Infinity</category>
      <category>isNaN</category>
      <category>부동소수점</category>
      <category>정수</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/23</guid>
      <comments>https://ccambo.tistory.com/entry/Divide-by-Zero-Exception-Really#entry23comment</comments>
      <pubDate>Tue, 28 Mar 2017 12:29:36 +0900</pubDate>
    </item>
    <item>
      <title>[SecureShell] Profile 리스트 정렬하기</title>
      <link>https://ccambo.tistory.com/entry/SecureShell-Profile-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1 id=&quot;secureshell-profile-list-&quot;&gt;SecureShell Profile List 정렬하기&lt;/h1&gt;
&lt;p&gt;AWS, Azure 등의 클라우드 연결에 Chrome Extension인 ScureShell을 이용하고 있다.
이런 저런 연결을 관리하다가 보면 어느새 Profile 순서가 엉망이다. 리스트에서 찾는것도 지겹고 해서 여러 가지 키워드로 검색을 해 보니 결국 수동으로 정리를 하는 방법뿐이 없었다.&lt;/p&gt;
&lt;p&gt;우선 간단하게 Profile id 기준으로 정렬은 다음과 같이 처리하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-javascript&quot;&gt;(function(){
    var list = nassh_.prefs_.get('profile-ids');
    list = list.slice(0);
    var out = [];

    var doprofile = function(){
        var id = list.pop();
        console.log(id);

        if(id){
            var p = nassh_.prefs_.getProfile(id);
            out.push({k: id, v: p.prefRecords_.description.currentValue});
            doprofile();
        } else {
            out.sort(function(a,b){
                    if (a.v &amp;lt; b.v) return -1;
                    if (a.v &amp;gt; b.v) return 1;
                    return 0;
            });
            for(var i=0;i&amp;lt;out.length;i++){
                out[i] = out[i].k;
            }
            if(out.length){
                nassh_.prefs_.set('profile-ids', out);
                console.log('done')
            } else {
                console.log('result empty, please refresh the page and try again.');
            }
        }
    };
    doprofile();
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 스크립트 함수를 &lt;code&gt;Ctrl + Shift + J&lt;/code&gt; 를 사용해서 스크립트 콘솔을 열고 붙여넣고 실행하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;Written by Morris (&lt;a href=&quot;mailto:ccambo@gmail.com&quot;&gt;ccambo@gmail.com&lt;/a&gt; - MSFL)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;</description>
      <category>개발/기타공통</category>
      <category>AWS</category>
      <category>azure</category>
      <category>Cloud</category>
      <category>Profile Sort</category>
      <category>Script Console</category>
      <category>SecureShell</category>
      <category>ssh</category>
      <author>ccambo</author>
      <guid isPermaLink="true">https://ccambo.tistory.com/22</guid>
      <comments>https://ccambo.tistory.com/entry/SecureShell-Profile-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0#entry22comment</comments>
      <pubDate>Sat, 4 Mar 2017 14:38:53 +0900</pubDate>
    </item>
  </channel>
</rss>