[WEB] 도커 101

#1. 개념을 짚고 갑시다.

컨테이너 이미지

  • OS의 컨테이너 내부에서 애플리케이션을 실행하기 위해 필요한 모든 파일을 캡슐화한 바이너리 패키지
  • 간단히 압축 파일과 같다.
  • 바이너리 파일
  • 고유 id 를 가지며, 컨테이너를 실행하는데 필요한 모든 것을 담고 있다.
  • 컨테이너 실행에 필요한 모든 것이 담긴 단일 파일


도커

  • 가장 famous한 컨테이너 이미지 포맷 === 도커 이미지 포맷
  • 도커 파일을 사용해 도커 컨테이너 이미지를 자동으로 생성할 수 있음
  • 도커 파일 : 컨테이너를 빌드하는 명령어가 들어 있는 텍스트 파일
  • 파일 형식 Dockerfile


컨테이너 생성 및 실행 명령어

  • CMD 방식
    • 이미지를 바탕으로 생성된 컨테이너 실행 시 매번 명령을 실행
    • 해당 명령으로 컨테이너가 실행됨
    • 하나의 CMD 만 작성 가능하며, 중복일 경우 맨 마지막 하나만 유효
  • ENTRYPOINT 방식
    • 도커파일에서 빌드한 이미지를 컨테이너로 생성할 때 단 한번만 실행
    • m1 환경에서 이미지 빌드시, 멀티os를 환경에 맞춰진 이미지를 위해서는 해당 명령어를 사용해야함)


도커 허브 유의점

  • 도커 허브에서 이미지 불러올 시, 오피셜 태그 붙은 것만 사용하기!
    • 이미지는 가벼울수록 좋으니 경량 리눅스를 사용하기
      • 데비안 (devian)
      • 알파인



#2. 도커 파일 작성하기

뭅스터를 이미지로 만드는 실습을 진행해보자


  1. 클라이언트 소스코드를 클론해서 의존성 파일 설치 후, 실행시키는 이미지
// Dockerfile
FROM node:14.20.0-bullseye-slim
RUN apt-get update -y
RUN apt-get install -y git
RUN git clone https://github.com/movester/movester_client_product
ADD .env /movester_client_product

WORKDIR /movester_client_product

RUN npm install
RUN npm install @material-ui/core
EXPOSE 7001

