Gradle 은 application 의 빌드와 배포 문제를 해결하기 위한 framework 임
Java 의 경우, java class files, doc files, XML files, property files, 그 외 resources 등
하나의 프로젝트(application)에 굉장히 다양한 파일들을 담고 있음
게다가 각 프로젝트마다 각기 다른 구조 및 버전을 갖고 있어서 빌드하기가 굉장히 까다로움
Gradle 이 이러한 문제 해결에 도움을 줌

Gradle 이 하는 것
- 프로젝트 빌드하고 배포
- 의존성 관리

 

Gradle 이 사용하는 build file 의 default 명은 "build.gradle"

Gradle 은 Domain-Specific Language (DSL) 을 정의함 (DSL : 어떤 도메인에서 특정 목적만을 달성하기 위해 사용되는 언어)
예를 들어 프로젝트는 Java 지만, build.gradle 은 Groovy 나 Kotlin 으로 쓰여질 수 있음

Gradle 이 진행하는 작업 단위 하나하나를 "task" 라고 부름
build.gradle 파일을 파싱해서 task 를 뽑아내고 작업을 진행함

task 들은 DAG 를 구성함.
따라서 모든 task 는 실행의 순서가 있으며 반복 실행되지 않음

먼저 실행된 task 의 output 이 나중에 실행된 task 의 input 이 되기도 함

Gradle 은 각 task 의 output 을 저장함
어떤 task 의 input 및 output 이 동일하다면, 다음에 동작할 때 해당 task 는 실행하지 않음으로써 리소스를 아낄 수 있음


Gradle 은 자기 자신을 자동으로 업데이트 할 수 있음
Gradle 은 의존성을 야기하는 lib 들 또한 자동으로 업데이트 할 수 있음


Gradle 의 핵심 구조는 다음과 같음

- Project : 소프트웨어 요소들의 능력(?) 이나 범위를 정의 
- build script : "build.gradle". build 하기 위한 지시사항들(task)을 갖추고 있음
- task : 실질적인 작업 사항


hello world 를 프린트하는 굉장히 간단한 build.gradle 예제 (Groovy)

< build.gradle >
task helloWorld {
    doLast {
        println "Hello World"
    }
}

위 내용을 갖는 build.gradle 을 만든 후,
"gradle helloWorld" 명령어를 실행하면, gradle 이 실행되며 Hello World 가 실행됨


"gradle wrapper" 명령어를 실행하면, 기본적인 gradle 구조를 만들어 줌

- build.gradle
- gradle
- gradlew
- gradlew.bat

build.gradle 은 task 들을 모아둔 명령 파일이고, gradlew 로 시작하는 것들은 실행 파일임

각 OS 마다 사용되는 gradle 실행 파일이 다름.
- gradlew : unix(MacOS) 에서 사용 가능한 gradle 실행파일
- graldew.bat : Window 에서 사용 가능

MacOS 에서 "./gradlew helloWorld" 같은 명령어 실행이 가능
해당 명령어로 위에서 만든 hello world 출력이 가능함

이렇게 만들어진 gradlew 를 사용하여 빌드하면
내 로컬 환경에 상관없이, 즉 내 컴퓨터에 java 나 gradle 이 설치되지 않아도 빌드가 가능함






 

 

 

 

 

 

 

 

< First-Class 함수 >

 

First-Class 함수 : 프로그래밍 언어가 함수(Function)를 first-class 시민으로 취급하는 것

함수가 다른 함수의 인자로 전달될 수 있고, 함수의 결과로 리턴될 수 있고,

변수에 함수를 할당하거나 list 등의 구조 안에 함수를 할당하는 것이 가능하다는 말임

 

 

 

< closures >

 

closure : 자신의 영역 밖에서 호출된 함수의 변수값과 레퍼런스를 복사하고 저장한 뒤,

  이 캡처한 값들에 액세스할 수 있게 도와줌

free variable : 코드블럭안에서 사용은 되었지만, 그 코드블럭안에서 정의되지 않은 변수

def outer_func():
    message = 'Hi'

    def inner_func():
        print(message)

    return inner_func

my_func = outer_func()

my_func()  # Hi 가 출력됨

 

my_func 에서 __closure__ 라는 속성을 확인할 수 있으며, __closuer__ 는 tuple 임

__closuer__ 에는 cell 이라는 요소가 포함되어있으며

이 cell.cell_contents 를 열어보면 "Hi" 라는 글자가 있음

 

클로저는 지역 변수와 코드를 묶어서 사용하고 싶을 때 활용

클로저에 속한 지역 변수는 바깥에서 직접 접근할 수 없으므로 데이터를 숨기고 싶을 때 활용

 

 

 

 

 

 

< Iterator, Genrator, Decorator >

 

- Iterator : 요소가 복수인 컨테이너 타입 객체들(list, tuple, set, dironary, string)에서

  각 요소를 하나씩 꺼내 반복적으로 처리할 수 있도록 하는 방법을 제공하는 객체

  list, tuple 등의 객체들이 갖는 __iter__() 함수를 통해 Iterator 구현이 가능함

mytuple = (1,2,3)
myit = iter(mytuple)

print(next(myit))  #1
print(next(myit))  #2
print(next(myit))  #3

 

 

- Generator : Iterator 의 한 종류이며, 메모리를 효율적으로 사용하면서 반복을 수행하도록 돕는 객체

  미리 요소를 리스트에 만들어 놓고 꺼내는 것이 아니라,

  yield 및 __next__() 함수를 통해서 필요할 때마다 접근/생성하여 요소를 빼냄

def yield_test():
    for i in range(5):
        yield i
        print(i,'번째 호출!')

print(type(yield_test())) # <class 'generator'> yield 가 사용되면 generator 가 되나 봄

t = yield_test()
print(t.__next__()) # 0
print(t.__next__()) # 0 번째 호출! 1
print(t.__next__()) # 1 번째 호출! 2
print(t.__next__()) # 2 번째 호출! 3
print(t.__next__()) # 3 번째 호출! 4
#print(t.__next__()) #Error

 

  함수 내에서 yield 함수를 사용하여 값을 지정해두면,

  추후 __next__() 를 통해 값을 불러올 수 있음

  __next__() 함수가 실행되면, 내부에서 yield 를 실행하고 멈추며, 다시 실행하면 멈춘 곳부터 다시 실행

  yield 키워드 위치에서 함수가 끝나지 않은 유휴 상태로 대기

  이렇게 generator 를 사용하면, 필요할 때 마다 해당 객체를 통해 요소를 반환할 수 있음

  함수를 완전 실행시키는 것이 아니라, 일부를 실행시키고 일시 정지하기 때문에, 함수의 재사용성이 높아지고

  전체 요소를 메모리에 저장할 필요가 없어 비용효율적

 

- Decorator : 기존의 코드에 여러가지 기능을 덧붙여 실행하는 파이썬 구문

  이미 만들어져 있는 기존의 코드를 수정하지 않고

  (기존 코드와 새로  추가한 코드가 혼합된 래퍼(wrapper) 함수를 이용하여) 여러가지 기능을 앞뒤에 추가하기 위해 사용함

  대개 로그를 남기는데 사용되거나, 프로그램의 성능 테스트(실행 시간 측정)하기 위해서도 많이 쓰임

  기존 코드 중간에 뭔가 넣는 작업은 할 수 없고,

  단지 작업의 앞 뒤에 추가적인 작업을 손쉽게 넣어 사용하도록 하는 역할만 함

 

def decorator_function(original_function):
    def wrapper_function():
        print('{} 함수가 호출되기전 입니다.'.format(original_function.__name__)) # 꾸미고 싶어 추가한 부분
        return original_function()

    return wrapper_function


def display_1(): #수정하고 싶지 않은 함수 display_1
    print('display_1 함수가 실행됐습니다.')

display_1 = decorator_function(display_1) # display_1 함수를 인자로 넘기고 래퍼함수를 받음

display_1()

 

일반적으로는 @ 를 사용하여 데코레이터 함수와 연결한다고 함

def decorator_function(original_function):  # original_function 에 display_1 이 들어감
    def wrapper_function():
        print('{} 함수가 호출되기전 입니다.'.format(original_function.__name__))
        return original_function()

    return wrapper_function


@decorator_function
def display_1():  # 꾸미고 싶은 대상. display_1 이 decorator_function 의 인자( original_function )로 들어감
    print('display_1 함수가 실행됐습니다.')

# display_1 = decorator_function(display_1)

display_1()

 

 

  복수의 데코레이터를 동시에 사용하면, 아래쪽 데코레이터부터 실행되는데

  첫번째 데코레이터를 통해 리턴받은 래퍼 함수 대상으로 다시 두번째 데코레이터가 실행됨

  import functools.wraps 의 wraps 를 (데코레이터 내부의) 래퍼 함수에 @ 로 걸어줌

 

 

 

 

 

 

 

< REPL >

REPL : Read-Eval-Print-Loop

인터프리터 언어인 python 는 REPL 이 기본

 

 

 

 

< magic method >

 

매직 메소드란, 클래스안에 정의할 수 있는 스페셜 메소드.

클래스를 (int, str, list 등의) 파이썬 빌트인 타입(built-in type)과 같이 작동하도록 만들어 줌

 

클래스를 만들때 항상 사용하는 __init__이나 __str__는 가장 대표적인 매직 메소드

