반응형

Kubebuilder 의 아키텍처에 대해서 살펴보고 Kubebuilder 로 프로젝트를 생성하는 방법을 알아본다.

Kubebuilder Architeture

[출처: https://book.kubebuilder.io/architecture.html]


위의 다이어그램에서 Kubebuilder 는 controller-runtime 모듈을 사용하는 것을 알 수 있다. 또한 사용자의 비즈니스 로직은 Reconciler 에 위치 시킨다는 것을 알 수 있다.

 

Kubebuilder 로 프로젝트 생성

Kubebuilder 를 사용하기 위해서 사전 준비 작업이 필요하다.


사전 준비 작업

  • go version v1.19.0+
  • docker version 17.03+.
  • kubectl version v1.11.3+.
  • Access to a Kubernetes v1.11.3+ cluster.

 

Kubebuilder 설치

kubebuilder 는 간단히 다운 받아서 설치할 수 있다. ~/bin/ 디렉토리가 path 로 잡혀있기 때문에 다운 받은 바이너리 파일을 이 곳으로 이동시켰다.

$ cd ~/Documents/tmp

$ curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
$ chmod +x kubebuilder
$ mv ~/Documents/tmp/kubebuilder ~/bin/kubebuilder

$ kubebuilder version
--- output ---
Version: main.version{KubeBuilderVersion:"3.10.0", KubernetesVendor:"1.26.1", GitCommit:"0fa57405d4a892efceec3c5a902f634277e30732", BuildDate:"2023-04-15T08:10:35Z", GoOs:"darwin", GoArch:"amd64"}

 

kustomize 설치

$ cd ~/Documents/tmp

$ curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"  | bash
$ mv ~/Documents/tmp/kustomize ~/bin/kustomize

$ kustomize version
--- output ---
v5.0.3

 

kind 설치 및 cluster 생성

$ cd ~/Documents/tmp

$ [ $(uname -m) = x86_64 ]&& curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.19.0/kind-darwin-amd64
$ chmod +x kind
$ mv ~/Documents/tmp/kind ~/bin/kind

$ kind version
--- output ---
kind v0.19.0 go1.20.4 darwin/amd64

$ kind create cluster

 

kubectl 설치

$ cd ~/Documents/tmp

$ curl -LO "https://dl.k8s.io/release/v1.27.1/bin/darwin/amd64/kubectl"
$ chmod +x kubectl
$ mv kubectl ~/bin/kubectl

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"27", GitVersion:"v1.27.1", GitCommit:"4c9411232e10168d7b050c49a1b59f6df9d7ea4b", GitTreeState:"clean", BuildDate:"2023-04-14T13:21:19Z", GoVersion:"go1.20.3", Compiler:"gc", Platform:"darwin/amd64"}
Kustomize Version: v5.0.1
Server Version: version.Info{Major:"1", Minor:"27", GitVersion:"v1.27.1", GitCommit:"4c9411232e10168d7b050c49a1b59f6df9d7ea4b", GitTreeState:"clean", BuildDate:"2023-05-12T19:03:40Z", GoVersion:"go1.20.3", Compiler:"gc", Platform:"linux/amd64"}

 


프로젝트 생성

kubebuilder 명령어로 간단히 프로젝트와 API 를 생성할 수 있다. 즉, 필요한 코드들이 자동으로 생성된다.

먼저 프로젝트를 생성한다.

$ mkdir -p guestbook-kubebuilder
$ cd guestbook-kubebuilder

$ kubebuilder init --domain my.domain --repo my.domain/guestbook
--- output ---
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.14.4
go: downloading sigs.k8s.io/controller-runtime v0.14.4
go: downloading k8s.io/apimachinery v0.26.1
go: downloading github.com/prometheus/client_golang v1.14.0
go: downloading k8s.io/client-go v0.26.1
go: downloading k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
go: downloading github.com/prometheus/client_model v0.3.0
go: downloading k8s.io/api v0.26.1
go: downloading k8s.io/component-base v0.26.1
go: downloading golang.org/x/time v0.3.0
go: downloading k8s.io/apiextensions-apiserver v0.26.1
go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.2
go: downloading golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10
go: downloading github.com/imdario/mergo v0.3.6
go: downloading k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280
go: downloading golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
Update dependencies:
$ go mod tidy
go: downloading go.uber.org/goleak v1.2.0
Next: define a resource with:
$ kubebuilder create api

 

다음으로 api 를 생성한다.

$ kubebuilder create api --group webapp --version v1 --kind Guestbook
--- output ---
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/guestbook_types.go
api/v1/groupversion_info.go
internal/controller/suite_test.go
internal/controller/guestbook_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin
test -s /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen && /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen --version | grep -q v0.11.3 || \
    GOBIN=/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.3
go: downloading sigs.k8s.io/controller-tools v0.11.3
go: downloading golang.org/x/tools v0.4.0
go: downloading k8s.io/utils v0.0.0-20221107191617-1a15be271d1d
go: downloading github.com/mattn/go-colorable v0.1.9
/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests

CR 이나 CRD 를 수정하면 마지막의 make manifests 를 수행하여 다신 generation 해야 한다고 친절히 알려주고 있다.

CR 과 CRD 는 아래 guestbook_types.go 파일에 struct 로 생성되어 있다. 이곳을 원하는 대로 변경하면 된다.

 


테스트로 아래과 같이 변경하자.

// GuestbookSpec defines the desired state of Guestbook
type GuestbookSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // Quantity of instances
    // +kubebuilder:validation:Minimum=1
    // +kubebuilder:validation:Maximum=10
    Size int32 `json:"size"`

    // Name of the ConfigMap for GuestbookSpec's configuration
    // +kubebuilder:validation:MaxLength=15
    // +kubebuilder:validation:MinLength=1
    ConfigMapName string `json:"configMapName"`

    // +kubebuilder:validation:Enum=Phone;Address;Name
    Type string `json:"alias,omitempty"`
}

// GuestbookStatus defines the observed state of Guestbook
type GuestbookStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // PodName of the active Guestbook node.
    Active string `json:"active"`

    // PodNames of the standby Guestbook nodes.
    Standby []string `json:"standby"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Guestbook is the Schema for the guestbooks API
type Guestbook struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   GuestbookSpec   `json:"spec,omitempty"`
    Status GuestbookStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// GuestbookList contains a list of Guestbook
type GuestbookList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`
    Items           []Guestbook `json:"items"`
}

Guestbook struct 에 있는 metav1.TypeMetametav1.ObjectMeta 를 설명하면, 전자는 우리가 흔히 보는 yaml 파일에서 apiVersionKind 이고 후자는 metadataname, namespace 등을 나타낸다. 다음에 우리가 정의한 SpecStatus 가 있음을 알 수 있다.



테스트 방법 1 - Cluster 밖에서 테스트 하기

CRD 를 cluster 에 설치한다.

$ make install
--- output ---
/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/kustomize || { curl -Ss "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" --output install_kustomize.sh && bash install_kustomize.sh 5.0.0 /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin; rm install_kustomize.sh; }
v5.0.0
kustomize installed to /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/kustomize
/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/guestbooks.webapp.my.domain created

 

controller 를 실행시킨다. (터미널에서 포그라운드로 실행한다)

$ make run
--- output ---
test -s /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen && /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen --version | grep -q v0.11.3 || \
        GOBIN=/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.3
/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...

