์ตœ์ € ์ž„๊ธˆ์œผ๋กœ ๋ธ”๋ฃจ-๊ทธ๋ฆฐ ๋ฐฐํฌ

์ด ๊ธฐ์‚ฌ์—์„œ ์šฐ๋ฆฌ๋Š” ์„ธ๊ฒŒ ๋•Œ๋ฆฌ๋‹ค, SSH, ๋„์ปค ะธ Nginx์— ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์›ํ™œํ•œ ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋ธ”๋ฃจ-๊ทธ๋ฆฐ ๋ฐฐํฌ ๋‹จ์ผ ์š”์ฒญ์„ ๊ฑฐ๋ถ€ํ•˜์ง€ ์•Š๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๊ฐ€๋™ ์ค‘์ง€ ์‹œ๊ฐ„์ด ์—†๋Š” ๋ฐฐํฌ ์ „๋žต ์ค‘ ํ•˜๋‚˜์ด๋ฉฐ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๊ฐ€ ์žˆ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๊ฐ€์žฅ ์ ํ•ฉํ•˜์ง€๋งŒ ๊ทผ์ฒ˜์— ๋ฐ”๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๋‘ ๋ฒˆ์งธ ์ธ์Šคํ„ด์Šค๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

๋ฉด์ฑ… ์กฐํ•ญ: ๊ธฐ์‚ฌ์˜ ๋Œ€๋ถ€๋ถ„์€ ์ฝ˜์†” ์„ธ์…˜์„ ๋…น์Œํ•˜๋Š” ์‹คํ—˜์ ์ธ ํ˜•์‹์œผ๋กœ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์ดํ•ดํ•˜๊ธฐ ๋„ˆ๋ฌด ์–ด๋ ต์ง€ ์•Š๊ณ  ์ฝ”๋“œ ์ž์ฒด๊ฐ€ ์ถฉ๋ถ„ํžˆ ๋ฌธ์„œํ™”๋˜๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค. ๋Œ€๊ธฐ์˜ ๊ฒฝ์šฐ ์ด๊ฒƒ์ด ๋‹จ์ˆœํ•œ ์ฝ”๋“œ ์กฐ๊ฐ์ด ์•„๋‹ˆ๋ผ "์ฒ " ํ…”๋ ˆํƒ€์ดํ”„์˜ ์ข…์ด๋ผ๊ณ  ์ƒ์ƒํ•ด ๋ณด์‹ญ์‹œ์˜ค.

์ตœ์ € ์ž„๊ธˆ์œผ๋กœ ๋ธ”๋ฃจ-๊ทธ๋ฆฐ ๋ฐฐํฌ

์ฝ”๋“œ๋งŒ ์ฝ์–ด์„œ๋Š” ๊ตฌ๊ธ€๋งํ•˜๊ธฐ ์–ด๋ ค์šด ํฅ๋ฏธ๋กœ์šด ๊ธฐ์ˆ ๋“ค์ด ๊ฐ ์„น์…˜์˜ ์‹œ์ž‘ ๋ถ€๋ถ„์— ์„ค๋ช…๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ์™ธ ๋ถˆ๋ถ„๋ช…ํ•œ ๋‚ด์šฉ์ด ์žˆ์œผ๋ฉด ๊ตฌ๊ธ€๋งํ•ด์„œ ํ™•์ธํ•ด ๋ณด์„ธ์š”. ์„ค๋ช…ํ•˜๋‹ค (๋‹คํ–‰ํžˆ ํ…”๋ ˆ๊ทธ๋žจ ์ฐจ๋‹จ์ด ํ•ด์ œ๋˜์–ด ๋‹ค์‹œ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.) Google์—์„œ ์•„๋ฌด๊ฒƒ๋„ ํ•  ์ˆ˜ ์—†๋‹ค๋ฉด ๋Œ“๊ธ€๋กœ ๋ฌผ์–ด๋ณด์„ธ์š”. ํ•ด๋‹น ์„น์…˜์ธ "ํฅ๋ฏธ๋กœ์šด ๊ธฐ์ˆ "์— ๊ธฐ๊บผ์ด ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์‹œ์ž‘ํ•˜์ž.

$ mkdir blue-green-deployment && cd $_

์„œ๋น„์Šค

์‹คํ—˜์ ์ธ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด ์ปจํ…Œ์ด๋„ˆ์— ๋„ฃ์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํฅ๋ฏธ๋กœ์šด ๊ธฐ์ˆ 

  • cat << EOF > file-name (์—ฌ๊ธฐ ๋ฌธ์„œ + I/O ๋ฆฌ๋””๋ ‰์…˜)์€ ํ•˜๋‚˜์˜ ๋ช…๋ น์œผ๋กœ ์—ฌ๋Ÿฌ ์ค„์˜ ํŒŒ์ผ์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. bash๊ฐ€ ์ฝ๋Š” ๋ชจ๋“  ๊ฒƒ /dev/stdin ์ด ์ค„ ๋’ค์™€ ์ค„ ์•ž EOF ์— ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค file-name.
  • wget -qO- URL (์„ค๋ช…ํ•˜๋‹ค) โ€” HTTP๋ฅผ ํ†ตํ•ด ์ˆ˜์‹ ๋œ ๋ฌธ์„œ๋ฅผ ๋‹ค์Œ์œผ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. /dev/stdout (๋น„์Šทํ•œ ๋ฌผ๊ฑด curl URL).

