NGINX 튜토리얼: 마이크로서비스 배포 및 구성 방법

NGINX | 2023년 3월 07일

이 게시물은 다음의 개념을 실제로 적용하는 데 도움이 되는 네 가지 튜토리얼 중 하나입니다. Microservices March 2023: 마이크로서비스 제공 시작하기:

모든 앱에는 구성이 필요하지만, 마이크로서비스를 구성할 때 고려해야 할 사항은 모놀리식 앱의 경우와 다를 수 있습니다. 두 유형의 앱 모두에 적용할 수 있는 지침은 요소 3(구성을 환경에 저장)에 해당하는 12요소 앱에서 찾을 수 있지만, 이 지침은 마이크로서비스 앱에 맞게 조정할 수 있습니다. 특히 서비스 구성을 정의하는 방식, 서비스에 구성을 제공하는 방식, 그리고 해당 서비스에 의존할 수 있는 다른 서비스에서 사용할 수 있도록 서비스를 구성 값으로 제공하는 방식을 조정할 수 있습니다.

마이크로서비스에 요소 3을 적용하는 방법, 특히 구성 파일, 데이터베이스 및 서비스 검색에 관한 모범 사례를 개념적으로 이해하려면 블로그에서 마이크로서비스 앱 구성을 위한 모범 사례를 읽어보세요. 이 게시물은 해당 지식을 실무에 적용하는 데 매우 유용합니다.

참고: 이 튜토리얼의 목적은 몇 가지 핵심 개념을 설명하는 것이지, 프로덕션 환경에 마이크로서비스를 배포하는 올바른 방법을 보여주는 것이 아닙니다. 실제 “마이크로서비스” 아키텍처를 사용하지만, 몇 가지 중요한 유의 사항이 있습니다:

  • 이 튜토리얼에서는 Kubernetes나 Nomad와 같은 컨테이너 오케스트레이션 프레임워크를 사용하지 않습니다. 이는 특정 프레임워크의 세부 사항에 얽매이지 않고 마이크로서비스의 개념을 학습할 수 있도록 하기 위함입니다. 여기서 소개하는 패턴은 이러한 프레임워크 중 하나를 실행하는 시스템에도 적용할 수 있습니다.
  • 서비스는 소프트웨어 엔지니어링의 엄밀성보다는 이해하기 쉽도록 최적화되어 있습니다. 핵심은 코드의 세부 사항이 아니라 시스템에서 각 서비스가 수행하는 역할과 통신 패턴을 살펴보는 것입니다. 자세한 내용은 각 서비스의 README 파일을 참조하세요.

튜토리얼 개요

이 튜토리얼에서는 Factor 3 개념을 마이크로서비스 앱에 적용하는 방법을 설명합니다. 네 가지 과제를 통해 일반적인 마이크로서비스 구성 패턴을 살펴보고, 이러한 패턴을 사용하여 서비스를 배포하고 구성합니다:

  • In 과제 1과 과제 2에서는 첫 번째 패턴을 살펴봅니다. 이 패턴은 마이크로서비스 앱의 구성을 어디에 둘 것인지에 관한 것입니다. 일반적으로 다음 세 위치 중 하나에 둡니다:
    • 애플리케이션 코드
    • 애플리케이션 배포 스크립트
    • 배포 스크립트가 액세스하는 외부 소스
  • In 챌린지 3에서는 두 가지 패턴을 추가로 설정했습니다. NGINX를 리버스 프록시로 사용하여 앱을 외부에 공개하고, Consul을 사용하여 서비스 검색을 활성화했습니다.
  • In 과제 4에서는 마지막 패턴을 구현합니다. 마이크로서비스 인스턴스를 평소 기능과 다른 일회성 작업(이 경우 데이터베이스 마이그레이션을 모방하는 작업)을 수행하는 ‘작업 실행기’로 사용하는 패턴입니다.

이 튜토리얼에서는 네 가지 기술을 사용합니다:

  • messenger – 이 튜토리얼을 위해 만든 메시지 저장 기능을 갖춘 간단한 채팅 API
  • NGINX Open Source – messenger 서비스 및 전체 시스템으로 연결되는 진입점
  • Consul – 동적 서비스 레지스트리 및 키-값 저장소
  • RabbitMQ – 서비스 간 비동기 통신을 지원하는 널리 사용되는 오픈 소스 메시지 브로커

컨테이너에서 함께 실행되는 NGINX Open Source와 Consul 템플릿을 보여 주는 토폴로지 다이어그램. Consul 템플릿은 Consul 클라이언트와 통신합니다. NGINX Open Source는 메신저 서비스의 리버스 프록시이며, 이 서비스는 messenger_db에 데이터를 저장하고 Rabbit MQ와 통신합니다.

이 동영상을 시청하여 튜토리얼의 전반적인 내용을 살펴보세요. 동영상의 단계가 이 게시물과 정확히 일치하지는 않지만, 개념을 이해하는 데 도움이 됩니다.

 

사전 요구 사항 및 설정

사전 요구 사항

자체 환경에서 튜토리얼을 완료하려면 다음이 필요합니다:

  • Linux/Unix 호환 환경
  • Linux 명령줄, JavaScript 및 bash에 대한 기본적인 이해(단, 모든 코드와 명령이 제공되고 설명되므로 관련 지식이 부족해도 충분히 따라 할 수 있습니다)
  • Docker 및 Docker Compose
  • Node.js 19.x 이상
    • 19.x 버전에서 테스트했지만, 최신 버전의 Node.js에서도 작동할 것으로 예상합니다.
    • Node.js 설치에 대한 자세한 내용은 README를 참조하세요. 해당 문서는 messenger 서비스 리포지토리에 있습니다. 또한 컨테이너에서 사용하는 것과 정확히 동일한 Node.js 버전을 사용하려면 asdf를 설치할 수도 있습니다.
  • curl (대부분의 시스템에 이미 설치되어 있음)
  • 다음에 나열된 네 가지 기술: 튜토리얼 개요: messenger(다음 섹션에서 다운로드합니다), NGINX Open Source, Consul, 그리고 RabbitMQ.

설정

  1. 터미널 세션을 시작합니다(이후 지침에서는 이를 앱 터미널이라고 합니다).
  2. 홈 디렉터리에 microservices-march 디렉터리를 만들고 이 튜토리얼에 사용할 GitHub 리포지토리를 해당 디렉터리에 복제합니다. (다른 디렉터리 이름을 사용하고 그에 맞게 지침을 조정해도 됩니다.) 참고: 명령을 터미널에 더 쉽게 복사하여 붙여넣을 수 있도록 이 튜토리얼 전체에서 Linux 명령줄 프롬프트는 생략합니다. 물결표(~)는 홈 디렉터리를 나타냅니다.
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/platform.git --branch mm23-twelve-factor-start git clone https://github.com/microservices-march/messenger.git --branch mm23-twelve-factor-start
  1. Change to the platform 리포지토리로 이동한 후 Docker Compose를 시작합니다:
cd platformdocker compose up -d --build
  1. 이렇게 하면 이후 과제에서 사용할 RabbitMQ와 Consul이 모두 시작됩니다.
    • The -d 플래그는 컨테이너가 시작된 후 Docker Compose가 컨테이너에서 분리되도록 합니다(그렇지 않으면 컨테이너가 터미널에 연결된 상태로 유지됩니다).
    • The --build 플래그는 Docker Compose가 실행 시 모든 이미지를 다시 빌드하도록 합니다. 따라서 파일이 변경될 가능성이 있더라도 실행 중인 이미지를 최신 상태로 유지할 수 있습니다.
  2. Change to the messenger 리포지토리로 이동한 후 Docker Compose를 시작합니다:
cd ../messengerdocker compose up -d --build
  1. 이렇게 하면 messenger 서비스용 PostgreSQL 데이터베이스가 시작됩니다. 이 데이터베이스는 이 튜토리얼의 나머지 부분에서 messenger-database라고 부르겠습니다.

과제 1: 애플리케이션 수준의 마이크로서비스 구성 정의