go run ./cmd/main.go
2023-05-24T17:18:18+09:00       INFO    controller-runtime.metrics      Metrics server is starting to listen    {"addr": ":8080"}
2023-05-24T17:18:18+09:00       INFO    setup   starting manager
2023-05-24T17:18:18+09:00       INFO    Starting server {"kind": "health probe", "addr": "[::]:8081"}
2023-05-24T17:18:18+09:00       INFO    Starting server {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"}
2023-05-24T17:18:18+09:00       INFO    Starting EventSource    {"controller": "guestbook", "controllerGroup": "webapp.my.domain", "controllerKind": "Guestbook", "source": "kind source: *v1.Guestbook"}
2023-05-24T17:18:18+09:00       INFO    Starting Controller     {"controller": "guestbook", "controllerGroup": "webapp.my.domain", "controllerKind": "Guestbook"}
2023-05-24T17:18:18+09:00       INFO    Starting workers        {"controller": "guestbook", "controllerGroup": "webapp.my.domain", "controllerKind": "Guestbook", "worker count": 1}

참고로 앞서 api 를 생성할 때 Create Resource [y/n] y 로 했다면 CR 이 config/samples 디렉토리 아래에 생성되어 있다.


여기에 Spec 부분만 추가한다.

$ tree config/samples
config/samples
├── kustomization.yaml
└── webapp_v1_guestbook.yaml

$ vi config/samples/webapp_v1_guestbook.yaml
--- output ---
apiVersion: webapp.my.domain/v1
kind: Guestbook
metadata:
  labels:
    app.kubernetes.io/name: guestbook
    app.kubernetes.io/instance: guestbook-sample
    app.kubernetes.io/part-of: guestbook-kubebuilder
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: guestbook-kubebuilder
  name: guestbook-sample
spec:
  # TODO(user): Add fields here
  size: 1
  configMapName: "myconfig"
  alias: "Address"

 

터미널을 새로 열어서 이를 설치한다.

$ kubectl apply -k config/samples/
--- output ---
guestbook.webapp.my.domain/guestbook-sample created

$ kubectl get guestbook
--- output ---           
NAME               AGE
guestbook-sample   29s


테스트 방법 2 - Cluster 안에서 돌리기

controller 를 cluster 안에서 돌리기 위해서는 먼저 이미지를 만들어야 한다.

$ docker login -u seungkyua
--- output ---         
Password: 
Login Succeeded

$ make docker-build docker-push IMG=docker.io/seungkyua/guestbook-kubebuilder:1.0

 

다음은 image 를 가지고 deploy 한다.

$ make deploy IMG=docker.io/seungkyua/guestbook-kubebuilder:1.0
--- output ---
test -s /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen && /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen --version | grep -q v0.11.3 || \
        GOBIN=/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.3
/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/kustomize || { curl -Ss "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" --output install_kustomize.sh && bash install_kustomize.sh 5.0.0 /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin; rm install_kustomize.sh; }
cd config/manager && /Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/kustomize edit set image controller=docker.io/seungkyua/guestbook-kubebuilder:1.0
/Users/ahnsk/Documents/github.com/seungkyua/guestbook-kubebuilder/bin/kustomize build config/default | kubectl apply -f -
# Warning: 'patchesStrategicMerge' is deprecated. Please use 'patches' instead. Run 'kustomize edit fix' to update your Kustomization automatically.
namespace/guestbook-kubebuilder-system created
customresourcedefinition.apiextensions.k8s.io/guestbooks.webapp.my.domain configured
serviceaccount/guestbook-kubebuilder-controller-manager created
role.rbac.authorization.k8s.io/guestbook-kubebuilder-leader-election-role created
clusterrole.rbac.authorization.k8s.io/guestbook-kubebuilder-manager-role created
clusterrole.rbac.authorization.k8s.io/guestbook-kubebuilder-metrics-reader created
clusterrole.rbac.authorization.k8s.io/guestbook-kubebuilder-proxy-role created
rolebinding.rbac.authorization.k8s.io/guestbook-kubebuilder-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/guestbook-kubebuilder-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/guestbook-kubebuilder-proxy-rolebinding created
service/guestbook-kubebuilder-controller-manager-metrics-service created
deployment.apps/guestbook-kubebuilder-controller-manager created

 

확인하면 다음과 같이 pod 가 설치된 것을 알 수 있다.

$ kubectl get pods -n guestbook-kubebuilder-system 
--- output ---
NAME                                                        READY   STATUS    RESTARTS   AGE
guestbook-kubebuilder-controller-manager-5f74f9d765-r68gn   2/2     Running   0          2m55s

 


삭제하기

crd 삭제

$ make uninstall

 

Cluster 에 설치된 controller 삭제

$ make undeploy
반응형
Posted by seungkyua@gmail.com
,
반응형

DevOps 는 개발자와 운영자의 역할을 함께 수행하는 것으로 개발과 운영의 책임을 공동으로 진다. 처음 이 단어를 접한 것이 2011년 OpenStack Summit 에 참석했을 때인데 클라우드, 그 중에서 IaaS(Infrastructure as a Service)가 널리 퍼지기 시작했을 때다. DevOps 는 클라우드 기반에서 빠르게 개발하고, 배포하고, 운영하기 위해서 스타트업 회사를 중심으로 빠르게 퍼지기 시작했다.

아래는 클라우드 가상머신 기반의 DevOps 영역 중 CI/CD 에 대한 프로세스이다. 개발 언어는 Java 를 기준으로 표현하였으며, 이하 모든 설명은 Java 를 기준으로 설명한다.

문제

2015년 7월 쿠버네티스 1.0 버전이 릴리즈 되면서 DevOps 는 가상머신이 아니라 컨테이너 기반으로 점차 변화하였다. 쿠버네티스 이전의 Ops는 가상머신을 빠르게 만들고 개발된 소스 코드를 자동으로 통합 빌드하여 배포하는 영역이었다. 하지만 쿠버네티스가 나오고, 컨테이너 관리가 효율적/안정적으로 변하면서 Ops 는 소스 코드 통합 빌드, 컨테이너 이미지 만들기와 쿠버네티스에 배포, 운영하는 영역으로 바뀌었다. 즉, Ops 영역을 맡은 운영자는 컨테이너도 알아야 하고, 쿠버네티스도 알아야 한다는 의미이다.

결국 쿠버네티스 기반의 DevOps 는 소스 코드 개발, 통합 빌드, 컨테이너 이미지화, 배포의 영역 모두를 의미한다. 이를 간략하게 프로세스로 표시하면 다음과 같다.

여기서 부터 문제가 발생한다. 기존의 Dev 역할은 인프라스트럭처가 가상 머신이든 쿠버네티스이든 상관이 없지만 Ops 역할은 컨테이너와 쿠버네티스라는 새로운 기술을 알아야 하는데 해당 기술을 습득하기까지는 어느 정도의 기술 허들을 넘어야 하고 일정 기간이 지나야 한다. (클라우드 기술이 널리 퍼지기까지 기간을 생각해 보면 쉽게 이해될 것이다)

또한 배포 영역을 생각해 보면 결코 쉬운 문제가 아니다. 배포 전략에는 아래와 같은 3가지 방법이 존재한다. (크게는 4가지 이지만 가장 단순한 Recreate 배포는 생략하였다)

  • 이미지 출처: 쿠버네티스 패턴 (책만출판사)

사족이지만 카나리아 배포를 “까나리”라고 발음하지 말자. “까나리”는 액젓이다.

해결책

개발자는 개발의 영역 즉, Dev 영역에 집중하게 하자. 어려운 Ops 영역은 시스템으로 자동으로 동작하도록 제공하자.

앞서 간단히 살펴본 개발/배포 프로세스를 다시 살펴보자.

  1. 개발자가 IDE 툴을 통해 프로그램을 개발한다.
  2. Maven 혹은 Gradle 로 소스 코드를 빌드한다. 로컬 빌드, Jenkins 혹은 기타 다른 CI 툴을 활용한 빌드 결과물로 jar 혹은 war 파일이 생성된다. 일반적으로 스프링 부트 프로젝트는 jar 파일로 만들어지며, war 파일은 일반 스프링 프로젝트이다. 해당 결과 파일은 저장소(e.q. nexus)에 저장된다.
  3. jar 혹은 war 파일을 로컬 빌드 혹은 기타 다른 CI 툴을 활용하여 컨테이너 이미지로 빌드한다.
  4. 빌드된 컨테이너 이미지를 이미지 저장소에 저장한다.
  5. 쿠버네티스에 배포하기 위해 deployment.yaml, service.yaml 등을 포함한 helm chart 를 만들고 이를 서버에 배포한다. 배포할 때는 배포 전략에 따라서 Rolling update, Blue-Green, Canary 로 배포한다.

1번과 2번은 개발자가 이제까지 하던 방식 그대로 개발하면 된다. 우리가 시스템으로 만들어 제공해야 할 부분은 3, 4, 5 번 영역이다.

구현 방법

해당 시스템에 대한 아키텍처를 구성하면 다음과 같다.

사용 오픈소스 S/W

  • Nexus
    • Maven 저장소로 사용되며 테스트 용도의 jar 파일을 저장하고 다운로드 할 수 있음
  • Keycloak
    • 인증 서버로 활용
    • OIDC 접속 백엔드로 활용할 수 있음
  • Argo Workflow
    • CNCF Graduated Project
    • 워크플로우 서버로 파이프라인을 설계하고 실행할 수 있음.
    • 워크플로우 템플릿을 작성하면 재사용 가능함
    • 워크플로우 실행은 컨테이너 단위로 실행됨
  • Harbor
    • CNCF Graduated Project
    • 이미지 저장소로 활용
  • Gitea
    • Helm chart 저장소로 활용
  • Helm
    • Helm chart template 관리
  • Argo Rollout
    • 배포 전략을 다양하게 지원함
    • 지원 배포 전략: Rolling update, Blue-Green, Canary

프로세스 설명

  1. 사용자는 cli (golang)로 앱 배포를 요청한다.
  2. API 서버 (golang) 는 해당 요청을 받아서 Argo workflow 를 호출한다.
  3. Argo Workflow 에서 Nexus 로 부터 jar 파일을 가져온다.
  4. Argo Workflow 에서 jar 파일을 컨테이너 이미지 파일로 빌드하고 이미지 저장소인 하버에 저장한다.
  5. Argo Workflow 에서 이미지를 가져온다.
  6. Argo Workflow 에서 Helm chart 템플릿을 가져온다.
  7. Argo Workflow 에서 Helm chart 와 이미지를 조합하여 Argo Rollout 으로 배포한다.
  8. Argo Rollout 은 초기 배포를 Blue 로 배포한다. 배포된 Blue 는 로드 발랜서와 연결된다.
  9. 사용자가 Cli 로 앱 업그레이드를 Blue-Green 전략으로 요청한다.
  10. Argo Workflow 에서는 Rollout 으로 Green 으로 배포한다.
반응형
Posted by seungkyua@gmail.com
,
반응형

Kubernetes custom controller 개발에 가장 잘 맞는 프로그래밍 언어는 Go 이다. Kubernetes 가 Go 로 개발된 S/W 이다 보니 Custom controller 도 Go 로 만드는 것이 좋을 것 같다는 생각이다.

그래서 겸사겸사 Custom Controller 개발에 필요한 Go 문법만 정리해 보기로 했다.

  • 변수 선언 : var, Short Variable Declaration Operator(:=)
  • Package 선언 및 활용
  • Struct (json 으로 변환)
  • Receiver function
  • Interface 선언, 활용

변수 선언

변수는 var 키워드로 쉽게 선언할 수 있다.

// var 변수명 변수타입
var message string

변수를 선언하면서 값을 대입하면 마지막의 변수 타입은 생략 가능하다.

var message = "Hi, Welcome!"

:= operator 를 사용하면 shortcut 으로 선언하여 var 도 생략할 수 있다.

message := "Hi, Welcome!"

Package 선언 및 활용

모듈로 만들어서 import 하여 사용할 수 있다.

아래와 같이 디렉토리를 만들어보자. greetings 는 모듈로 선언하고 basic 에서 greetings 모듈을 import 하여 사용할 예정이다.

$ mkdir -p go-sample
$ cd go-sample

$ mkdir -p {basic,greetings}
$ cd greetings

go-sample 이라는 프로젝트 아래에 greetings 라는 모듈을 만든다.

모듈을 만들기 위해서 go.mod 파일을 만든다.

$ go mod init github.com/seungkyua/go-sample/greetings

go.mod 파일이 생성되며 파일 내용은 아래와 같다.

module github.com/seungkyua/go-sample/greetings

go 1.19

greetings.go 파일을 만들어서 아래와 같이 입력한다.

여기도 error 핸들링과 string format 을 위해서 모듈을 import 하고 있는데 이를 이해할려고 하지 말고 그냥 Hello function 이 있다는 것만 이해하자.

package greetings

import (
	"errors"
	"fmt"
)

func Hello(name string) (string, error) {
	if name == "" {
		return "", errors.New("empty name")
	}

	message := fmt.Sprintf("Hi, %v. Welcome!", name)
	return message, nil
}

go-sample 홈디렉토리에서 보는 greetings 디렉토리 구조는 아래와 같다.

greetings
├── go.mod
└── greetings.go

golang 1.19 버전 부터는 로컬 하위 디렉토리를 인식하기 위해서 [go.work](<http://go.work>) 를 사용한다. 그리니 아래와 같이 하여 파일을 만들어 보자.

$ go work use ./basic
$ go work use ./greetins

go-sample 홈디렉토리 아래에 [go.work](<http://go.work>) 파일이 아래와 같이 만들어 진다.

go 1.19

use (
	./greetings
	./basic
)

이제 basic 디렉토리로 가서 똑같이 go.mod 파일을 만들고 main.go 파일도 만들어 본다.

$ cd basic

$ go mod init github.com/seungkyua/go-sample/basic

go.mod 파일이 아래와 같이 생성되었음을 알 수 있다.

module github.com/seungkyua/go-sample/basic

go 1.19

greetings 모듈을 로컬로 인식하게 변경한다.

$ go mod edit -replace github.com/seungkyua/go-sample/greetings=../greetings

그리고 로컬 버전을 사용하기 위해서 pseudo 버전을 사용하게 tidy 명령을 활용한다.

$ go mod tidy

그럼 최종 go.mod 는 다음과 같다.

module github.com/seungkyua/go-sample/basic

go 1.19

// go mod edit 으로 로컬 경로를 보도록 수정
replace github.com/seungkyua/go-sample/greetings => ../greetings

// 로컬이므로 pseudo 버전을 만든다 
require github.com/seungkyua/go-sample/greetings v0.0.0-00010101000000-000000000000

main.go 파일을 만들어서 greetings 모듈을 import 하여 활용해 본다.

package main

import (
	"fmt"
	"log"

	"github.com/seungkyua/go-sample/greetings"
)

func main() {
	message, err := greetings.Hello("Seungkyu")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(message)
}

go-sample 디렉토리 구조는 아래와 같다.

basic
├── go.mod
└── main.go
greetings
├── go.mod
└── greetings.go
go.work

Struct 활용 (json 변환)

struct 를 json data 로 변환하는 것을 marshal (encoding) 이라고 하고 json data를 struct 로 변환하는 것을 unmarshal (decoding) 이라고 한다.

json 패키지의 Marshal function 은 다음과 같다.

func Marshal(v interface{}) ([]byte, error)

Struct 를 만들어서 json data (byte slice) 로 변환해 보자.

type album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

a := album{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}
b, err := json.Marshal(a)
if err != nil {
	log.Fatal(err)
}

b2 := []byte(`{"id":"1","title":"Blue Train","artist":"John Coltrane","price":56.99}`)
fmt.Println(bytes.Equal(b, b2))

Unmarshal function 은 다음과 같다.

func Unmarshal(data []byte, v interface{}) error

json data (byte slice) 를 struct 로 다시 변환한다.

var a2 album
err = json.Unmarshal(b, &a2)
if err != nil {
	log.Fatal(err)
}
fmt.Println(a2)

Receiver Function

Receiver function 은 struct 의 멤버 변수를 활용할 수 있는 function 이다.

Vertex struct 를 만들고 그에 속한 멤버 변수를 활용하는 function 을 만들면 된다.

func 와 함수명 사이에 Struct 를 변수와 함께 넣으면 Receiver function 이 된다.

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

아래와 같이 main 함수를 실행하면 값은 50 이 나온다.

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}

그런데 만약 위의 func (v *Vertex) Scale(f float64) **에서 Vertex 를 포인터가 아닌 value 로 만들면 어떤 결과가 나올까? 위의 함수에서 * 를 지우고 다시 실행해 보자.

결과는 5 가 된다.

즉, Struct 의 멤버 변수의 값을 바꾸고 싶으면 Pointer Receiver 를 사용해야 한다.

Interface 선언, 활용

Interface 는 method 를 모아둔 것이라 할 수 있다. interface 역시 type 키워드로 선언하기 때문에 interface 타입이라고도 말한다.

아래와 같이 Abs() 메소드를 선언하고 나서 Abs 를 Receiver function 으로 구현했다면 Abs 를 구현한 타입은 Abser 타입이라고 할 수 있다.

type Abser interface {
	Abs() float64
}

아래는 MyFloat 타입도 Abser 타입이라고 할 수 있다. 하지만 Abs 의 Receiver 는 value Receiver 이다.

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

Vertex 타입도 역시 Abser 타입이며 pointer Receiver 이다.

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

main 함수에서 interface 를 활용해 본다.

a = v 에서 에러가 발생한다.

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	a = f  // MyFloat 은 Abser 인터페이스를 구현했으니 가능함
	a = &v // *Vertex 는 Abser 인터페이스를 구현했으니 가능함

	a = v  // v 는 Vertext 타입임 (*Vertex 타입이 아님), value receiver 는 구현안했으니 에러 

	fmt.Println(a.Abs())
}

 

Interface 선언, 활용 (2)

interface 는 interface 를 포함할 수 있다.

아래와 같이 Client interface 가 Reader interface 를 가지면 Reader interface 에 선언된 함수가 그 대로 Client 에도 속하게 된다.

package interfaces

import (
	"fmt"
)

type Reader interface {
	Get(obj string) error
	List(list []string) error
}

type Client interface {
	Reader
}

var ReconcileClient Client = &client{}

type client struct {
	name string
}

func (c *client) Get(obj string) error {
	fmt.Println(obj)
	return nil
}

func (c *client) List(list []string) error {
	fmt.Println(list)
	return nil
}

그런 다음 struct 에서 interface 를 또 다시 포함할 수 있다.

아래와 같이 GuestbookReconciler struct 에 Client interface 를 포함하면 GuestbookReconciler 는 마치 Client interface 가 가지는 함수를 자신의 메소드 처럼 사용할 수 있다.

package main

import "github.com/seungkyua/go-sample/interfaces"

type GuestbookReconciler struct {
	interfaces.Client
}

func main() {
	g := GuestbookReconciler{interfaces.ReconcileClient}
	g.Get("Seungkyu")
	g.Client.Get("Seungkyu")
}

이 때 메소드 활용은 g.Get 혹은 g.Client.Get 둘 다 가능하다.

반응형
Posted by seungkyua@gmail.com
,
반응형

지난 설명에 이어서 이번에는 tmux 의 설정에 대해서 자세히 살펴보자.

tmux 는 conf 파일로 단축키와 기능을 설정할 수 있다.

tmux.conf 설정 파일

tmux.conf 파일을 홈디렉토리 아래에 다음과 같이 생성한다.

$ vi ~/.tmux.conf

bind r source-file ~/.tmux.conf \; display "Reloaded!"

위의 설정은 현재의 session 에서 설정 파일인 tmux.conf 을 동적으로 바로 리로딩하는 기능을 가진다.

새로운 session 을 만들고 설정 파일을 리로딩해보자.

$ tmux new -s sample

이제 prefix + r 을 해보면 설정 파일이 리로딩된 것을 볼 수 있다. (prefixctrl + b 를 나타내며 Reloaded! 문구가 나타난 후 바로 삭제돼서 화면 캡쳐가 어렵다. ㅠㅠ)

설정 파일에 적용할 수 있는 내용을 더 알아보자.

set -s escape-time 1

prefixcommand 사이에 인식할 수 있는 간격이 1초라는 의미이다. 리로딩의 경우 prefix 를 누르고 r 을 1초 이내에 눌러야 한다.

set -g base-index 1
setw -g pane-base-index 1

윈도우와 패인의 번호가 0 번이 아닌 1 번 부터 시작한다.

bind -r H resize-pane -L 5
bind -r J resize-pane -D 5
bind -r K resize-pane -U 5
bind -r L resize-pane -R 5

패인의 사이즈를 조절할 수 있다. prefix + H 는 왼쪽으로 5 만큼 늘린다는 의미이며, 수평(좌우)으로 2개의 패인으로 나눠진 경우에서 우측 패인에서 사용할 수 있다. 패인이 수직(위아래)으로 나눠진 경우에는 prefix + H 가 아니라 prefix + J 혹은 K 를 사용할 수 있다.

아래 이미지를 보면 오른쪽 패인이 조금 더 넓은 것을 볼 수 있다.

set -g mouse on

마우스 클릭으로 패인의 포커스를 이동할 수 있다.

setw -g pane-border-style fg=green,bg=black
setw -g pane-active-border-style fg=white,bg=yellow

현재 선택된 패인을 컬러바로 보여준다. 바로 위의 그림에서 노란색바가 아래에 있는데 이는 우측 패인이 선택되었다는 의미이다.

set -g status-left-length 40
set -g status-left "#[fg=green][#S] "

맨 아래 status 라인 왼쪽 사이드에 세션의 이름을 녹색으로 보여준다. 여기서는 [sample] 로 보여진다.

bind _ split-window -v -c "#{pane_current_path}"
bind / split-window -h -c "#{pane_current_path}"

패인을 나누면서 현재 패인의 디렉토리를 그대로 적용해 준다. prefix + % 는 수평(좌우)로 나누지만 세션을 만들 때의 디렉토리를 적용해준다. 하지만 prefix + / 는 수평(좌우)로 나누면서도 좌측 패인의 디렉토리를 우측패인에 그대로 적용해 준다.

아래 seungkyua 라는 디렉토리 위치가 그대로 적용된 것을 볼 수 있

Window / Pane 추가 기능

window 옮기기

세션에 있는 윈도우를 또 다른 세션으로 옮길 수 있으며, 서로 다른 윈도우를 하나의 윈도우에 패인으로 나눠서 통합할 수 있으며, 윈도우에 있는 패인을 또 다른 윈도우로 옮길 수 있다.

추가로 panes 세션을 만들고 거기에 first 윈도우를 만든다.

$ tmux new -s panes -n first -d

panes 세션에 second 윈도우를 만든다.

$ tmux new-window -t panes -n second

panes 세션에 들어간다. 윈도우는 second 윈도우에 접속된다.

$ tmux attach -t panes

first 윈도우를 second 윈도우에 패인으로 붙힌다.

prefix + :               ---> 명령 모드로 변경됨

join-pane -s panes:1     ---> 명령 모드에서 명령어를 실행

결과로는 first 윈도우가 second 윈도우로 합쳐지면서 second 윈도우는 2개의 패인으로 남는다.

패인 layout 바꾸기

prefix + space 는 수직(위아래) 패인 레이아웃을 수평(좌우) 패인 레이아웃으로 바꿀 수 있다. 이 때 토글로 작동한다.

여러 패인에 동시에 명령어 실행하기

prefix + ctrl + s 로 여러 패인에 동시에 명령어를 실행할 수 있다. 이 명령어도 토글로 작동된다.

현재 패인을 새로운 윈도우로 옮기기

prefix + ! 는 현재 패인을 새로운 윈도우로 만들면서 옮길 수 있다. 위에 2:second 윈도우가 있고 우측 패인이 있는데, 우측 패인에서 해당 키를 수행하면 1:zsh 윈도우가 만들어지면서 해당 패인이 윈도우가 된다.

패인 최대화하기

prefix + z 는 현재 패인을 최대화해서 보여준다. 보통 패인은 창을 나눠서 쓰기 때문에 화면이 좁을 수 있는데 일시적으로 현재 패인을 전체화면 윈도우로 만들어 준다고 생각하면 쉽다.

토글 기능이라 전체화면 윈도우에서 prefix + z 를 수행하면 원래 패인 크기도 돌아온다.

최대화가 되면 화면 아래에 status 에서 패인이름 끝에 Z 표시가 붙어 있어 현재 상태를 알기 쉽게 되어 있다.

반응형
Posted by seungkyua@gmail.com
,
반응형

이전 글에서는 Kubernetes Cluster 상에서 App 을 Scratch 방식으로 Blue/Green 배포를 하였다. 이번에는 Argo Rollout 을 사용한 Blue/Green 배포하는 방식을 살표보자.

Nginx 혹은 AWS ALB 를 직접 연결하여 사용할 수 있지만, Blue/Green 배포는 Traffic Shifting 이 필요하지 않으므로 AWS LB → Ingress Controller 를 연결한 상태를 만들어 놓고 배포하는 방식을 설명한다.

1. Argo Rollout 설치

helm chart 를 이용하여 argo rollout 을 설치한다.

argo rollout dashboard 를 포햄하여 설치하고 싶으면 dashboard.enabled=true 를 추가하면 된다.

$ helm repo add argo https://argoproj.github.io/argo-helm
$ helm repo update

$ helm search repo argo/argo-rollouts -l
NAME                    CHART VERSION   APP VERSION     DESCRIPTION
argo/argo-rollouts      2.22.2          v1.4.0          A Helm chart for Argo Rollouts
argo/argo-rollouts      2.22.1          v1.4.0          A Helm chart for Argo Rollouts

$ helm upgrade -i argo-rollout argo/argo-rollouts --version 2.22.2 -n argo --set dashboard.enabled=true --create-namespace

argo rollout dashboard 는 인증 체계가 없다. 그러므로 포트 포워딩으로 dashboard 에 접속 하는 것을 추천한다.

$ kubectl port-forward service/argo-rollouts-dashboard 31000:3100

2. Argo Rollout kubectl plugin 설치

kubectl 로 cli 호출이 가능하도록 plugin 을 설치한다.

$ curl -LO https://github.com/argoproj/argo-rollouts/releases/download/v1.4.0/kubectl-argo-rollouts-linux-amd64
$ chmod +x kubectl-argo-rollouts-linux-amd64
$ sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

$ kubectl argo rollouts version
--- output ---
kubectl-argo-rollouts: v1.4.0+e40c9fe
  BuildDate: 2023-01-09T20:20:38Z
  GitCommit: e40c9fe8a2f7fee9d8ee1c56b4c6c7b983fce135
  GitTreeState: clean
  GoVersion: go1.19.4
  Compiler: gc
  Platform: linux/amd64

argo rollout bash complete 도 설치한다.

$ kubectl argo rollouts completion bash | tee /home/ubuntu/.kube/kubectl-argo-rollouts > /dev/null

$ vi ~/.bash_profile
source '/home/ubuntu/.kube/completion.bash.inc'
source '/home/ubuntu/.kube/kubectl-argo-rollouts'

PATH=/home/ubuntu/bin:$PATH

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

3. 최초 App 배포 (Blue Deployment)

초기 app (blue)을 배포한다. 이전 글에서 사용된 seungkyua/nginx:blue 이미지를 배포한다. 단 replicas 를 0 으로 배포한다. 이렇게 배포하면 실제 pod 는 실행되지 않지만 pod template 은 배포된 상태가 된다. pod template 은 나중에 rollout 에서 참조하여 사용한다.

$ cat nginx-blue-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue-green
  name: nginx-blue-green
spec:
  replicas: 0
  selector:
    matchLabels:
      app: nginx-blue-green
      version: blue-green
  template:
    metadata:
      labels:
        app: nginx-blue-green
        version: blue-green
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx

$ kubectl apply -f nginx-blue-deploy.yaml

$ kubectl get deploy,pod
NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-blue-green   0/0     0            0           11s

Service 를 배포한다.

$ cat nginx-blue-green-svc.yaml
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: nginx-blue-green
  name: nginx-blue-green-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue-green
    version: blue-green
  type: ClusterIP

$ kubectl apply -f nginx-blue-green-svc.yaml

그리고 blue deployment app 에 웹접속이 가능하게 ingress 를 배포한다. ingress 를 배포하더라도 아직 웹 접속은 불가능하다. 앞에서 deployment 의 replicas 를 0 으로 생성했기 때문에 실행되고 있는 pod 가 없기 때문이다. (나중에 접속을 위해서 /etc/hostsnginx-blue-green.taco-cat.xyz 를 등록해 놓자)

$ cat nginx-blue-green-ingress.yaml

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-blue-green-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: nginx-blue-green.taco-cat.xyz
    http:
      paths:
      - pathType: ImplementationSpecific
        backend:
          service:
            name: nginx-blue-green-svc
            port:
              number: 80

$ kubectl apply -f nginx-blue-green-ingress.yaml

Rollout 커스텀 리소스 배포

이제 초기 환경으로 Rollout 을 배포한다. Rollout 을 배포하면 pod 가 생성된다. workloadRef 영역은 Deployment 에서 Pod Template 영역과 일치한다. 그래서 이미 배포된 Deployment 를 참조하게 정의했다.

마지막 라인의 autoPromotionEnabledfalse 로 하여 수동으로 Blue/Green 을 확인하면서 배포를 할 수 있게 한다.

$ cat nginx-rollout.yaml

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: nginx-rollout
spec:
  replicas: 2
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: nginx-blue-green
  workloadRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx-blue-green
  strategy:
    blueGreen:
      activeService: nginx-blue-green-svc
      autoPromotionEnabled: false

$ kubectl apply -f nginx-rollout.yaml

이제 배포가 완료되면 아래와 같이 리소스가 생성되었음을 확인할 수 있다. rollout pod 와 rollout 에서 사용하는 ReplicaSet 이 생성되어 있음을 알 수 있다.

$ kubectl argo rollouts list rollout
NAME           STRATEGY   STATUS        STEP  SET-WEIGHT  READY  DESIRED  UP-TO-DATE  AVAILABLE
nginx-rollout  BlueGreen  Healthy       -     -           2/2    2        2           2

$ kubectl get pods,deploy,rs
NAME                                 READY   STATUS    RESTARTS   AGE
pod/nginx-rollout-85c4bfb654-jmts7   1/1     Running   0          2m29s
pod/nginx-rollout-85c4bfb654-s46sm   1/1     Running   0          2m29s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-blue-green   0/0     0            0           4m50s

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-blue-green-8b4f9cddb   0         0         0       4m50s
replicaset.apps/nginx-rollout-85c4bfb654     2         2         2       2m29s

웹으로 접속하면 아래와 같은 화면을 볼 수 있다.

혹은 curl 로도 확인할 수 있다.

$ curl nginx-blue-green.taco-cat.xyz

--- output ---
<!DOCTYPE html>
<html>
<body style="background-color:blue;">
<h1>This is a blue webserver</h1>
</body>
</html>

Rollout dashboard 에는 아래와 같이 나온다.

4. App 업그레이드 배포 (Green Deployment)

app 을 수정하여 배포해 보자. app 은 Deployment 를 수정해서 배포하면 된다.

$ cat nginx-green-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue-green
  name: nginx-blue-green
spec:
  replicas: 0
  selector:
    matchLabels:
      app: nginx-blue-green
      version: blue-green
  template:
    metadata:
      labels:
        app: nginx-blue-green
        version: blue-green
    spec:
      containers:
      - image: seungkyua/nginx:green
        name: nginx

$ kubectl apply -f nginx-green-deploy.yaml

Deployment 가 업그레이드 되어 배포했기 때문에 Rollout 이 이를 인식하고 Green 에 해당하는 추가 Pod 와 ReplicaSet 을 생성한다. 그리고 Rollout 리소스의 상태는 Paused 가 된. (앞에서 Rollout 리소스 배포 시에 autoPromotionEnabledfalse 로 하였기 때문이다)

$ kubectl argo rollouts list rollout
NAME           STRATEGY   STATUS        STEP  SET-WEIGHT  READY  DESIRED  UP-TO-DATE  AVAILABLE
nginx-rollout  BlueGreen  Paused        -     -           2/4    2        2           2

Pod 와 ReplicaSet 을 조회해 보면 아래와 같다. Blue 해당하는 ReplicaSet 1 개, Pod 2개, Green 에 해당하는 ReplicaSet 1개 Pod 2 개가 떠 있는 것을 알 수 있다.

Rollout Dashboar 는 아래와 같은 Pause 상태이다.

5. Rollout 진행 완료 (promote)

완전히 Green 으로 변경하려면 Rollout 을 promote 하여 최종 적용을 하던지 abort 하여 중단, 혹은 undo 하여 Pause 보다 이전 단계이 최초 Blue app 배포 단계로 돌아가는 방법이 있다.

Green 으로 진행하는 promote 를 해보자.

$ kubectl argo rollouts promote nginx-rollout

--- output ---
rollout 'nginx-rollout' promoted

ReplicaSet 은 남아 있지만 Pod 는 Green 으로 배포된 것만 남아있는 것을 확인할 수 있다.

$ kubectl get pods,rs -l rollouts-pod-template-hash --show-labels
NAME                                 READY   STATUS    RESTARTS   AGE   LABELS
pod/nginx-rollout-569b8595bf-8s94v   1/1     Running   0          10m   app=nginx-blue-green,rollouts-pod-template-hash=569b8595bf,version=blue-green
pod/nginx-rollout-569b8595bf-c7fgg   1/1     Running   0          10m   app=nginx-blue-green,rollouts-pod-template-hash=569b8595bf,version=blue-green

NAME                                       DESIRED   CURRENT   READY   AGE   LABELS
replicaset.apps/nginx-rollout-569b8595bf   2         2         2       10m   app=nginx-blue-green,rollouts-pod-template-hash=569b8595bf,version=blue-green
replicaset.apps/nginx-rollout-85c4bfb654   0         0         0       19m   app=nginx-blue-green,rollouts-pod-template-hash=85c4bfb654,version=blue-green

Rollout Dashboard 에서도 완료된 것을 알 수 있다.

마지막으로 웹 화면으로 확인한다.

마치며…

Argo Rollout 은 Deployment 변경에서만 인식을 한다. ConfigMap 이나 Secret 과 같은 다른 리소스는 지원하지 않으니 Rollout 에서 이를 지원하는 방법은 추가로 고민해야 한다.

반응형
Posted by seungkyua@gmail.com
,
반응형

TACO 에서는 Kubernetes 에 워크로드를 배포하기 위해서 Decapod 라는 자체 빌드 및 배포 체계를 갖고 있다. Decapod 는 Helm Chart 의 value override 기능과 Kustomize 의 plugin 기능을 개발하여 적용한 또 다른 value override 를 모두 사용하고 있다.

Helm Chart 와 Kustomize 모두 value oeverride 기능을 모두 제공하는데 왜 2가지를 모두 사용할까? 그 이유는 Helm Chart 는 이미 다양하게 제공되고 있는 것들이 많아 가져다 쓰면 되고, value 값들을 하나의 yaml 파일로 합쳐서 관리하기 위해서 Kustomize 의 plugin 을 개발하여 사용하고 있다.

즉, Decapod 체계는 다음과 같은 장점이 있다.

  1. Helm Chart 기반으로 default custom value 값을 지정할 수 있다. (decapod-base-yaml)
  2. Kustomize plugin 을 개발하여 각 사이트마다 갖는 여러 helm chart 의 고유 value 값들을 1개의 yaml 파일에 합쳐서 관리할 수 있다.

하지만 Helm Chart 를 제공하지 않는 app 들은 어떻게 지원할까? 예를 들어 Kubeflow 의 경우 Helm Chart 를 제공하지 않지만 Kustomize 를 제공하고 있으니 이를 지원하는 방법도 필요해 보인다.

Kustomize 활용을 위한 기본 디렉토리 (base repo)

kustomize 는 설치되어 있다고 가정하고 바로 활용을 위한 기본 디렉토리를 살펴보자.

$ tree
.
├── LICENSE
├── README.md
└── service-mesh
    └── nginx
        ├── aws-msa-reference
        │   ├── kustomization.yaml
        │   └── site-values.yaml
        └── base
            ├── kustomization.yaml
            ├── nginx-deployment.yaml
            ├── nginx-service.yaml
            └── site-values.yaml

소스 홈 디렉토리 아래에는 service-mesh 라는 서비스 디렉토리가 있다. 여기에는 nginx, istio, jaeger, kaili 등 다양한 application 이 동시에 설치되어야 하는데 각 app 을 나타내는 디렉토리 (여기서는 편의상 nginx 만 설명한다)가 존재한다.

nginx 설치를 위해서는 보통 nginx-deployment.yaml 과 nginx-service.yaml 이 필요하며 이를 base 디렉토리에 위치시킨다.

$ cat nginx-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue
  name: nginx-blue
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-blue
      version: blue
  template:
    metadata:
      labels:
        app: nginx-blue
        version: blue
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx 
$ cat nginx-service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue
    version: blue
  type: LoadBalancer

여기까지는 Helm chart 와 동일하다. 다만 chart 는 template 을 활용하여 value 값을 override 할 수 있는데 반하여 kustomize 는 kustomize.yaml 을 통해서 value 값을 override 할 수 있다.

kustomize.yaml 을 지정하고 기본적으로 업데이트할 디폴트 값을 site-values.yaml 이라는 파일에 지정한다.

resources 는 kubernetes 에 설치할 리소스들에 대한 yaml 리스트이고 patchesStrategicMerge 는 kustomize 에서 제공하는 yaml 합성 기능 중에 하나의 방법이다.

아래의 경우에는 site-values.yaml 값과 nginx-deployment.yaml, nginx-service.yaml 값을 합쳐서 만든다.

중복되는 경우에는 site-values.yaml 값을 우선시 한다.

$ cat kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- nginx-deployment.yaml
- nginx-service.yaml

patchesStrategicMerge:
- site-values.yaml

site-values.yaml 에는 override 할 default value 값을 가진다.

deployment 는 replicas 값을, service 는 NodePort 타입과 nodePort 값을 가진다. (이것을 base 값이라 생각하면 이해하기 쉽다. nginx helm chart 는 기본값이 replicas 1 인데 우리는 기본 값을 replicas 2 로 의도한 것이다.)

$ cat site-values.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-blue
spec:
  replicas: 2

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 32080
  type: NodePort

여기서 잠깐. nginx-deployment.yaml 과 nginx-service.yaml 값을 처음부터 바꿔서 넣으면 site-values.yaml 이 필요하지 않을 텐데 왜 굳이 이걸 만들지?

그건 kustomize 혹은 helm chart 를 제공하기 때문에 가져다 쓰는 경우도 많은데 원래의 작성 값들을 바꿔서 관리하지 않으려고 하는 의도이다. 업스트림에서 만들어 진 것이 있으면 원본은 그대로 가져다 쓰는 것이 추후 유지 보수 관점에서 편리하기 때문이다.

이제 여기까지 만든 내용을 kustomize 로 build 해 보자.

$ cd service-mesh/nginx

$ kustomize build base

--- output ---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - nodePort: 32080
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue
    version: blue
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue
  name: nginx-blue
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-blue
      version: blue
  template:
    metadata:
      labels:
        app: nginx-blue
        version: blue
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx

Site 별 디렉토리 (Site repo)

이제 특정한 사이트에 다른 값으로 설치해야 한다고 가정해 보자.

aws-msa-reference 라는 신규 사이트 디렉토리를 만들고 kustomize.yaml 과 value 값을 모아둔 site-values.yaml 을 만든다.

$ tree
.
├── LICENSE
├── README.md
└── service-mesh
    └── nginx
        ├── aws-msa-reference
        │   ├── kustomization.yaml
        │   └── site-values.yaml
        └── base
            ├── kustomization.yaml
            ├── nginx-deployment.yaml
            ├── nginx-service.yaml
            └── site-values.yaml

지금은 service-mesh/nginx 디렉토리 아리에 aws-msa-reference 가 있지만 이 디렉토리는 다른 repo 에서 관리하다가 kustomize build 를 하려할 때 해당 디렉토리로 복사해 오는 방법을 쓸 수 있다. (decapod 에서는 실제로 decapod-site 라는 repo 에 따로 사이트 값들을 관리하고 build 할 때 복사하는 방식을 사용하고 있다)

kustomize.yaml 과 site-values.yaml 을 보자.

$ cat kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base

patchesStrategicMerge:
- site-values.yaml
$ cat site-values.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-blue
spec:
  replicas: 3

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: LoadBalancer

새로운 신규 사이트에는 replicas 를 3으로 Service Type 을 LoadBalancer 로 설치하고자 한다.

이를 build 하면 다음과 같다.

$ kustomize build aws-msa-reference

--- output ---
apiVersion: v1
kind: Service
metadata:
  name: nginx-blue-svc
spec:
  ports:
  - nodePort: 32080
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue
    version: blue
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue
  name: nginx-blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-blue
      version: blue
  template:
    metadata:
      labels:
        app: nginx-blue
        version: blue
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx

결과가 site 별로 지정한 값으로 잘 변경되었다.

어디에 적용할까?

kustomize 로 빌드된 결과 yaml 들을 특정 repo 에 저장하고 Argo CD 와 같은 tool 을 적용하면 효과적인 GitOps 체계를 만들 수 있다.

Tekton pipeline 이나 Argo workflow, Jenkins pipeline 으로 CI 를 구축하고 Argo CD 로 CD 를 연결하면 GitOps CICD 를 구축할 수 있다. (이것까지 글로 써볼까? ㅎ)

반응형
Posted by seungkyua@gmail.com
,
반응형

Kubernetes 에서 Blue/Green 배포하는 방법을 알아보자.

  1. Blue 와 Green 버전의 Container 이미지 만들기
  2. Blue 버전의 Deployment 와 LoadBalancer 타입의 Service 배포
  3. Green 버전의 Deployment 와 NodePort 타입의 Service 배포
  4. Patch 로 Service EndPoint 변경

1. Blue 와 Green 버전의 Container 이미지 만들기

Container 이미지는 nginx 를 상속받아 쉽게 만들 수 있다. nginx 가 바라보는 web root 는 /usr/share/nginx/html 이므로 여기에 blue 버전을 표시할 수 있는 html 을 넣어준다.

index-blue.html

<!DOCTYPE html>
<html>
<body style="background-color:blue;">
<h1>This is a blue webserver</h1>
</body>
</html>

index-blue.htmlindex.html 로 변경하여 복사한다.

Dockerfile-blue

FROM nginx
COPY index-blue.html /usr/share/nginx/html/index.html

seungkyua/nginx:blue 이름과 태그를 갖는 Container 이미지를 만든다.

$ docker build -t seungkyua/nginx:blue -f Dockerfile-blue . 

이미지가 정확한지 test 해본다.

$ docker run --rm -d -p 8080:80 --name nginx-blue seungkyua/nginx:blue

$ curl localhost:8080
<!DOCTYPE html>
<html>
<body style="background-color:blue;">
<h1>This is a blue webserver</h1>
</body>
</html>

이미지가 잘 확인되었으니 docker hub 에 push 한다.

$ docker login -u seungkyua
password: 

$ docker push seungkyua/nginx:blue

같은 방식으로 Green 이미지를 만들어서 docker hub 에 push 한다.

index-green.html

<!DOCTYPE html>
<html>
<body style="background-color:green;">
<h1>This is a green webserver</h1>
</body>
</html>

Dockerfile-green

$ cat Dockerfile-green
FROM nginx
COPY index-green.html /usr/share/nginx/html/index.html
$ docker build -t seungkyua/nginx:green -f Dockerfile-green .
$ docker push seungkyua/nginx:green

2. Blue 버전의 Deployment 와 LoadBalancer 타입의 Service 배포

이미지가 준비되었으니 Blue 버전의 deployment 와 service yaml 을 만들어 배포한다.

nginx-blue-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-blue
  name: nginx-blue
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-blue
      version: blue
  template:
    metadata:
      labels:
        app: nginx-blue
        version: blue
    spec:
      containers:
      - image: seungkyua/nginx:blue
        name: nginx
$ kubectl apply -f nginx-blue-deploy.yaml

Service 를 배포할 때는 Blue pod 의 label 을 selector 로 지정해 줘야 한다.

nginx-blue-green-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app: nginx-blue-green
  name: nginx-blue-green-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-blue
    version: blue
  type: LoadBalancer
$ kubectl apply -f nginx-blue-green-svc.yaml

blue version 은 웹 브라우저에서 접속하기 위해서 Service 를 LoadBalancer 타입으로 생성하였다.

$ kubectl get svc
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP                                                                  PORT(S)        AGE
kubernetes             ClusterIP      10.233.0.1      <none>                                                                       443/TCP        12d
nginx-blue-green-svc   LoadBalancer   10.233.30.78    aa1d4e1994e454eb5aea607cfdfd3dcf-23761053.ap-northeast-2.elb.amazonaws.com   80:32234/TCP   5m18s

웹브라우저에 접속하면 아래와 같이 blue 버전으로 접속이 된다.

3. Green 버전의 Deployment 와 NodePort 타입의 Service 배포

이제 새로운 버전인 Green 버전을 배포해 보자. image 와 label 을 잘 확인해야 한다.

nginx-green-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-green
  name: nginx-green
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-green
      version: green
  template:
    metadata:
      labels:
        app: nginx-green
        version: green
    spec:
      containers:
      - image: seungkyua/nginx:green
        name: nginx
$ kubectl apply -f nginx-green-deploy.yaml

Service 는 Green 배포가 잘 되었는지 확인하기 위해서 Service 를 NodeType 으로 적용하였다.

nginx-green-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app: nginx-green
  name: nginx-green-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 32080
  selector:
    app: nginx-green
    version: green
  type: NodePort
$ kubectl apply -f nginx-green-svc.yaml

green pod 가 떠 있는 노드를 확인한다.

여기서는 172.31.49.87 노드와 172.31.46.179 노드에 pod 가 생성되어 있다.

$ kubectl get pods -o wide
NAME                           READY   STATUS    RESTARTS   AGE    IP               NODE                                               NOMINATED NODE   READINESS GATES
nginx-blue-69dd468cf4-nts5h    1/1     Running   0          6m7s   10.233.114.152   ip-172-31-49-87.ap-northeast-2.compute.internal    <none>           <none>
nginx-blue-69dd468cf4-xvxqz    1/1     Running   0          6m7s   10.233.110.80    ip-172-31-46-179.ap-northeast-2.compute.internal   <none>           <none>
nginx-green-7df845c6cf-mpj6b   1/1     Running   0          8s     10.233.114.153   ip-172-31-49-87.ap-northeast-2.compute.internal    <none>           <none>
nginx-green-7df845c6cf-pvmrc   1/1     Running   0          8s     10.233.110.81    ip-172-31-46-179.ap-northeast-2.compute.internal   <none>           <none>

curl 명령어로 Green 이 정상적으로 배포되었는지 확인한다.

$ kubectl get svc
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP                                                                  PORT(S)        AGE
kubernetes             ClusterIP      10.233.0.1      <none>                                                                       443/TCP        12d
nginx-blue-green-svc   LoadBalancer   10.233.30.78    aa1d4e1994e454eb5aea607cfdfd3dcf-23761053.ap-northeast-2.elb.amazonaws.com   80:32234/TCP   5m18s
nginx-green-svc        NodePort       10.233.33.157   <none>                                                                       80:32080/TCP   12s
$ curl 172.31.49.87:32080
<!DOCTYPE html>
<html>
<body style="background-color:green;">
<h1>This is a green webserver</h1>
</body>
</html>

4. Patch 로 Service EndPoint 변경

이제 LoadBalancer 타입으로 생성된 nginx-blue-green-svc 가 Green Pod 로 연결되게 selector 를 Green pod 가 선택되게 변경한다.

$ kubectl patch svc nginx-blue-green-svc -p '{"spec": {"selector": {"app": "nginx-green", "version": "green"}}}'

웹브라우저로 접속하면 Green 으로 변경된 것을 확인할 수 있다.

반응형
Posted by seungkyua@gmail.com
,
반응형

이전에는 aws 에 Kubernetes Cluster 를 설치한 후 Load Balancer 를 연결한는 방법을 설명하였다. Kubernetes Cluster 를 사용하려면 Load Balancer 외에도 필요한 기능이 있는데 그것이 바로 Storage 이다.

Pod 에서 영구적으로 데이터를 저장하기 위해서는 ebs 와 같은 Block Storage 를 생성해서 연결해야 하는데 Kubernetes 에서는 CSI 로 이를 지원하고 있다.

aws 에서 ebs 를 사용하려면 아래의 순서대로 적용한다.

  1. IAM Policy 생성
  2. CSI Driver (Provisioner) 설치
  3. Storage Class 생성
  4. PVC, POD 로 테스트

1. IAM Policy 생성

CSI 를 위한 IAM Policy 는 생성하기 전에 이미 만들어서 제공되고 있는 Managed Policy 를 사용해도 된다. arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy

하지만 새로 만든다고 하면 아래와 같이 만들수 있다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateSnapshot",
        "ec2:AttachVolume",
        "ec2:DetachVolume",
        "ec2:ModifyVolume",
        "ec2:DescribeAvailabilityZones",
        "ec2:DescribeInstances",
        "ec2:DescribeSnapshots",
        "ec2:DescribeTags",
        "ec2:DescribeVolumes",
        "ec2:DescribeVolumesModifications"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateTags"
      ],
      "Resource": [
        "arn:aws:ec2:*:*:volume/*",
        "arn:aws:ec2:*:*:snapshot/*"
      ],
      "Condition": {
        "StringEquals": {
          "ec2:CreateAction": [
            "CreateVolume",
            "CreateSnapshot"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteTags"
      ],
      "Resource": [
        "arn:aws:ec2:*:*:volume/*",
        "arn:aws:ec2:*:*:snapshot/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/ebs.csi.aws.com/cluster": "true"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/CSIVolumeName": "*"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/ebs.csi.aws.com/cluster": "true"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/CSIVolumeName": "*"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteVolume"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/kubernetes.io/created-for/pvc/name": "*"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteSnapshot"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/CSIVolumeSnapshotName": "*"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DeleteSnapshot"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "ec2:ResourceTag/ebs.csi.aws.com/cluster": "true"
        }
      }
    }
  ]
}