์ธ์‡„๋ฌผ

Python์— ๋Œ€ํ•œ ๊ฐ•์กฐ ํ‘œ์‹œ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๊ธฐ ์œ„ํ•ด ํŠน๋ณ„ํžˆ ์ฝ”๋“œ ์กฐ๊ฐ์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰์—๋Š” ์ด๋Ÿฐ ์ž‘ํ’ˆ์ด ๋˜ ๋‚˜์˜ฌ ๊ฒ๋‹ˆ๋‹ค. ์ด ์žฅ์†Œ์—์„œ ์ข…์ด๋ฅผ ์ž˜๋ผ์„œ ๊ฐ•์กฐ ํ‘œ์‹œ ๋ถ€์„œ(ํ˜•๊ด‘ํŽœ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์†์œผ๋กœ ์น ํ•œ ์ฝ”๋“œ)๋กœ ๋ณด๋‚ธ ๋‹ค์Œ ์ด ์กฐ๊ฐ๋“ค์„ ๋‹ค์‹œ ์ ‘์ฐฉํ–ˆ๋‹ค๋Š” ์ ์„ ๊ณ ๋ คํ•˜์‹ญ์‹œ์˜ค.

$ cat << EOF > uptimer.py
from http.server import BaseHTTPRequestHandler, HTTPServer
from time import monotonic

app_version = 1
app_name = f'Uptimer v{app_version}.0'
loading_seconds = 15 - app_version * 5

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            try:
                t = monotonic() - server_start
                if t < loading_seconds:
                    self.send_error(503)
                else:
                    self.send_response(200)
                    self.send_header('Content-Type', 'text/html')
                    self.end_headers()
                    response = f'<h2>{app_name} is running for {t:3.1f} seconds.</h2>n'
                    self.wfile.write(response.encode('utf-8'))
            except Exception:
                self.send_error(500)
        else:
            self.send_error(404)

httpd = HTTPServer(('', 8080), Handler)
server_start = monotonic()
print(f'{app_name} (loads in {loading_seconds} sec.) started.')
httpd.serve_forever()
EOF

$ cat << EOF > Dockerfile
FROM python:alpine
EXPOSE 8080
COPY uptimer.py app.py
CMD [ "python", "-u", "./app.py" ]
EOF

$ docker build --tag uptimer .
Sending build context to Docker daemon  39.42kB
Step 1/4 : FROM python:alpine
 ---> 8ecf5a48c789
Step 2/4 : EXPOSE 8080
 ---> Using cache
 ---> cf92d174c9d3
Step 3/4 : COPY uptimer.py app.py
 ---> a7fbb33d6b7e
Step 4/4 : CMD [ "python", "-u", "./app.py" ]
 ---> Running in 1906b4bd9fdf
Removing intermediate container 1906b4bd9fdf
 ---> c1655b996fe8
Successfully built c1655b996fe8
Successfully tagged uptimer:latest

$ docker run --rm --detach --name uptimer --publish 8080:8080 uptimer
8f88c944b8bf78974a5727070a94c76aa0b9bb2b3ecf6324b784e782614b2fbf

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                    NAMES
8f88c944b8bf        uptimer             "python -u ./app.py"   3 seconds ago       Up 5 seconds        0.0.0.0:8080->8080/tcp   uptimer

$ docker logs uptimer
Uptimer v1.0 (loads in 10 sec.) started.

$ wget -qSO- http://localhost:8080
  HTTP/1.0 503 Service Unavailable
  Server: BaseHTTP/0.6 Python/3.8.3
  Date: Sat, 22 Aug 2020 19:52:40 GMT
  Connection: close
  Content-Type: text/html;charset=utf-8
  Content-Length: 484

$ wget -qSO- http://localhost:8080
  HTTP/1.0 200 OK
  Server: BaseHTTP/0.6 Python/3.8.3
  Date: Sat, 22 Aug 2020 19:52:45 GMT
  Content-Type: text/html
<h2>Uptimer v1.0 is running for 15.4 seconds.</h2>

$ docker rm --force uptimer
uptimer