이 과제에서는 튜토리얼에서 살펴볼 세 가지 위치 중 첫 번째인 애플리케이션 수준에서 구성을 설정합니다. (과제 2에서는 두 번째와 세 번째 위치인 배포 스크립트와 외부 소스를 설명합니다.)

12요소 앱에서는 애플리케이션 수준의 구성을 명시적으로 제외합니다. 이러한 구성은 서로 다른 배포 환경(12요소 앱에서는 이를 배포라고 부름) 간에 변경할 필요가 없기 때문입니다. 하지만 완전성을 위해 세 가지 유형을 모두 다룹니다. 서비스를 개발하고 빌드하고 배포할 때 각 범주를 다루는 방식이 서로 다르기 때문입니다.

The messenger 서비스는 Node.js로 작성되었으며, 진입점은 app/index.mjs이고 messenger 저장소에 있습니다. 이 파일의 다음 줄을 살펴보겠습니다:

app.use(express.json());

애플리케이션 수준 구성의 예입니다. Express 프레임워크가 application/json 유형의 요청 본문을 역직렬화하여 JavaScript 객체로 변환하도록 구성합니다.

이 로직은 애플리케이션 코드와 긴밀하게 결합되어 있으며, 12요소 앱에서 말하는 ‘설정’에 해당하지 않습니다. 하지만 소프트웨어에서는 모든 것이 상황에 따라 달라지지 않나요?

다음 두 섹션에서는 이 줄을 수정하여 애플리케이션 수준 구성의 두 가지 예를 구현합니다.

예제 1

이 예제에서는 messenger 서비스가 허용하는 요청 본문의 최대 크기를 설정합니다. 이 크기 제한은 Express API 문서에서 설명한 대로 express.json 함수의 limit 인수로 설정합니다. 여기서는 위에서 설명한 Express 프레임워크의 JSON 미들웨어 구성에 limit 인수를 추가합니다.

  1. 선호하는 텍스트 편집기에서 app/index.mjs 파일을 열고 다음 내용을 바꿉니다:
app.use(express.json())
  1. 다음으로 변경합니다:
app.use(express.json({ limit: "20b" }));
  1. 앱 터미널(설정에서 사용한 터미널)에서 app 디렉터리로 이동한 후 messenger 서비스를 시작합니다:
cd app npm install node index.mjs
messenger_service listening on port 4000
  1. 두 번째 별도의 터미널 세션(이후 지침에서는 이를 클라이언트 터미널이라고 함)을 시작하고 POST 요청을 messenger 서비스로 전송합니다. 오류 메시지는 요청 본문이 1단계에서 설정한 20바이트 제한보다 작아 요청이 성공적으로 처리되었지만, JSON 페이로드의 내용이 올바르지 않음을 나타냅니다:
curl -d '{ "text": "hello" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations...
{ "error": "Conversation must have 2 unique users" }
  1. 조금 더 긴 메시지 본문을 전송합니다(다시 클라이언트 터미널에서 실행). 3단계보다 훨씬 많은 출력이 표시되며, 이번에는 요청 본문이 20바이트를 초과했음을 나타내는 오류 메시지도 포함됩니다:
curl -d '{ "text": "hello, world" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations...
\”PayloadTooLargeError: request entity too large"

예시 2

이 예제에서는 전체 구성 ‘스키마’를 하나의 파일에 정의할 수 있게 해 주는 라이브러리인 convict를 사용합니다. 또한 12요소 앱의 요소 3에 나오는 두 가지 지침을 보여 줍니다:

  • 구성을 환경 변수에 저장 – 앱 코드에 하드코딩하는 대신 환경 변수(JSON_BODY_LIMIT)를 사용하여 최대 본문 크기를 설정하도록 앱을 수정합니다.
  • 서비스 구성을 명확하게 정의하세요 – 이는 요소 3을 마이크로서비스에 맞게 적용한 것입니다. 이 개념이 익숙하지 않다면 잠시 시간을 내어 저희 블로그의 마이크로서비스 앱 구성을 위한 모범 사례를 읽어보시기 바랍니다.

이 예제에서는 챌린지 2에서 활용할 몇 가지 기반 작업도 설정합니다. 해당 챌린지에서 만들 messenger 배포 스크립트는 JSON_BODY_LIMIT 환경 변수를 설정하며, 여기서는 배포 스크립트에 지정된 구성을 보여 주기 위해 이 변수를 앱 코드에 삽입합니다.

  1. Open the convict 구성 파일인 app/config/config.mjs을 열고, amqpport 키 뒤에 다음 내용을 새 키로 추가합니다:
jsonBodyLimit: {  doc: `The max size (with unit included) that will be parsed by the
        JSON middleware. Unit parsing is done by the
        https://www.npmjs.com/package/bytes library.
        ex: "100kb"`,
  format: String,
  default: null,
  env: "JSON_BODY_LIMIT",
},
  1. convict 라이브러리는 아래 3단계에서 명령줄을 통해 최대 본문 크기를 설정할 때 JSON_BODY_LIMIT 환경 변수를 자동으로 파싱합니다:
    • 올바른 환경 변수에서 값을 가져옵니다
    • 변수의 유형을 확인합니다(String)
    • 애플리케이션에서 jsonBodyLimit 키를 통해 액세스할 수 있도록 합니다
  2. In app/index.mjs에서 다음을 교체합니다:
app.use(express.json({ limit: "20b" }));
  1. 포함
app.use(express.json({ limit: config.get("jsonBodyLimit") }));
  1. 앱 터미널(예제 1의 2단계에서 messenger 서비스를 시작한 터미널)에서 Ctrl+c를 눌러 서비스를 중지합니다. 그런 다음 JSON_BODY_LIMIT 환경 변수를 사용하여 최대 본문 크기를 27바이트로 설정한 후 서비스를 다시 시작합니다:
^cJSON_BODY_LIMIT=27b node index.mjs
  1. 이는 사용 사례에 적합하도록 구성 방식을 변경한 예입니다. 즉, 애플리케이션 코드에 값(이 경우 크기 제한)을 하드코딩하는 대신, 12요소 앱 방법론의 권장 사항에 따라 환경 변수로 설정하도록 변경했습니다. 위에서 언급했듯이 과제 2에서는 JSON_BODY_LIMIT 환경 변수를 명령줄에서 설정하는 대신 messenger 서비스의 배포 스크립트를 사용해 설정하게 되며, 이는 두 번째 구성 위치의 예가 됩니다.
  2. 클라이언트 터미널에서 curl 명령을 예제 1의 4단계에서와 같이 더 큰 요청 본문을 사용하여 다시 실행합니다. 이제 크기 제한을 27바이트로 늘렸으므로 요청 본문이 더 이상 제한을 초과하지 않으며, 요청은 처리되었지만 JSON 페이로드의 내용이 잘못되었음을 나타내는 오류 메시지가 표시됩니다.
curl -d '{ "text": "hello, world" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations{ "error": "Conversation must have 2 unique users" }
  1. 원한다면 클라이언트 터미널을 닫아도 됩니다. 이 튜토리얼의 나머지 과정에서는 모든 명령을 앱 터미널에서 실행합니다.
  2. 앱 터미널에서 Ctrl+c를 눌러 messenger 서비스를 중지합니다(위의 3단계에서 이 터미널을 사용해 서비스를 중지했다가 다시 시작했습니다).
^c
  1. Stop the messenger-database를 중지합니다. 네트워크가 platform 리포지토리에 정의된 인프라 요소에서 아직 사용 중이므로 표시되는 오류 메시지는 무시해도 됩니다. 이 명령은 messenger 리포지토리의 루트에서 실행합니다.
docker compose down
...failed to remove network mm_2023....

과제 2: 서비스를 위한 배포 스크립트 만들기

“설정은 코드와 엄격하게 분리되어야 한다(그렇지 않으면 배포할 때마다 어떻게 다르게 설정할 수 있겠는가?)” – 12요소 앱의 세 번째 요소에서