__init__ 메소드는 class 의 instance 를 생성할 때 자동으로 실행됨

 

int(3)+2 를 실행하면 5가 나오는데, 그 이유는 '+' 가 매직메소드인 __add__() 를 호출하여

int(3).__add__(2) 를 실행하기 때문

 

직접 만든 class 의 +, -, <, init 등의 매직 메소드들을 커스터마이징하여

원하는 기능대로 동작할 수 있도록 만들면 편함

 

 

 

< 클래스 변수 >

 

클래스의 인스턴스가 아닌, 클래스 자체에 붙어있는 변수

class MyClass:
    class_var = "Hi!"
    def __init__(self):
        print(MyClass.class_var)
        print(self.class_var)

my_class = MyClass()

 

self.class_var 로 접근 가능함

python 은 네임스페이스를 찾을 때 아래와 같은 순서로 찾기 때문.

인스턴스 네임스페이스(self.class_var) 로 찾아서 없으면

바로 위 클래스 네임스페이스(MyClass.class_var) 로 찾음

 

 

__init__ 함수를 통해 인스턴스 변수를 설정하는 방법 외에

아래와 같이 직접 인스턴스를 통해 변수를 집어넣는 방법도 있음

 

클래스 변수는 해당 클래스의 인스턴스들끼리 공유하는 전역변수처럼 사용이 가능......

 

...그럼 클래스 변수는 메모리에 올라와 있다는 말이 됨

 

 

 

< 인스턴스 메소드, 클래스 메소드, 스태틱 메소드 >

 

인스턴스 메소드 : 인스턴스를 통해서만 호출이 됨

  인스턴스 메소드의 첫 번째 인자로 인스턴스 자신을 자동으로 전달

  관습적으로 이 인수를 ‘self’라고 칭함

 

클래스 메소드 : 클래스 변수와 마찬가지로, 클래스 내 모든 인스턴스가 공유 가능한 메소드

  클래스 메소드는 ‘cls’인 클래스를 인자로 받고,

  (모든 인스턴스가 공유하는 클래스 변수 같은) 데이터를 생성, 변경 또는 참조하기 위한 메소드

< 클래스 메소드를 사용하지 않은 버전 > < 클래스 메소드를 사용한 버전 >

 

  클래스 메소드에서 사용되는 cls 는 클래스 자기 자신을 의미함.

  따라서 위의 오른쪽 예제처럼, 클래스 메소드를 호출함으로서 (클래스를 리턴받아 ) 클래스를 만드는 게 가능

  팩토리 메소드 역할을 함

 

  클래스 변수를 업데이트해야 한다면, 클래스 메소드를 이용해서 업데이트하는 게 좋다고 함

  데이터 검사나 다른 부가 기능 추가가 용이해서

 

스태틱 메소드 : 위의 두 메소드와는 다르게, 인스턴스나 클래스를 첫 번째 인자로 받지 않음

  스태틱 메소드는 클래스 안에 정의되어, 클래스 네임스페이스 안에 있을뿐 

  일반 함수와 전혀 다를게 없음

  클래스와 연관성이 있는 함수를 클래스 안에 정의해두고

  클래스나 인스턴스를 통해서 호출하여 편하게 사용

 

 

staticmethod 인 func_a 와 func_b 를 통해

staticmethod 에서는 self 에 접근이 불가능하다는 것과

staticmethod 는 클래스 및 인스턴스를 통해 호출 가능하다는 것을 확인

추가로 staticmethod 는 클래스 변수에도 접근 가능

자바의 static method 와 동일하다고 보면 될 듯

 

staticmethod 는 클래스와 연관된 순수함수를 설정할 때 사용하고,

classmethod 는 (위 예제처럼) 팩토리 메소드로 사용하거나, 클래스 변수를 변경할 때 사용

(그렇다고 staticmethod 가 클래스 변수를 변경하지 못하는 건 아님)

 

 

 

 

 

< 클래스 속성(인스턴스 변수들) 확인하는 방법 >

 

__dict__ 를 사용하여 확인 가능

 

위 예제에서 볼 수 있듯이

클래스 변수는 보이지 않고 인스턴스 변수만 확인 가능

dir() 는 해당 객체로 사용 가능한 내장 함수 리스트를 보여줌

 

 

 

 

< 오버라이딩에서 사용하는 super >

 

부모 클래스의 메소드를 호출할 때 사용하는 super 에 자기 자신의 클래스를 넣어줘야 함....

안 그러면 에러가 남

 

 

 

 

 

< underscore(_) 사용법 >

 

1. Python Interpreter 에서 마지막에 실행된 결과값으로 사용

>>> 10
10
>>> _
10
>>> _ * 3
30

 

2. 값을 무시하고 싶을 때 사용

x, _, y = (1, 2, 3) # x = 1, y = 3
x, *_, y = (1, 2, 3, 4, 5) # x = 1, y = 5

for _ in range(1, 4):
    print(_)  # 1,2,3 이 출력됨

 

3. private 으로 만들고 싶을 때 사용

가령 _로 시작하는 변수와 메소드를 갖는 모듈을 from module import * 로 임포트 한다면

_ 로 시작하는 변수, 메소드는 임포트에서 무시됨

(하지만 직접 module._... 을 통해 접근은 가능하여, 진정한 private 접근 제어는 아님)

def __init__(self):
    pass

_a = "a"  #private 변수. import * 에서 제외됨

b = "b"

def _echo(self, x):  #private 메소드. import * 에서 제외됨
    print x

def func(self):
    print("eyeballs")

 

4. 매직메서드에 사용

__init__, __len__ 등

__file__ 은 현재 파이썬 파일의 위치를 나타내며

__eq__ 은 'a == b' 같은 식이 수행될 때 호출되는 메소드

 

5. 맹글링 규칙을 이용하여 오버라이드를 피하기 위해 사용

맹글링이란, 컴파일러나 인터프리터가 변수/함수명을 그대로 사용하지 않고

일정한 규칙에 의해 변형시키는 것을 의미함

파이썬의 맹글링 규칙중에는 다음과 같은 규칙이 있음

"속성명이 double underscore (__) 로 시작한다면, 이때 속성명 앞에 _ClassName 을 붙임"

class MyClass:
    a = "a"
    __d = "d"  #맹글링 대상
    def b(self):
         pass
    def __c(self):  #맹글링 대상
         pass

dir(MyClass())
['_MyClass__c', '_MyClass__d', ... 'a', 'b']

 

이게 어디서 유용한가?

상속받은 자식클래스에서 동일한 이름의 변수/함수명을 사용하고 싶은데, 오버라이딩은 하기 싫을 때 사용함.

 

 

 

 

< Asterisk(*) 이해하기 >

 

1. 곱셈(2*3 = 6) 및 제곱(2**3 = 8) 에 사용

2. 리스트형 컨테이너 타입의 반복 확장에 사용

>>> zero_list = [0]*10
>>> zero_list
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> zero_tuple = (0,)*10
>>> zero_tuple
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

 

3. 가변인자 (Variadic Parameters)에 사용

packing : args, kwargs 를 통해 컨테이너 타입 형태로 데이터(가변인자)를 받는 것

 

*args : positional arguments 를 받음. arguments 몇개 받는지 모르는 경우 사용. 튜플 형태로 넘어옴

 

**kwargs : keyword argument 의 약자. keyword arguments 를 받음

  args 와 동일한 역할을 하며 key, value 를 함께 전달해야 함. key value 형태가 아니면 에러가 남

  dict 형태로 넘어옴

 

두 가지 모두 함께 쓸 수 있음. args 가 먼저 와야하며, 함수 사용할때도 args 인자가 먼저 와야 함

 

4. 컨테이너 타입 데이터(list, tuple, dict) 를 unpacking 할 때 사용
unpacking : 컨테이너 타입 데이터를 (가변인자나 변수에 넣기 위해) 앞에 * 를 붙이는 것

 

또한 리스트 혹은 튜플을 unpacking 한 후 다른 변수에 packing 하여 넣기도 함

numbers = [1, 2, 3, 4, 5, 6]

# unpacking의 좌변은 리스트 또는 튜플의 형태를 가져야하므로 단일 unpacking의 경우 *a가 아닌 *a,를 사용
*a, = numbers
# a = [1, 2, 3, 4, 5, 6]

*a, b = numbers
# a = [1, 2, 3, 4, 5]
# b = 6

a, *b, = numbers
# a = 1
# b = [2, 3, 4, 5, 6]

a, *b, c = numbers
# a = 1
# b = [2, 3, 4, 5]
# c = 6

 

 

 

 

 

 

< print 대신 logging 을 사용하는 이유 >

 

1. 로깅 레벨 조정 가능 (debug, info, warning, error, critical)

2. stdout 뿐 아니라 file 로 로그를 저장할 수 있음

3. 로깅 포맷 편집 가능 (파일 이름, 라인 넘버, 로그 메세지, 프로세스/스레드 이름, 시간 등)

4. 시간에 따라 로그 파일 생성 가능. 가령 5분 단위로 로깅하여 파일을 생성

 

 

 

 

 

< Trailing comma >

리스트나 튜플을 만들 때 가장 마지막에 넣는 comma ( , )

사람의 실수를 방지하기 위해 넣음

 

 

 