์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ

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

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ๋Š” ๋‹ค์Œ์„ ์‚ฌ์šฉํ•˜์—ฌ Docker ๋‚ด์—์„œ ์—ฐ๊ฒฐ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋„์ปค ๋„คํŠธ์›Œํฌ. ๋”ฐ๋ผ์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ํฌํ•จ๋œ ์ปจํ…Œ์ด๋„ˆ๋Š” ํ˜ธ์ŠคํŠธ ์‹œ์Šคํ…œ์˜ ํฌํŠธ๋ฅผ ์ „๋‹ฌํ•  ํ•„์š”์กฐ์ฐจ ์—†์œผ๋ฉฐ ์ด๋ฅผ ํ†ตํ•ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์™ธ๋ถ€ ์œ„ํ˜‘์œผ๋กœ๋ถ€ํ„ฐ ์ตœ๋Œ€ํ•œ ๊ฒฉ๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ๊ฐ€ ๋‹ค๋ฅธ ํ˜ธ์ŠคํŠธ์— ์žˆ๋Š” ๊ฒฝ์šฐ Docker ๋„คํŠธ์›Œํฌ๋ฅผ ํฌ๊ธฐํ•˜๊ณ  ํ˜ธ์ŠคํŠธ ๋„คํŠธ์›Œํฌ๋ฅผ ํ†ตํ•ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ์— ์—ฐ๊ฒฐํ•˜์—ฌ ํฌํŠธ๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์• ํ”Œ ๋ฆฌ์ผ€์ด์…˜ ๋งค๊ฐœ๋ณ€์ˆ˜ --publish, ์ฒ˜์Œ ์‹œ์ž‘ํ•  ๋•Œ์™€ ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ์˜ ๊ฒฝ์šฐ์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ํฌํŠธ 80์—์„œ ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ๋ฅผ ์‹คํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ด๊ฒƒ์ด ๋ฐ”๋กœ ์™ธ๋ถ€ ๋„คํŠธ์›Œํฌ๋ฅผ ์ˆ˜์‹ ํ•ด์•ผ ํ•˜๋Š” ์—”ํ„ฐํ‹ฐ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ํ˜ธ์ŠคํŠธ์—์„œ ํฌํŠธ 80์ด ์‚ฌ์šฉ ์ค‘์ธ ๊ฒฝ์šฐ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•˜์„ธ์š”. --publish 80:80 ์— --publish ANY_FREE_PORT:80.

ํฅ๋ฏธ๋กœ์šด ๊ธฐ์ˆ 

์ธ์‡„๋ฌผ

$ docker network create web-gateway
5dba128fb3b255b02ac012ded1906b7b4970b728fb7db3dbbeccc9a77a5dd7bd

$ docker run --detach --rm --name uptimer --network web-gateway uptimer
a1105f1b583dead9415e99864718cc807cc1db1c763870f40ea38bc026e2d67f

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer:8080
<h2>Uptimer v1.0 is running for 11.5 seconds.</h2>

$ docker run --detach --publish 80:80 --network web-gateway --name reverse-proxy nginx:alpine
80695a822c19051260c66bf60605dcb4ea66802c754037704968bc42527bf120

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
80695a822c19        nginx:alpine        "/docker-entrypoint.โ€ฆ"   27 seconds ago       Up 25 seconds       0.0.0.0:80->80/tcp   reverse-proxy
a1105f1b583d        uptimer             "python -u ./app.py"     About a minute ago   Up About a minute   8080/tcp             uptimer

$ cat << EOF > uptimer.conf
server {
    listen 80;
    location / {
        proxy_pass http://uptimer:8080;
    }
}
EOF

$ docker cp ./uptimer.conf reverse-proxy:/etc/nginx/conf.d/default.conf

$ docker exec reverse-proxy nginx -s reload
2020/06/23 20:51:03 [notice] 31#31: signal process started

$ wget -qSO- http://localhost
  HTTP/1.1 200 OK
  Server: nginx/1.19.0
  Date: Sat, 22 Aug 2020 19:56:24 GMT
  Content-Type: text/html
  Transfer-Encoding: chunked
  Connection: keep-alive
<h2>Uptimer v1.0 is running for 104.1 seconds.</h2>

์›ํ™œํ•œ ๋ฐฐํฌ

์ƒˆ๋กœ์šด ๋ฒ„์ „์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ถœ์‹œํ•˜๊ณ (์‹œ์ž‘ ์„ฑ๋Šฅ์ด XNUMX๋ฐฐ ํ–ฅ์ƒ๋จ) ์›ํ™œํ•˜๊ฒŒ ๋ฐฐํฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํฅ๋ฏธ๋กœ์šด ๊ธฐ์ˆ 

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' โ€” ํ…์ŠคํŠธ ์“ฐ๊ธฐ my text ํŒŒ์ผ๋กœ /my-file.txt ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ my-container.
  • cat > /my-file.txt โ€” ํ‘œ์ค€ ์ž…๋ ฅ์˜ ๋‚ด์šฉ์„ ํŒŒ์ผ์— ์”๋‹ˆ๋‹ค. /dev/stdin.

์ธ์‡„๋ฌผ

$ sed -i "s/app_version = 1/app_version = 2/" uptimer.py

$ docker build --tag uptimer .
Sending build context to Docker daemon  39.94kB
Step 1/4 : FROM python:alpine
 ---> 8ecf5a48c789
Step 2/4 : EXPOSE 8080
 ---> Using cache
 ---> cf92d174c9d3
Step 3/4 : COPY uptimer.py app.py
 ---> 3eca6a51cb2d
Step 4/4 : CMD [ "python", "-u", "./app.py" ]
 ---> Running in 8f13c6d3d9e7
Removing intermediate container 8f13c6d3d9e7
 ---> 1d56897841ec
Successfully built 1d56897841ec
Successfully tagged uptimer:latest

$ docker run --detach --rm --name uptimer_BLUE --network web-gateway uptimer
96932d4ca97a25b1b42d1b5f0ede993b43f95fac3c064262c5c527e16c119e02