언뜻 보면 이를 “구성 정보를 소스 제어 시스템에 커밋하지 말라”는 의미로 해석할 수 있습니다. 이 과제에서는 이 규칙을 어기는 것처럼 보일 수 있지만, 실제로는 규칙을 준수하면서 마이크로서비스 환경에 필수적인 유용한 프로세스 개선 효과를 제공하는 일반적인 마이크로서비스 환경 패턴을 구현합니다.

이 챌린지에서는 마이크로서비스에 구성을 제공하는 코드형 인프라 및 배포 매니페스트의 기능을 모방하기 위해 배포 스크립트를 만들고, 외부 구성 소스를 사용하도록 스크립트를 수정하고, 비밀을 설정한 다음, 스크립트를 실행하여 서비스와 해당 인프라를 배포합니다.

새로 만든 infrastructure 디렉터리에 배포 스크립트를 생성합니다. 이 디렉터리는 messenger 리포지토리에 있습니다. infrastructure라는 이름의 디렉터리(또는 이와 유사한 이름의 디렉터리)는 최신 마이크로서비스 아키텍처에서 흔히 사용되는 패턴으로, 다음과 같은 항목을 저장하는 데 사용됩니다.

이 패턴의 이점은 다음과 같습니다:

  • 서비스 배포와 서비스별 인프라(예: 데이터베이스) 배포에 대한 책임을 해당 서비스를 소유한 팀에 부여합니다.
  • 팀은 이러한 요소에 대한 모든 변경 사항이 자체 개발 프로세스(코드 검토, CI 등)를 거치도록 할 수 있습니다.
  • 팀은 외부 팀에 작업을 의존하지 않고도 서비스와 이를 지원하는 인프라의 배포 방식을 쉽게 변경할 수 있습니다.

앞서 언급했듯이, 이 튜토리얼의 목적은 실제 시스템을 설정하는 방법을 보여 주는 것이 아니며, 이 과제에서 배포하는 스크립트도 실제 프로덕션 시스템과 유사하지 않습니다. 대신 이 스크립트는 마이크로서비스 관련 인프라 배포를 다룰 때 도구별 구성으로 해결할 수 있는 몇 가지 핵심 개념과 문제를 보여 주는 동시에, 특정 도구에 대한 의존성을 가능한 한 최소화하도록 추상화되어 있습니다.

초기 배포 스크립트 만들기

  1. 앱 터미널에서 infrastructure 디렉터리를 messenger 리포지토리의 루트에 만들고, messenger 서비스와 messenger-database의 배포 스크립트를 저장할 파일을 만듭니다. 환경에 따라 chmod 명령 앞에 sudo를 붙여야 할 수도 있습니다:
mkdir infrastructurecd infrastructure touch messenger-deploy.sh chmod +x messenger-deploy.sh touch messenger-db-deploy.sh chmod +x messenger-db-deploy.sh
  1. 선호하는 텍스트 편집기에서 messenger-deploy.sh 파일을 열고 다음 내용을 추가하여 messenger 서비스의 초기 배포 스크립트를 만듭니다:
#!/bin/bashset -e

JSON_BODY_LIMIT=20b

docker run \
  --rm \
  -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
  messenger

이 스크립트는 아직 완성되지 않았지만, 몇 가지 개념을 잘 보여 줍니다:

  • 배포 스크립트에 해당 구성을 직접 포함하여 환경 변수에 값을 할당합니다.
  • 런타임에 컨테이너에 환경 변수를 주입하기 위해 -e 플래그를 docker run 명령어에 사용합니다.

이런 방식으로 환경 변수 값을 설정하는 것이 불필요해 보일 수 있지만, 이렇게 하면 배포 스크립트가 아무리 복잡해지더라도 스크립트 맨 위만 빠르게 살펴보고 구성 데이터가 배포에 어떻게 제공되는지 파악할 수 있습니다.

또한 실제 배포 스크립트에서는 명시적으로 docker run 명령을 호출하지 않을 수도 있지만, 이 예제 스크립트는 Kubernetes 매니페스트와 같은 도구가 해결하는 핵심 문제를 보여 주기 위한 것입니다. Kubernetes와 같은 컨테이너 오케스트레이션 시스템을 사용하면 배포 시 컨테이너가 시작되고, Kubernetes 구성 파일에서 파생된 애플리케이션 구성이 해당 컨테이너에 제공됩니다. 따라서 이 예제 배포 파일은 Kubernetes 매니페스트와 같은 프레임워크별 배포 파일과 동일한 역할을 하는 배포 스크립트의 최소 버전이라고 볼 수 있습니다.

실제 개발 환경에서는 이 파일을 소스 관리 시스템에 커밋하고 코드 리뷰를 진행할 수 있습니다. 그러면 팀의 다른 구성원들이 설정에 대해 의견을 제시할 수 있으므로, 잘못 구성된 값으로 인해 예기치 않은 동작이 발생하는 문제를 방지하는 데 도움이 됩니다. 예를 들어 이 스크린샷에서는 한 팀원이 수신 JSON 요청 본문의 제한이 20바이트(JSON_BODY_LIMIT로 설정됨)로 지정된 것은 너무 낮다고 올바르게 지적하고 있습니다.

메시지 본문 제한이 20바이트로 설정된 것은 너무 작다는 내용의 코드 리뷰 스크린샷

외부 소스에서 구성 값을 조회하도록 배포 스크립트 수정

이 챌린지의 이 부분에서는 마이크로서비스 구성을 저장할 세 번째 위치, 즉 배포 시 쿼리되는 외부 소스를 설정합니다. 값을 하드코딩하면 지속적으로 업데이트해야 하고 장애가 발생할 수 있으므로, 값을 동적으로 등록하고 배포 시 외부 소스에서 가져오는 것이 훨씬 더 좋은 방법입니다. 자세한 내용은 블로그의 마이크로서비스 앱 구성 모범 사례를 참조하세요.

이 시점에는 두 개의 인프라 구성 요소가 백그라운드에서 실행되며 messenger 서비스에 필요한 보조 서비스를 제공합니다:

  1. 실제 배포 환경에서는 플랫폼 팀이 관리하는 RabbitMQ(설정의 3단계에서 시작됨)
  2. 실제 배포 환경에서는 팀에서 소유하는 messenger-database(은)는 설정의 4단계에서 시작됨)

The convict 스키마는 messenger 서비스의 app/config/config.mjs 파일에서 이러한 외부 구성 요소에 해당하는 필수 환경 변수를 정의합니다. 이 섹션에서는 변숫값을 공통으로 액세스할 수 있는 위치에 설정하여 이 두 구성 요소가 구성을 제공하도록 설정합니다. 그러면 messenger 서비스가 배포될 때 해당 값을 조회할 수 있습니다.

RabbitMQ 및 messenger-database에 필요한 연결 정보는 Consul 키/값(KV) 저장소에 등록됩니다. 이 저장소는 모든 서비스가 배포될 때 접근할 수 있는 공용 위치입니다. Consul KV 저장소는 이러한 유형의 데이터를 저장하는 일반적인 장소는 아니지만, 이 튜토리얼에서는 편의상 사용합니다.

  1. 다음 내용으로 infrastructure/messenger-deploy.sh 파일( 이전 섹션의 2단계에서 생성됨)의 내용을 바꿉니다:
#!/bin/bashset -e

# This configuration requires a new commit to change
NODE_ENV=production
PORT=4000
JSON_BODY_LIMIT=100kb

# Postgres database configuration by pulling information from 
# the system
POSTGRES_USER=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-application-user?raw=true)
PGPORT=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-port?raw=true)
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)