< Comprehension >

Comprehension : iterable한 오브젝트를 생성하기 위한 방법 중 하나.

for 문과 if 문이 혼합된 단 한 줄의 코드로 iterable 한 객체를 생성할 수 있음

4 종류가 있으며, 예제는 아래와 같음

- List Comprehension

  - numbers = [n for n in range(10)]  # [0,1,2,3,4,5,6,7,8,9]

  - evens = [n for n in range(10) if n%2==0]  # [0, 2, 4, 6, 8]

- Set Comprehension 

  - evens = {n for n in range(10) if n%2==0}  # {0, 2, 4, 6, 8}

- Dict Comprehension 

  - example = {k:v for k,v in (('a',1),('b',2))}  # {'a': 1, 'b': 2}

- Generator Expression

  - 한 번에 모든 원소를 반환하지 않음. 한 번 실행 할 때 원소 하나만 반환

 

 

 

 

 

< if __name__ == "__main__":의 의미 >

eye.py 라는 이름의 파이썬 파일이 아래와 같이 존재하는 경우

< eye .py >

def echo(a):
    print(a)

if __name__ == "__main__":
    echo("Hello eyeballs!")

 

이 eye .py 를 직접 실행하면 eye .py의 __name__ 변수에는 "__main__" 값이 저장

따라서 if __name__=="__main__" 의 값이 True 이므로 echo("Hello eyeballs!") 를 실행함

 

만약 파이썬 쉘이나 다른 파이썬 모듈에서 eye 를 import 하는 경우

import eye

from eye import echo

eye.py의 __name__ 변수에 eye.py의 모듈 이름인 "eye" 가 저장

따라서 if __name__=="__main__" 의 값이 False 이므로 echo("Hello eyeballs!") 를 실행하지 않음

 

즉, if __name__=="__main__" 는 python 모듈을 직접 실행할 때

수행하는 코드를 넣는 부분임

 

 

 

< 특정한 위치에 존재하는 모듈 import 하기 >

import sys

print(sys.path)

위 명령어로 나오는 path 들은 python lib 가 설치되어있는 위치들임

이 sys.path 에 원하는 모듈이 존재하는 path 를 추가하면, 해당 모듈은 import 가 가능하게 됨

sys.path.append("/my/module/path")

 

 

 

 

 

 

 

 

 

< package 에 포함된 __init__.py 용도 >

1. 해당 dir 가 패키지의 일부임을 알려주는 역할

2. 해당 패키지 내 모듈에서 사용 가능한 공통 변수 및 함수를 넣을 수 있음

3. 해당 패키지 내 모듈에서 공통적으로 import 해야 할 다른 모듈을 import 해두어, 미리 import 할 수 있음

4. 해당 패키지 내 모듈에서 먼저 실행되어야 하는 공통 코드(이를테면 초기화 코드 등) 를 넣을 수 있음

 

 

 

 

 

< 일부러 에러 발생시키기 >

raise NotImplementedError

 

 

 

< GIL >

python global interpreter lock 에 대해 반드시 공부

 

 

 

 

 

 

참고

https://schoolofweb.net/blog/posts/category/%ed%94%84%eb%a1%9c%ea%b7%b8%eb%9e%98%eb%b0%8d/%ed%8c%8c%ec%9d%b4%ec%8d%ac/%ed%8c%8c%ec%9d%b4%ec%8d%ac-%ec%a4%91%ea%b8%89-%ea%b0%95%ec%a2%8c/

https://mingrammer.com/underscore-in-python/

https://mingrammer.com/understanding-the-asterisk-of-python/

https://wikidocs.net/91564#:~:text=print%20%ED%95%A8%EC%88%98%20%EB%8C%80%EC%8B%A0%20%EB%A1%9C%EA%B9%85%20%EB%AA%A8%EB%93%88,%EC%B6%9C%EB%A0%A5%EB%90%A8%EC%9D%84%20%ED%99%95%EC%9D%B8%ED%95%A0%20%EC%88%98%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4.

 

 

 

 

 

 

 

 

 

 

 

네트워킹 테스트용 데모 버전 실행을 위해

다음과 같은 API 서버들을 kube 위에 띄우려고 함

Kube 는 회원제 서비스를 위한 API 를 제공하며

Client 는 새로 회원가입을 하기 위해 Users API 를 호출함

Users API 는 토큰 생성을 위해 Auth API 를 호출함

회원가입이 마무리 된 이후, Client 는 Tasks API 를 사용하여 task 를 수행함.

Client 가 직접 부를 수 있는 API 는 Users API, Tasks API 두 개이며

UsersAPI 와 Auth API 는 하나의 Pod 내부 통신을 사용함

 

 

 


 

 

 

Users API 를 생성하는 kube yaml 파일을 생성해 봄

이름은 users-deployment.yaml


< users-deployment.yaml >

apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: users
  template:
    metadata:
      labels:
        app: users
    spec:
      containers:
        - name: users
          image: my-user-image

 

kubectl apply -f=users-deployment.yaml 명령어로 kube 위에 pod 를 띄움

그리고 kubectl get pods, kubectl get deployments 로 pods , deployments 가 잘 떴는지 확인

 

 


 

 

users API 앱이 올라온 뒤

외부 세계(즉 Client) 에서 Users API 에 접근할 수 있도록

Service 를 추가로 설정해야 함

 

Service 를 설정하여 Service 의 두 가지 기능을 사용

- Service 는 항상 변경되지 않는 안정적이고 고정된 주소를 제공

  (Pod 자체가 default IP (ClusterIP) 를 갖고 있긴 하지만, Pod 가 재실행되면 바뀌게 됨)

- Service 는 외부 세계에서 pod 내부 앱으로의 접근을 허용하도록 구성 가능케 함

 

Service 추가를 위해 Service yaml 파일을 새로 생성

이름은 users-service.yaml


< users-service.yaml >

apiVersion: v1
kind: Service
metadata:
  name: users-service
spec:
  selector: 
    app: users  #위 deployment 에서 생성한 모든 users pods 에 service 를 할당함
  type: LoadBalancer  #외부 세계에서 접근 가능한 ip 를 제공하는 유일한 type
  ports:
    - protocol: TCP
      port: 8080  # 외부 세계에서 접근 가능한 포트
      targetPort: 8080  #내부 앱에서 받는 포트

 

 

아래 명령어를 사용하여 service yaml 를 kube 에 띄움

kubectl apply -f=users-service.yaml

그 후 kubectl get services 로 제대로 떴는지 확인

 

만약 Cloud Provider 를 이용하여 Service 를 띄웠다면

Cloud Provider 에서 제공하는 Service 의 IP를 얻을 수 있지만,

강의에서는 minikube 를 사용하기 때문에

아래 명령어를 사용하여 Service 의 IP 를 확인

 

minikube service users-service

 

위 스샷에서 보이는 것처럼 192.168.99.100:32023 이 Service 가 제공하는 IP 같은데...

yaml 에 넣어준 8080은 앞에 따로 표기되어있음

32023 은 무슨 의미지(????)

게다가 실제로 IP 에 접근할 때 8080 대신 32023 을 사용하고 있음

 

8080 은 왜 넣은거야 (???)

 

 

 


 

 

가장 위의 아키텍처에서 설명했듯이,

하나의 Pod 내에 두 가지 UserAPI Container, AuthAPI Container 두 개를 각각 띄우려고 함


< users-deployment.yaml >

apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: users
  template:
    metadata:
      labels:
        app: users
    spec:
      containers:
        - name: users
          image: my-user-image
          env:
            - name: AUTH_ADDRESS
              value: localhost
        - name: auth
          image: my-auth-image


 

 

위 yaml 로 띄운, 하나의 Pod 내 두 개의 Container 가 서로 통신하기 위해서는?

pod 내에 서로 다른 Container 는 'localhost' 라는 이름의 주소를 통해 서로 통신 가능함

my-user-image 이미지를 사용하는 users Container 에 

AUTH_ADDRESS 라는 환경변수를 설정하고 값을 localhost 로 지정함

users Container 내부에서는 AUTH_ADDRESS 환경변수(=localhost) 를 다음과 같이 이용함

 

결과적으로 localhost/token 으로 요청을 보내게 되는데

localhost/token 은 두번째 Container 인 auth Container 가 받음

즉, user Container 에서 localhost 라는 이름의 주소를 이용하여 auth Container 에 접근함(서로 통신함)

 

위와 같이 pod 내에 두 개의 Container 를 설정하고

kubectl apply -f=users-deployment.yaml 명령어로 kube 위에 pod 를 띄우고

kubectl get pods 로 확인해보면, READY 에 2/2 라고 뜨게 됨 (Container 개수)

 

 


 

 

조금 더 발전시켜 볼 테스트용 데모 버전은 다음과 같음

각 API 는 서로 다른 Pod 에 존재하고, 각 Pod 는 각 Service 를 갖고 있음

이 때 Users API 와 Tasks API 를 담는 Pod 두 개에 붙은 Service 는 Client 가 접근 가능하게 설정되어야하고

Auth API 를 담는 Pod 에 붙은 Service 는 Client 가 접근 불가능하게 설정되어야 함

하지만 Users API 와 Tasks API 는 Auth API 에 접근 가능해야 함 

 

위 데모 환경 구축을 위해 아래와 같은 yaml 파일들을 작성