$ docker logs uptimer_BLUE
Uptimer v2.0 (loads in 5 sec.) started.

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer_BLUE:8080
<h2>Uptimer v2.0 is running for 23.9 seconds.</h2>

$ sed s/uptimer/uptimer_BLUE/ uptimer.conf | docker exec --interactive reverse-proxy sh -c 'cat > /etc/nginx/conf.d/default.conf'

$ docker exec reverse-proxy cat /etc/nginx/conf.d/default.conf
server {
    listen 80;
    location / {
        proxy_pass http://uptimer_BLUE:8080;
    }
}

$ docker exec reverse-proxy nginx -s reload
2020/06/25 21:22:23 [notice] 68#68: signal process started

$ wget -qO- http://localhost
<h2>Uptimer v2.0 is running for 63.4 seconds.</h2>

$ docker rm -f uptimer
uptimer

$ wget -qO- http://localhost
<h2>Uptimer v2.0 is running for 84.8 seconds.</h2>

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
96932d4ca97a        uptimer             "python -u ./app.py"     About a minute ago   Up About a minute   8080/tcp             uptimer_BLUE
80695a822c19        nginx:alpine        "/docker-entrypoint.โ€ฆ"   8 minutes ago        Up 8 minutes        0.0.0.0:80->80/tcp   reverse-proxy

์ด ๋‹จ๊ณ„์—์„œ๋Š” ์ด๋ฏธ์ง€๊ฐ€ ์„œ๋ฒ„์— ์ง์ ‘ ๊ตฌ์ถ•๋˜๋ฏ€๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์†Œ์Šค๊ฐ€ ์žˆ์–ด์•ผ ํ•˜๋ฉฐ ๋ถˆํ•„์š”ํ•œ ์ž‘์—…์œผ๋กœ ์„œ๋ฒ„๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„๋Š” ์ด๋ฏธ์ง€ ์–ด์…ˆ๋ธ”๋ฆฌ๋ฅผ ๋ณ„๋„์˜ ์‹œ์Šคํ…œ(์˜ˆ: CI ์‹œ์Šคํ…œ)์— ํ• ๋‹นํ•œ ํ›„ ์„œ๋ฒ„๋กœ ์ „์†กํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ์ „์†ก ์ค‘

์•ˆํƒ€๊น๊ฒŒ๋„ ์ด๋ฏธ์ง€๋ฅผ localhost์—์„œ localhost๋กœ ์ „์†กํ•˜๋Š” ๊ฒƒ์€ ์˜๋ฏธ๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์ด ์„น์…˜์€ Docker๊ฐ€ ์žˆ๋Š” ๋‘ ๊ฐœ์˜ ํ˜ธ์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์†Œํ•œ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

$ ssh production-server docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

$ docker image save uptimer | ssh production-server 'docker image load'
Loaded image: uptimer:latest

$ ssh production-server docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
uptimer             latest              1d56897841ec        5 minutes ago       78.9MB

ํŒ€ docker save ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ .tar ์•„์นด์ด๋ธŒ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์••์ถ•๋œ ํ˜•ํƒœ์˜ ๋ฌด๊ฒŒ๋ณด๋‹ค ์•ฝ 1.5๋ฐฐ ๋” ๋ฌด๊ฒ์Šต๋‹ˆ๋‹ค. ์‹œ๊ฐ„๊ณผ ํŠธ๋ž˜ํ”ฝ์„ ์ ˆ์•ฝํ•œ๋‹ค๋Š” ๋ช…๋ชฉ์œผ๋กœ ํ”๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

$ docker image save uptimer | gzip | ssh production-server 'zcat | docker image load'
Loaded image: uptimer:latest

๋‹ค์šด๋กœ๋“œ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค(์ด ๊ฒฝ์šฐ ํƒ€์‚ฌ ์œ ํ‹ธ๋ฆฌํ‹ฐ๊ฐ€ ํ•„์š”ํ•จ).

$ docker image save uptimer | gzip | pv | ssh production-server 'zcat | docker image load'
25,7MiB 0:01:01 [ 425KiB/s] [                   <=>    ]
Loaded image: uptimer:latest

ํŒ: SSH๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ~/.ssh/config.

๋‹ค์Œ์„ ํ†ตํ•ด ์ด๋ฏธ์ง€ ์ „์†กํ•˜๊ธฐ docker image save/load - ์ด๊ฒƒ์€ ๊ฐ€์žฅ ์ตœ์†Œํ•œ์˜ ๋ฐฉ๋ฒ•์ด์ง€๋งŒ ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€ ์•„๋‹™๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ฒƒ๋“ค๋„ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ์ปจํ…Œ์ด๋„ˆ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ(์—…๊ณ„ ํ‘œ์ค€).
  2. ๋‹ค๋ฅธ ํ˜ธ์ŠคํŠธ์—์„œ docker ๋ฐ๋ชฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
    1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ DOCKER_HOST.
    2. ๋ช…๋ น์ค„ ์˜ต์…˜ -H ๋˜๋Š” --host ๋„๊ตฌ docker-compose.
    3. docker context

