AWS EMR Console 에서 직접 step 을 추가하여 shell command 실행하는 방법은 다음과 같음

 

1. EMR Steps 탭에서 Add Step 누름

2. Type 은 "Custom JAR" 선택

3. Name 은 원하는 대로 정함

4. JAR location 은 "command-runner.jar" 입력

5. Arguments - optional 에 bash -c "[원하는 명령어]"  입력

 

예를 들어 echo hello world! 를 실행하려면

bash -c "echo hello world!"

 

ping 8.8.8.8 을 실행하려면

bash -c "ping 8.8.8.8"

 

 

 

 

 


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' 가 될 것임

 

더 복잡한데..?

 

 

 

 

 

+ Recent posts