auth deployment yaml 을 생성하고,

(auth pod 재시작시 IP 가 계속 바뀌는 것을 방지하기 위해) auth Service yaml 을 생성

 


< auth-deployment.yaml >

apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: auth
  template:
    metadata:
      labels:
        app: auth
    spec:
      containers:
        - name: auth
          image: my-auth-image

< auth-service.yaml >

apiVersion: v1
kind: Service
metadata:
  name: auth-service
spec:
  selector: 
    app: auth
  type: ClusterIP  # pod 주소 고정 및 Load Balancer 역할을 해주지만 외부 세계로 노출은 막아주는 타입. 오직 Cluster 내부 세계로부터만 auth pod 에 접근 가능하게 됨
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

 

위와 같이 ClusterIP 타입을 사용하는 auth service 를 만들고

kubectl apply 명령 진행하고 kubectl get services 로 확인하면

ClusterIP 타입 Service 가 제공하는 (Cluster 내부에서만 사용 가능한) IP 를 확인 가능

 

 

그리고 아래와 같이 users deployment 에서 auth Service 의 IP 를 사용할 수 있도록 업데이트


< users-deployment.yaml >

apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: users
  template:
    metadata:
      labels:
        app: users
    spec:
      containers:
        - name: users
          image: my-user-image
          env:
            - name: AUTH_ADDRESS
              value: "10.99.104.252"


< users-service.yaml >

apiVersion: v1
kind: Service
metadata:
  name: users-service
spec:
  selector: 
    app: users
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080

 

 

위와 같이 kubectl get services 로 일일이 IP 를 확인하고

IP 를 직접 하드코딩하는 방법은 가능하지만 불편한 방법임

더 좋은 방법은, Kube 에서 자동으로 만들어주는 Service IP 를 의미하느 환경 변수를 사용하는 것임

 

Kube 는 service 를 실행하면, 실행된 Service IP 를 의미하는 환경 변수를 생성함

"[Service_이름]_[SERVICE_HOST]" (모두 대문자이며 - 는 _ 로 변환됨)

예를 들어 위에 auth Service yaml 의 경우 이름이 "auth-service" 이기 때문에

Kube 에 의해 자동으로 만들어지는 IP 환경변수 이름은 AUTH_SERVICE_SERVICE_HOST

(물론 user service yaml 의 경우, USERS_SERVICE_SERVICE_HOST 가 됨)

이를 users Container 내부에서 다음과 같이 사용할 것임

 

(위와 같이 변경하면, users-deployment.yaml 내부에서 AUTH_ADDRESS 를 굳이 주지 않아도 됨)

 

kube 가 자동 생성하는 환경변수를 사용하는 것 외에,

(자동으로 생성되는) Service IP 의 도메인 주소를 사용할 수 있음

(kube 내에서 실행되는 Core DNS 에 의해) Service IP 의 도메인 주소를 Service name.namespace 로 설정함

[서비스명].[namespace]

즉, 아래와 같이 변경이 가능하다는 말이 됨

 


< users-deployment.yaml 변경 전 >

.....
      containers:
        - name: users
          image: my-user-image
          env:
            - name: AUTH_ADDRESS
              value: "10.99.104.252"


< users-deployment.yaml 변경 후 >

.....
      containers:
        - name: users
          image: my-user-image
          env:
            - name: AUTH_ADDRESS
              value: "auth-service.default"

 

service 이름(auth-service) 뒤에 붙은 namespace(default) 는 기본 값이 default 임 

우리가 kube 에게 특별한 namespace 를 사용하라고 명령하지 않는 한,

kube 는 default namespace 를 계속 사용함

(이 도메인네임은 Container 내부 코드에서 사용 가능할까? 왠지 가능할 것 같음)

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

kube 를 통해 띄워진 Container 에서 생성한 데이터들은

(Docker 에서 그러했듯이) 어딘가에 저장하지 않으면 Container 가 제거되거나, 확장되는 등

이벤트가 발생했을 때 데이터를 잃어버릴 수 있음

따라서 kube 에서 데이터를 유지할 수 있는 방법인 Volume 에 대해 알아봄

 

deployment 와 service 가 함께 이미 실행되고 있는 상황을 전제로 설명 이어감

 

 


 

 

 

deployment 에 의해 생성된 (pod를 포함하는) container 가 crash 충돌 등에 의해 재시작하게 되면,

pod 에 의해 저장하고 있던 데이터는 모두 날아가게 됨

이를 방지하기 위해 volume 기능을 사용하여 데이터를 기존과 다른 어느 외부에 지정

 

[volume types documentation]

kube 는 다양한 volume 및 driver 를 지원한다고 함

kube 는 다양한 환경(Cloud provider (AWS, Google, Azure 등) , IDC 등)위에서 운영될 수 있기 때문에

이에 맞는 volume 과 driver 를 제공하는 거라고 함

 

 volume 은 pods 의 spec 부분에 추가하여 설정 가능함

아래와 같이 pods spec (in deployment yaml) 을 설정하면 됨

 


 

 

emptyDir 타입의 volume 이 적용된 yaml 예제를 들어 설명함

 

....
    spec:
      containers:
        - name: demo
          image: demo
          volumeMounts:
            - mountPath: /app/demo  #Container 내부 경로
              name: demo-volume

      volumes:
        - name: demo-volume
          emptyDir: {}

 

 

여기서 사용된 emptyDir 타입의 volume

pod 가 시작 될 때마다 단순히 새로운 빈 dir 를 생성

(여기서 말하는 빈 dir 의 path 는 mountPath 로 설정한 위치가 됨)

pod 가 살아있는 동안 그 dir 를 활성 상태로 유지하고, pod 의 데이터를 그 dir 에 채움

container 가 재시작되거나 심지어 제거되더라도 해당 dir 에 들어있는 데이터는 유지됨

하지만 dir 은 pod 와 연결되어있기 때문에 pod 가 제거되면 dir 도 같이 제거됨(데이터도 함께 삭제) 

pod 가 제거된 이후, 다시 생성되면 volume 에 의해 새로운 빈 dir 가 생성됨(데이터 돌아오지 않음)

 

containers.volumeMounts.name 에 설정된 volume명(위 예에서 demo-volume) 과

volumes.name 에 설정된 volume명이 동일한 것끼리 서로 매칭(mount)됨

즉, 위 예제에서는 emptyDir 타입을 갖는 demo-volume 이 container 에 mount 됨

 

emptyDir 타입의 volume 은 하나의 pod 에 연결되어있음

만약 pod가 2대 이상인 경우라면 아래와 같은 상황이 연출됨

- podA, podB 가 존재

- podA 의 emptyDir volume 에 데이터가 저장됨

- podA 의 emptyDir volume 에서 데이터를 읽어보니 잘 읽힘

- podA 의 container 가 죽음

- (loadBalancer 에 의해 트래픽이 podB 로 이동) podB 의 emptyDir volume 에서
  데이터를 읽어보니 실패함(데이터가 없어서)

- podA 가 다시 살아남

- podA 의 emptyDir volume 에서 데이터를 읽어보니 잘 읽힘

 

위와 같은 상황을 해소하기 위해선 pod 당 새로운 빈 dir 를 생성하는 것으로 전환해야 함

그리고 그것을 도와주는 것이 hostPath driver

 

 

 


 

 

[hostPath driver documentation]

hostPath driver 를 통해 호스트 머신(=node) 의 path 를

(각 Pod 를 실행하는 실제 머신에 연결되도록) 설정할 수 있음

설정한 hostPath 위치 내 데이터는 각각의 pod 에 연결됨

그래서 2대 이상의 pods 가 존재해도,

여러 pods 가 (pod 의 특정 경로 대신) 하나의 호스트 머신 위의 동일한 dir 를 공유할 수 있음

(물론 이는 동일한 pod 에서 모든 요청을 처리하는 경우에만 유용함)

 

hostPath 는 아래와 같이 deployment 의 yaml 내 pod spec 설정 부분에 추가 설정함

 

....
    spec:
      containers:
        - name: demo
          image: demo
          volumeMounts:
            - mountPath: /app/demo  #Container 내부 경로
              name: demo-volume

      volumes:
        - name: demo-volume
          hostPath:
            path: /data  #외부 경로
            type: DirectoryOrCreate

 

 

hostPath.path 에 경로를 설정

hostPath 는 항상 빈 dir 를 생성하지 않으며, 이미 존재하는 경로를 설정할수 도 있음

마치 docker 의 bind mount 하는 것과 같음 (-v /data:/app/demo) 

위 예제에서는 호스트 머신의 volumes.hostPath.path 와 pods 내의 containers.volumeMounts.mountPath 가 연결됨

 

type: DirectoryOrCreate 은 path 로 설정한 dir 가 존재하지 않으면 새로 생성하라는 의미

type: Directory 로 설정할 수 있으나, 만약 path 로 설정한 dir 가 존재하지 않으면 실패함

 

hostPath driver 를 설정해두면, pod 가 죽어도 데이터는 유지됨

왜냐하면 데이터는 호스트 머신 위에 존재하기 때문

 

hostPath 를 통해 여러 pods 에서 하나의 동일 dir 를 바라보게 한 것까지는 좋았지만

다른 노드(즉, 다른 호스트 머신)에 존재하는 다양한 pods 는 하나의 dir 를 바라볼 수 없음