๋‘ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•(๊ตฌํ˜„์„ ์œ„ํ•œ ์„ธ ๊ฐ€์ง€ ์˜ต์…˜ ํฌํ•จ)์€ ๊ธฐ์‚ฌ์— ์ž˜ ์„ค๋ช…๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. docker-compose๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์›๊ฒฉ Docker ํ˜ธ์ŠคํŠธ์— ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•.

deploy.sh

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

ํฅ๋ฏธ๋กœ์šด ๊ธฐ์ˆ 

  • ${parameter?err_msg} - ๊ฐ•ํƒ€ ๋งˆ๋ฒ• ์ฃผ๋ฌธ ์ค‘ ํ•˜๋‚˜(์ผ๋ช… ๋งค๊ฐœ๋ณ€์ˆ˜ ๋Œ€์ฒด). ๋งŒ์•ฝ์— parameter ์ง€์ •๋˜์ง€ ์•Š์Œ, ์ถœ๋ ฅ err_msg ์ฝ”๋“œ 1๋กœ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
  • docker --log-driver journald โ€” ๊ธฐ๋ณธ์ ์œผ๋กœ docker ๋กœ๊น… ๋“œ๋ผ์ด๋ฒ„๋Š” ํšŒ์ „์ด ์—†๋Š” ํ…์ŠคํŠธ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค. ์ด ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ๋กœ๊ทธ๊ฐ€ ์ „์ฒด ๋””์Šคํฌ๋ฅผ ๋น ๋ฅด๊ฒŒ ์ฑ„์šฐ๋ฏ€๋กœ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์˜ ๊ฒฝ์šฐ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ๋” ์Šค๋งˆํŠธํ•œ ๋“œ๋ผ์ด๋ฒ„๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ

deploy() {
    local usage_msg="Usage: ${FUNCNAME[0]} image_name"
    local image_name=${1?$usage_msg}

    ensure-reverse-proxy || return 2
    if get-active-slot $image_name
    then
        local OLD=${image_name}_BLUE
        local new_slot=GREEN
    else
        local OLD=${image_name}_GREEN
        local new_slot=BLUE
    fi
    local NEW=${image_name}_${new_slot}
    echo "Deploying '$NEW' in place of '$OLD'..."
    docker run 
        --detach 
        --restart always 
        --log-driver journald 
        --name $NEW 
        --network web-gateway 
        $image_name || return 3
    echo "Container started. Checking health..."
    for i in {1..20}
    do
        sleep 1
        if get-service-status $image_name $new_slot
        then
            echo "New '$NEW' service seems OK. Switching heads..."
            sleep 2  # Ensure service is ready
            set-active-slot $image_name $new_slot || return 4
            echo "'$NEW' service is live!"
            sleep 2  # Ensure all requests were processed
            echo "Killing '$OLD'..."
            docker rm -f $OLD
            docker image prune -f
            echo "Deployment successful!"
            return 0
        fi
        echo "New '$NEW' service is not ready yet. Waiting ($i)..."
    done
    echo "New '$NEW' service did not raise, killing it. Failed to deploy T_T"
    docker rm -f $NEW
    return 5
}

์‚ฌ์šฉ๋œ ๊ธฐ๋Šฅ:

  • ensure-reverse-proxy โ€” ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ๊ฐ€ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค(์ฒซ ๋ฒˆ์งธ ๋ฐฐํฌ์— ์œ ์šฉํ•จ)
  • get-active-slot service_name โ€” ํŠน์ • ์„œ๋น„์Šค์— ๋Œ€ํ•ด ํ˜„์žฌ ํ™œ์„ฑ ์ƒํƒœ์ธ ์Šฌ๋กฏ์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค(BLUE ๋˜๋Š” GREEN)
  • get-service-status service_name deployment_slot โ€” ์„œ๋น„์Šค๊ฐ€ ๋“ค์–ด์˜ค๋Š” ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ์ค€๋น„๊ฐ€ ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • set-active-slot service_name deployment_slot โ€” ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ ์ปจํ…Œ์ด๋„ˆ์—์„œ nginx ๊ตฌ์„ฑ์„ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

์œ„ํ•ด :

ensure-reverse-proxy() {
    is-container-up reverse-proxy && return 0
    echo "Deploying reverse-proxy..."
    docker network create web-gateway
    docker run 
        --detach 
        --restart always 
        --log-driver journald 
        --name reverse-proxy 
        --network web-gateway 
        --publish 80:80 
        nginx:alpine || return 1
    docker exec --interactive reverse-proxy sh -c "> /etc/nginx/conf.d/default.conf"
    docker exec reverse-proxy nginx -s reload
}

is-container-up() {
    local container=${1?"Usage: ${FUNCNAME[0]} container_name"}

    [ -n "$(docker ps -f name=${container} -q)" ]
    return $?
}

get-active-slot() {
    local service=${1?"Usage: ${FUNCNAME[0]} service_name"}

    if is-container-up ${service}_BLUE && is-container-up ${service}_GREEN; then
        echo "Collision detected! Stopping ${service}_GREEN..."
        docker rm -f ${service}_GREEN
        return 0  # BLUE
    fi
    if is-container-up ${service}_BLUE && ! is-container-up ${service}_GREEN; then
        return 0  # BLUE
    fi
    if ! is-container-up ${service}_BLUE; then
        return 1  # GREEN
    fi
}