Role 에 Policy 연결

이전 글에서 설명한 control-plane.cluster-api-provider-aws.sigs.k8s.ionodes.cluster-api-provider-aws.sigs.k8s.io role 에 위의 Policy 를 연결한다.

Role : control-plane.cluster-api-provider-aws.sigs.k8s.io

Role : nodes.cluster-api-provider-aws.sigs.k8s.io

2. CSI Driver (Provisioner) 설치

EBS 용 CSI Driver 를 helm chart repo 를 등록한다.

$ helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver
$ helm repo update

$ helm search repo aws-ebs-csi-driver -l
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION
aws-ebs-csi-driver/aws-ebs-csi-driver   2.16.0          1.15.0          A Helm chart for AWS EBS CSI Driver
aws-ebs-csi-driver/aws-ebs-csi-driver   2.15.1          1.14.1          A Helm chart for AWS EBS CSI Driver
...

지금 최신 버전 차트는 2.16.0 이다. 해당 차트는 Kubernetes 1.17+ 이상만 호환되는데 현재의 웬만한 Kubernetes 버전은 지원된다고 보면 된다.

Helm chart 로 설치한다.

$ helm upgrade -i aws-ebs-csi-driver -n kube-system aws-ebs-csi-driver/aws-ebs-csi-driver --version 2.16.0