# RabbitMQ configuration by pulling from the system
AMQPHOST=$(curl -X GET http://localhost:8500/v1/kv/amqp-host?raw=true)
AMQPPORT=$(curl -X GET http://localhost:8500/v1/kv/amqp-port?raw=true)

docker run \
  --rm \
  -e NODE_ENV="${NODE_ENV}" \
  -e PORT="${PORT}" \
  -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
  -e PGUSER="${POSTGRES_USER}" \
  -e PGPORT="${PGPORT}" \
  -e PGHOST="${PGHOST}" \
  -e AMQPPORT="${AMQPPORT}" \
  -e AMQPHOST="${AMQPHOST}" \
  messenger
  1. 이 스크립트는 두 가지 유형의 구성을 보여 줍니다:
    • 배포 스크립트에 직접 지정된 구성 – 배포 환경(NODE_ENV)과 포트(PORT)를 설정하고, JSON_BODY_LIMIT를 20바이트보다 현실적인 값인 100KB로 변경합니다.
    • 외부 소스에서 조회한 구성 – Consul KV 저장소에서 POSTGRES_USER, PGPORT, PGHOST, AMQPHOST, AMQPPORT 환경 변수의 값을 가져옵니다. 다음 두 단계에 따라 Consul KV 저장소에서 환경 변수의 값을 설정합니다.
  2. Open messenger-db-deploy.sh 파일을 열고 다음 내용을 추가하여 messenger-database의 초기 배포 스크립트를 만듭니다:
#!/bin/bashset -e

PORT=5432
POSTGRES_USER=postgres

docker run \
  -d \
  --rm \
  --name messenger-db \
  -v db-data:/var/lib/postgresql/data/pgdata \
  -e POSTGRES_USER="${POSTGRES_USER}" \
  -e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
  -e PGPORT="${PORT}" \
  -e PGDATA=/var/lib/postgresql/data/pgdata \
  --network mm_2023 \
  postgres:15.1

# Register details about the database with Consul
curl -X PUT http://localhost:8500/v1/kv/messenger-db-port \
  -H "Content-Type: application/json" \
  -d "${PORT}"

curl -X PUT http://localhost:8500/v1/kv/messenger-db-host \
  -H "Content-Type: application/json" \
  -d 'messenger-db' # This matches the "--name" flag above
                    # (the hostname)

curl -X PUT http://localhost:8500/v1/kv/messenger-db-application-user \
  -H "Content-Type: application/json" \
  -d "${POSTGRES_USER}"
  1. 배포 시 messenger 서비스에서 조회할 수 있는 구성을 정의하는 것 외에도, 이 스크립트는 messenger 서비스용 초기 스크립트인 초기 배포 스크립트 만들기에서 설명한 것과 동일한 두 가지 개념을 보여 줍니다):
    • 배포 스크립트에서 특정 구성을 직접 지정합니다. 이 경우에는 PostgreSQL 데이터베이스에 실행할 포트와 기본 사용자의 사용자 이름을 알려 줍니다.
    • Docker를 -e 플래그와 함께 실행하여 런타임에 컨테이너에 환경 변수를 주입합니다. 또한 실행 중인 컨테이너의 이름을 messenger-db로 설정합니다. 이 이름은 설정의 2단계에서 platform 서비스를 시작할 때 생성한 Docker 네트워크에서 데이터베이스의 호스트 이름이 됩니다.
  2. 실제 배포 환경에서는 일반적으로 플랫폼 팀(또는 이와 유사한 팀)이 platform 리포지토리에서 RabbitMQ와 같은 서비스의 배포 및 유지 관리를 담당합니다. 이는 messenger 리포지토리에서 messenger-database를 관리하는 것과 같습니다. 그런 다음 플랫폼 팀은 해당 인프라의 위치를 이에 의존하는 서비스가 검색할 수 있도록 합니다. 이 튜토리얼에서는 RabbitMQ 값을 직접 설정하세요:
curl -X PUT --silent --output /dev/null --show-error --fail \ -H "Content-Type: application/json" \ -d "rabbitmq" \ http://localhost:8500/v1/kv/amqp-host curl -X PUT --silent --output /dev/null --show-error --fail \ -H "Content-Type: application/json" \ -d "5672" \ http://localhost:8500/v1/kv/amqp-port
  1. (왜 amqp를 사용해 RabbitMQ 변수를 정의하는지 궁금할 수 있습니다. 그 이유는 AMQP가 RabbitMQ에서 사용하는 프로토콜이기 때문입니다.)

배포 스크립트에 시크릿 설정

배포 스크립트에서 누락된 (매우 중요한) 데이터는 단 하나입니다. 바로 messenger 서비스의 messenger-database 비밀번호입니다!

참고: 이 튜토리얼에서는 비밀 정보 관리에 중점을 두지 않으므로, 편의상 비밀 정보를 배포 파일에 정의합니다. 실제 환경(개발, 테스트 또는 프로덕션)에서는 절대로 이렇게 하지 마세요. 심각한 보안 위험을 초래합니다.

올바른 보안 비밀 관리 방법을 알아보려면 Microservices March 2023의 유닛 2: 마이크로서비스 보안 비밀 관리 입문을 확인하세요. (미리 알려드리자면, 보안 비밀을 저장하는 유일하게 안전한 방법은 보안 비밀 관리 도구를 사용하는 것입니다.)

  1. 다음 내용으로 infrastructure/messenger-db-deploy.sh의 내용을 교체하여 messenger-database의 비밀번호 보안 비밀을 Consul KV 저장소에 저장합니다:
#!/bin/bashset -e

PORT=5432
POSTGRES_USER=postgres
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
POSTGRES_PASSWORD=postgres

docker run \
  --rm \
  --name messenger-db-primary \
  -d \
  -v db-data:/var/lib/postgresql/data/pgdata \
  -e POSTGRES_USER="${POSTGRES_USER}" \
  -e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
  -e PGPORT="${PORT}" \
  -e PGDATA=/var/lib/postgresql/data/pgdata \
  --network mm_2023 \
  postgres:15.1

echo "Register key messenger-db-port\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-port \
  -H "Content-Type: application/json" \
  -d "${PORT}"

echo "Register key messenger-db-host\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-host \
  -H "Content-Type: application/json" \
  -d 'messenger-db-primary' # This matches the "--name" flag above
                            # which for our setup means the hostname

echo "Register key messenger-db-application-user\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-application-user \
  -H "Content-Type: application/json" \
  -d "${POSTGRES_USER}"

echo "Register key messenger-db-password-never-do-this\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-password-never-do-this \
  -H "Content-Type: application/json" \
  -d "${POSTGRES_PASSWORD}"

printf "\nDone registering postgres details with Consul\n"
  1. 다음 내용으로 infrastructure/messenger-deploy.sh의 내용을 교체하여 Consul KV 저장소에서 messenger-database 비밀번호 시크릿을 가져옵니다:
#!/bin/bashset -e

# This configuration requires a new commit to change
NODE_ENV=production
PORT=4000
JSON_BODY_LIMIT=100kb

# Postgres database configuration by pulling information from 
# the system
POSTGRES_USER=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-application-user?raw=true)
PGPORT=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-port?raw=true)
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
PGPASSWORD=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-password-never-do-this?raw=true)