오직 하나의 노드(하나의 호스트 머신) 위에 존재하는 pods 만이 동일 dir 를 바라볼 수 있음

그래서 hostPath driver 는 여러 호스트 머신 위에 kube 가 실행되는 실제 운영 환경 등에서 적용 불가

 


 

 

[persistent volume documentation]

 

kube 는 데이터를 영구적으로 저장 가능한 Persistent Volumes 기능을 지원함

pods 및 Nodes 독립성에 대한 아이디어를 기반으로 구축되었기 때문에

Persistent Volume 은 pods 나 Nodes 로부터 완전히 독립되어있음

(pods 나 Nodes 의 life cycle 에 영향을 받지 않는다는 말)

엔지니어는 이 volume 이 구성되는 방식에 대한 완전한 권한을 갖게 됨

각 pods 와 각 deployment yaml 파일 등에 volume 설정을 여러번 할 필요가 없음

대신 한 번만 정의하고 여러 pods 에서 사용하도록 만듦

 

 

Persistent Volume(이하 PV) 는 Cluster 내에 존재하며,

Nodes 외부에 존재함 (따라서 Nodes 및 Pods 로부터 독립성을 갖게 됨)

 

Nodes 내부에 PV 와의 연결을 위한 PV Claim 이란 것을 생성함

PV Claim 은 (Pods 가 실행되는) Nodes 에 귀속됨(Nodes 에 의존성이 있음)

PV Claim 은 PV 에 도달 및 접근하여 데이터를 읽어올 수 있음

그래서 Pod 내의 Container 가 PV 에 접근하여 데이터를 읽을 때 중간에서 도움을 줌.

 

여러 PV 를 바라보는 PV Claim 을 가질 수 있으며

여러 PV Claim 이 하나의 PV 를 바라볼 수 있음

 

 

참고) [AWS Documentation]

CSI (Container Storage Interface)는 Kubernetes에서 다양한 스토리지 솔루션을 쉽게 사용할 수 있도록 설계된 추상화임
다양한 스토리지 공급업체는 CSI 표준을 구현하는 자체 드라이버를 개발하여
스토리지 솔루션이 Kubernetes와 함께 작동하도록 할 수 있음
(연결되는 스토리지 솔루션의 내부에 관계없이)
AWS는 Amazon EBS , Amazon EFS 및 Amazon FSx for Lustre 용 CSI 플러그인을 제공함

 

 

 


 

 

 

 

hostPath 를 사용하는 Persistent Volume 샘플을 만들어서 이해도를 높여봄

(Persistent Volume 에서 hostPath 를 사용한다는 말은,

Cluster 를 단일 노드 하나에서만 정의한다는 말이 됨)

 

host-pv.yaml 이라는 이름의 yaml 파일을 새로 만들어서 Persistent Volume 을 정의해 봄

 


< host-pv.yaml >

apiVersion: v1
kind: PersistentVolume
metadata:
  name: host-pv
spec: 
  capacity:
    storage: 4Gi
  volumeMode: Filesystem  #Filesystem 과 Block 으로 나뉨
  storageClassName: standard  #default sc 이름이 standard 라서 standard 를 넣음
  accessModes:
    - ReadWriteOnce
  hostPath: 
    path: /data  #노드의 /data 가 PV 에 연결됨
    type: DirectortOrCreate


 

 

accessModes 는 아래와 같은 mode 를 제공함

 - ReadWriteOnce : 단일 노드로부터의 rw 요청을 받을 수 있도록 mount 되는 Volume
 - ReadOnlyMany : 여러 노드로부터의 r 요청만 받을 수 있도록 mount 되는 Volume
 - ReadWriteMany : 여러 노드로부터의 rw 요청을 받을 수 있도록 mount 되는 Volume

(위 예제에서 hostPath 는 단일 노드 환경에서만 사용 가능하므로

사용할 수 있는 mode 는 오직 ReadWriteOnce 뿐임)

 

Kube 는 Storage Class 라는 개념을 갖고 있음

kubectl get sc 명령으로 확인 가능하며 default sc 도 존재함

 

Storage Class 는 kube 에서 관리자에게

Storage 관리 방법과 Volume 구성 방법을 세부적으로 제어할 수 있게 해주는 개념임

SC 는 hostPath Storage 를 프로비저닝 해야하는 정확한 Storage 를 정의함 (?뭔말임?) 

 

PV 는 한 번만 설정하면(심지어 다른 사람이 설정해도 됨)

여러 pods 에서 함께 접근하여 사용 가능한 저장소가 됨

이렇게 생성한 PV 를 Pods 가 접근해서 사용 가능하게 만들려면

pods 가 PV 에 접근하는 것을 도와주는 PV Claim 을 생성해야 함

 

두 가지를 추가 설정하면 됨

1. PV Claim 을 생성하는 yaml

2. (PV 를 사용하려는) pods 의 yaml 에서 PV Claim 사용하도록 설정

 

1번(PV Claim 을 위한 yaml)을 생성해보자.

이름은 host-pvc.yaml

 


< host-pvc.yaml >

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: host-pvc
spec:
  volumeName: host-pv  #위에 PV 를 정의할 때 설정한 이름을 여기 넣어서, 해당 PV 와 PVC를 연결함
  accessModes:
    - ReadWriteOnce
  storageClassName: standard  #default sc 이름이 standard 라서 standard 를 넣음
  resources:
    requests:
      storage: 4Gi  #PVC 에서 요청하는 크기는, PV 의 stoage 이하만 가능

 

2번(pods 의 yaml 에 PVC 사용 설정)을 추가 설정해보자

deployment yaml 에서 pods 의 spec 을 정의하는 부분에 volumes 를 아래와 같이 설정

 

....
    spec:
      containers:
        - name: demo
          image: demo
          volumeMounts:
            - mountPath: /app/demo
              name: demo-volume
      volumes:
        - name: demo-volume
          persistentVolumeClaim:
            claimName: host-pvc  #위에서 PVC 를 정의할 때 사용한 이름을 가져다가 설정함

 

 

PV 가 데이터를 저장하는 곳은 어디이기에 Nodes 와 Pods 로부터 독립적인걸까?

PVC 가 필요한 이유는 무엇일까?

왜 PV 와 Pods 사이에 중간다리 역할이 필요한거지?

PV 의 권한과 PVC 의 권한은 왜 구분되어있는걸까?

 


 

 

 

위에서 만든 PV yaml 와 PVC yaml 및 업데이트한 deployment yaml 을 kube 에 띄움

kubectl apply -f=host-pv.yaml

kubectl apply -f=host-pvc.yaml

kubectl apply -f=deployment.yaml

 

PV, PVC 가 잘 올라왔는지 아래 명령어로 확인

kubectl get pv 

kubectl get pvc

 


 

 

일반 Volume 와 Persistent Volume 의 차이를 알아봄

 

  일반 Volumes Persistent Volumes
Container 의존성 Container 재시작, 제거 되어도 데이터 유지 Container 재시작, 제거 되어도 데이터 유지
Pod 의존성 emptyDir 타입이라면, Pods 가 사라지면 데이터도 사라짐
hostPath 를 사용한다면 Pods 가 사라져도 데이터가 사라지진 않지만, Nodes 에 종속되기 때문에 (Nodes 가 내려가면 사용 불가, 단일 Node 에서만 사용 등에 이유로) 글로벌 수준에서 관리하기 어려움
Pod 의존성 없음
Node 의존성 없음
설정 위치 Pods 를 정의하는 deployment yaml 에 설정 PV 를 위한 yaml 에 설정
구성이 하나의 파일에 standalone 으로 존재하므로 재사용하거나 관리하기 용이함
프로젝트 크기 소규모 프로젝트에서 사용하기에 용이 대규모 프로젝트에서 사용하기에 용이

 

 


 

 

 

deployment yaml 에 설정된 값을 (Container 내에서 동작하는) application code 내에서 사용 가능

마치 환경 변수마냥 사용하는 것임

예를 들어 다음과 같은 app.js 코드가 존재하고


< app.js >

...
const filePath = path.join(__dirname, 'story', 'text.txt');
...

 

위 코드는, 아래와 같은 deployment yaml 로 실행될 Pods 내에서 실행될꺼라고 해보자


< deployment yaml>

....
    spec:
      containers:
        - name: story
          image: story-image
          volumeMounts:
            - mountPath: /app/story
              name: story-volume
....

 

 

deployment yaml 에 코드에서 사용 가능한 환경 변수를 추가해주고,


< deployment yaml>

....
    spec:
      containers:
        - name: story
          image: story-image
          env:
            - name: STORY_DIR
              value: 'story'
          volumeMounts:
            - mountPath: /app/story
              name: story-volume
....

 

app.js 코드 내의 'story' 를 설정한 환경변수명으로 바꿔줌


< app.js >

...
const filePath = path.join(__dirname, process.env.STORY_DIR, 'text.txt');
...

 

 

모든 언어에서 process.env.[name] 형식으로 환경변수를 가져오진 않을 것 같은데

이건 언어에서 환경 변수 가져오는 방법에 따라 달라질 듯

 

 


 

 

위와 같이 yaml 자체에 환경 변수값을 넣는 방법 외에,