설치가 제대로 되었는지는 아래의 명령어로 확인할 수 있다.

3. Storage Class 설치

ebs volume 이 생성될 때 CSI Driver 에게 필요한 설정 값을 전달해야 하는데 이것이 바로 Storage Class 라고 보면 된다.

아래와 같이 생성한다.

$ vi standard-ebs-sc.yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations: 
    storageclass.kubernetes.io/is-default-class: "true"
  name: standard
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

$ kubectl apply -f standard-ebs-sc.yaml

storage class 에 storage type (gp2, gp3, io1 등), iops, 데이터 암호화 여부, 토폴로지 등을 넣을 수 있는데 이를 적용하면 현재 csi driver 에서 에러가 나므로 추가 확인이 필요하다. (볼륨 생성이 안된다던지, 볼륨 attach 가 안된다든지 하는 문제가 발생했는데 자세히 소스까지 찾아보지는 않았음 ㅠㅠ)

storage class 가 잘 생성되었는지 확인한다.

$ kubectl get sc
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path           rancher.io/local-path   Delete          WaitForFirstConsumer   false                  12d
standard (default)   ebs.csi.aws.com         Delete          WaitForFirstConsumer   false                  138m

4. PVC, POD 로 테스트

pvc 를 생성하고 pod 에서 이를 mount 하여 활용해본다.