# RabbitMQ configuration by pulling from the system
AMQPHOST=$(curl -X GET http://localhost:8500/v1/kv/amqp-host?raw=true)
AMQPPORT=$(curl -X GET http://localhost:8500/v1/kv/amqp-port?raw=true)

docker run \
  --rm \
  -d \
  -e NODE_ENV="${NODE_ENV}" \
  -e PORT="${PORT}" \
  -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
  -e PGUSER="${POSTGRES_USER}" \
  -e PGPORT="${PGPORT}" \
  -e PGHOST="${PGHOST}" \
  -e PGPASSWORD="${PGPASSWORD}" \
  -e AMQPPORT="${AMQPPORT}" \
  -e AMQPHOST="${AMQPHOST}" \
  --network mm_2023 \
  messenger

배포 스크립트 실행

  1. Change to the app 디렉터리로 이동한 후 messenger 리포지토리에서 messenger 서비스용 Docker 이미지를 빌드합니다:
cd ../appdocker build -t messenger .
  1. 다음에 속한 컨테이너만 실행 중인지 확인합니다: platform 서비스
docker ps --format '{{.Names}}'consul-server
consul-client
rabbitmq
  1. Change to the root of the messenger 리포지토리의 루트 디렉터리로 이동하여 messenger-database와 messenger 서비스를 배포합니다:
cd .../infrastructure/messenger-db-deploy.sh ./infrastructure/messenger-deploy.sh
  1. messenger-db-deploy.sh 스크립트는 messenger-database를 시작하고 적절한 정보를 시스템(이 경우 Consul KV 저장소)에 등록합니다. 그런 다음 messenger-deploy.sh 스크립트는 애플리케이션을 시작하고 messenger-db-deploy.sh에서 등록한 구성을 시스템(마찬가지로 Consul KV 저장소)에서 가져옵니다. 힌트: 컨테이너가 시작되지 않으면 배포 스크립트의 docker run 명령에서 두 번째 매개변수(-d \ 줄)를 제거한 후 스크립트를 다시 실행하세요. 그러면 컨테이너가 포그라운드에서 시작되므로 로그가 터미널에 표시되어 문제를 파악하는 데 도움이 될 수 있습니다. 문제를 해결한 후에는 실제 컨테이너가 백그라운드에서 실행되도록 -d \ 줄을 복원하세요.
  2. 배포가 성공했는지 확인하려면 애플리케이션에 간단한 상태 확인 요청을 보냅니다:
curl localhost:4000/healthcurl: (7) Failed to connect to localhost port 4000 after 11 ms: Connection refused
  1. 이런, 실패했습니다! 알고 보니 아직 중요한 설정 하나가 빠져 있어 messenger 서비스가 외부 시스템에 노출되지 않은 상태입니다. 이 서비스는 mm_2023 네트워크 내부에서 정상적으로 실행되고 있지만, 해당 네트워크는 Docker 내부에서만 접근할 수 있습니다.
  2. 다음 과제에서 새 이미지를 생성할 수 있도록 실행 중인 컨테이너를 중지합니다:
docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}"))

과제 3: 외부에 서비스 공개하기

프로덕션 환경에서는 일반적으로 서비스를 직접 외부에 노출하지 않습니다. 대신 일반적인 마이크로서비스 패턴에 따라 기본 서비스 앞에 리버스 프록시 서비스를 배치합니다.

이 챌린지에서는 서비스 검색을 설정하여 messenger 서비스를 외부에 공개합니다. 서비스 검색은 새로운 서비스 정보를 등록하고, 다른 서비스가 해당 정보에 액세스할 때 이를 동적으로 업데이트하는 기능입니다. 이를 위해 다음 기술을 사용합니다:

  • Consul, 동적 서비스 레지스트리와 Consul template, Consul 데이터를 기반으로 파일을 동적으로 업데이트하는 도구
  • NGINX Open Source, 컨테이너에서 실행되는 여러 개별 애플리케이션 인스턴스로 구성될 messenger 서비스에 단일 진입점을 제공하는 리버스 프록시 및 로드 밸런서

서비스 디스커버리에 대해 자세히 알아보려면 블로그의 서비스를 구성으로 제공하기가 수록된 마이크로서비스 앱 구성 모범 사례를 참조하세요.

Consul 설정

The app/consul/index.mjs 파일에는 messenger 저장소의 messenger 서비스를 시작할 때 Consul에 등록하고 정상 종료 시 등록을 해제하는 데 필요한 모든 코드가 포함되어 있습니다. 이 파일은 register라는 함수 하나를 제공하며, 이 함수는 새로 배포된 서비스를 Consul의 서비스 레지스트리에 등록합니다.

  1. 선호하는 텍스트 편집기에서 app/index.mjs 파일을 열고, 다른 import 문 뒤에 다음 코드 조각을 추가하여 register 함수를 app/consul/index.mjs에서 가져옵니다:
import { register as registerConsul } from "./consul/index.mjs";
  1. 그런 다음 그림과 같이 스크립트 끝의 SERVER START 섹션을 수정하여 애플리케이션이 시작된 후 registerConsul()을 호출하도록 합니다:
/* =================  SERVER START
================== */
app.listen(port, async () => {
  console.log(`messenger_service listening on port ${port}`);
  registerConsul();
});

export default app;
  1. Open the convict 스키마를 app/config/config.mjs에서 열고, Example 2의 1단계에서 추가한 jsonBodyLimit 키 뒤에 다음 구성 값을 추가합니다.
  consulServiceName: {    doc: "The name by which the service is registered in Consul. If not specified, the service is not registered",
    format: "*",
    default: null,
    env: "CONSUL_SERVICE_NAME",
  },
  consulHost: {
    doc: "The host where the Consul client runs",
    format: String,
    default: "consul-client",
    env: "CONSUL_HOST",
  },
  consulPort: {
    doc: "The port for the Consul client",
    format: "port",
    default: 8500,
    env: "CONSUL_PORT",
  },
  1. 이렇게 하면 새 서비스가 등록될 이름과 Consul 클라이언트의 호스트 이름 및 포트가 설정됩니다. 다음 단계에서는 messenger 서비스의 배포 스크립트를 수정하여 이 새로운 Consul 연결 및 서비스 등록 정보를 포함합니다.
  2. Open infrastructure/messenger-deploy.sh 파일을 열고 내용을 다음과 같이 바꿔 messenger 서비스 구성에 이전 단계에서 설정한 Consul 연결 및 서비스 등록 정보를 포함하세요:
#!/bin/bashset -e

# This configuration requires a new commit to change
NODE_ENV=production
PORT=4000
JSON_BODY_LIMIT=100kb

CONSUL_SERVICE_NAME="messenger"

# Consul host and port are included in each host since we
# cannot query Consul until we know them
CONSUL_HOST="${CONSUL_HOST}"
CONSUL_PORT="${CONSUL_PORT}"

# Postgres database configuration by pulling information from 
# the system
POSTGRES_USER=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-application-user?raw=true")
PGPORT=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-port?raw=true")
PGHOST=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-host?raw=true")
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
PGPASSWORD=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-password-never-do-this?raw=true")

# RabbitMQ configuration by pulling from the system
AMQPHOST=$(curl -X GET "http://localhost:8500/v1/kv/amqp-host?raw=true")
AMQPPORT=$(curl -X GET "http://localhost:8500/v1/kv/amqp-port?raw=true")

docker run \
  --rm \
  -d \
  -e NODE_ENV="${NODE_ENV}" \
  -e PORT="${PORT}" \
  -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
  -e PGUSER="${POSTGRES_USER}" \
  -e PGPORT="${PGPORT}" \
  -e PGHOST="${PGHOST}" \
  -e PGPASSWORD="${PGPASSWORD}" \
  -e AMQPPORT="${AMQPPORT}" \
  -e AMQPHOST="${AMQPHOST}" \
  -e CONSUL_HOST="${CONSUL_HOST}" \
  -e CONSUL_PORT="${CONSUL_PORT}" \
  -e CONSUL_SERVICE_NAME="${CONSUL_SERVICE_NAME}" \
  --network mm_2023 \
  messenger
  1. 주요 참고 사항은 다음과 같습니다. 참고: 실제 배포 환경에서는 팀 간에 합의해야 하는 구성의 예입니다. Consul 담당 팀은 서비스가 이 연결 정보 없이는 Consul을 조회할 수 없으므로 모든 환경에서 CONSUL_HOST 및 CONSUL_PORT 환경 변수를 제공해야 합니다.
    • The CONSUL_SERVICE_NAME 환경 변수는 messenger 서비스 인스턴스가 Consul에 자신을 등록할 때 사용할 이름을 지정합니다.
    • The CONSUL_HOST 및 CONSUL_PORT 환경 변수는 배포 스크립트가 실행되는 위치에서 실행 중인 Consul 클라이언트에 사용됩니다.
  2. 앱 터미널에서 app 디렉터리로 이동하고, 실행 중인 messenger 서비스 인스턴스를 모두 중지한 다음, 새 서비스 등록 코드가 포함되도록 Docker 이미지를 다시 빌드합니다:
cd appdocker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}")) docker build -t messenger .
  1. 브라우저에서 http://localhost:8500(으)로 이동하여 Consul UI가 작동하는 모습을 확인합니다(아직은 별다른 동작이 없습니다).
  2. At the root of the messenger 리포지토리의 루트에서 배포 스크립트를 실행하여 messenger 서비스 인스턴스를 시작합니다:
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
  1. 브라우저의 Consul UI에서 헤더 표시줄의 서비스를 클릭하여 단일 messenger 서비스가 실행 중인지 확인합니다.
    consul 및 messenger 서비스의 인스턴스가 각각 하나씩 표시된 Consul UI의 Services 탭
  2. 배포 스크립트를 몇 차례 더 실행하여 messenger 서비스 인스턴스를 추가로 시작합니다. Consul UI에서 해당 인스턴스가 실행 중인지 확인합니다.
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
  1. 서비스 인스턴스 5개가 표시된 Consul UI의 'messenger' 탭