get-service-status() {
    local usage_msg="Usage: ${FUNCNAME[0]} service_name deployment_slot"
    local service=${1?usage_msg}
    local slot=${2?$usage_msg}

    case $service in
        # Add specific healthcheck paths for your services here
        *) local health_check_port_path=":8080/" ;;
    esac
    local health_check_address="http://${service}_${slot}${health_check_port_path}"
    echo "Requesting '$health_check_address' within the 'web-gateway' docker network:"
    docker run --rm --network web-gateway alpine 
        wget --timeout=1 --quiet --server-response $health_check_address
    return $?
}

set-active-slot() {
    local usage_msg="Usage: ${FUNCNAME[0]} service_name deployment_slot"
    local service=${1?$usage_msg}
    local slot=${2?$usage_msg}
    [ "$slot" == BLUE ] || [ "$slot" == GREEN ] || return 1

    get-nginx-config $service $slot | docker exec --interactive reverse-proxy sh -c "cat > /etc/nginx/conf.d/$service.conf"
    docker exec reverse-proxy nginx -t || return 2
    docker exec reverse-proxy nginx -s reload
}

๊ธฐ๋Šฅ get-active-slot ์•ฝ๊ฐ„์˜ ์„ค๋ช…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ˆซ์ž๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ๋ฌธ์ž์—ด์„ ์ถœ๋ ฅํ•˜์ง€ ์•Š๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

์–ด์จŒ๋“  ํ˜ธ์ถœ ํ•จ์ˆ˜์—์„œ ์ž‘์—… ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๊ณ  bash๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ข…๋ฃŒ ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ๋ฌธ์ž์—ด์„ ํ™•์ธํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ์‰ฝ์Šต๋‹ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ๋ฌธ์ž์—ด์„ ์–ป๋Š” ๊ฒƒ๋„ ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค:
get-active-slot service && echo BLUE || echo GREEN.

์„ธ ๊ฐ€์ง€ ์กฐ๊ฑด์ด ์‹ค์ œ๋กœ ๋ชจ๋“  ์ƒํƒœ๋ฅผ ๊ตฌ๋ณ„ํ•˜๊ธฐ์— ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๊นŒ?

์ตœ์ € ์ž„๊ธˆ์œผ๋กœ ๋ธ”๋ฃจ-๊ทธ๋ฆฐ ๋ฐฐํฌ

๋‘ ๊ฐœ๋ฉด ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰ ํ•˜๋‚˜๋Š” ์™„์ „์„ฑ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์— ์žˆ์œผ๋ฏ€๋กœ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. else.

nginx ๊ตฌ์„ฑ์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋งŒ ์ •์˜๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. get-nginx-config service_name deployment_slot. ์ƒํƒœ ํ™•์ธ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ ์—ฌ๊ธฐ์—์„œ๋Š” ๋ชจ๋“  ์„œ๋น„์Šค์— ๋Œ€ํ•œ ๊ตฌ์„ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํฅ๋ฏธ๋กœ์šด ๊ฒƒ๋“ค ์ค‘ - ์˜ค์ง cat <<- EOF, ์ฒ˜์Œ์— ๋ชจ๋“  ํƒญ์„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์‹ค, ์ข‹์€ ์„œ์‹ ์ง€์ •์˜ ๋Œ€๊ฐ€๋Š” ํƒญ๊ณผ ๊ณต๋ฐฑ์ด ํ˜ผํ•ฉ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋ฉฐ ์˜ค๋Š˜๋‚  ์ด๋Š” ๋งค์šฐ ๋‚˜์œ ํ˜•์‹์œผ๋กœ ๊ฐ„์ฃผ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ bash๋Š” ํƒญ์„ ๊ฐ•์ œํ•˜๋ฏ€๋กœ nginx ๊ตฌ์„ฑ์—์„œ ์ผ๋ฐ˜ ํ˜•์‹์„ ์ง€์ •ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์š”์ปจ๋Œ€, ์—ฌ๊ธฐ์—์„œ ํƒญ๊ณผ ๊ณต๋ฐฑ์„ ํ˜ผํ•ฉํ•˜๋Š” ๊ฒƒ์ด ์ตœ์•…์˜ ์†”๋ฃจ์…˜ ์ค‘ ๊ฐ€์žฅ ์ข‹์€ ์†”๋ฃจ์…˜์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Habr์ด ๋ชจ๋“  ํƒญ์„ 4๊ฐœ์˜ ๊ณต๋ฐฑ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  EOF๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ "์ž˜ ์ˆ˜ํ–‰"ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜ ์Šค๋‹ˆํŽซ์—์„œ๋Š” ์ด ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์—ฌ๊ธฐ์„œ ๋ˆˆ์— ๋„๋Š” ๊ฒƒ์€.

