Kubernetes ๋‚ด๋ถ€ ๋ฐ ์™ธ๋ถ€์˜ ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ

๋‚˜๋Š” ์ตœ๊ทผ์— ์ผ๋‹ค Docker์˜ ํ”„๋กœ์ ํŠธ ์ˆ˜๋ช… ๋ฐ ์™ธ๋ถ€ ์ฝ”๋“œ ๋””๋ฒ„๊น…์— ๋Œ€ํ•œ ๋‹ต๋ณ€, ๊ทธ๋Š” ์„œ๋น„์Šค๊ฐ€ Kuber์—์„œ ์ž˜ ์ž‘๋™ํ•˜๊ณ , ๋น„๋ฐ€์„ ๊ฐ€์ ธ์˜ค๊ณ , Docker ์™ธ๋ถ€์—์„œ๋„ ๋กœ์ปฌ๋กœ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‹คํ–‰๋˜๋„๋ก ์ž์‹ ๋งŒ์˜ ๊ตฌ์„ฑ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ„๋žตํ•˜๊ฒŒ ์–ธ๊ธ‰ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ณต์žกํ•œ ๊ฒƒ์€ ์—†์ง€๋งŒ ์„ค๋ช…๋œ "๋ ˆ์‹œํ”ผ"๋Š” ๋ˆ„๊ตฐ๊ฐ€์—๊ฒŒ ์œ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค :) ์ฝ”๋“œ๋Š” Python์œผ๋กœ ๋˜์–ด ์žˆ์ง€๋งŒ ๋…ผ๋ฆฌ๋Š” ์–ธ์–ด์— ๋ฌถ์—ฌ ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Kubernetes ๋‚ด๋ถ€ ๋ฐ ์™ธ๋ถ€์˜ ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ

์งˆ๋ฌธ์˜ ๋ฐฐ๊ฒฝ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์˜›๋‚ ์— ํ•˜๋‚˜์˜ ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์—ˆ๋Š”๋ฐ ์ฒ˜์Œ์—๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ์™€ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํฌํ•จ๋œ ์ž‘์€ ๋‹จ์ผ์ฒด์˜€์ง€๋งŒ ์‹œ๊ฐ„์ด ์ง€๋‚จ์— ๋”ฐ๋ผ ์„ฑ์žฅํ•˜์—ฌ ์„œ๋น„์Šค๋กœ ๋‚˜๋‰˜๊ณ  ๋‹ค์‹œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ๋‚˜๋‰˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ ํ™•์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ์— ์ด ๋ชจ๋“  ์ž‘์—…์€ Ansible์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ํ™”๋œ ์ฝ”๋“œ ์„ค์ • ๋ฐ ๋ฐฐํฌ ํ”„๋กœ์„ธ์Šค์ธ ๋ฒ ์–ด VPS์—์„œ ์ˆ˜ํ–‰๋˜์—ˆ์œผ๋ฉฐ, ๊ฐ ์„œ๋น„์Šค๋Š” ํ•„์š”ํ•œ ์„ค์ •๊ณผ ํ‚ค๊ฐ€ ํฌํ•จ๋œ YAML ๊ตฌ์„ฑ์œผ๋กœ ์ปดํŒŒ์ผ๋˜์—ˆ์œผ๋ฉฐ ์œ ์‚ฌํ•œ ๊ตฌ์„ฑ ํŒŒ์ผ์ด ์‚ฌ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ ์‹คํ–‰์€ ๋งค์šฐ ํŽธ๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. .k ์ด ๊ตฌ์„ฑ์€ ํ”„๋กœ์ ํŠธ์˜ ์–ด๋Š ๊ณณ์—์„œ๋‚˜ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š” ์ „์—ญ ๊ฐœ์ฒด์— ๋กœ๋“œ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์˜ ์ˆ˜, ์—ฐ๊ฒฐ ๋ฐ ์—ฐ๊ฒฐ์ด ์ฆ๊ฐ€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ค‘์•™ ์ง‘์ค‘์‹ ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง์˜ ํ•„์š”์„ฑ, Kuber๋กœ์˜ ์ด์ „์„ ์˜ˆ๊ณ ํ–ˆ์œผ๋ฉฐ ์•„์ง ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค. ์–ธ๊ธ‰๋œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ์ง€์›๊ณผ ํ•จ๊ป˜ Kubernetes๋Š” ๋‹ค์Œ์„ ํฌํ•จํ•˜์—ฌ ์ธํ”„๋ผ ๊ด€๋ฆฌ์— ๋Œ€ํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์†Œ์œ„ ๋น„๋ฐ€ ะธ ๊ทธ๋“ค๊ณผ ํ•จ๊ป˜ ์ผํ•˜๋Š” ๋ฐฉ๋ฒ•. ์ด ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ํ‘œ์ค€์ ์ด๊ณ  ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์€ ๋ฌธ์ž ๊ทธ๋Œ€๋กœ ์ฃ„์•…์ž…๋‹ˆ๋‹ค! ๊ทธ๋Ÿฌ๋‚˜ ๋™์‹œ์— ๊ตฌ์„ฑ ์ž‘์—…์„ ์œ„ํ•ด ํ˜„์žฌ ํ˜•์‹์„ ์œ ์ง€ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ์ฒซ์งธ, ํ”„๋กœ์ ํŠธ์˜ ๋‹ค์–‘ํ•œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์—์„œ ๊ท ์ผํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ , ๋‘˜์งธ, ๊ฐ„๋‹จํ•œ ํ•˜๋‚˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ์ปฌ ์ปดํ“จํ„ฐ์—์„œ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ตฌ์„ฑ ํŒŒ์ผ.