환경 변수를 정의한 yaml 파일을 만들어서 환경변수값을 가져와 넣는 방법이 존재함

이해를 위해 env.yaml 을 만들어보자

 


< env.yaml >

apiVersion: v1
kind: ConfigMap
metadata:
  name: myenv
data:
  mydir: 'story'
  mykey1: 'myvalue1'
  mykey2: 'myvalue2'

 

kubectl apply -f=env.yaml

kubectl get configmap

 

그리고 deployment yaml 에 코드에서 env.yaml 에서 설정한 값을 가져옴


< deployment yaml>

....
    spec:
      containers:
        - name: story
          image: story-image
          env:
            - name: STORY_DIR
              valueFrom:
                configMapKeyRef:
                  name: myenv
                  key: mydir

          volumeMounts:
            - mountPath: /app/story
              name: story-volume
....

 

app.js 에서 process.env.STORY_DIR 로 읽은 값은 여전히 'story' 가 될 것임

 

더 복잡한데..?

 

 

 

 

 

 

 

 

Kubenetes는, 프로그램(애플리케이션)을 Container 로 배포, 확장 및 관리할 때 사용하는 시스템

Docker Container 를 배포하고 orchestration 하는 표준 시스템임

Kube 는 여러 머신을 위한 docker-compose 라고 생각할 수 있음

docker-compose 는 하나의 머신 위에 여러 Container 를 띄우고 관리하고

Kube 는 더 나아가 여러 머신 위에 여러 Container 를 띄우고 모두 관리(배포, 모니터링 등)함

 


 

 

 

Kube 없이 수동으로 Container 를 배포하면, Container 가 떠야 할 서버를 직접 구성 및 관리해야하며

Container 관리, 즉 보안, 구성, Container 간 충돌, Container down 등의 이슈도 직접 처리해야 함

Container down 이 발생 할 때마다 직접 모니터링하고 이슈를 해결하고

수동으로 재배포해줘야 하는 번거로움이 있음 (근데 이건 Kube 써도 마찬가지 아닌가)

Container 로 들어오는 트래픽이 양이 변할 때 Container Scaling 을 수동으로 처리해야 하며

트래픽을 모든 Container 에 고르게 분산시키기 위해 추가 작업을 해야 함

 

Kube 를 사용하면 자동 배포, Scaling, 로드 밸런싱, Container 관리 등을 손쉽게 할 수 있음

 

 


 

 

 

Kube 는 (EC2 처럼 CPU, RAM 이 장착된 Physical/Virtual 머신인) 하나 이상의 Worker Nodes 로 이루어져 있으며 

Worker Nodes 는 (실제 Container 와 동일한) 하나 이상의 Pods (=Containers) 와

(Pods 의 네트워크 트래픽 제어를 위한) Proxy/Config 로 이루어져 있음

Prox/Config 는 Pods 가 외부 인터넷에 연결 가능한지 여부와

외부 인터넷에서 (Pods 내부에서 실행되는) Container 로 어떻게 접근할 수 있는지를 제어함

 

Kube 를 사용하여 Container 및 Pod 를 동적으로 추가/제거하는 경우

Kube 가 사용 가능한 Worker nodes 에 Pods 가 자동으로 배포됨

따라서 서로 다른 여러 Worker Nodes 위에서, 여러 Containers 를 실행하여 워크로드를 고르게 분배 가능함

 

Worker Nodes 에 Pods 를 새로 띄우거나, (실패하거나 필요하지 않은 경우) Pods 를 교체하거나 종료하는 역할은

Master Node 에 의해 진행됨

Master Node 는 Worker Nodes 처럼 하나의 머신임

Master Node 위에서 실행되는 Control Plane 이 위 작업을 수행하게 됨

Control Plane 은 Master Node 에서 실행되는 다양한 서비스의 다양한 도구 모음이며,

Worker Nodes 를 제어하는 컨트롤 센터같은 거라고 생각하자.

우리가 Kube 를 사용할 때, Worker Nodes, Pods 와 직접 상호작용하지 않음(일반적으로 하지 않음)

Kube 와 Control Plane 이 우리 대신 Worker Nodes, Pods 와 상호작용 함

 

Master Node 와 Worker Nodes 가 하나의 Cluster 안에 포함되며

Cluster 내 Nodes 들은 하나의 네트워크를 형성하여 서로 통신이 가능함

 

EC2 위에 띄운 Master Node 는 Cloud Provider(예를 들어 AWS) 의 API 에 다음과 같은 명령을 보내 Cluster 를 구축함

"내가 지금부터 너의 서비스들을 이용하여 Kube 를 실행할 Cluster 를 만들꺼야.

Worker Nodes 만들 EC2 3대를 생성하고, 로드 밸런싱을 위한 로드 밸런서, ... 등을 실행해 줘"

그리고 Master Node 에서 Kube 와 Kube 툴을 실행하고

AWS 로부터 받은 리소스(EC2, 로드 밸런서 등)를 이용하여 Pods 를 띄우는 등의 작업을 진행함

 

 


 

 

 

Kube 사용자가 (Cloud Provider 도움 없이) Kube 를 사용하기 위해 해야 할 일들이 있음

Kube 는 infrastructure 에 대해 전혀 신경쓰지 않기 때문에

사용자가 infra 를 미리 구축해주어야 함

 

Nodes 로 사용될 머신들 (AWS 의 경우 EC2) 을 준비해야하며

준비된 모든 Nodes 에 Kube (API Server, kubelet 등의 Kube services, softwares) 를 설치해야 함

필요하다면, cloud provider 로부터 얻을 수 있는 로드 밸런서 등의 리소스를 미리 준비

 

사용자가 위와 같은 infra 리소스들(Nodes, 로드 밸런서 등)을 준비해두면

Kube 는 이 리소스들을 알아서 운용함

 


 

 

Worker Node 에 대해 자세히 알아보자.

 

Worker Node 는 (위에서 설명한 것처럼) 하나의 머신이며

Master Node 에 의해 관리됨

 

 

Worker Node 안에서 하나 이상의 Pods 가 있고

이 Pods 또한 Master Node 에 의해 관리됨

(따라서 Worker Node 가 Pod 를 삭제할 수 없음)

Worker Node 에 꼭 설치되어 있어야하는 것들은 다음과 같음

- Docker : Pods 내 Container 실행에 사용됨

- kubelet : Master Node 와 통신할 때 사용됨

- kube-proxy : (Pods 내 Container 에 의해) 해당 node 로 들어오고 나가는 네트워크 트래픽을 처리할 때 사용됨

AWS 를 사용하는 경우, 위에서 언급한 필요한 머신(instance) 및 소프트웨어들을 AWS 가 알아서 설치해준다고 함

 

Pods 는 하나 이상의 Containers 를 실행하고 있음

일반적으로 pod 하나에 Container 하나 실행

Pods 는 Container 외에 Volumes 같은 리소스도 실행 가능(..!)

이 Volume 은 Container 가 사용 가능한 저장소임

 

Pods 는 Containers(와 Volumes) 를 묶는 논리적인 단위 같은 거라고 이해하면 편할 듯

 

 


 

kubelet 은 Worker Node 에 존재하며,

다음과 같이 노드 및 파드 운영에 큰 역할을 담당하고 있음

 

- 파드 관리 : Master Node 로부터 할당된 파드를 Worker Node 에 배치하고 실행

  파드 상태 주기적으로 모니터링하고 문제 발생시 다시 시작

- 컨테이너 실행 : 파드에 정의된 Container 를 실행하고 관리

  Container 시작/중지 및 리소스 할당

- 리소스 모니터링 및 보고 : Worker Node 의 리소스 사용량 모니터링하여 클러스터 상태 파악

  모니터링 정보는 Master 에 보고되고, 클러스터 스케줄링할 때 사용됨

- 상태 모니터링 및 보고 : Worker Node 및 파드 상태를 Master 에 보고함

  이를 통해 Master 는 전체 클러스터 상태 파악이 가능

- Worker Node 자동 복구 : Worker Node 가 비정상적으로 종료되면 자동으로 복구 시도

  이를 통해 Worker Node 의 가용성을 유지하고 안정성을 보장

 

참고 : https://velog.io/@gun_123/Kubernetes-Kubelet#:~:text=%EC%9D%84%20%EC%A7%80%EC%9B%90%ED%95%A9%EB%8B%88%EB%8B%A4.-,1.%20Kubelet%EC%9D%B4%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94%3F,%EB%A5%BC%20%EC%8B%A4%ED%96%89%20%EB%B0%8F%20%EA%B4%80%EB%A6%AC%ED%95%A9%EB%8B%88%EB%8B%A4.

 


 

 

Master Node 에 대해 자세히 알아보자.

 

Master Node 는 (위에서 설명한 것처럼) 하나의 머신이며

Worker Node 를 관리함

 

 

Master Node 에 꼭 설치되어 있어야하는 것들은 다음과 같음

- API Server : Worker Node 의 kubelet 과 통신할 때 사용

- Scheduler : Pods 들을 관찰하고, Pod 를 생성할 Worker Node 를 선택하는 일을 담당

  (Pod 다운되었거나, 비정상적이거나, 스케일링으로 새로 Pod 를 생성하는 경우) 

  Scheduler 가 API Server 에게 "Pods 2 대 새로 생성해" 같은 요청 전달하면

  API Server 가 그걸 받고 실제 작업을 하나 봄