๋‘๋ฒˆ๋‹ค์‹œ ์ผ์–ด๋‚˜์ง€ ์•Š๋„๋ก ๋ฐ”๋กœ ์•Œ๋ ค๋“œ๋ฆด๊ป˜์š” cat << 'EOF', ๋‚˜์ค‘์— ๋งŒ๋‚˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ์ ์–ด๋ณด๋ฉด cat << EOF, ๊ทธ๋Ÿฐ ๋‹ค์Œ heredoc ๋‚ด๋ถ€์—์„œ ๋ฌธ์ž์—ด์ด ๋ณด๊ฐ„๋ฉ๋‹ˆ๋‹ค(๋ณ€์ˆ˜๋Š” ํ™•์žฅ๋ฉ๋‹ˆ๋‹ค($foo), ๋ช…๋ น ํ˜ธ์ถœ($(bar)) ๋“ฑ), ๋ฌธ์„œ ๋์„ ์ž‘์€๋”ฐ์˜ดํ‘œ๋กœ ๋ฌถ์œผ๋ฉด ๋ณด๊ฐ„์ด ๋น„ํ™œ์„ฑํ™”๋˜๊ณ  ๊ธฐํ˜ธ $ ๊ทธ๋Œ€๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์Šคํฌ๋ฆฝํŠธ ์•ˆ์— ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ฒƒ.

get-nginx-config() {
    local usage_msg="Usage: ${FUNCNAME[0]} service_name deployment_slot"
    local service=${1?$usage_msg}
    local slot=${2?$usage_msg}
    [ "$slot" == BLUE ] || [ "$slot" == GREEN ] || return 1

    local container_name=${service}_${slot}
    case $service in
        # Add specific nginx configs for your services here
        *) nginx-config-simple-service $container_name:8080 ;;
    esac
}

nginx-config-simple-service() {
    local usage_msg="Usage: ${FUNCNAME[0]} proxy_pass"
    local proxy_pass=${1?$usage_msg}

cat << EOF
server {
    listen 80;
    location / {
        proxy_pass http://$proxy_pass;
    }
}
EOF
}

์ด๊ฒƒ์ด ์ „์ฒด ์Šคํฌ๋ฆฝํŠธ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด ์Šคํฌ๋ฆฝํŠธ์˜ ์š”์  wget ๋˜๋Š” ์ปฌ์„ ํ†ตํ•ด ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.

์›๊ฒฉ ์„œ๋ฒ„์—์„œ ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰

์ด์ œ ๋Œ€์ƒ ์„œ๋ฒ„๋ฅผ ๋…ธํฌํ•  ์‹œ๊ฐ„์ด๋‹ค. ์ด ์‹œ๊ฐ„ localhost ๋งค์šฐ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค:

$ ssh-copy-id localhost
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
himura@localhost's password: 

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'localhost'"
and check to make sure that only the key(s) you wanted were added.

์‚ฌ์ „ ๊ตฌ์ถ•๋œ ์ด๋ฏธ์ง€๋ฅผ ๋Œ€์ƒ ์„œ๋ฒ„์— ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ์„œ๋น„์Šค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์›ํ™œํ•˜๊ฒŒ ๊ต์ฒดํ•˜๋Š” ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ–ˆ์ง€๋งŒ ์›๊ฒฉ ์‹œ์Šคํ…œ์—์„œ ์–ด๋–ป๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? ์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ๋ฒ”์šฉ์ ์ด๊ณ  ํ•˜๋‚˜์˜ ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ์—์„œ ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๋ฅผ ๋™์‹œ์— ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ธ์ˆ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค(nginx ๊ตฌ์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์–ด๋–ค URL์ด ์–ด๋–ค ์„œ๋น„์Šค๊ฐ€ ๋ ์ง€ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค). ์ด ๊ฒฝ์šฐ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—(๋ฒ„๊ทธ ์ˆ˜์ • ๋ฐ ์ƒˆ ์„œ๋น„์Šค ์ถ”๊ฐ€ ๋ชฉ์ ์œผ๋กœ) ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์„œ๋ฒ„์— ์ €์žฅํ•  ์ˆ˜ ์—†์œผ๋ฉฐ ์ผ๋ฐ˜์ ์œผ๋กœ ์ƒํƒœ = ์•…์ž…๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• 1: ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์„œ๋ฒ„์— ๊ณ„์† ์ €์žฅํ•˜๋˜ ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ๋งค๋ฒˆ ๋ณต์‚ฌํ•˜์„ธ์š”. scp. ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋‹ค์Œ์„ ํ†ตํ•ด ์—ฐ๊ฒฐํ•˜์‹ญ์‹œ์˜ค. ssh ํ•„์š”ํ•œ ์ธ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์  :

  • ํ•˜๋‚˜๊ฐ€ ์•„๋‹Œ ๋‘ ๊ฐ€์ง€ ์ž‘์—…
  • ๋ณต์‚ฌํ•œ ๊ณณ์ด ์—†๊ฑฐ๋‚˜, ์ ‘๊ทผ์ด ๋ถˆ๊ฐ€๋Šฅํ•  ์ˆ˜๋„ ์žˆ๊ณ , ๋Œ€์ฒด ์‹œ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์Šค์Šค๋กœ ์ •๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค(์Šคํฌ๋ฆฝํŠธ ์‚ญ์ œ).
  • ์ด๋ฏธ ์„ธ ๊ฐ€์ง€ ์ž‘์—…์ด ์ˆ˜ํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ… 2:

  • ์Šคํฌ๋ฆฝํŠธ์— ํ•จ์ˆ˜ ์ •์˜๋งŒ ์œ ์ง€ํ•˜๊ณ  ์ „ํ˜€ ์‹คํ–‰ํ•˜์ง€ ์•Š์Œ
  • ๊ณผ sed ๋์— ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”
  • ํŒŒ์ดํ”„(|)