pvc 생성

$ vi pvc-example.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: standard
  resources:
    requests:
      storage: 4Gi

$ kubectl apply -f pvc-example.yaml

sc 설정에서 volumeBindingMode: WaitForFirstConsumer 이기 때문에 pod 가 생성되기 전까지는 pv 는 만들어지지 않는다.

POD 생성

$ vi pod-example.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
  - name: app
    image: centos
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: ebs-claim

$ kubectl apply -f pod-example.yaml

out.txt 에 date 값이 설정된 것을 확인할 수 있다.

$ kubectl exec -it app -- cat /data/out.txt
반응형
Posted by seungkyua@gmail.com
,
반응형

kubernetes 에 서비스를 올릴 때 Service 의 Type 으로 LoadBalancer 를 선택하면 cloud 에서 자동으로 LB (external-ip ) 가 생성되어 서비스 pod 에 연결된다. 어떻게 Kubernetes 에서 설정한 값이 cloud 에 연결될까? 이는 Cloud Provider 가 있어 가능하다.

Cloud Provider 는 초기에 Kubernetes Controller 에 포함되어 있다. 하지만 지금은 External Kubernete Cloud Provider 로 Kubernetes 에서 제외되었으며, 이전 Kubernetes Controller 에 포함된 Cloud Provider 는 Legacy Cloud Provider 로 불리고 있다.