CMD ["npm","run","start"]
  • 노드 이미지를 가져옴 (도커 허브로부터, 경량화된 리눅스인 bullseye)
  • apt 업데이트 진행
  • 깃 설치
  • 깃 클론해옴 (public 레파지토리여서 인증 필요없지만, private 레파지토리라면 사전에 깃 계정 인증하는 코드 추가해야함)
  • .env 파일 copy함 (해당 이미지를 작성중인 파일과 동일한 위치의 .env 파일을 /movester_client_product 내에 복사함
    • .env 파일은 깃 클론시 받아올 수 없기 때문에, 도커 파일 작성하는 환경에서 주입해주어야함
    • 단! 이렇게 작성하면 이미지 안에 .env 를 가지고 있게 됨
    • 컨테이너를 띄운 뒤, 컨테이너로 직접 들어가며 .env 가 노출됨 → 지양해야함!
    • k8s에서는 secret 으로 관리할 수 있음
  • 디렉토리를 이동 (cd 명령어대신 해당 명령어를 사용해야 함)
  • 의존성을 위해 npm 설치
  • 7001 포트를 노출시킴
  • 해당 명령어와 함께 컨테이너가 실행됨


  1. 도커 이미지를 빌드하자

    • docker build -t [image이름] .
    • 해당 작업 파일에는 .env 가 존재해야 한다!

    Untitled


  1. 이미지가 만들어졌는지 확인하자

    • docker images

    Untitled


  1. 컨테이너를 실행시켜보자

    • docker run —name [컨테이너이름] -itd -p [외부포트:내부포트] [이미지이름]
    • docker ps -a 로 실행중인 컨테이너를 확인하자
      • 포트도 7001 로 연결된 걸 확인할 수 있다!

    Untitled

    Untitled


  1. 컨테이너의 내부로 직접 들어가보기

    • docker exec -it [컨테이너이름] sh
      • .env 가 잘 복사됐는지 확인해보자
      • 나갈때는 exit

    Untitled


  1. 컨테이너 로그 확인하기

    • docker logs -f [컨테이너이름]

    Untitled


  1. 컨테이너 상세 정보 확인하기
    • docker inspect [컨테이너이름]


  1. 컨테이너 내리기
    • docker stop [컨테이너이름]
    • 컨테이너를 내려도 볼륨은 남는다! 볼륨까지 깔끔하기 지우자 docker rm $(docker ps -qa)


  1. 사용하지 않는 이미지 지우기
    • docker rmi $(docker images -qa)



#3. 여러 컨테이너를 하나의 네트워크로 묶기 (심화과정)

자! 이제 뭅스터에 필요한 FE, BE, redis 까지 3가지의 설정을 모두 해보자!

// BE Dockerfile
FROM node:lts
RUN apt-get update -y
RUN apt-get install -y git
RUN git clone https://github.com/movester/movester_server_product.git
ADD .env /movester_server_product

WORKDIR /movester_server_product

RUN npm install
EXPOSE 8000

CMD ["npm","run","start"]
  • 여기서는 경량리눅스가 아닌 일반 노드를 가져왔다.

    • 경량 리눅스에는 파이썬이 설치되어 있지 않기때문! (서버 코드에는 bcrypt 가 사용되는데 해당 라이브러리는 파이썬이 있어야한다.)

    docker에서 bcrypt 사용하기

해당 이미지를 빌드하고, 컨테이너에 올려보자 (참고로 redis 컨테이너는 이미 띄어져있다.)

Untitled

앗…! 실행이 안된다?

Untitled

백앤드 컨테이너의 로그를 조회하면, 다음과 같이 레디스 연결이 안된 것을 확인할 수 있다.

흐음…좀 더 디버깅을 해보자

be 컨테이너 내부로 접속하여 telnet 을 설치한 후, ping을 해보았다.

  • apt-get update -y && apt-get install telnet -y
  • telnet redis 6379

Untitled

  • 역시 서로 네트워크 연결이 안되어있다. 왜그럴까?


컨테이너를 동일 네트워크로 묶기

이유는 소제목으로 짐작 가능하다시피, 각각 컨테이너의 네트워크가 동일하지 않기 때문이다.

네트워크를 생성하여 해당 컨테이너를 동일 네트워크로 묶어주자


  1. 뭅스터 네트워크 생성
    • docker network create [네트워크 이름]
    • 네트워크 리스트 확인 docker network ls
    • 컨테이너를 해당 네트워크로 띄우기 (fe, be, redis 세개 모두 동일 네트워크에 작업해야한다) docker run —name [컨테이너이름] -itd —networkt [네트워크이름] -p [포트:포트] [이미지이름]

Untitled

Untitled


  • 네트워크에 잘 묶였는지 확인해보자

    docker network inspect [네트워크이름]

Untitled



BE-REDIS 접속

자. 이제 be와 redis 의 접속을 확인해보자!

be 컨테이너 접속 후 telnet redis를 확인해보자

Untitled

연결이 잘 됐다! 이때 172.29.0.2 ip를 기억하자!

  • telnet 세션 접속 나가기 control + ]
  • telnet 나가기 q 누른 뒤 엔터

  • 이때 redis는 엄밀히 말하면 localhost 가 아닌 private ip인 172.29.0.2 를 가지고 있기 때문에 be 코드에서 redis에 접근했던 host 를 변경해주어야 한다.
  • 해당 ip는 컨테이너마다 유동적으로 변한다! k8s 에서는 service를 사용하여 도메인네임을 지정할 수 있다.
// .env
// as-is
REDIS_HOST = "127.0.0.1";
// to-be
REDIS_HOST = "172.29.0.2";
  • be 컨테이너 내부로 직접 들어가서 .env 를 변경한 후 재실행해보자!

    • 이때 우리의 컨테이너에는 vim이 없기때문에 vim을 한번 설치해주자
    • apt-get install vim -y
    • vi .env
  • 잘 연결이 되었다!🤭