์žฅ์  :

  • ์ง„์ •ํ•œ ๋ฌด๊ตญ์ 
  • ์ƒ์šฉ๊ตฌ ํ•ญ๋ชฉ ์—†์Œ
  • ์‹œ์›ํ•œ ๋Š๋‚Œ

Ansible ์—†์ด ๊ทธ๋ƒฅ ํ•ด๋ณด์ž. ์˜ˆ, ๋ชจ๋“  ๊ฒƒ์ด ์ด๋ฏธ ๋ฐœ๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์‘, ์ž์ „๊ฑฐ์•ผ. ์ž์ „๊ฑฐ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๋‹จ์ˆœํ•˜๊ณ  ์šฐ์•„ํ•˜๋ฉฐ ๋ฏธ๋‹ˆ๋ฉ€ํ•œ์ง€ ์‚ดํŽด๋ณด์„ธ์š”.

$ cat << 'EOF' > deploy.sh
#!/bin/bash

usage_msg="Usage: $0 ssh_address local_image_tag"
ssh_address=${1?$usage_msg}
image_name=${2?$usage_msg}

echo "Connecting to '$ssh_address' via ssh to seamlessly deploy '$image_name'..."
( sed "$a deploy $image_name" | ssh -T $ssh_address ) << 'END_OF_SCRIPT'
deploy() {
    echo "Yay! The '${FUNCNAME[0]}' function is executing on '$(hostname)' with argument '$1'"
}
END_OF_SCRIPT
EOF

$ chmod +x deploy.sh

$ ./deploy.sh localhost magic-porridge-pot
Connecting to localhost...
Yay! The 'deploy' function is executing on 'hut' with argument 'magic-porridge-pot'

๊ทธ๋Ÿฌ๋‚˜ ์›๊ฒฉ ํ˜ธ์ŠคํŠธ์— ์ ์ ˆํ•œ bash๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์‹ ํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์ฒ˜์Œ์— ์ž‘์€ ํ™•์ธ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค(๋Œ€์‹  ์‰˜๋ฑ…):

if [ "$SHELL" != "/bin/bash" ]
then
    echo "The '$SHELL' shell is not supported by 'deploy.sh'. Set a '/bin/bash' shell for '$USER@$HOSTNAME'."
    exit 1
fi

๊ทธ๋ฆฌ๊ณ  ์ด์ œ ๊ทธ๊ฒƒ์€ ํ˜„์‹ค์ด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

$ docker exec reverse-proxy rm /etc/nginx/conf.d/default.conf

$ wget -qO deploy.sh https://git.io/JUURc

$ chmod +x deploy.sh

$ ./deploy.sh localhost uptimer
Sending gzipped image 'uptimer' to 'localhost' via ssh...
Loaded image: uptimer:latest
Connecting to 'localhost' via ssh to seamlessly deploy 'uptimer'...
Deploying 'uptimer_GREEN' in place of 'uptimer_BLUE'...
06f5bc70e9c4f930e7b1f826ae2ca2f536023cc01e82c2b97b2c84d68048b18a
Container started. Checking health...
Requesting 'http://uptimer_GREEN:8080/' within the 'web-gateway' docker network:
  HTTP/1.0 503 Service Unavailable
wget: server returned error: HTTP/1.0 503 Service Unavailable
New 'uptimer_GREEN' service is not ready yet. Waiting (1)...
Requesting 'http://uptimer_GREEN:8080/' within the 'web-gateway' docker network:
  HTTP/1.0 503 Service Unavailable
wget: server returned error: HTTP/1.0 503 Service Unavailable
New 'uptimer_GREEN' service is not ready yet. Waiting (2)...
Requesting 'http://uptimer_GREEN:8080/' within the 'web-gateway' docker network:
  HTTP/1.0 200 OK
  Server: BaseHTTP/0.6 Python/3.8.3
  Date: Sat, 22 Aug 2020 20:15:50 GMT
  Content-Type: text/html

New 'uptimer_GREEN' service seems OK. Switching heads...
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
2020/08/22 20:15:54 [notice] 97#97: signal process started
The 'uptimer_GREEN' service is live!
Killing 'uptimer_BLUE'...
uptimer_BLUE
Total reclaimed space: 0B
Deployment successful!

์ด์ œ ์—ด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค http://localhost/ ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฐฐํฌ๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ณ  ๋ ˆ์ด์•„์›ƒ ์ค‘์— CD์— ๋”ฐ๋ผ ํŽ˜์ด์ง€๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์›ํ™œํ•˜๊ฒŒ ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

ํ‡ด๊ทผ ํ›„์—๋Š” ์ฒญ์†Œํ•˜๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋งˆ์„ธ์š” :3

$ docker rm -f uptimer_GREEN reverse-proxy 
uptimer_GREEN
reverse-proxy

$ docker network rm web-gateway 
web-gateway

$ cd ..

$ rm -r blue-green-deployment

์ถœ์ฒ˜ : habr.com