- Kube-Controller-Manager : Worker Node 전체를 감시하고 제어, Pods 가 의도된 수만큼 떠있는지 확인하는 역할

  그래서 API Server, Scheduler 와 긴밀하게 연동된다고 함

- Cloud-Controller-Manager : Cloud-Controller-Manager 는 Cloud Provider 에게 무슨 일을 해야하는지 알려줌

  Cloud Provider 에 의해 달라진다고 하는데 구체적으로 뭐가 다른지는 아직 모르겠음

 

 


 

 

Pods 를 묶는 논리적인 그룹을 Service 라고 함

(위에서 언급된) Poxy 와 연관이 있음

Service 는 특정 Pod 를 외부 세계에 노출하여

특정 IP 혹은 Domain 으로 특정 Pod 에 연결할 수 있도록 하는 용어임...(???)

그럼 Pod 끼리 (Service 에 의해 제공된) IP 로 통신한다는 말이 되나?

 


 

Kube 사용자는 kubectl 을 통해 사용자가 원하는 명령을 kube(정확히는 Master Node) 에 대신 전달하도록 할 수 있음

kubectl 는, 새로운 deployment 생성, deployment 삭제, 변경 같은 명령을

kube Cluster 에 보내는 데 사용되는 도구임

 

 


 

 

minikube 를 통해 local 컴퓨터에 kube 를 설치해서 테스트베드로 사용할 수 있음

minikube 를 통해 kube 가 구체적으로 어떻게 동작하는지 알아보자

 

kube 는 몇 가지 Objects 들이 제 역할을 함으로써 동작함

여기서 Objects 는 언어의 객체 이런 게 아니라, kube 의 핵심 파트 한 부분 부분을 의미한다고 보면 됨

kube 의 Objects 는 Pods, Services, Deployments, Volume..... 등임

사용자가 명령적 방식 혹은 선언적 방식 두 방식 중 하나를 통해 

kube 에 "이러저러한 Objects 를 사용하는 이 코드를 실행시켜줘" 라고 명령함

그럼 kube 는 그 코드를 그대로 실행하고, 사용자가 원하는 것이 실행됨

 

 

 


 

 

 

Pod object 를 알아보자

 

Pod 는 kube 에 의해 상호 작용하는 가장 작은 유닛. kube 는 pods 와 pods 내 containers 를 관리함

Pods 는 기본적으로 cluster 내부 IP 를 갖고 있음 (사용자가 ip 수정 가능)

Pods 내에 여러 containers 가 있는 경우, 이 containers 은 localhost 를 통해 서로 통신한다고 함

사용자가 코드를 통해 kube 에게 Pods 를 생성하라고 명령을 보내면

kube 는 worker nodes 중 하나를 선택하고 그 위에 pods 를 생성 및 실행함

pods 는 container 처럼 일시적으로 올라가는 것임. pods 가 내려가면 그 동안의 모든 데이터가 손실

pods 에 이상이 생기면, kube 가 알아서 pods 를 재실행함

 

 


 

 

deployment object 를 알아보자

 

deployment 는 사용자의 desired state 를 담고 있는, 일종의 명령서같은 것

deployment 는 kube 로 작업할 때 만지게 될 주요 object 중 하나임.

왜냐하면 deployment 를 통해 kube 에 다양한 실행 명령을 내릴 수 있기 때문 

deployment 를 통해 하나 이상의 pods 를 제어할 수 있고, 내부적으로 컨트롤러 객체를 생성할 수 있음

사용자는 deployment 를 통해 pods 를 띄우거나 함 (사용자가 deployment 없이 pods 를 직접 띄우지는 않는 듯)

deployment 를 일시중지 하거나, 삭제하거나, 롤백할 수 있음

만약 새로 배포한 deployment에 이상이 생겼다면 바로 롤백해서 그 전 deployment 상태로 되돌릴 수 있다는 말임

이런 기능을 시스템적으로 제공함

deployment 는 다이나믹하게 그리고 자동으로 scaling 이 가능

사용자는 deployment 에 n 개의 pods 를 띄워달라고 설정할 수 있고 그에 맞게 scaling 이 됨

혹은 traffic 이 많아지면 pods 개수를 늘리는 등의 scaling 설정도 가능함

 

 


 

 

 


service object 를 알아보자

Service 는 Pods 혹은 Pods 내의 Container 에 접근하기 위해 필요한 object 임

Service 는 Pods 의 ip 를 노출시켜, 다른 Pods 가 ip 가 노출된 Pods 에 접근 가능하도록 만들거나

혹은 외부에서 ip 가 노출된 Pods 에 접근 가능하도록 만듦

각 Pods 는 기본적으로 cluster 내부 IP 를 갖고있는데, 외부에서 이 ip 를 통해 Pods 에 접근이 불가능함

또한 Pods 의 내부 ip 는 Pods 가 교체 될 때마다 변경됨

이런 특징들은 Pods 를 사용할 때 꾸준히 문제가 되는데, Service 가 이를 해결해줄 수 있음

Service 는 Pods 를 그룹화하고 고정된 공유 IP 주소를 제공함

하나의 Service 내에 여러 pods 가 포함되어있고, Service 는 이 pods 에 접근 가능한 하나의 고정된 ip 를 제공하는 것임.

service 가 제공하는 ip 는 다른 Pods 혹은 외부에서 접근 가능함

 

흠.. service 가 제공하는 ip 가 하나뿐이라면,

내부에 각 pods 는 어떻게 구분해서 접근 가능한거지? port number 로 구분하나..?

 

 

 


 

 

 

kube 에서 객체들을 띄우기 위한 방법을 두 가지 제공함

명령적 접근방식 vs 선언적 접근방식

 

명령적 접근 방식은 deployment, service 등을 띄우는 명령어를 직접 입력하여 하나씩 띄우는 것

마치 docker run 을 여러번 하여 containers 를 띄우는 것과 같음

 

선언적 접근 방식은 띄우고 싶은 객체들의 정보를 입력해 둔 config 파일(yaml) 을 만들고 이 yaml 을 이용하여 한 번에 띄우는 것

마치 docker-compose 를 통해 한 번에 여러 container 를 띄우는 것과 같음

예를 들어 다음과 같이 config 파일인 yaml 파일을 만들 수 있음

 


apiVersion: apps/v1
kind: Deployment
metadata:
  name: first-app
spec:
  selector:
    matchLabels:
      app: first-dummy
    replicas: 3
    template:
      metadata:
        labels:
          app: first-dummy
      spec:
        containers:
        - name: first-node
          image: "first-app"

 

config 파일을 이용하려면 아래처럼 kubectl apply 라는 명령어 사용

 

kubectl apply -f first-app.yaml

 

kube 는 yaml 에 설정된 것을 유지하기 위해 노력함

예를 들어 pods 가 하나 죽어서 live pods 가 3개에서 2개가 되었다면

yaml 의 상태(pods 의 replicas 는 3)를 맞추기 위해, 하나를 새로 띄워서 3개를 맞춤

사용자가 yaml 을 업데이트하면, kube 는 그 업데이트 된 사항을 확인하고 그대로 환경을 구성함

 

 


 

 

Deployment 를 생성하는 yaml 파일을 만들어봄.

공식 문서를 계속 참고 및 확인할 것

https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#deployment-v1-apps

 

apiVersion: 현재 kube 에 맞는 apiVersion 을 찾아서 넣음

  선언적 접근 방식을 사용한다면 반드시 넣어야 함

  "kubenetes deployment apiversion" 등으로 검색해서 나온 최신 버전 yaml 샘플 등에서 발췌하여 넣으면 됨

  예를 들면 아래 스샷과 같이 "apps/v1"

 


kind: Deployment Kube 에서 생성하고 싶은 Object 를 넣음

  예를 들어 Deployment, Service, Job 등

 

metadata : 생성하는 Object 의 이름 등의 정보를 넣음

  예를 들어 kind 에 Deployment 가 추가되었다면, 아래처럼 Deployment 의 이름을 first-app 으로 명명

  name: first-app

 

spec : 생성하는 Object 의 사양(spec) 정보를 넣음

  예를 들어 위 kind 에 Deployment 가 추가되었다면, (아래처럼) 구성하고 싶은 Deployment 의 사양을 입력
  replicas: 3 pods 의 개수를 지정. default 는 1이며 (처음에 pod 를 띄우고 싶지 않다면) 0으로도 지정 가능

  selector: deployment 가 계속 관리해야 할 pod 의 라벨을 넣음. pod 의 라벨은 아래 다시 설명됨

    matchLabels:

      app: first-dummy

      deploy: second-dummy

      tier: third-dummy
      