AWS Cloud Provider 의 경우에는 아직 1.23 (Kubernetes 와 같이 버전을 맞춰가고 있음) alpha 버전이라 아직은 Legacy Cloud Provider 를 사용하는 것이 안정적이다.

aws 에서는 아래의 순서대로 적용한다.

  1. IAM Policy, Role 생성
  2. VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성
  3. VM 생성
  4. aws resource 에 Tag 적용
  5. Kubernetes Cluster 생성

1. IAM Policy, Role 생성

Control plane 과 Node 2개의 Policy 를 생성한다.

Control Plane Policy

control node 에서 사용할 policy 이다.

정책명: control-plane.cluster-api-provider-aws.sigs.k8s.io

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "autoscaling:DescribeAutoScalingGroups",
        "autoscaling:DescribeLaunchConfigurations",
        "autoscaling:DescribeTags",
        "ec2:DescribeInstances",
        "ec2:DescribeRegions",
        "ec2:DescribeRouteTables",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeVolumes",
        "ec2:DescribeAvailabilityZones",
        "ec2:CreateSecurityGroup",
        "ec2:CreateTags",
        "ec2:CreateVolume",
        "ec2:ModifyInstanceAttribute",
        "ec2:ModifyVolume",
        "ec2:AttachVolume",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:CreateRoute",
        "ec2:DeleteRoute",
        "ec2:DeleteSecurityGroup",
        "ec2:DeleteVolume",
        "ec2:DetachVolume",
        "ec2:RevokeSecurityGroupIngress",
        "ec2:DescribeVpcs",
        "elasticloadbalancing:AddTags",
        "elasticloadbalancing:AttachLoadBalancerToSubnets",
        "elasticloadbalancing:ApplySecurityGroupsToLoadBalancer",
        "elasticloadbalancing:CreateLoadBalancer",
        "elasticloadbalancing:CreateLoadBalancerPolicy",
        "elasticloadbalancing:CreateLoadBalancerListeners",
        "elasticloadbalancing:ConfigureHealthCheck",
        "elasticloadbalancing:DeleteLoadBalancer",
        "elasticloadbalancing:DeleteLoadBalancerListeners",
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeLoadBalancerAttributes",
        "elasticloadbalancing:DetachLoadBalancerFromSubnets",
        "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
        "elasticloadbalancing:ModifyLoadBalancerAttributes",
        "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
        "elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer",
        "elasticloadbalancing:AddTags",
        "elasticloadbalancing:CreateListener",
        "elasticloadbalancing:CreateTargetGroup",
        "elasticloadbalancing:DeleteListener",
        "elasticloadbalancing:DeleteTargetGroup",
        "elasticloadbalancing:DescribeListeners",
        "elasticloadbalancing:DescribeLoadBalancerPolicies",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetHealth",
        "elasticloadbalancing:ModifyListener",
        "elasticloadbalancing:ModifyTargetGroup",
        "elasticloadbalancing:RegisterTargets",
        "elasticloadbalancing:DeregisterTargets",
        "elasticloadbalancing:SetLoadBalancerPoliciesOfListener",
        "iam:CreateServiceLinkedRole",
        "kms:DescribeKey"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

