Kubernetes ํŒ ๋ฐ ์š”๋ น: NGINX Ingress์˜ ์‚ฌ์šฉ์ž ์ง€์ • ์˜ค๋ฅ˜ ํŽ˜์ด์ง€

Kubernetes ํŒ ๋ฐ ์š”๋ น: NGINX Ingress์˜ ์‚ฌ์šฉ์ž ์ง€์ • ์˜ค๋ฅ˜ ํŽ˜์ด์ง€

์ด ๊ธฐ์‚ฌ์—์„œ๋Š” ๊ฐœ์ธํ™”๋œ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€ ํ‘œ์‹œ์™€ ๊ด€๋ จ๋œ NGINX Ingress์˜ ๋‘ ๊ฐ€์ง€ ๊ธฐ๋Šฅ๊ณผ ํ•ด๋‹น ๊ธฐ๋Šฅ์— ์กด์žฌํ•˜๋Š” ์ œํ•œ ์‚ฌํ•ญ ๋ฐ ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์„ค๋ช…ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

1. ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ ๋ณ€๊ฒฝ

๊ธฐ๋ณธ์ ์œผ๋กœ NGINX Ingress๋Š” ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” Ingress ๋ฆฌ์†Œ์Šค์— ์—†๋Š” ํ˜ธ์ŠคํŠธ๋ฅผ ์ง€์ •ํ•˜๋Š” Ingress๋ฅผ ์š”์ฒญํ•  ๋•Œ 404 ์‘๋‹ต ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋œ ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ์ˆ˜์‹ ํ•จ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

Kubernetes ํŒ ๋ฐ ์š”๋ น: NGINX Ingress์˜ ์‚ฌ์šฉ์ž ์ง€์ • ์˜ค๋ฅ˜ ํŽ˜์ด์ง€

๊ทธ๋Ÿฌ๋‚˜ ์ ์  ๋” ๋งŽ์€ ๊ณ ๊ฐ์ด ํ‘œ์ค€ 404 ๋Œ€์‹  ํšŒ์‚ฌ ๋กœ๊ณ  ๋ฐ ๊ธฐํƒ€ ํŽธ์˜ ์‹œ์„ค์„ ํŽ˜์ด์ง€์— ํ‘œ์‹œํ•ด ๋‹ฌ๋ผ๋Š” ์š”์ฒญ์„ ๋ฐ›๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด NGINX Ingress๋Š” ๋‚ด์žฅ๋œ ๊ธฐ๋Šฅ ์žฌ์ •์˜ํ•˜๋‹ค default-backend-service. ํ˜•์‹ ํ•ญ๋ชฉ์„ ๋™์ผํ•œ ์ด๋ฆ„์˜ ์˜ต์…˜์— ๋Œ€ํ•œ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. namespace/servicename. ์„œ๋น„์Šค ํฌํŠธ๋Š” 80์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ ค๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž์ฒด Pod(๋ฐฐํฌ) ๋ฐ ์„œ๋น„์Šค๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค(YAML์˜ ๊ตฌํ˜„ ์˜ˆ ingress-nginx ์ €์žฅ์†Œ์—์„œ), ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ ๋Œ€์‹  ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ์ž‘์€ ๊ทธ๋ฆผ์ž…๋‹ˆ๋‹ค.