여기서 pod 의 라벨이 3개 추가되었음. 이 말 뜻은, 3개 라벨이 충족하지 않거나, 다른 라벨을 갖는 pod 는
      해당 deployment 에 의해 관리되지 않는다는 말임.

      deployment 에 속한 pods 를 deployment 에게 알려준다고 할 수 있음...

  template: pod 에서 동작할 Container Image 를 정의하는 부분

    위 kind 가 Deployment 이기 때문에, 여기서의 template 은 PodTemplateSpec 이 되며 [문서]

    PodTemplateSpec 에는 metadata 와 spec 두 가지를 추가할 수 있음 [문서]

    (kind: Pod) 파드Object 만들기 위해 추가.

      위 kind 가 Deployment 라면 여기 kind 는 자동으로 Pod 가 되어

      여기 kind (kind: Pod) 생략 가능
    metadata: pod 는 kube 세계의 새로운 Object 이기 때문에 metadata 를 한 번 더 중첩시켜 넣어줌 
      labels: deployment 에 의해 관리될 pod 의 라벨 설정. 사용자가 원하는 labels 을 여럿 추가할 수 있음
        app: first-dummy

        deploy: second-dummy

        tier: third-dummy

    spec: Pod 의 사양을 정의. 위 Spec 은 Deployment 의 사양이고, 여기 Spec 은 Pod 의 사양
      containers: Pod 안에 올라갈 Container 리스트를 정의. 예를 들어 두 가지 Container 를 띄운다면
        - name: first-node
          image: mydockerhub/first-app

        - name: second-node
          image: mydockerhub/second-app:4

 

이렇게 만들어진 yaml 파일 이름이 mykubecluster.yaml 이라고 하자

이 yaml 을 기반으로 kube 에 클러스터를 띄우려면 kubectl apply 명령어 사용

 

kubectl apply -f=mykubecluster.yaml

 

deployment 를 실행한 이후에, 아래 명령어들을 이용하여 Objects 가 잘 떴는지 확인

 

kubectl get deployments

kubectl get pods

 

 

 


 

 

위 deployment 를 위한 yaml 외에, service 를 위한 yaml 을 추가해봄

만들어지는 yaml 파일 이름은 사용자가 정하면 됨. 이를테면 myservice.yaml 등

 

[service v1 core Documentation]


apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: first-dummy
    deploy: second-dummy
    tier: third-dummy
  port:
    - protocol: 'TCP'
      port: 80  #외부 (로 노출되는) 포트
      targetPort: 8080  #(외부 포트와 연결되는) 내부 포트
    - protocol: 'TCP'
      port: 443
      targetPort: 443
  type: ClusterIP
  


 

service 의 selector 는 deployment 의 selector 와 마찬가지로,

해당 service 에게 제어되거나, 연결되어야 하는 다른 리소스를 식별하는 데 사용함

deployment 에 의해 생성된 pods 를 service 의 selector 에 추가하여 설정 가능

만약 두 개의 deployments A, B 가 있고

A deployment 는 x:x, y:y 라벨을 갖는 pods 를 갖고

B deployment 는 x:x 라벨만 갖는 pods 를 갖는다고 하자

Service select 에 x:x 만 설정해두면 A deployment 의 pod 와 B deployment 의 pod 모두를 대상으로 설정하여

해당 Service 의 그룹에 포함시켜 Service 에 의해 제어되도록 할 수 있음

 

참고로 Service 의 selector 는 matchLabels 밖에 없음

 

이렇게 Service 의 selector 로 pods 그룹을 설정한다고 해도

pods 그룹만 알 뿐이지 구체적으로 어떤 pods 가 (Service 에 의한 port 에) 노출되어야 하는지는 아직 모름

 

port 를 통해 pods 가 외부에서 오는 어떤 port 를 받을 수 있게 할건지(port: 80)

외부에서 오는 요청이 내부 시스템의 어떤 port 로 연결되게 할 건지(targetPort: 8080) 설정할 수 있음

 

type 에는 여러 값을 넣을 수 있음

- ClusterIP : default. 내부적으로 노출된 IP. 클러스터 내부에서만 접근 가능하며, 클러스터 내부 pod 들끼리 통신 가능 

    해당 타입의 Service 에 속한 pod 에 들어오는 요청을 자동으로 모든 pod 로 분산

- NodePort : 기본적으로 실행되는 worker node 의 ip 와 port 에 노출

- LoadBalancer : 외부 세계에서 pod 로 접근을 원하는 경우 사용. 가장 일반적으로 사용됨

    외부에서 사용 가능한 IP 주소를 생성하고

    실행되는 (pod 가 어떤 노드에 떠 있느냐에 관계없이) 모든 pod 에 들어오는 요청을 자동으로 분산하고

    생성된 고정 IP주소는 Pod 가 실행되는 노드와 독립적임

 

이렇게 만들어진 myservice.yaml 을 kube 에 올릴때는 아래 명령어 사용

 

kubectl apply -f myservice.yaml

 

service 를 실행한 이후에, 아래 명령어를 이용하여 service 가 잘 떴는지 확인

 

kubectl get services

 


 

 

kube 에 올라간 deployments 나 services 의 설정을 업데이트하고 싶다면

yaml 파일을 업데이트하고 다시 실행(kubectl apply -f ...)하면 됨

그러면 업데이트 된 내용이 kube 클러스터에 자동으로 반영됨

 


 

 

kube 에 올라간 리소스(deployments, services...) 를 삭제하고 싶을 때는

delete 명령어에 삭제할 리소스 이름(metadata.name)을 넣어서 사용

 

kubectl delete deployment first-app

kubectl delete service backend

 

혹은 아래처럼 yaml 파일 이름 자체를 넣어, yaml 에서 생성한 모든 리소스들 삭제 가능

 

kubectl delete -f=mykubecluster.yaml

kubectl delete -f=myservice.yaml

kubectl delete -f=mykubecluster.yaml, myservice.yaml

kubectl delete -f=mykubecluster.yaml -f=myservice.yaml

 


 

 

위에 예제에선 두 개의 yaml 을 사용하여 리소스를 띄웠고

두 yaml 의 리소스들이 서로 협력하여 일 하도록 만들었음

어짜피 같이 일 하게 될꺼라면 하나의 yaml 에 넣어도 되지 않을까? 가능함.

두 개의 yaml 내용을 하나의 yaml 에 합치려면 아래와 같이 진행하면 됨

 

< myservice.yaml >

apiVersion: v1
kind: Service
metadata:
  name: backend
....
< mykubecluster.yaml >

apiVersion: apps/v1
kind: Deployment
metadata:
  name: first-app
....
< total.yaml >
apiVersion: v1
kind: Service
metadata:
  name: backend
....
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: first-app
....

 

 

두 yaml 내용 사이에 "---" 를 꼭 넣어줘야 함

"---" 는 yaml 에서 Object 를 구분하는 기호라고 함

 

위와 같이, 여러 yaml 을 묶을 때는 Service Object 를 위한 yaml 을 먼저 배치하는 게 좋다고 함

리소스는 위에서 아래로 내려오면서 차례대로 생성되어 Service 가 먼저 생성됨

Service 는 이후 생성되는 pods 등의 라벨을 보면서, Service 에 지정된 라벨이 생성되는지 확인함

지정된 라벨의 pods 가 생성되면 동적으로 Service 에 추가됨 

 


 

 

위 예제의 deployments 설정 yaml 에서 selector 로 matchLabels 을 사용했는데

matchLabels 말고 matchExpressions 를 사용할 수 있음

 

https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#labelselector-v1-meta

https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#labelselectorrequirement-v1-meta

 

예를 들어 아래와 같은 matchLabels selector 를 matchExpressions selector 로 바꾼다고 하자

< matchLabels selector >

selector:
  matchLables:
    app: second-app
    tier: backend
< matchExpressions selector >

selector:
  matchExpressions:
    - {key: app, operator: In, values: [first-app, second-app]}

 

key 는 Label 의 key 값

values 는 Label 의 value 값

operator 에 In, NotIn, Exists, DoesNotExist 등이 들어갈 수 있음

  위에서 사용된 In 은 values 리스트에 포함되어있으면 match 됨

  예를 들어 어떤 pod 의 Label 이 app:second-app 이라면, 위 matchExpressions selector 에 의해 match 됨

  만약 NotIn 을 선택했다면, app:second-app Label 을 갖는 pods 는 match 에서 제외될 것임

 

 


 

 

kube 에서 실행된 Container 가 우리 의도에 따라 정상적으로 실행 중인지 검사하는 방법을 직접 정의할 수 있음

예를 들어 위 예제에서 deployment yaml 의 pods spec.containers 를 보자.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: first-app
spec:
  selector:
    matchLabels:
      app: first-dummy
    replicas: 3
    template:
      metadata:
        labels:
          app: first-dummy
      spec:
        containers:
        - name: first-node
          image: "first-app"
          livenessProbe:
            httpGet:
              path: /
              port: 8080
            periodSeconds: 10
            initialDelaySeconds: 5

 

 

livenessProbe : Container 가 구동된 이후 잘 실행중인지 검사하기 위해 설정하는 값들

httpGet : http 의 get 요청이 pods 에서 실행중인 application 으로 전달되어야 함을 의미

periodSeconds : 작업을 수행 빈도(초단위). 10을 넣으면 10초 

initialDelaySeconds : kube 가 처음으로 상태를 확인할 때까지 기다려야 하는 시간(초단위) 5를 넣으면 5초

 

kube 자체적으로 Container 가 죽으면 다시 띄워주긴 하지만

Container 내부 app 이 이상한 방향으로 동작하거나

kube 가 알아차리지 못하는 방식으로 죽어버리는 상황을 대비하기 위해

사용자가 직접 "/:8080 으로 10초간 ping 을 보내서 제대로 동작하는지 확인해봐" 라고 검사 설정을 해둘 수 있음

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

+ Recent posts