Untitled

  • api 응답도 잘 온다

Untitled



FE-BE 접속

이제 fe-be 통신을 연결해주자!

마찬가지로 기존의 로컬호스트를 바라보던 api 호출 host를 연결된 네트워크 내의 private ip 로 변경해주어야 한다.

// fe
// src/services/defaultClient.js
baseURL: '/api',
// .env
// as-is
REACT_APP_SERVER_HOST = "http://127.0.0.1:8000";
// to-be
REACT_APP_SERVER_HOST = "http://[서버 컨테이너 ip]:[서버포트]";


fe 컨테이너를 restart 해주자!

Untitled

접속이 잘 된다!

  • 디버깅까지 해보자
    • fe 컨테이너 내부 접속 후 curl 요청을 해보자
    • api-get install curl -y
    • curl [http://172.29.0.3:8000/api/weeks/expose](http://172.29.0.3:8000/api/weeks/expose)

Untitled

  • 역시 잘 연결되어진 것을 확인할 수 있다!👍

Untitled

이로써 3개의 컨테이너를 띄우고 컨테이너들을 하나의 네트워크로 연결하였다🎉



한 가지 의문점 🤔

// fe
// src/services/defaultClient.js
baseURL: `http://${process.env.REACT_APP_SERVER_HOST}/api`,
REACT_APP_SERVER_HOST = "[서버 컨테이너 ip]:[서버포트]";

이렇게 작업을 하면 api 호출을 컨테이너 네트워크 내부의 private ip로 던지지 않고, 외부 public ip ( = 브라우저 검색창 GET 요청)로 요청이 되어 제대로 api 요청이 이루어지지 않는다. 🤔



#4. ECR 에 업로드하기

k8s 와 연동하기 위해 ECR에 이미지를 업로드하자!

유의점은 이미지 빌드 환경이 현재 M1 이기 때문에 multi os 를 지원하는 이미지를 생성해야 한다.

  • entrypoint 방식을 사용해야 한다.
  • 빌드 명령어 docker buildx build --platform linux/amd64 -t [image이름] .
// Dockerfile
FROM node:14.20.0-bullseye-slim
RUN apt-get update -y
RUN apt-get install -y git
RUN git clone https://github.com/movester/movester_client_product

WORKDIR /movester_client_product
ADD entrypoint.sh ./

RUN npm install
RUN npm install @material-ui/core
RUN chmod 755 entrypoint.sh
EXPOSE 7001

ENTRYPOINT ["./entrypoint.sh"]
// entrypoint.sh
#!/bin/bash
npm run start


ECR push 인증하기

 aws ecr get-login-password --region ap-northeast-2 --profile dev | docker login --username AWS --password-stdin [ecr 이미지 url]


참고자료

Docker - Dockerfile 작성 / Build (이미지 제작)

Docker 네트워크 사용법

[WEB] 네트워크 이해하기(IP, VPC, CIDR)

#1. IP

  • internet protocal
  • 컴퓨터가 연결된 네트워크 끝단의 주소
  • 기기가 인터넷에 접속한 곳의 네트워크상 위치



IPv4

255.255.255.255 ⇒ 256^4 ⇒ 46억개

세계인구는 78억

전세계에서 ip 주소 안겹치고 사용할 수가 없어!

  • 공인 ip (public ip)
    • 서울시 개발구 코딩로 12 케어닥 아파트
    • 케어닥의 공인 ip내에서 직원들은 사설 ip로 할당받음
    • 일부만 public > ELB 연결하여 통신
      • ELB에는 ALB(L7), NLB(L4)
  • 사설 ip (private ip)
    • 101동 202호, 102동 304호
    • 특정 컴퓨터들을 하나의 공통된 공인ip에 묶어서 그 안에서만 안 겹치는 사설 ip를 각각 줌
    • 내부 네트워크 내에서 위치를 찾아갈 때 사용
    • 사설 ip에서 다른 공인 ip로는 접근 가능
    • 다른 공인 ip에서 사설 ip로는 접근 불가능 (6동 404호 가지고 어떻게 찾아!)
    • 사설ip에서 웹사이트 실행시키는 법
      • 포트포워딩 : 원하는 기기에다 포트를 하나씩 줌
        • 공인아이피:101, 공인아이피:104 …
      • DMZ : 모든 포트를 싸그리 한 군데 몰빵
    • 사설 IP 대역 (RFC1918)
      • 인터넷을 사용하지 않고 우리끼리 사용하는!
    • 10.0.0.0 - 10.255.255.255 (10.0.0.0/8)
    • 172.16.0.0 - 172.31.255.255 (172.16.0.0/12)
    • 192.169.0.0 - 192.168.255.255 (192.168.0.0/16)
  • 고정 ip
    • 서버 같은 곳에 사용
    • 비쌈
    • 한국은 1억개의 ip를 가지고 있음
  • 유동 ip
    • 일반 가정이나 기기들에는, 주기적으로 ip를 회수해서 인터넷을 사용 중인 곳에만 나눠줌
    • 놀고있는 ip는 회수
    • ip가 이따금씩 변경됨 (해킹으로부터 안전)
    • 유동 ip인 공인 ip를 썼다면..? 매번 변경해줘야해??
      • DDNS(dynamin DNS)
        • 수시로 바뀌는 유동 ip를 조회해서 고정된 도메인에 연결해줌



IPv6

  • 16진수 4개가 8개로 연결
  • 2^128, 3.4*10^38 개 고정 ip 사용 가능
  • 1234:5678:9ABC:DEF0:1234:5678:0ABC:DEF0



#2. IP 쪼개기 (클래스 < 사이더)

IP주소 클래스 종류(A클래스, B클래스 등등..)란 무엇인가?

[네트워크] CIDR이란?(사이더 란?)

  • 이제는 클래스 방식보다 사이더를 선호함 (ip대역대를 더 세부적으로 쪼갤 수 있어서)



#3. VPC

  • Virtaul Private Cloud
  • 독립된 하나의 네트워크를 구성하기 위한 가장 큰 단위
  • 🤔 이 대역대가 VPC 에 포함되어 있는지 아닌지를 구분할 정도의 공부 필요!
    • bottle rocket: 컨테이너용 리눅스 os (운영 자동화 관점에서 컨테이너 환경에 최적화)



서브넷

  • VPC 를 쪼개어 놓은 것
  • 각각의 서브넷의 대역대가 VPC 에 포함되어 있도록 설계해야함!
  • private이 public보다 더 많은 대역대를 갖도록 설정 (for 보안)
  • public subnet
    • 인터넷에 연결되어 있음
  • private subnet
    • 인터넷에 연결되어 있지 않음
    • 나누어놓은 이유는? for 보안 강화
    • 그러면 private subnet은 외부 통신 못해?
      • 가능하게끔 만든 것이 NAT gateway!



라우팅 테이블

  • 각각의 서브넷의 요청을 라우팅할 테이블 작성
  • 보통 public, private 서브넷을 위해 각각 설정
  • public은 0.0.0.0 이 IGA 로
  • private은 0.0.0.0 이 NAT 게이트웨이로



NAT 게이트웨이

  • private 서브넷의 요청을 외부와 연결하기 위한 게이트웨이
  • public 서브넷 내부에 생성해야함
  • private 요청을 public으로 넘겨줘서 소통함 (public이 다리 역할)
  • EIP(탄력적 IP)가 필요한 시점! (고정 IP임)

[AWS강좌]6.VPC(1)



#4. 실습해봅시다!

  1. AWS 계정에 VPC 생성하기
  2. public/private 서브넷 각각 2개씩 생성
  3. 대역대 알아서 쪼개보기
  4. 라우팅 테이블, 인터넷 게이트웨이, NAT 게이트웨이, EIP 붙여보기

Untitled

[WEB] express에서 path variable 로 인해 우연히 동일한 URI 가 될 때 어떤 라우터가 선택될까?

오늘도 제리의 한마디로 시작된 2시간의 라우터 공부….🙀

오늘의 주제는, express 환경에서, REST API 의 URI가 path varibale 로 인해 우연히 동일해진다면 어떤 라우터가 호출될까 입니다.

우리가 열심히 시간을 소비했으니,,, 이걸 보는 다른 사람들은 이 글을 통해 시간을 절약할 수 있기를 바라며 알게 된 것을 기록해봅니다🏝



Untitled

A 라우터B 라우터path variable의 이름만 다르고 동일한 URI를 가진다.

오른쪽과 같이 api 가 요청됐을 때 선택되는 라우터는 먼저 선언된 A 라우터 이다. (절대 B 라우터는 선택될 수 없다.)


여기서 의문.

그렇다면 B 라우터 는 아에 실행자체도 되지 않는 것일까?

Untitled

B 라우터의 console 이 찍히지 않았다.


Untitled

A 라우터에서 next() 를 호출하니, B 라우터가 실행되었다.

클라이언트는 A 라우터의 response 를 받은 것을 확인하자.

또한 클라이언트측에서는 200 status 로 오류없이 받았지만 서버에서는 A 라우터의 response 후에 B 라우터에서도 response 하는 것을 할 수 없다며 에러를 나타내고 있다.


Untitled

A 라우터 에서 response, next 를 하지 않았다.

클라이언트에서 response를 무한 대기한다. (timeout 시간까지)

또한 B 라우터는 실행되지 않았다.


A 라우터 에서 response를 하지 않고 next() 만 한다면?

Untitled

B 라우터를 실행하며 response가 1개이므로 정상 동작한다.



중간 path variable 값이 우연이 일치한다면?

Untitled

해당 경우 A 라우터가 호출된 것을 볼 수 있다.



A 라우터B 라우터의 선언 위치를 바꾼다면?

Untitled

이번엔 B 라우터가 호출되었다.


이를 통해 path variable 값이 우연히 일치해서 동일 URI를 가지게 될 때도 먼저 선언된 라우터가 호출되는 것을 알 수 있다 (변수로 인한 중복 매칭의 경우 ⇒ 위의 경우 fruits)

(이런 상황이 발생하지 않게끔 설계를 잘 해야한다!🤯)

(spring의 경우, 라우터의 선언 위치와 별개로 path variable 의 갯수가 적은 순(명시적 URI 갯수가 많은 순)으로 라우터가 호출된다고 한다. 참고 https://okky.kr/article/546504)



여기까지 결론

  • path variable 변수명만 다르고 동일한 URI를 가진 REST API 는 먼저 선언된 라우터로 호출된다.
  • 해당 라우터에서 next() 를 호출하지 않으면 해당 라우터 실행 후 종료된다.
  • next() 를 호출하면, 동일한 URI를 가진 라우터가 순차적으로 호출된다.
  • 두 라우터가 모두 response 를 한다면, 클라이언트는 첫번째 라우터의 응답값만 받고 서버에서는 에러가 난다.



번외. express에서 REST API는 대소문자를 구분하지 않는다?

Untitled

Untitled

APPLE 과 banana 의 대소문자를 바꿨음에도 C 라우터가 호출된다.

별도로 설정하지 않은 express 환경에서 REST API는 대소문자를 구분하지 않는다는 것을 확인할 수 있다.

(아래 공식문서를 보면 설정을 통해 대소문자를 구분하도록 변경 가능해보인다)

https://expressjs.com/ko/api.html#express.router

http://daplus.net/url-url은-대소-문자를-구분해야합니까/



나의 회고 🤫

node 는 왜 알면 알수록 새로울까…

나만 라우터에 이런 뻘짓(?)을 해본걸까? 😭

함께 고민한 제리가 말하길.. “애초에 이걸 고민하지 않는게 정배이거나 다른 사람들도 모르거나..”

후… 우선 2시간의 뻘짓을 통해 해당 경우에 라우터가 어떻게 동작하는지 명확하게 알았으니 오늘의 2시간은 값진걸로! (출근전 새벽이라는게 문제지만..)

아쉬운 것은 위의 행위의 근거를 찾지 못했다는거..? ㅠㅠ

가장 best 는 해당 사항을 고려하여 미리미리 URI 설계를 명확하게 하는 것이 좋은 것 같다!