~$ curl -i -XGET http://sadsdasdas.kube-cloud.my/
HTTP/1.1 404 Not Found
Date: Mon, 11 Mar 2019 05:38:15 GMT
Content-Type: */*
Transfer-Encoding: chunked
Connection: keep-alive

<span>The page you're looking for could not be found.</span>

๋”ฐ๋ผ์„œ YAML์„ ํ†ตํ•ด ๋ช…์‹œ์ ์œผ๋กœ ์ƒ์„ฑ๋˜์ง€ ์•Š์€ ๋ชจ๋“  ๋„๋ฉ”์ธ์€ kind: Ingress, ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ์— ์†ํ•ฉ๋‹ˆ๋‹ค. ์œ„ ๋ชฉ๋ก์—์„œ ์ด ๋„๋ฉ”์ธ์€ sadsdasdas.

2. ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ HTTP ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

๋˜ ๋‹ค๋ฅธ ์ƒํ™ฉ์€ ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๋Š”(ํ•ด๋‹น ์•„๋ฆ„๋‹ค์šด ํŽ˜์ด์ง€๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ) ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•œ HTTP ์˜ค๋ฅ˜(404, 500, 502...)๋กœ ๋๋‚˜๋Š” ์š”์ฒญ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๋˜ํ•œ ์—ฌ๋Ÿฌ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์—์„œ ๋™์ผํ•œ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ์ œ๊ณตํ•˜๋ ค๋Š” ๊ฐœ๋ฐœ์ž์˜ ๋ฐ”๋žŒ ๋•Œ๋ฌธ์ผ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์ธก์—์„œ ์ด ์‚ฌ๋ก€๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ๋‹ค์Œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

  1. ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ์— ๋Œ€ํ•œ ๋‹จ๋ฝ์˜ ์œ„ ์ง€์นจ์„ ๋”ฐ๋ฅด์„ธ์š”.
  2. nginx-ingress ๊ตฌ์„ฑ ConfigMap์— ํ‚ค ์ถ”๊ฐ€ custom-http-errors, ์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ 404,503 (๋ถ„๋ช…ํžˆ ์ƒˆ ๊ทœ์น™์ด ์ ์šฉ๋˜๋Š” ์˜ค๋ฅ˜ ์ฝ”๋“œ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค).

์˜ˆ์ƒํ•œ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ฌ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹คํ–‰ ์ค‘์ด๊ณ  ์‘๋‹ต ์ฝ”๋“œ 404 ๋˜๋Š” 503๊ณผ ํ•จ๊ป˜ ์˜ค๋ฅ˜๋ฅผ ์ˆ˜์‹ ํ•˜๋ฉด ์š”์ฒญ์ด ์ž๋™์œผ๋กœ ์ƒˆ ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ๋กœ ๋ฆฌ๋””๋ ‰์…˜๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ ๋ฐ custom-http-errors์šฉ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

!!! Important The custom backend is expected to return the correct HTTP status code instead of 200. NGINX does not change the response from the custom default backend.

์‚ฌ์‹ค์€ ์š”์ฒญ์ด ๋ฆฌ๋””๋ ‰์…˜๋  ๋•Œ ํ—ค๋”์— ์ด์ „ ์‘๋‹ต ์ฝ”๋“œ ๋ฐ ์ถ”๊ฐ€ ์ •๋ณด์™€ ํ•จ๊ป˜ ์œ ์šฉํ•œ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค(์ „์ฒด ๋ชฉ๋ก์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ). ์—ฌ๊ธฐ์—).

์ด๊ฒƒ์€ ๋‹น์‹ ์ด ์Šค์Šค๋กœํ•ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค ์˜ฌ๋ฐ”๋ฅธ ์‘๋‹ต ์ฝ”๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜์„ธ์š”. ๋‹ค์Œ์€ ๊ทธ ์˜ˆ์ž…๋‹ˆ๋‹ค. ๋ฌธ์„œ์—์„œ ์ž‘๋™ ๋ฐฉ์‹์„ ํ™•์ธํ•˜์„ธ์š”.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๋งˆ๋‹ค ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

์†”๋ฃจ์…˜์ด ์ „์ฒด ํด๋Ÿฌ์Šคํ„ฐ์— ๋Œ€ํ•ด ์ „์—ญ์ ์ด์ง€ ์•Š๊ณ  ํŠน์ • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—๋งŒ ์ ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋ ค๋ฉด ๋จผ์ € Ingress ๋ฒ„์ „์„ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ผ์น˜ํ•˜๋Š” ๊ฒฝ์šฐ 0.23 ์ด์ƒ, ๊ธฐ๋ณธ Ingress ์ฃผ์„์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค.

  1. ์šฐ๋ฆฌ๋Š” ์žฌ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค default-backend ์— ๊ฐ ์ธ๊ทธ๋ ˆ์Šค์˜ ์ฃผ์„ ์‚ฌ์šฉ;
  2. ์šฐ๋ฆฌ๋Š” ์žฌ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค custom-http-errors ์— ๊ฐ ์ธ๊ทธ๋ ˆ์Šค์˜ ์ฃผ์„ ์‚ฌ์šฉ.

๊ฒฐ๊ณผ์ ์œผ๋กœ Ingress ๋ฆฌ์†Œ์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ .Chart.Name }}-app2
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/custom-http-errors: "404,502"
    nginx.ingress.kubernetes.io/default-backend: error-pages
spec:
  tls:
  - hosts:
    - app2.example.com
    secretName: wildcard-tls
  rules:
  - host: app2.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: {{ .Chart.Name }}-app2
          servicePort: 80

์ด ๊ฒฝ์šฐ ์˜ค๋ฅ˜ 404 ๋ฐ 502๋Š” ํ•„์š”ํ•œ ๋ชจ๋“  ํ—ค๋”์™€ ํ•จ๊ป˜ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€ ์„œ๋น„์Šค๋กœ ๋ฆฌ๋””๋ ‰์…˜๋ฉ๋‹ˆ๋‹ค.

ะ’ ์ด์ „ ๋ฒ„์ „์˜ Ingress์—๋Š” ์ด ๊ธฐ๋Šฅ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค. (0.23์˜ ์šด๋ช…์ ์ธ ์ปค๋ฐ‹). ๊ทธ๋ฆฌ๊ณ  ํด๋Ÿฌ์Šคํ„ฐ์—์„œ ์™„์ „ํžˆ ๋‹ค๋ฅธ 2๊ฐœ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹คํ–‰๋˜๊ณ  ์žˆ๊ณ  ์„œ๋กœ ๋‹ค๋ฅธ ๊ธฐ๋ณธ ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค๋ฅผ ์ง€์ •ํ•˜๊ณ  ๊ฐ๊ฐ์— ๋Œ€ํ•ด ์„œ๋กœ ๋‹ค๋ฅธ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋Š” ๊ฒฝ์šฐ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์ค‘ ๋‘ ๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์‹  < 0.23: ์ ‘๊ทผ ๋ฐฉ์‹ XNUMX

์ด ์˜ต์…˜์€ ๋” ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ œ๊ณตํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ์„œ ํ—ค๋”๋ฅผ ๋ณด๊ณ  ์˜ฌ๋ฐ”๋ฅธ ์‘๋‹ต ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ชจ๋ฅด๋Š” ์ผ๋ฐ˜ HTML์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ URL์—์„œ Ingress๋ฅผ ํ†ตํ•ด ์ถœ์‹œ๋ฉ๋‹ˆ๋‹ค. /error-pages, ๊ทธ๋ฆฌ๊ณ  ์นดํƒˆ๋กœ๊ทธ์—์„œ ws ๋ฐ˜ํ™˜๋œ HTML์ด ๋ฉ๋‹ˆ๋‹ค.

YAML์˜ ๊ทธ๋ฆผ:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ .Chart.Name }}-app2
  annotations:
    kubernetes.io/ingress.class: "nginx"
    ingress.kubernetes.io/server-snippet: |
      proxy_intercept_errors on;
      error_page 500 501 502 503 504 @error_pages;
      location @error_pages {
        rewrite ^ /error-pages/other/index.html break;
        proxy_pass http://error-pages.prod.svc.cluster.local;
      }
spec:
  tls:
  - hosts:
    - app2.example.com
    secretName: wildcard-tls
  rules:
  - host: app2.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: {{ .Chart.Name }}-app2
          servicePort: 80

์ด ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ์„œ๋น„์Šค๋Š” ClusterIP ์œ ํ˜•์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋™์‹œ์— ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ Ingress์— ๋‹ค์Œ ๋‚ด์šฉ์ด ํฌํ•จ๋œ ์„œ๋ฒ„ ์Šค๋‹ˆํŽซ ๋˜๋Š” ๊ตฌ์„ฑ ์Šค๋‹ˆํŽซ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

nginx.ingress.kubernetes.io    /server-snippet: |
      proxy_intercept_errors on;
      error_page 500 501 502 503 504 @error_pages;
      location @error_pages {
        rewrite ^ /error-pages/ws/index.html break;
        proxy_pass http://error-pages.prod.svc.cluster.local;
      }

Ingress < 0.23: ๋‘ ๋ฒˆ์งธ ์ ‘๊ทผ ๋ฐฉ์‹

ํ—ค๋”๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•œ ์˜ต์…˜... ๊ทธ๋ฆฌ๊ณ  ์ผ๋ฐ˜์ ์œผ๋กœ ์ด๊ฒƒ์€ custom-http-errors์—์„œ ๋นŒ๋ฆฐ ๋” ์ •ํ™•ํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ์ˆ˜๋™์œผ๋กœ(๋ณต์‚ฌ) ์‚ฌ์šฉํ•˜๋ฉด ์ „์—ญ ์„ค์ •์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

๋‹จ๊ณ„๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ฐฝ์กฐํ•œ๋‹ค ๋™์ผํ•œ ๋ฐฐํฌ ํ•„์š”ํ•œ ํ—ค๋“œ๋ผ์ธ์„ ๋“ฃ๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‘๋‹ตํ•  ์ˆ˜ ์žˆ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ์ฝ˜ํ…์ธ ๋กœ Ingress ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์„œ๋ฒ„ ์Šค๋‹ˆํŽซ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

nginx.ingress.kubernetes.io    /server-snippet: |
      proxy_intercept_errors off;
      error_page 404 = @custom_404;
      error_page 503 = @custom_503;
      location @custom_404 {
        internal;
        proxy_intercept_errors off;
        proxy_set_header       X-Code             404;
        proxy_set_header       X-Format           $http_accept;
        proxy_set_header       X-Original-URI     $request_uri;
        proxy_set_header       X-Namespace        $namespace;
        proxy_set_header       X-Ingress-Name     $ingress_name;
        proxy_set_header       X-Service-Name     $service_name;
        proxy_set_header       X-Service-Port     $service_port;
        proxy_set_header       Host               $best_http_host;
        rewrite ^ /error-pages/ws/index.html break;
        proxy_pass http://error-pages.prod.svc.cluster.local;
      }
      location @custom_503 {
        internal;
        proxy_intercept_errors off;
        proxy_set_header       X-Code             503;
        proxy_set_header       X-Format           $http_accept;
        proxy_set_header       X-Original-URI     $request_uri;
        proxy_set_header       X-Namespace        $namespace;
        proxy_set_header       X-Ingress-Name     $ingress_name;
        proxy_set_header       X-Service-Name     $service_name;
        proxy_set_header       X-Service-Port     $service_port;
        proxy_set_header       Host               $best_http_host;
        rewrite ^ /error-pages/ws/index.html break;
        proxy_pass http://error-pages.prod.svc.cluster.local;
      }

๋ณด์‹œ๋‹ค์‹œํ”ผ, ์ฒ˜๋ฆฌํ•˜๋ ค๋Š” ๊ฐ ์˜ค๋ฅ˜์— ๋Œ€ํ•ด "๊ธฐ๋ณธ" ํ—ค๋”์™€ ๊ฐ™์ด ํ•„์š”ํ•œ ๋ชจ๋“  ํ—ค๋”๊ฐ€ ์‚ฝ์ž…๋  ์ž์ฒด ์œ„์น˜๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ •์˜ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐœ๋ณ„ ์œ„์น˜์™€ ์„œ๋ฒ„์— ๋Œ€ํ•ด์„œ๋„ ๋‹ค์–‘ํ•œ ๊ฐœ์ธํ™”๋œ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

PS

๊ธฐํƒ€ K8s ํŒ ๋ฐ ์š”๋ น ์‹œ๋ฆฌ์ฆˆ:

๋ธ”๋กœ๊ทธ์—์„œ๋„ ์ฝ์–ด๋ณด์„ธ์š”.

์ถœ์ฒ˜ : habr.com

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