์ด์™€ ๊ด€๋ จํ•˜์—ฌ ๊ตฌ์„ฑ ๊ฐœ์ฒด๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ํด๋ž˜์‹ ๊ตฌ์„ฑ ํŒŒ์ผ๊ณผ Kuber์˜ ๋น„๋ฐ€ ์ •๋ณด ๋ชจ๋‘์—์„œ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์„ธ ๋ฒˆ์งธ Python์˜ ์–ธ์–ด์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณด๋‹ค ์—„๊ฒฉํ•œ ๊ตฌ์„ฑ ๊ตฌ์กฐ๋„ ์ง€์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์ „[str, ์‚ฌ์ „[str, Union[str, int, float]]]

์ฆ‰, ์ตœ์ข… cogfig๋Š” ๋ช…๋ช…๋œ ์„น์…˜์ด ์žˆ๋Š” ์‚ฌ์ „์ด๋ฉฐ, ๊ฐ ์„น์…˜์€ ๋‹จ์ˆœ ์œ ํ˜•์˜ ๊ฐ’์ด ํฌํ•จ๋œ ์‚ฌ์ „์ž…๋‹ˆ๋‹ค. ์„น์…˜์—์„œ๋Š” ํŠน์ • ์œ ํ˜•์˜ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ๊ตฌ์„ฑ ๋ฐ ์•ก์„ธ์Šค๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ๊ตฌ์„ฑ์˜ ์ผ๋ถ€ ์˜ˆ:

adminka:
  django_secret: "ExtraLongAndHardCode"

db_main:
  engine: mysql
  host: 256.128.64.32
  user: cool_user
  password: "SuperHardPassword"

redis:
  host: 256.128.64.32
  pw: "SuperHardPassword"
  port: 26379

smtp:
  server: smtp.gmail.com
  port: 465
  email: [email protected]
  pw: "SuperHardPassword"

์ด์™€ ๋™์‹œ์— ํ•ด๋‹น ๋ถ„์•ผ engine ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” SQLite์— ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ redis ๋กœ ์„ค์ • mock, ์ €์žฅํ•  ํŒŒ์ผ ์ด๋ฆ„๋„ ์ง€์ • - ์ด๋Ÿฌํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธ์‹๋˜๊ณ  ์ฒ˜๋ฆฌ๋˜๋ฏ€๋กœ ๋””๋ฒ„๊น…, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋ฐ ๊ธฐํƒ€ ์š”๊ตฌ ์‚ฌํ•ญ์„ ์œ„ํ•ด ๋กœ์ปฌ์—์„œ ์ฝ”๋“œ๋ฅผ ์‰ฝ๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋‹ค๋ฅธ ๋งŽ์€ ์š”๊ตฌ ์‚ฌํ•ญ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ์—๊ฒŒ ํŠนํžˆ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ ์ฝ”๋“œ์˜ ์ผ๋ถ€๋Š” ๋‹ค์–‘ํ•œ ๋ถ„์„ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ๊ฒƒ์ด๋ฉฐ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜์ด ์žˆ๋Š” ์„œ๋ฒ„๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋‹ค์–‘ํ•œ ์Šคํฌ๋ฆฝํŠธ์™€ ํ•จ๊ป˜ ์ž‘์—…ํ•ด์•ผ ํ•˜๋Š” ๋ถ„์„๊ฐ€์˜ ์ปดํ“จํ„ฐ์—์„œ๋„ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋ฐฑ์—”๋“œ ๋ฌธ์ œ๋ฅผ ๊ฑฑ์ •ํ•˜์ง€ ์•Š๊ณ  ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋””๋ฒ„๊น…ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฑด ๊ทธ๋ ‡๊ณ , ๊ตฌ์„ฑ ๋ ˆ์ด์•„์›ƒ ์ฝ”๋“œ๋ฅผ ํฌํ•จํ•œ ์ฃผ์š” ๋„๊ตฌ๊ฐ€ ๋‹ค์Œ์„ ํ†ตํ•ด ์„ค์น˜๋œ๋‹ค๋Š” ์‚ฌ์‹ค์„ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ์ด ๋‚˜์  ๊ฒƒ์€ ์—†์Šต๋‹ˆ๋‹ค. setup.py โ€“ ์ด๋ฅผ ํ†ตํ•ด ํ”Œ๋žซํผ ๋ฐ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์— ๊ด€๊ณ„์—†์ด ์ฝ”๋“œ๋ฅผ ๋‹จ์ผ ์ƒํƒœ๊ณ„๋กœ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.