NGINX 설정

다음 단계는 NGINX Open Source를 리버스 프록시 및 로드 밸런서로 추가하여 들어오는 트래픽을 실행 중인 모든 messenger 인스턴스로 라우팅하는 것입니다.

  1. 앱 터미널에서 messenger 리포지토리의 루트 디렉터리로 이동한 다음, load-balancer라는 디렉터리와 다음 세 개의 파일을 만듭니다:
mkdir load-balancercd load-balancer touch nginx.ctmpl touch consul-template-config.hcl touch Dockerfile
  1. Dockerfile은 NGINX와 Consul Template이 실행되는 컨테이너를 정의합니다. Consul Template은 서비스 레지스트리에서 messenger 서비스가 변경될 때(서비스 인스턴스가 시작되거나 종료될 때) 다른 두 파일을 사용하여 NGINX 업스트림을 동적으로 업데이트합니다.
  2. 1단계에서 생성한 nginx.ctmpl 파일을 열고, Consul 템플릿이 NGINX 업스트림 그룹을 동적으로 업데이트하는 데 사용하는 다음 NGINX 구성 코드 조각을 추가합니다:
upstream messenger_service {    {{- range service "messenger" }}
    server {{ .Address }}:{{ .Port }};
    {{- end }}
}

server {
    listen 8085;
    server_name localhost;

    location / {
        proxy_pass http://messenger_service;
        add_header Upstream-Host $upstream_addr;
    }
}
  1. 이 코드 조각은 Consul에 등록된 각 messenger 서비스 인스턴스의 IP 주소와 포트 번호를 NGINX messenger_service 업스트림 그룹에 추가합니다. NGINX는 들어오는 요청을 동적으로 정의된 업스트림 서비스 인스턴스 집합으로 프록시합니다.
  2. 1단계에서 만든 consul-template-config.hcl 파일을 열고 다음 구성을 추가합니다:
consul {  address = "consul-client:8500"

  retry {
    enabled  = true
    attempts = 12
    backoff  = "250ms"
  }
}
template {
  source      = "/usr/templates/nginx.ctmpl"
  destination = "/etc/nginx/conf.d/default.conf"
  perms       = 0600
  command     = "if [ -e /var/run/nginx.pid ]; then nginx -s reload; else nginx; fi"
}
  1. 이 Consul 템플릿 구성은 source 템플릿(이전 단계에서 생성한 NGINX 구성 스니펫)을 다시 렌더링하여 지정된 destination에 배치한 다음, 마지막으로 지정된 command(NGINX가 구성을 다시 로드하도록 지시하는 명령)를 실행하도록 설정합니다. 실제로는 Consul에서 서비스 인스턴스가 등록, 업데이트 또는 등록 해제될 때마다 새로운 default.conf 파일이 생성됩니다. 그러면 NGINX는 중단 없이 구성을 다시 로드하여 트래픽을 전송할 수 있는 최신 상태의 정상 서버 집합(messenger 서비스 인스턴스)을 유지합니다.
  2. 1단계에서 만든 Dockerfile 파일을 열고 NGINX 서비스를 빌드하는 다음 내용을 추가합니다. (이 튜토리얼을 진행하기 위해 Dockerfile의 내용을 이해할 필요는 없지만, 편의를 위해 코드에 설명을 주석으로 달아 두었습니다.)
FROM nginx:1.23.1
ARG CONSUL_TEMPLATE_VERSION=0.30.0

# Set an environment variable for the location of the Consul
# cluster. By default, it tries to resolve to consul-client:8500
# which is the behavior if Consul is running as a container in the 
# same host and linked to this NGINX container (with the alias 
# consul, of course). But this environment variable can also be
# overridden as the container starts if we want to resolve to
# another address.

ENV CONSUL_URL consul-client:8500

# Download the specified version of Consul template
ADD https://releases.hashicorp.com/consul-template/${CONSUL_TEMPLATE_VERSION}/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip /tmp

RUN apt-get update \
  && apt-get install -y --no-install-recommends dumb-init unzip \
  && unzip /tmp/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip -d /usr/local/bin \
  && rm -rf /tmp/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip

COPY consul-template-config.hcl ./consul-template-config.hcl
COPY nginx.ctmpl /usr/templates/nginx.ctmpl

EXPOSE 8085

STOPSIGNAL SIGQUIT

CMD ["dumb-init", "consul-template", "-config=consul-template-config.hcl"]
  1. Docker 이미지를 빌드합니다:
docker build -t messenger-lb .
  1. Change to the root of the messenger 디렉터리의 루트로 이동한 후, messenger-load-balancer-deploy.sh라는 파일을 만들어 NGINX 서비스의 배포 파일로 사용합니다(이 튜토리얼에서 지금까지 배포한 다른 서비스와 동일한 방식입니다). 환경에 따라 chmod 명령 앞에 sudo를 붙여야 할 수도 있습니다:
cd .. touch infrastructure/messenger-load-balancer-deploy.sh chmod +x infrastructure/messenger-load-balancer-deploy.sh
  1. Open messenger-load-balancer-deploy.sh 파일을 열고 다음 내용을 추가합니다:
#!/bin/bashset -e

# Consul host and port are included in each host since we
# cannot query Consul until we know them
CONSUL_HOST="${CONSUL_HOST}"
CONSUL_PORT="${CONSUL_PORT}"

docker run \
  --rm \
  -d \
  --name messenger-lb \
  -e CONSUL_URL="${CONSUL_HOST}:${CONSUL_PORT}"  \
  -p 8085:8085 \
  --network mm_2023 \
  messenger-lb
  1. 이제 모든 준비가 완료되었으므로 NGINX 서비스를 배포합니다:
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-load-balancer-deploy.sh
  1. 외부에서 messenger 서비스에 액세스할 수 있는지 확인합니다:
curl -X GET http://localhost:8085/health
OK
  1. 작동합니다! 이제 NGINX가 생성된 모든 messenger 서비스 인스턴스에 걸쳐 부하를 분산합니다. X-Forwarded-For 헤더에 이전 섹션의 8단계에 나온 Consul UI와 동일한 messenger 서비스 IP 주소가 표시되는 것을 보면 알 수 있습니다.

과제 4: 서비스를 작업 실행기로 사용하여 데이터베이스 마이그레이션하기

대규모 애플리케이션에서는 데이터를 수정하는 것과 같은 일회성 작업을 수행할 수 있는 소규모 워커 프로세스로 구성된 ‘작업 실행기’를 흔히 사용합니다(예: Sidekiq 및 Celery). 이러한 도구에는 Redis 또는 RabbitMQ와 같은 추가 지원 인프라가 필요한 경우가 많습니다. 여기서는 messenger 서비스 자체를 ‘작업 실행기’로 사용하여 일회성 작업을 실행합니다. 이 서비스는 이미 매우 작고, 데이터베이스 및 자신이 의존하는 기타 인프라 구성 요소와 충분히 상호 작용할 수 있으며, 트래픽을 처리하는 애플리케이션과 완전히 분리되어 실행되므로 이러한 방식이 적합합니다.