Node Policy

일반 Node 에 대한 policy 이다.

정책명: nodes.cluster-api-provider-aws.sigs.k8s.io

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeRegions",
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:GetRepositoryPolicy",
        "ecr:DescribeRepositories",
        "ecr:ListImages",
        "ecr:BatchGetImage"
      ],
      "Resource": "*"
    }
  ]
}

policy 를 생성했으면 이제 Role 을 생성한다.

Control plane Role

Policy 를 활용할 수 있는 Role 을 만들어서 Policy 와 연결한다.

Role 명: control-plane.cluster-api-provider-aws.sigs.k8s.io

연결할 Policy 리스트
- control-plane.cluster-api-provider-aws.sigs.k8s.io
- nodes.cluster-api-provider-aws.sigs.k8s.io

Node Role

Role 명: nodes.cluster-api-provider-aws.sigs.k8s.io

연결할 Policy 리스트
- nodes.cluster-api-provider-aws.sigs.k8s.io

2. VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성

서울 리전의 경우 VPC 를 1개 만들고, public 용도의 subnet 4개, private 용도의 subnet 4개를 만든다.

Internet Gateway 1개를 만들어서 public subnet 에 연결하고, Nat Gateway 4개를 만들어서 각각 private subnet 에 연결한다.

Routing Table 은 subnet 갯수에 맞는 8개를 만들어서 각각 연결한다. private 용 4개의 Routing table 은 0.0.0.0/0 → nat gateway 를 대상으로 설정하고, public 용 4개의 Routing table 은 0.0.0.0/0 → internet gateway 대상으로 설정한다.

1. vpc : 1개
   - vpc

2. subent : 8개
   - public-subnet-a
   - public-subnet-b
   - public-subnet-c
   - public-subnet-d
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c
   - private-subnet-d

3. internat gateway : 1개
   - igw

4. Nat Gateway : 4개
   - nat-private-a
   - nat-private-b
   - nat-private-c
   - nat-private-d

5. Routing Table : 4개
   - rt-public-a (0.0.0.0/0 -> igw)
   - rt-public-b (0.0.0.0/0 -> igw)
   - rt-public-c (0.0.0.0/0 -> igw)
   - rt-public-d (0.0.0.0/0 -> igw)
   - rt-priabe-a (0.0.0.0/0 -> nat-private-a)
   - rt-priabe-b (0.0.0.0/0 -> nat-private-b)
   - rt-priabe-c (0.0.0.0/0 -> nat-private-c)
   - rt-priabe-d (0.0.0.0/0 -> nat-private-d)

3. VM 생성

VM 은 Controler Plane 3대는 각 private subnet 에 1대씩 생성하고(subnet 1개는 남는다), Node 용 4대는 각 private subnet 1대씩 생성하다.

bastion 노드로 public subnet 에 1대 생성한다.

1. bastion VM 1대
   - public-subnet-a

2. Control Plane VM 3대
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c

3. Node VM 4대
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c
   - private-subnet-d

4. Aws Resource 에 Tag 설정

aws cloud provider 가 리소스를 파악하기 위해서는 aws 에 적절한 값을 설정해야 한다.

4-1. VM 에 IAM Role 을 할당

VM 에서 권한을 얻기 위해서는 반드시 IAM Role 을 할당해야 한다.

Control node 에는 control-plane.cluster-api-provider-aws.sigs.k8s.io role 을 할당한다.

일반 Node 에는 nodes.cluster-api-provider-aws.sigs.k8s.io role 을 할당한다.

4-2 VM 에 Tag 설정

VM 에는 Kubernetes Cluster Name 을 Tag 로 지정한다. kubernetes.io/cluster/<cluster name> 과 같이 지정하는데 Cluster Name 은 Kubernetes 를 설치할 때 지정할 수 있다. 기본 값은 cluster.local 인데 여기서는 ahnsk 로 이름을 지정하였다.

그리고 vm 의 역할을 지정해야 하는데 Controler Node 는 control-plane 으로, Node 는 node 로 지정한다.

4-3. Subnet 에 Tag 설정

Load Balancer 를 핸들링 하기 위해서 Kubernetes Cluster 가 어느 Subnet 과 Routing Table 을 사용해야 하는지 알아야 한다.