Kubernetes ํฌ๋“œ์— ๋Œ€ํ•œ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

containers:
  - name : enter-api
    image: enter-api:latest
    ports:
      - containerPort: 80
    volumeMounts:
      - name: db-main-secret-volume
        mountPath: /etc/secrets/db-main

volumes:
  - name: db-main-secret-volume
    secret:
      secretName: db-main-secret

์ฆ‰, ๊ฐ ๋น„๋ฐ€์€ ํ•˜๋‚˜์˜ ์„น์…˜์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ๋น„๋ฐ€ ์ž์ฒด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

apiVersion: v1
kind: Secret
metadata:
  name: db-main-secret
type: Opaque
stringData:
  db_main.yaml: |
    engine: sqlite
    filename: main.sqlite3

์ด๋กœ ์ธํ•ด ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ YAML ํŒŒ์ผ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. /etc/secrets/db-main/section_name.yaml

๋กœ์ปฌ ์‹คํ–‰์˜ ๊ฒฝ์šฐ ํ”„๋กœ์ ํŠธ์˜ ๋ฃจํŠธ ๋””๋ ‰ํ„ฐ๋ฆฌ๋‚˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ์ง€์ •๋œ ๊ฒฝ๋กœ์— ์žˆ๋Š” ๊ตฌ์„ฑ์ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํŽธ์˜๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์ฝ”๋“œ๋Š” ์Šคํฌ์ผ๋Ÿฌ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

config.py

__author__ = 'AivanF'
__copyright__ = 'Copyright 2020, AivanF'

import os
import yaml

__all__ = ['config']
PROJECT_DIR = os.path.abspath(__file__ + 3 * '/..')
SECRETS_DIR = '/etc/secrets'
KEY_LOG = '_config_log'
KEY_DBG = 'debug'

def is_yes(value):
    if isinstance(value, str):
        value = value.lower()
        if value in ('1', 'on', 'yes', 'true'):
            return True
    else:
        if value in (1, True):
            return True
    return False

def update_config_part(config, key, data):
    if key not in config:
        config[key] = data
    else:
        config[key].update(data)

def parse_big_config(config, filename):
    '''
    Parse YAML config with multiple section
    '''
    if not os.path.isfile(filename):
        return False
    with open(filename) as f:
        config_new = yaml.safe_load(f.read())
        for key, data in config_new.items():
            update_config_part(config, key, data)
        config[KEY_LOG].append(filename)
        return True

def parse_tiny_config(config, key, filename):
    '''
    Parse YAML config with a single section
    '''
    with open(filename) as f:
        config_tiny = yaml.safe_load(f.read())
        update_config_part(config, key, config_tiny)
        config[KEY_LOG].append(filename)

def combine_config():
    config = {
        # To debug config load code
        KEY_LOG: [],
        # To debug other code
        KEY_DBG: is_yes(os.environ.get('DEBUG')),
    }
    # For simple local runs
    CONFIG_SIMPLE = os.path.join(PROJECT_DIR, 'config.yaml')
    parse_big_config(config, CONFIG_SIMPLE)
    # For container's tests
    CONFIG_ENVVAR = os.environ.get('CONFIG')
    if CONFIG_ENVVAR is not None:
        if not parse_big_config(config, CONFIG_ENVVAR):
            raise ValueError(
                f'No config file from EnvVar:n'
                f'{CONFIG_ENVVAR}'
            )
    # For K8s secrets
    for path, dirs, files in os.walk(SECRETS_DIR):
        depth = path[len(SECRETS_DIR):].count(os.sep)
        if depth > 1:
            continue
        for file in files:
            if file.endswith('.yaml'):
                filename = os.path.join(path, file)
                key = file.rsplit('.', 1)[0]
                parse_tiny_config(config, key, filename)
    return config

def build_config():
    config = combine_config()
    # Preprocess
    for key, data in config.items():
        if key.startswith('db_'):
            if data['engine'] == 'sqlite':
                data['filename'] = os.path.join(PROJECT_DIR, data['filename'])
    # To verify correctness
    if config[KEY_DBG]:
        print(f'** Loaded config:n{yaml.dump(config)}')
    else:
        print(f'** Loaded config from: {config[KEY_LOG]}')
    return config