이렇게 하면 세 가지 이점이 있습니다:

  1. 작업 실행기(실행되는 스크립트 포함)는 프로덕션 서비스와 정확히 동일한 검사 및 검토 절차를 거칩니다.
  2. 데이터베이스 사용자와 같은 구성 값은 프로덕션 배포의 보안을 강화하도록 쉽게 변경할 수 있습니다. 예를 들어, 기존 테이블에 데이터를 쓰고 쿼리만 실행할 수 있는 ‘낮은 권한’의 사용자로 프로덕션 서비스를 실행할 수 있습니다. 테이블을 생성하고 삭제할 수 있는 더 높은 권한의 사용자로 데이터베이스 구조를 변경하도록 별도의 서비스 인스턴스를 구성할 수도 있습니다.
  3. 일부 팀은 서비스의 프로덕션 트래픽도 처리하는 인스턴스에서 작업을 실행합니다. 이는 작업에 문제가 생기면 컨테이너 내 애플리케이션이 수행하는 다른 기능에도 영향을 줄 수 있어 위험합니다. 애초에 이런 상황을 피하려고 마이크로서비스를 도입한 것 아닌가요?

이 챌린지에서는 일부 데이터베이스 구성 값을 변경하여 아티팩트가 새로운 역할을 수행하도록 수정하는 방법을 살펴보고, 새 값을 사용하도록 messenger 데이터베이스를 마이그레이션한 후 성능을 테스트합니다.

messenger 데이터베이스 마이그레이션

실제 프로덕션 환경에 배포할 때는 서로 다른 권한을 가진 두 명의 사용자, 즉 ‘애플리케이션 사용자’와 ‘마이그레이션 사용자’를 생성할 수 있습니다. 이 예제에서는 편의상 기본 사용자를 애플리케이션 사용자로 사용하고, 슈퍼유저 권한을 가진 마이그레이션 사용자를 생성합니다. 실제 환경에서는 각 사용자의 역할에 따라 구체적으로 어떤 최소 권한이 필요한지 충분한 시간을 들여 결정하는 것이 좋습니다.

  1. 앱 터미널에서 슈퍼유저 권한을 가진 새 PostgreSQL 사용자를 생성합니다:
echo "CREATE USER messenger_migrator WITH SUPERUSER PASSWORD 'migrator_password';" | docker exec -i messenger-db-primary psql -U postgres
  1. 데이터베이스 배포 스크립트(infrastructure/messenger-db-deploy.sh)를 열고 새 사용자의 자격 증명을 추가하도록 내용을 바꿉니다. 참고: 다시 한번 강조하지만, 실제 환경에 배포할 때는 절대로 데이터베이스 자격 증명과 같은 비밀 정보를 배포 스크립트나 비밀 관리 도구 이외의 위치에 저장해서는 안 됩니다. 자세한 내용은 Microservices March 2023의 유닛 2: 마이크로서비스 비밀 관리 101을 참조하세요.
#!/bin/bash
set -e

PORT=5432
POSTGRES_USER=postgres
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
# Because we’re focusing on other concepts in this tutorial, we
# set the password this way here for convenience.
POSTGRES_PASSWORD=postgres

# Migration user
POSTGRES_MIGRATOR_USER=messenger_migrator
# NOTE: As above, never do this in a real deployment.
POSTGRES_MIGRATOR_PASSWORD=migrator_password

docker run \
  --rm \
  --name messenger-db-primary \
  -d \
  -v db-data:/var/lib/postgresql/data/pgdata \
  -e POSTGRES_USER="${POSTGRES_USER}" \
  -e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
  -e PGPORT="${PORT}" \
  -e PGDATA=/var/lib/postgresql/data/pgdata \
  --network mm_2023 \
  postgres:15.1

echo "Register key messenger-db-port\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-port \
  -H "Content-Type: application/json" \
  -d "${PORT}"

echo "Register key messenger-db-host\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-host \
  -H "Content-Type: application/json" \
  -d 'messenger-db-primary' # This matches the "--name" flag above
                            # which for our setup means the hostname

echo "Register key messenger-db-application-user\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-application-user \
  -H "Content-Type: application/json" \
  -d "${POSTGRES_USER}"

curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-password-never-do-this \
  -H "Content-Type: application/json" \
  -d "${POSTGRES_PASSWORD}"

echo "Register key messenger-db-application-user\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-migrator-user \
  -H "Content-Type: application/json" \
  -d "${POSTGRES_MIGRATOR_USER}"

curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-migrator-password-never-do-this \
  -H "Content-Type: application/json" \
  -d "${POSTGRES_MIGRATOR_PASSWORD}"

printf "\nDone registering postgres details with Consul\n"
  1. 이 변경 사항은 데이터베이스가 배포된 후 Consul에 설정되는 사용자 목록에 마이그레이션 사용자를 추가할 뿐입니다.
  2. Create a new file in the infrastructure 디렉터리에 messenger-db-migrator-deploy.sh라는 새 파일을 만듭니다(이 경우에도 chmod 명령 앞에 sudo를 붙여야 할 수 있습니다):
touch infrastructure/messenger-db-migrator-deploy.shchmod +x infrastructure/messenger-db-migrator-deploy.sh
  1. Open messenger-db-migrator-deploy.sh 파일을 열고 다음 내용을 추가합니다:
#!/bin/bashset -e

# This configuration requires a new commit to change
NODE_ENV=production
PORT=4000
JSON_BODY_LIMIT=100kb

CONSUL_SERVICE_NAME="messenger-migrator"

# Consul host and port are included in each host since we
# cannot query Consul until we know them
CONSUL_HOST="${CONSUL_HOST}"
CONSUL_PORT="${CONSUL_PORT}"

# Get the migrator user name and password
POSTGRES_USER=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-migrator-user?raw=true")
PGPORT=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-port?raw=true")
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
PGPASSWORD=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-migrator-password-never-do-this?raw=true")

# RabbitMQ configuration by pulling from the system
AMQPHOST=$(curl -X GET "http://localhost:8500/v1/kv/amqp-host?raw=true")
AMQPPORT=$(curl -X GET "http://localhost:8500/v1/kv/amqp-port?raw=true")

docker run \--rm \
  -d \
  --name messenger-migrator \
  -e NODE_ENV="${NODE_ENV}" \
  -e PORT="${PORT}" \
  -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
  -e PGUSER="${POSTGRES_USER}" \
  -e PGPORT="${PGPORT}" \
  -e PGHOST="${PGHOST}" \
  -e PGPASSWORD="${PGPASSWORD}" \
  -e AMQPPORT="${AMQPPORT}" \
  -e AMQPHOST="${AMQPHOST}" \
  -e CONSUL_HOST="${CONSUL_HOST}" \
  -e CONSUL_PORT="${CONSUL_PORT}" \
  -e CONSUL_SERVICE_NAME="${CONSUL_SERVICE_NAME}" \
  --network mm_2023 \
  messenger
  1. 이 스크립트는 infrastructure/messenger-deploy.sh 스크립트의 최종 형태와 매우 유사합니다. 이 스크립트는 3단계의 Consul 설정에서 작성했습니다. 주요 차이점은 CONSUL_SERVICE_NAME이 messenger-migrator로 설정되어 messenger가 아니라는 점과, PGUSER가 위의 1단계에서 생성한 “migrator” 슈퍼유저에 해당한다는 점입니다. CONSUL_SERVICE_NAME을 messenger-migrator로 설정하는 것이 중요합니다. 이 값이 messenger로 설정되면 NGINX가 API 호출을 수신하도록 이 서비스를 자동으로 로테이션에 포함하지만, 이 서비스는 트래픽을 처리하기 위한 것이 아닙니다. 이 스크립트는 마이그레이터 역할을 하는 단기 실행 인스턴스를 배포합니다. 이렇게 하면 마이그레이션 관련 문제가 기본 messenger 서비스 인스턴스의 트래픽 처리에 영향을 미치는 것을 방지할 수 있습니다.
  2. PostgreSQL 데이터베이스를 다시 배포합니다. 이 튜토리얼에서는 bash 스크립트를 사용하므로 데이터베이스 서비스를 중지했다가 다시 시작해야 합니다. 프로덕션 애플리케이션에서는 일반적으로 변경된 요소만 추가하도록 코드형 인프라 스크립트를 대신 실행합니다.