주의해야 할 점은 public subnet 의 경우 [kubernetes.io/role/elb](http://kubernetes.io/role/elb) 이지만, private subnet 의 경우에는 [kubernetes.io/role/internal-elb](http://kubernetes.io/role/internal-elb) 로 해야 한다.

4-4. Routing Table 에 Tag 설정

5. Kubernetes Cluster 생성

Kubernetes cluster 이름은 앞에서 설명했듯이 ahnsk 로 설정한다.

kubeadm 혹은 kubespray 를 사용할 수 있으며 여기서 생성 방법을 생략한다.

aws cloud provider 를 활성화 하기 위해서는 API Server, Controller, Kubelet 에 --cloud-provider=aws 옵션을 추가해야 한다.

kube-apiserver.yaml

apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 172.31.22.52:6443
  creationTimestamp: null
  labels:
    component: kube-apiserver
    tier: control-plane
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=172.31.22.52
...
    **- --cloud-provider=aws**
...
    image: registry.k8s.io/kube-apiserver:v1.24.6
...

kube-controller-manager.yaml

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    component: kube-controller-manager
    tier: control-plane
  name: kube-controller-manager
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-controller-manager
    - --allocate-node-cidrs=true
...
    **- --cloud-provider=aws
...**
    image: registry.k8s.io/kube-controller-manager:v1.24.6
...

kubelet.env

...
KUBELET_CLOUDPROVIDER="**--cloud-provider=aws --cloud-config=/etc/kubernetes/cloud_config**"
...

kubelet 은 aws 의 리소스를 위해 추가해야 할 값들이 있는데 cloud_config 을 만들어서 옵션을 전달하였다.

[Global]
zone=
vpc=vpc-b342d5d8
subnetId=
routeTableId=
roleArn=
kubernetesClusterTag=ahnsk
kubernetesClusterId=ahnsk
disableSecurityGroupIngress=false
disableStrictZoneCheck=false
elbSecurityGroup=

값을 다 채워 넣으면 apiserver 가 kube-apiserver-master-dummy 라는 이름으로 잘못 실행되므로 조심해야 한다. master-dummy 로 띄우는 방법은 aws account 가 다를 경우에만 사용하는 방법이다. 이는 아래 소스를 보면 알 수 있다.

https://github.com/kubernetes/legacy-cloud-providers/blob/707ecda639b086132369678680a1b34d4d2b5c7c/aws/aws.go#L1251

...
  tagged := cfg.Global.KubernetesClusterTag != "" || cfg.Global.KubernetesClusterID != ""
    if cfg.Global.VPC != "" && (cfg.Global.SubnetID != "" || cfg.Global.RoleARN != "") && tagged {
        // When the master is running on a different AWS account, cloud provider or on-premise
        // build up a dummy instance and use the VPC from the nodes account
        klog.Info("Master is configured to run on a different AWS account, different cloud provider or on-premises")
        awsCloud.selfAWSInstance = &awsInstance{
            nodeName: "master-dummy",
            vpcID:    cfg.Global.VPC,
            subnetID: cfg.Global.SubnetID,
        }
        awsCloud.vpcID = cfg.Global.VPC
    } else {
        selfAWSInstance, err := awsCloud.buildSelfAWSInstance()
        if err != nil {
            return nil, err
        }
        awsCloud.selfAWSInstance = selfAWSInstance
        awsCloud.vpcID = selfAWSInstance.vpcID
    }
...

RoleARN 의 값을 넣으면 안되는데 값이 없으면서도 어떻게 kubelet 이 해당 Role 로 인증을 받을 수 있을까? 이는 앞에서 설명한 VM 에 IAM Role 인 control-plane.cluster-api-provider-aws.sigs.k8s.io 이나 nodes.cluster-api-provider-aws.sigs.k8s.io 이 설정되어 있기 때문에 가능하다.

kubelet-config.yaml

kubernetes node 정보에 providerID 값이 들어가 있어야 한다. 만약 이 정보가 없다면 LoadBalancer 가 생성된다고 하더라도 인스턴스가 LoadBalancer 에 할당되지 않아 제대로 사용할 수 가 없다.

providerID 는 kubelet-config.yaml 에 추가한다. aws:///<zone-id>/<instance-id> 값으로 추가한다.

$ sudo vi /etc/kubernetes/kubelet-config.yaml
...
providerID: "aws:///ap-northeast-2d/i-0f59f059e2d64213f"
...

이미 노드가 생성된 경우에는 해당 값을 변경하고 kubelet 서비스를 다시 restart 한다고 값이 추가되지는 않는다. 그래서 patch 명령으로 동적으로 추가하는 것이 좋다.

$ kubectl patch node ip-172-31-46-179.ap-northeast-2.compute.internal -p '{"spec": {"providerID": "aws:///ap-northeast-2d/i-08cb6f884239f894c"}}'

5. Nginx 로 테스트

nginx 를 생성하고 service 를 LoadBalancer type 으로 생성하여 잘 접속이 되는지 확인해 보자.

$ kubectl create deploy nginx --image=nginx
deployment.apps/nginx created
$ kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
nginx-8f458dc5b-nhkfd   1/1     Running   0          55s
$ kubectl expose deployment nginx --name nginx-svc --target-port=80 --port=80 --type=LoadBalancer
$ kubectl get svc
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP                                                                   PORT(S)        AGE
kubernetes   ClusterIP      10.233.0.1     <none>                                                                        443/TCP        5h28m
nginx-svc    LoadBalancer   10.233.7.131   a76a315f5d7c14652a4db83cc3b25125-113127685.ap-northeast-2.elb.amazonaws.com   80:30796/TCP   27s
반응형
Posted by seungkyua@gmail.com
,
반응형

command line 명령을 사용할 때 터미널에서 여러 기능들을 사용해야 할 때가 있다. 예를 들어 여러 윈도우(창)을 띄워놓고 필요에 따라 옮긴다던가 하나의 윈도우을 상하, 혹은 좌우로 구분하여 나눠서 사용하는 경우가 있다.

보통은 명령어를 여러 디렉토리를 옮겨가며 작업하기 때문에 디렉토리 마다 윈도우를 만들어 사용한다. 혹은 포그라운드로 프로세스를 띄워 출력되는 로그를 확인하고, 다른 윈도에서 작업하는 경우도 있다.

위와 같은 기능을 제공하는 툴에는 tmux 라는 sw가 있다. 일반적으로 linux 에는 기본으로 설치되어 있고 mac 에서는 쉽게 설치할 수 있으므로 쉽게 활용할 수 가 있다.

1. 세션 관리

세션 만들기

tmux 는 세션 단위로 구분하여 관리한다. 자신만의 새로운 세션을 만들어 독립으로 사용할 수 있다.

$ tmux new -s ahnsk

ahnsk 이라는 새로운 세션(session)이 생성되면서 1:zsh 라는 윈도우(window) 1개가 기본적으로 생성되었다.

$ exit

exit 는 현재의 윈도우를 삭제한다. 지금은 세션에 1개의 윈도우 밖에 없기 때문에 윈도우가 삭제되면 해당 세션도 삭제된다.

command prefix

tmux 에서는 vi 처럼 명령모드를 사용할 수 있다. Ctrl + b (control 키와 b 키를 동시에 누름)를 입력한 이후에 명령어를 입력하면 되는데 명령 모드를 알려주는 이 키 조합을 command prefix 라고 한다. 줄여서 앞으로는 prefix 라고 한다.

prefix + t 를 누르면 화면에 시간에 표시된다.

Detaching 과 Attaching

exit 는 윈도우를 삭제(경우에 따라서는 세션까지도 삭제) 하기 때문에 이전의 세션과 윈도우를 계속 유지 시키면서 tmux 에서 빠져나오고 싶을 때도 있다. 이 때 사용해야할 기능이 detaching 기능이다.

$ top

prefix + d 를 입력하여 detaching 한다.

[detached (from session ahnsk)] 가 출력되고 원래의 터미널 창으로 빠져나온 것이 확인 된다.

-d 옵션으로 새로운 세션을 만들고 바로 detaching 할 수 도 있다.

$ tmux new -s dummy -d

세션을 조회하여 기존에 만든 세션의 리스트를 확인할 수 있다.

$ tmux ls

--- output ---
ahnsk: 1 windows (created Sat Jan 14 14:51:09 2023)
dummy: 1 windows (created Sat Jan 14 15:25:39 2023)

기존의 만든 ahnsk 세션으로 들어가고 싶으면 attach 명령을 쓰면 된다.

$ tmux attach -t ahnsk

세션 삭제

kill-session 옵션으로 세션을 삭제할 수 있다.

$ tmux kill-session -t ahnsk
$ tmux kill-session -t dummy

2. 윈도우 관리

윈도우 만들기

session 을 만들면 새로운 윈도우도 기본으로 생성된다고 했다. 이 때 -n 옵션을 사용하여 생성되는 윈도우에 이름을 넣을 수 있다.

$ tmux new -s ahnsk -n shell

prefix + c 명령으로 새로운 윈도우를 만들 수 있다. 이렇게 새로 만든 윈도우에 top 명령을 실행해 보자.

윈도우 이름 변경

첫번째 윈도우의 이름은 shell 이고 두번째 top 이 실행되고 있는 윈도우의 이름은 top 이다. (top 이 실행되고 있으므로) 두번째 윈도우 이름을 process 로 변경해 보자.

prefix + , 로 윈도우 이름을 변경할 수 있다.

이름을 넣은 뒤에 enter 를 치면 된다.

윈도우 이동

작업 윈도우를 아래 명령으로 옮겨 다닐 수 있다.

prefix + n : 현재 윈도우 다음(next) 윈도우로 이동하기

prefix + p : 현재 윈도우 이전(previous) 윈도우로 이동하기

prefix + 1 : 첫번째(1) 윈도우로 이동하기

prefix + 5 : 다섯번째(5) 윈도우로 이동하기

prefix + w : 윈도우 리스트를 보여주고 선택하여 이동하기

윈도우 닫기

prefix + & 로 명령으로 확인 입력(y)을 받으면 윈도우를 닫을 수(삭제할 수) 있다. exit 를 입력하여 확인없이 바로 윈도우가 삭제된다.

윈도우 이름으로 생성하기

prefix + : 으로 명령어 입력창을 띄울 수 있다. 여기에 new-window -n monitor 라고 입력하고 enter 를 치면 새로운 윈도우에 이름을 지정하여 생성할 수 있다.

3. 패인 관리

패인(pane) 만들기

윈도우 안의 구분되는 영역을 패인이라 한다. 윈도우를 수평으로 2개의 패인으로 나누거나 수직으로 2개의 패인으로 나눌 수 있다.

prefix + % : 윈도우를 수평(좌우)으로 2개의 패인으로 나눈다.

prefix + " (double quote) : 윈도우를 수직(위아래)으로 2개의 패인으로 나눈다.

패인 이동하기

prefix + Left 화살표 : 좌측 패인으로 이동하기

prefix + Right 화살표 : 우측 패인으로 이동하기

prefix + Up 화살표 : 위 패인으로 이동하기

prefix + Downe 화살표 : 아래 패인으로 이동하기

prefix + o : 시계 방향으로 패인 이동하기

패인 삭제하기

prefix + x 명령으로 현재 선택된 패인을 삭제할 수 있다. exit 로도 삭제가 가능하다. 가끔 화면에 키보드 입력이 안돼서 exit 를 화면에 입력할 수 없는 경우가 있다. 이 경우에는 명령모드인 prefix + x 로 삭제해야 하므로 명령모드도 알아 두는 것이 좋다.

반응형
Posted by seungkyua@gmail.com
,