config = build_config()

์—ฌ๊ธฐ์„œ์˜ ๋…ผ๋ฆฌ๋Š” ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ ๋Œ€๊ทœ๋ชจ ๊ตฌ์„ฑ๊ณผ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ณ„ ๊ฒฝ๋กœ, Kuber ๋น„๋ฐ€์˜ ์ž‘์€ ๊ตฌ์„ฑ ์„น์…˜์„ ๊ฒฐํ•ฉํ•œ ๋‹ค์Œ ์•ฝ๊ฐ„ ์ „์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ๋ช‡ ๊ฐ€์ง€ ๋ณ€์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋น„๋ฐ€์—์„œ ํŒŒ์ผ์„ ๊ฒ€์ƒ‰ํ•  ๋•Œ K8s๋Š” ๋น„๋ฐ€ ์ž์ฒด๊ฐ€ ์ €์žฅ๋˜๋Š” ๊ฐ ๋น„๋ฐ€์— ์ˆจ๊ฒจ์ง„ ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋งํฌ๋งŒ ๋” ๋†’์€ ์ˆ˜์ค€์— ์œ„์น˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊นŠ์ด ์ œํ•œ์ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

์„ค๋ช…๋œ ๋‚ด์šฉ์ด ๋ˆ„๊ตฐ๊ฐ€์—๊ฒŒ ์œ ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค. ๋ณด์•ˆ์ด๋‚˜ ๊ธฐํƒ€ ๊ฐœ์„  ๋ถ„์•ผ์— ๊ด€ํ•œ ๋ชจ๋“  ์˜๊ฒฌ๊ณผ ๊ถŒ์žฅ ์‚ฌํ•ญ์„ ์ˆ˜์šฉํ•ฉ๋‹ˆ๋‹ค. ์ปค๋ฎค๋‹ˆํ‹ฐ์˜ ์˜๊ฒฌ๋„ ํฅ๋ฏธ๋กญ์Šต๋‹ˆ๋‹ค. ConfigMaps์— ๋Œ€ํ•œ ์ง€์›์„ ์ถ”๊ฐ€ํ•˜๊ณ (์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์•„์ง ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ) GitHub/PyPI์— ์ฝ”๋“œ๋ฅผ ๊ฒŒ์‹œํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์น˜๊ฐ€ ์žˆ์„๊นŒ์š”? ๊ฐœ์ธ์ ์œผ๋กœ ์ €๋Š” ๊ทธ๋Ÿฌํ•œ ๊ฒƒ๋“ค์ด ํ”„๋กœ์ ํŠธ๊ฐ€ ๋ณดํŽธ์ ์ด๊ธฐ์—๋Š” ๋„ˆ๋ฌด ๊ฐœ๋ณ„์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉฐ ์—ฌ๊ธฐ์— ์ œ๊ณต๋œ ๊ฒƒ๊ณผ ๊ฐ™์€ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์˜ ๊ตฌํ˜„์„ ์กฐ๊ธˆ ์—ฟ๋ณด๊ณ  ์˜๊ฒฌ์—์„œ ๋ณด๊ณ  ์‹ถ์€ ๋‰˜์•™์Šค, ํŒ ๋ฐ ๋ชจ๋ฒ” ์‚ฌ๋ก€์— ๋Œ€ํ•œ ํ† ๋ก ์„ ํ•ฉ๋‹ˆ๋‹ค. , ์ถฉ๋ถ„ํ•ด์š” ๐Ÿ˜‰

๋“ฑ๋ก๋œ ์‚ฌ์šฉ์ž๋งŒ ์„ค๋ฌธ ์กฐ์‚ฌ์— ์ฐธ์—ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ์ œ๋ฐœ

ํ”„๋กœ์ ํŠธ/๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๊ฒŒ์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ?

  • 0,0%์˜ˆ, /contribution0์„ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • 33,3%๋„ค, ์ •๋ง ์ข‹์€ ๊ฒƒ ๊ฐ™์•„์š”4

  • 41,7%์•„๋‹ˆ์š”, ์ž์‹ ๋งŒ์˜ ํ˜•์‹๊ณผ ์š”๊ตฌ ์‚ฌํ•ญ์— ๋งž๊ฒŒ ์ง์ ‘ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ์‚ฌ๋žŒ5

  • 25,0%๋‹ต๋ณ€์„ ์ž์ œํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค 3

12๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ ํˆฌํ‘œํ–ˆ์Šต๋‹ˆ๋‹ค. 3๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๊ถŒํ–ˆ์Šต๋‹ˆ๋‹ค.

์ถœ์ฒ˜ : habr.com

์ฝ”๋ฉ˜ํŠธ๋ฅผ ์ถ”๊ฐ€