docker stop messenger-db-primaryCONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-deploy.sh
  1. PostgreSQL 데이터베이스 마이그레이터 서비스를 배포합니다:
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-migrator-deploy.sh
  1. 인스턴스가 예상대로 실행 중인지 확인합니다:
docker ps --format "{{.Names}}"
...
messenger-migrator
  1. Consul UI에서 데이터베이스 마이그레이터 서비스가 Consul에 messenger-migrator로 올바르게 등록되었는지도 확인할 수 있습니다(이 서비스는 트래픽을 처리하지 않으므로, 앞서 설명한 것처럼 messenger라는 이름으로 등록되지 않습니다):
    Consul UI의 Services 탭에 consul 및 messenger-migrator 서비스 인스턴스가 각각 1개씩, messenger 서비스 인스턴스가 4개 표시되어 있음
  2. 이제 마지막 단계로 데이터베이스 마이그레이션 스크립트를 실행합니다! 이 스크립트는 실제 데이터베이스 마이그레이션 스크립트와는 다르지만, messenger-migrator 서비스를 사용하여 데이터베이스별 스크립트를 실행합니다. 데이터베이스 마이그레이션이 완료되면 messenger-migrator 서비스를 중지합니다:
docker exec -i -e PGDATABASE=postgres -e CREATE_DB_NAME=messenger messenger-migrator node scripts/create-db.mjsdocker exec -i messenger-migrator node scripts/create-schema.mjs docker exec -i messenger-migrator node scripts/create-seed-data.mjs docker stop messenger-migrator

messenger 서비스가 실제로 작동하는지 테스트하기

이제 messenger 데이터베이스를 최종 형식으로 마이그레이션했으므로 messenger 서비스가 실제로 작동하는 모습을 확인할 준비가 되었습니다! 이를 위해 NGINX 서비스에 몇 가지 기본적인 curl 쿼리를 실행합니다(NGINX는 NGINX 설정에서 시스템의 진입점으로 구성했습니다).

다음 명령어 중 일부는 JSON 출력을 보기 좋게 정리하기 위해 jq 라이브러리를 사용합니다. 필요한 경우 설치하거나 원한다면 명령줄에서 생략해도 됩니다.

  1. 대화를 생성합니다:
curl -d '{"participant_ids": [1, 2]}' -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations'{
  "conversation": { "id": "1", "inserted_at": "YYYY-MM-DDT06:41:59.000Z" }
}
  1. ID가 1인 사용자가 대화에 메시지를 보내도록 합니다:
curl -d '{"content": "This is the first message"}' -H "User-Id: 1" -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations/1/messages' | jq{
  "message": {
    "id": "1",
    "content": "This is the first message",
    "index": 1,
    "user_id": 1,
    "username": "James Blanderphone",
    "conversation_id": 1,
    "inserted_at": "YYYY-MM-DDT06:42:15.000Z"
  }
}
  1. 다른 사용자(ID 2)의 메시지로 답장합니다:
curl -d '{"content": "This is the second message"}' -H "User-Id: 2" -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations/1/messages' | jq{
  "message": {
    "id": "2",
    "content": "This is the second message",
    "index": 2,
    "user_id": 2,
    "username": "Normalavian Ropetoter",
    "conversation_id": 1,
    "inserted_at": "YYYY-MM-DDT06:42:25.000Z"
  }
}
  1. 메시지를 가져옵니다:
curl -X GET 'http://localhost:8085/conversations/1/messages' | jq{
  "messages": [
    {
      "id": "1",
      "content": "This is the first message",
      "user_id": "1",
      "channel_id": "1",
      "index": "1",
      "inserted_at": "YYYY-MM-DDT06:42:15.000Z",
      "username": "James Blanderphone"
    },
    {
      "id": "2",
      "content": "This is the second message",
      "user_id": "2",
      "channel_id": "1",
      "index": "2",
      "inserted_at": "YYYY-MM-DDT06:42:25.000Z",
      "username": "Normalavian Ropetoter"
    }
  ]
}

정리

이 튜토리얼을 진행하면서 상당히 많은 컨테이너와 이미지를 생성했습니다! 다음 명령어를 사용하여 보관할 필요가 없는 Docker 컨테이너와 이미지를 모두 삭제하세요.

  • 실행 중인 모든 Docker 컨테이너를 제거하려면:
docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}"))docker rm $(docker stop messenger-db-primary) docker rm $(docker stop messenger-lb)
  • To remove the 플랫폼 서비스를 제거하려면:
# From the platform repositorydocker compose down
  • 튜토리얼 전체에서 사용한 모든 Docker 이미지를 삭제하려면 다음을 실행하세요:
docker rmi messengerdocker rmi messenger-lb docker rmi postgres:15.1 docker rmi hashicorp/consul:1.14.4 docker rmi rabbitmq:3.11.4-management-alpine

다음 단계

“간단한 것을 설정하는 데 일이 너무 많은 것 같은데”라고 생각하실 수도 있습니다. 맞는 생각입니다! 마이크로서비스 중심 아키텍처로 전환하려면 서비스를 구조화하고 구성하는 방식에 세심한 주의를 기울여야 합니다. 이 모든 복잡성에도 불구하고, 여러분은 상당한 진전을 이루었습니다:

  • 다른 팀에서도 쉽게 이해할 수 있는 마이크로서비스 중심의 구성을 설정했습니다.
  • 마이크로서비스 시스템을 확장성과 관련된 다양한 서비스의 활용 측면에서 어느 정도 유연하게 구성합니다.

마이크로서비스에 대해 계속 학습하려면 Microservices March 2023을 확인해 보세요. 유닛 2: 마이크로서비스 보안 비밀 관리 입문에서는 마이크로서비스 환경의 보안 비밀 관리를 심층적이면서도 이해하기 쉽게 설명합니다.


공유

관련 블로그 게시물

시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric
NGINX | 2023. 11. 30.

시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric

이 영상에서는 KubeCon의 NGINX 부스를 찾아가, 저희가 공개한 Kubernetes Gateway API 구현을 살펴봅니다.

F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법
NGINX | 2023. 03. 23.

F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법

앱 보안을 강화하는 두 가지 방법을 소개합니다. NGINX Management Suite를 통해 WAF 운영에 대한 가시성을 높이고, NGINX App Protect WAF를 활용해 코드형 보안을 구현하는 것입니다.

Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션
NGINX | 2021. 09. 27.

Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션

개발 주체, 개발 철학, 프로덕션 환경 준비도, 보안 및 지원을 기준으로 어떤 NGINX Ingress 컨트롤러가 가장 적합한지 알아보세요.

진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법
NGINX | 2021. 07. 06.

진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법

NGINX App Protect Denial of Service(DoS)가 계층 7 DoS 공격을 방어하기 위해 완화 방법을 실시간으로 조정하는 방식을 설명합니다. 정상적인 사용자 행동과 서버 상태에 대한 모델을 생성하여 공격을 나타내는 이상 징후를 감지하고 맞춤형 완화 시그니처를 적용할 수 있습니다.

NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리
NGINX | 2021. 06. 30.

NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리

NGINX Instance Manager 버전 1.0에는 두 가지 새로운 기능이 도입되었습니다. NGINX 인스턴스에 태그와 사용자 역할을 지정하면 전체 그룹에 구성을 한 번에 적용할 수 있습니다. 인증서 검사는 만료 예정인 인증서를 감지하여 서비스 중단을 방지할 수 있도록 인증서를 교체할 수 있게 해줍니다.

Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유
NGINX | 2015. 03. 25.

Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유

Netflix가 글로벌 콘텐츠 전송 네트워크인 Open Connect의 스트리밍 미디어 기술로 NGINX를 선택한 이유를 NGINX 개발자 Gleb Smirnoff에게서 알아보세요.