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 영역은 시스템으로 자동으로 동작하도록 제공하자.
앞서 간단히 살펴본 개발/배포 프로세스를 다시 살펴보자.
개발자가 IDE 툴을 통해 프로그램을 개발한다.
Maven 혹은 Gradle 로 소스 코드를 빌드한다. 로컬 빌드, Jenkins 혹은 기타 다른 CI 툴을 활용한 빌드 결과물로 jar 혹은 war 파일이 생성된다. 일반적으로 스프링 부트 프로젝트는 jar 파일로 만들어지며, war 파일은 일반 스프링 프로젝트이다. 해당 결과 파일은 저장소(e.q. nexus)에 저장된다.
jar 혹은 war 파일을 로컬 빌드 혹은 기타 다른 CI 툴을 활용하여 컨테이너 이미지로 빌드한다.
빌드된 컨테이너 이미지를 이미지 저장소에 저장한다.
쿠버네티스에 배포하기 위해 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
프로세스 설명
사용자는 cli (golang)로 앱 배포를 요청한다.
API 서버 (golang) 는 해당 요청을 받아서 Argo workflow 를 호출한다.
Argo Workflow 에서 Nexus 로 부터 jar 파일을 가져온다.
Argo Workflow 에서 jar 파일을 컨테이너 이미지 파일로 빌드하고 이미지 저장소인 하버에 저장한다.
Argo Workflow 에서 이미지를 가져온다.
Argo Workflow 에서 Helm chart 템플릿을 가져온다.
Argo Workflow 에서 Helm chart 와 이미지를 조합하여 Argo Rollout 으로 배포한다.
Argo Rollout 은 초기 배포를 Blue 로 배포한다. 배포된 Blue 는 로드 발랜서와 연결된다.
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
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 에도 속하게 된다.
$ vi ~/.tmux.conf
bind r source-file ~/.tmux.conf \; display "Reloaded!"
위의 설정은 현재의 session 에서 설정 파일인 tmux.conf 을 동적으로 바로 리로딩하는 기능을 가진다.
새로운 session 을 만들고 설정 파일을 리로딩해보자.
$ tmux new -s sample
이제 prefix + r 을 해보면 설정 파일이 리로딩된 것을 볼 수 있다. (prefix 는 ctrl + b 를 나타내며 Reloaded! 문구가 나타난 후 바로 삭제돼서 화면 캡쳐가 어렵다. ㅠㅠ)
설정 파일에 적용할 수 있는 내용을 더 알아보자.
set -s escape-time 1
prefix 와 command 사이에 인식할 수 있는 간격이 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 를 사용할 수 있다.
$ 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 에서 참조하여 사용한다.
그리고 blue deployment app 에 웹접속이 가능하게 ingress 를 배포한다. ingress 를 배포하더라도 아직 웹 접속은 불가능하다. 앞에서 deployment 의 replicas 를 0 으로 생성했기 때문에 실행되고 있는 pod 가 없기 때문이다. (나중에 접속을 위해서 /etc/hosts 에 nginx-blue-green.taco-cat.xyz 를 등록해 놓자)
이제 배포가 완료되면 아래와 같이 리소스가 생성되었음을 확인할 수 있다. 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 를 수정해서 배포하면 된다.
Deployment 가 업그레이드 되어 배포했기 때문에 Rollout 이 이를 인식하고 Green 에 해당하는 추가 Pod 와 ReplicaSet 을 생성한다. 그리고 Rollout 리소스의 상태는 Paused 가 된. (앞에서 Rollout 리소스 배포 시에 autoPromotionEnabled 는 false 로 하였기 때문이다)
$ 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 배포 단계로 돌아가는 방법이 있다.
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 체계는 다음과 같은 장점이 있다.
Helm Chart 기반으로 default custom value 값을 지정할 수 있다. (decapod-base-yaml)
Kustomize plugin 을 개발하여 각 사이트마다 갖는 여러 helm chart 의 고유 value 값들을 1개의 yaml 파일에 합쳐서 관리할 수 있다.
하지만 Helm Chart 를 제공하지 않는 app 들은 어떻게 지원할까? 예를 들어 Kubeflow 의 경우 Helm Chart 를 제공하지 않지만 Kustomize 를 제공하고 있으니 이를 지원하는 방법도 필요해 보인다.
Kustomize 활용을 위한 기본 디렉토리 (base repo)
kustomize 는 설치되어 있다고 가정하고 바로 활용을 위한 기본 디렉토리를 살펴보자.
소스 홈 디렉토리 아래에는 service-mesh 라는 서비스 디렉토리가 있다. 여기에는 nginx, istio, jaeger, kaili 등 다양한 application 이 동시에 설치되어야 하는데 각 app 을 나타내는 디렉토리 (여기서는 편의상 nginx 만 설명한다)가 존재한다.
nginx 설치를 위해서는 보통 nginx-deployment.yaml 과 nginx-service.yaml 이 필요하며 이를 base 디렉토리에 위치시킨다.
site-values.yaml 에는 override 할 default value 값을 가진다.
deployment 는 replicas 값을, service 는 NodePort 타입과 nodePort 값을 가진다. (이것을 base 값이라 생각하면 이해하기 쉽다. nginx helm chart 는 기본값이 replicas 1 인데 우리는 기본 값을 replicas 2 로 의도한 것이다.)
지금은 service-mesh/nginx 디렉토리 아리에 aws-msa-reference 가 있지만 이 디렉토리는 다른 repo 에서 관리하다가 kustomize build 를 하려할 때 해당 디렉토리로 복사해 오는 방법을 쓸 수 있다. (decapod 에서는 실제로 decapod-site 라는 repo 에 따로 사이트 값들을 관리하고 build 할 때 복사하는 방식을 사용하고 있다)
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
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 에서는 아래의 순서대로 적용한다.
IAM Policy, Role 생성
VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성
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 대상으로 설정한다.
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 옵션을 추가해야 한다.
값을 다 채워 넣으면 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 명령으로 동적으로 추가하는 것이 좋다.
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 로 삭제해야 하므로 명령모드도 알아 두는 것이 좋다.
언어를 배울 때 가장 기본적인 것 중에 하나가 비교문(if), 반복문(for), 함수(function) 이다.
If 문
if 는 비교 구문에서 사용되는 키워드 이다. 변수 선언과 동시에 비교를 할 수 있는데 이 때 선언된 변수는 해당 if 문 block 안에서만 유효하다. 그렇기 때문에 마지막 라인은 num 변수는 인식하지 못해 에러가 난다.
func main() {
if num := 5; num == 0 {
fmt.Println("False")
} else if num < 10 {
fmt.Println("True")
} else {
fmt.Println("Big number")
}
fmt.Println(num)
}
--- output ---
./main_05_for_function.go:17:14: undefined: num
if 문 밖에서 선언한 변수는 if 문 안과 밖 모두에서 사용할 수 있다. 하지만 if 문 안에서 같은 변수명을 재정의 하면 해당 변수 값은 오버라이드 된다.
func main() {
num := 10
if num > 0 {
fmt.Println("outer num =", num)
num := 0
fmt.Println("inner num =", num)
}
fmt.Println("outer num =", num)
}
--- output ---
outer num = 10
inner num = 0
outer num = 10
For 문
for lloop 는 반복문이며 slice 나 map 타입에 대해서 range 를 사용하여 item 을 하나씩 가져온다. slice 를 range 로 가져오면 index, value 2개의 값으로 넘어온다. value 만을 사용하고 싶으면 index 부분은_로 처리하여 버릴 수 있다. 또한 index 1개의 값만 가져올 수 있다.
func main() {
nums := []int{10, 20, 30, 40, 50}
for i, v := range nums {
fmt.Println("index =", i, ",", "value =", v)
}
for _, v := range nums {
fmt.Println("value =", v)
}
for i := range nums {
fmt.Println("index =", i)
}
}
--- output ---
index = 0 , value = 10
index = 1 , value = 20
index = 2 , value = 30
index = 3 , value = 40
index = 4 , value = 50
value = 10
value = 20
value = 30
value = 40
value = 50
index = 0
index = 1
index = 2
index = 3
index = 4
map 도 slice 와 마찬가지로 range 로 가져올 수 있다. 이 때 return 되는 값은 key, value 이다.
func main() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
for k, v := range m {
fmt.Println("key =", k, ",", "value =", v)
}
for _, v := range m {
fmt.Println("value =", v)
}
for k := range m {
fmt.Println("key =", k)
}
}
--- output ---
key = c , value = 3
key = a , value = 1
key = b , value = 2
value = 2
value = 3
value = 1
key = a
key = b
key = c
Switch
선택문은 switch 로 가능하다. 각 case 마다 break 문이 없어도 되며, case 에는,로 여러 개의 값을 지정할 수 있다.
func main() {
alpha := []string{"a", "ab", "abc", "abcd", "abcde", "abcdef", "abedefg"}
for _, a := range alpha {
switch l := len(a); l {
case 1, 2:
fmt.Println(a, "length = 2 or 3")
case 3:
fmt.Println(a, "length = 3")
case 4, 5, 6:
fmt.Println(a, "length = 4, 5 or 6")
default:
fmt.Println(a, "length > 6")
}
}
}
--- output ---
a length = 2 or 3
ab length = 2 or 3
abc length = 3
abcd length = 4, 5 or 6
abcde length = 4, 5 or 6
abcdef length = 4, 5 or 6
abedefg length > 6
Function
함수는func,functionname,parameter,returnvalue로 만들 수 있다. 아래 2개의 숫자를 받아서 나누는 div 함수는 아래와 같이 정의 할 수 있다. parameter 가 같은 타입이면 앞의 타입은 생략할 수 있다.
func main() {
result := div(4, 2)
fmt.Println(result)
}
// func div(n int, d int) int { 는 n, d 파라미터의 타입이 같아 아래와 같이 바꿀 수 있음
func div(n, d int) int {
if d == 0 {
return 0
}
return n / d
}
--- output ---
2
,로 구분하여 여러 개의 아규먼트를 넘길 수 있으며 이 때 받는 함수에서는...를 활용하여 가변 파라미터로 받을 수 있다. 또한slice...와 같이 slice 를 가변인자로 보낼 수 있다.
func main() {
fmt.Println(addTo(1))
fmt.Println(addTo(1, 2))
fmt.Println(addTo(1, 2, 3))
i := []int{4, 5}
fmt.Println(addTo(3, i...)) // slice를 가변인자로 보내기
fmt.Println(addTo(6, []int{7, 8, 9}...))
}
// 가변인자 (variadic parameter)
func addTo(base int, vals ...int) []int {
result := make([]int, 0, len(vals))
for _, v := range vals {
result = append(result, base+v)
}
return result
}
--- output ---
[]
[3]
[3 4]
[7 8]
[13 14 15]
Function 의 리턴 값을 여러 개로 할 수 있다. (multi return values)
함수를 호출할 때 모든 것은CallbyValue이다. 아래과 같이 기본 타입이나 Struct 타입은 함수를 통해 넘겨받은 변수의 값을 변경하더라도 원래의 값은 변경되지 않는다. 즉, 아래와 같이 person 을 modify 함수를 통해 넘긴 다음, modify 함수에서 값을 수정해도 원래의 person 의 값은 수정되지 않는다.
func main() {
i := 1
s := "Hello"
p := person{}
modify(i, s, p)
fmt.Println(i, s, p)
}
type person struct {
name string
age int
}
// Call by Value (여기서 수정해도 원래 변수 값은 수정되지 않는다)
func modify(i int, s string, p person) {
i = i + 1
s = "modified"
p.name = "ask"
p.age = 100
}
--- output ---
1 Hello { 0}
그러나 포인터로 넘기면 원래의 값도 수정할 수 있다. 포인터는 다음에 설명하기로 하고 여기서는 slice 와 map 이 포인터와 같이 작동한다고 이해하면 된다.
한가지 중요한 것이 있다.modSlice(s)에서의s 변수가 가리키는 주소와modSlice(s[]int)함수에서s 변수가 가리키는 주소는 동일하다. (마치 포인터 처럼 동작한다) 하지만s 변수의 경우 modSlice 함수 통해 넘기는데 처음 생성할 때길이:2,크기:2로 생성했다. 즉, 길이와 크기가 같아 modSlice 함수에서 append 로 item 을 추가할 경우 크기를 늘린 새로운 slice 를 만들어서 리턴 된다.
s=append(s,3)에서 s 가 가리키는 주소는 처음modSlice(s[]int)일 때 가리키는 주소가 아니라 크기가 늘어나 새롭게 생성된 slice 를 가리키게 된다. 그래서 그 이전에 수정된 s 의 값은 변화가 있고, 그 이후에 수정된 s 의 값은 변화가 없다.
func main() {
m := map[string]int{
"one": 1,
"two": 2,
}
modMap(m)
fmt.Println(m)
s := []int{1, 2}
modSlice(s)
fmt.Println(s)
}
// Call by Value but slice and map like pointer
func modMap(m map[string]int) {
m["two"] = 20
m["three"] = 3
delete(m, "one")
}
func modSlice(s []int) {
s[0] = s[0] * 10
s = append(s, 3) // 길이 == 크기 이므로 새로운 크기의 slice 가 만들어져서 리턴된다
s[1] = s[1] * 10
}
--- output ---
map[three:3 two:20]
[10 2]
댓글을 달아 주세요