들어가며

근접성 서비스(Proximity Service)란 특정 위치를 기반으로 가까운 시설을 찾는 서비스를 의미합니다. 예를 들어 주변 맛집 찾기 서비스를 설계한다고 할 때, 2차원 형태(위도, 경도)로 표현되는 공간 데이터를 빠르게 검색하는 기술에 대한 이해를 바탕으로 아키텍쳐를 설계할 필요가 있습니다. 공간 데이터를 검색하는 기술들을 설펴보고, 그 중 지오해시(Geohash)를 기반으로 한 아키텍처 설계 방법에 대해 소개하겠습니다.

 

 

공간 데이터 검색 기술

다음과 같이 특정 위치와 반경을 기반으로 주변의 시설들을 검색하는 방법들에 대해 살펴보겠습니다.

 

1) 가장 단순한 공간 데이터 검색 : 2차원 검색

가장 단순한 방법입니다. 주어진 반경값을 토대로 검색해야 하는 위도와 경도의 범위를 계산하여 검색에 활용하는 방입니다. 그러나 별도의 인덱스 설정이 없으면 테이블 전체를 풀 스캔해야 하는 단점이 있습니다. 위도와 경도에 인덱스를 달아둔다고 해도, 각 인덱스를 통해 얻어낸 두 집합의 교집합을 구해야 하는 데서 많은 비용이 발생합니다. 이런 문제들은 기본적으론 DB 인덱스가 한 차원의 검색 속도를 개선시키는 것에서 비롯되므로, 2차원의 공간 데이터에 대한 인덱스 생성 방법을 고려할 필요가 있습니다.

 

공간 데이터에 대해 인덱스를 만드는 방법은 다음과 같이 분류 가능합니다.

 

  • Hash 기반
    1. 균등 격자 (even grid)
    2. 지오해시 (Geohash)
    3. 카르테시안 계층 (Cartesian tiers)
  • 트리 기반
    1. 쿼드트리 (Quadtree)
    2. 구글 S2
    3. R-tree

 

각 기술들의 세부 구현 방법은 서로 다르지만, 기본적으로는

 

  1. 공간을 여러 작은 영역들로 분할
  2. 공간 데이터들을 분할된 영역들로 매핑 (2차원 데이터가 1차원 데이터로 매핑되는 효과)
  3. "분할된 영역"들을 빠르게 검색 

 

하는 형태로 인덱스가 활용된다고 보면 됩니다. 여기서는 균등 격자와 지오 해시, 쿼드 트리 및 R-tree에 대해 소개하겠습니다.

 

 

2) Hash 기반 공간 데이터 검색 : 균등 격자

2차원 공간을 다음과 같이 균일한 크기를 가진 격자로 나누는 접근법입니다.

 

균등 격자 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)

 

각 격자들에 1부터 시작하는 식별자 번호를 할당할 수 있고, 공간 데이터들을 각 격자들로 매핑시켜 활용할 수 있게 됩니다. 그러나 뒤에서 소개할 다른 방법들과는 달리 격자 식별자를 할당하는 방법에 명확한 체계가 있는 방식은 아니기 때문에, 특정 격자 주변에 있는 격자를 찾는 것이 어렵다는 단점이 있습니다. 우리의 목적인 "특정 위치와 반경을 기반으로 주변의 시설을 검색하기"를 위해서는 인접한 격자를 쉽게 찾을 수 있어야 하지만 그 부분에 제약이 걸리는 것으로 볼 수 있습니다. 또한 각 격자별로 갖는 공간 데이터들의 분포가 균등하지 않다는 단점도 있습니다.

 

3) Hash 기반 공간 데이터 검색 : 지오해시

균등 격자와 비슷하게 2차원 공간을 균일한 크기를 가진 격자로 나누고 격자들에 식별자를 할당합니다. 다만 식별자 할당에 체계가 있다는 차이점이 있습니다. 

 

지오해시 개념 (가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서 발췌)

 

2차원 공간을 그림처럼 4개로 나눈 후, 위도 경도의 범위를 기준으로 4개 영역에 비트값을 할당합니다. 다시 각 격자를 4개로 나눈 후, 각 영역에 비트값을 추가하는 과정을 원하는 정밀도(즉 격자의 크기)가 나올 때까지 반복합니다. 이렇게 나온 비트값을 base 32 표현법으로 인코딩해 나온 문자열을 지오해시라고 부릅니다. 

 

ex) 비트값이 "1001 11010 01001 10001 11111 11110" 이면 지오해시는 "9q9hvu"

 

지오해시의 길이가 길수록 정밀도가 높은 것(사이즈가 작은 격자들로 분할된 것)을 의미하며, 지오해시의 길이에 따라 총 12 레벨로 분류할 수 있습니다. 길이 1짜리인 1레벨은 지구 전체에 해당(즉 격자 1개)하고 길이 12짜리인 12레벨은 격자 하나의 사이즈가 3.7cm X 1.9cm입니다. 일반적인 경우 4 ~ 6레벨을 사용하며, 사용자가 지정한 반경으로 그린 원을 덮는 최소 크기 격자를 만드는 지오해시 길이를 최적 정밀도로 계산할 수 있습니다.

 

ex) 지정한 반경이 1km일 때, 지오해시 레벨이 5인 격자의 크기가 4.9km X 4.9km으로 지정된 반경으로 그린 원을 덮을 수 있는 최소 크기 격자가 됩니다. 레벨 6짜리 격자의 크기는 1.2km X 0.6km입니다. 

 

앞서 살펴봤듯이, 지오해시는 격자를 4개로 나눈 후 원래 가지던 비트값 뒤에 01, 11, 00, 10을 추가하는 식으로 식별자 할당이 진행됩니다. 따라서 이 비트값들을 base 32로 인코딩한 지오해시값들은 비슷할 수밖에 없고, 이를 통해 "공통접두어가 긴 격자들은 인접해있다"를 도출할 수 있습니다. 또한 뒤에 붙어있는 비트값을 떼주면, 해당 격자를 포함하는 좀 더 넓은 격자로 범위를 확장시킬 수 있습니다. 따라서 지오해시는 이 점들을 활용해 범위 기반 지역 검색을 쉽게 할 수 있게 됩니다. 

 

 

다만 하나의 격자만 볼 때는 균등 격자와 마찬가지로 각 격자별로 갖는 공간 데이터들의 분포가 균등하지 않다는 단점은 남아있으며, 공통접두어가 긴 격자들은 인접하다라는 명제는 성립하나 "인접한 격자들은 공통접두어가 같다"는 성립하지 않음에 유의해야 합니다. 이유는 다음 그림과 같이 인접한 격자임에도 비트값들은 서로 완전히 다를 수 있기 때문입니다.

 

파란색은 아예 비트 구성이 다르다.

 

 

4) 트리 기반 공간 데이터 검색 : 쿼드트리

지오해시는 격자들에 할당된 식별자들을 DB에 저장해 가지는 방식이었다면, 쿼드트리는 격자를 재귀적으로 4개로 쪼개가며 만든 트리를 메모리에 둔 채로 활용하는 방식입니다. 구체적으로는 트리의 리프노드들에 담긴 공간 데이터가 내가 원하는 수(k) 이하가 될 때까지 분할하며, 리프노드에는 해당 격자에 포함된 공간 데이터의 정보들을 갖도록 구성합니다.

 

쿼드트리 개념 (https://pigbrain.github.io/datastructure/2016/01/01/QuadTree_on_DataStructure)

 

 

모든 격자를 균일한 사이즈로 해야했던 지오해시와는 달리, 쿼드트리는 맘만 먹으면 원하는 영역에 대해 세밀하게 격자를 분할하는 것이 쉽습니다. 또한 트리 구성 시 특정 숫자(k)를 기준으로 만들어 나가는 점을 활용해 "현재 내 위치에서 가까운 공간 데이터 x개 찾기"를 쉽게 할 수 있게 됩니다. 하지만 트리 구조인 만큼 공간 데이터 추가 / 삭제로 인한 인덱스 변경이 좀 더 까다롭고, 서버 시작 시 트리를 구축해서 메모리에 둬야 하기 때문에 서버 시작 시간이 길어질 수 있음을 유의해야 합니다.

 

 

5) 트리 기반 공간 데이터 검색 : R-tree

asd

ㅁㄴㅇ

 

 

설계

위에서 살펴본 기술 중, 지오해시를 사용할 때의 일반적인 설계 방법에 대해 살펴보겠습니다. 특정 위치와 반경을 기준으로 주변 시설을 조회하는 기능과 특정 시설의 상세 정보를 제공하는 서비스로, 다음과 같은 기능적 요구사항이 있다고 가정합니다.

 

  1. 사용자의 경도와 위도, 반경에 매치되는 시설 목록을 반환해야 함
  2. 시설 정보가 추가/갱신/삭제될 수 있으나 실시간으로 반영될 필요는 없음
  3. 시설의 상세 정보를 조회할 수 있어야 함

 

그리고 주변 시설을 빠르게 검색 가능해야 하고 트래픽이 급증해도 감당할 수 있어야 한다는 비기능적 요구사항이 있다고 하겠습니다. 또한  1초에 5,000번 정도의 검색이 발생하는 읽기 연산 위주의 시스템으로 가정하며, 다음과 같은 API들이 있다고 하겠습니다.

 

Method API 설명
GET /v1/search/nearby 인자로 위도, 경도, 반경을 받은 뒤 검색 기준에 맞는 사업장 목록 반환
GET /v1/businesses/:id 특정 사업장의 상세 정보 반환
POST /v1/businesses 신규 사업장 추가
PUT /v1/businesses/:id 사업장 상세 정보 갱신
DELETE /v1/businesses/:id 특정 사업장 정보 삭제

 

 

1) 서버  및 로드밸런서 설계

해당 시스템을 위치 기반으로 시설들을 검색하는 서비스(LBS)와 시설 서비스(시설 상세조회, 등록 등을 담당)로 구분할 수 있습니다. 사용자가 보내는 요청들을 각각의 알맞는 서비스로 전달하기 위해 로드밸런서에서 URL 경로를 분석해 다음과 같이 설정해줄 수 있습니다.

 

 

또한 LBS와 시설 서비스는 이전 요청의 상태나 데이터를 유지하지 않는 무상태 서비스이므로 확장성이 좋습니다. 따라서 트래픽이 몰릴 때는 서버를 추가하여 대응하고, 트래픽이 줄어들면 서버를 삭제하도록 유연하게 구성할 수 있습니다. 또한, 클라우드에 시스템을 띄운다면 여러 지역 또는 여러 가용 영역에 둠으로써 고가용성도 확보할 수 있겠습니다. 여러 지역에 둘 경우 DNS 라우팅을 통해 사용자와 가까운 서버에서 트래픽을 처리하도록 하여 응답 시간을 줄이는 효과 등을 기대할 수 있으며, 한 지역 안에서도 여러 가용 영역을 활용한 부하 분산을 기대할 수 있습니다.

 

2) 데이터베이스 설계

이 시스템은 앞서 가정한대로 읽기 연산이 많이 발생하는 특징이 있습니다. 따라서 데이터베이스를 master-slave 형태로 구성하여 master가 쓰기 요청을, slave가 읽기 연산을 처리하도록 구성하여 부하를 어느 정도 분산시키도록 구성할 수 있습니다.

 

그리고 테이블을 위치 정보 관련 연산의 효율을 위해 시설의 상세 정보를 담는 테이블(business 테이블)과 공간 인덱스 테이블(지오해시값과 시설id만 가지는)로 분리할 수 있습니다.

먼저 business 테이블의 경우 시설 데이터가 방대하다면 한 서버에 담기지 못할 수 있으므로, 샤딩을 통해 수평적으로 분리 확장시킬 수 있겠습니다.

공간 인덱스 테이블은 서버 한 대에 충분히 수용 가능할 것으로 예상되나, 읽기 연산이 많은 경우 가용한 리소스(CPU, 네트워크 대역폭 등)의 한계가 있으므로 부하 분산이 필요할 수 있는데요. 보통 부하 분산이 필요한 경우 Replica를 늘리는 것과 샤딩을 고려할 수 있으나,  지오해시는 격자별 공간 데이터 분포가 고르지 않아 샤딩이 까다롭고 공간 인덱스 테이블은 서버 한 대에 충분히 수용 가능할 것으로 예상되므로 Replica를 활용하는 것이 더 나은 방안이 될 수 있습니다.

 

3) 캐시 설계

다중 서버 환경이므로 로컬 캐시보다는 캐시 서버를 구축하는 형태의 글로벌 캐시를 사용하는 것이 좋습니다. 가장 직관적으로 떠올릴 수 있는 캐시 키는 사용자의 위도 경도입니다. 그러나 위도 경도는 사용자가 조금만 움직여도 달라지기 때문에, 캐시 키로 설정할 경우 캐시 히트가 발생하길 기대하기가 매우 어렵습니다. 이때 아까 배웠던 지오해시나 쿼드트리 등을 사용한다면 같은 격자 내의 시설들이 같은 격자 식별자 값을 갖도록 만들 수 있으므로 이 문제를 효과적으로 해결 가능합니다.

 

구체적으로 다음 데이터들을 캐시에 보관하도록 할 수 있습니다.

 

지오해시 해당 격자 내의 시설 id 목록
시설 id 시설 상세 정보 객체

 

 

특정 지오해시에 대응되는 시설 목록을 요청받은 경우, 다음과 같이 설계할 수 있겠습니다.

 

  1. 캐시 먼저 조회
  2. 없으면 DB에서 해당 지오해시에 대응되는 시설 id 목록 가져와서 캐싱

 

시설 서비스를 통해 시설의 상세 정보를 조회하는 경우도 위 플로우대로 설계할 수 있겠습니다. 새로운 시설을 추가하거나 기존 시설 정보를 편집 또는 삭제하는 경우, DB를 그에 맞춰 갱신하고 캐시에 보관된 항목은 무효화시킬 수 있습니다. 만약 검색 반경으로 가능한 값들이 정해진 경우, 각 반경에 맞춘 적절한 지오해시 레벨들을 계산 가능하므로 해당하는 레벨에 대한 검색 결과들을 미리 캐싱해둘 수도 있겠습니다. 또한 고가용성 보장을 위해 캐시 서버도 클러스터를 구성하는 것을 고려할 수 있으며, 서버들을 여러 지역에 둔 것과 마찬 가지로 캐시 서버 클러스터를 각 지역별로 구성하여 가까운 캐시 서버를 사용하도록 구성할 수 있겠습니다.

 

 

4) 최종 설계도

앞서 설명한 것들을 토대로 다음과 같은 아키텍처를 설계할 수 있게 됩니다.

 

 

 

해당 아키텍처에서의 플로우는 다음과 같이 설계할 수 있습니다.

 

 

a) 주변 시설들을 검색하는 경우

 

  1. 클라이언트는 위치(위도, 경도)와 검색 반경을 로드밸런서로 전송합니다.
  2. 로드밸런서는 URL을 보고 해당 요청을 LBS로 보냅니다.
  3. LBS는 요청에 포함된 위치와 반경 정보를 토대로 최적 정밀도를 갖는 지오해시 길이를 계산합니다.
  4. LBS는 사용자의 위치를 담는 지오해시와 인접한 지오해시들을 계산하여 목록으로 가집니다.
  5. LBS는 목록에 있는 각 지오해시값에 대해, 글로벌 캐시 서버로부터 해당 지오해시에 매핑된 시설들의 id목록을 가져옵니다. 만약 캐싱된 게 없다면 DB의 공간 인덱스 테이블에서 가져와서 캐싱합니다.
  6. 각 시설id들에 대해, 글로벌 캐시 서버로부터 해당 id에 매핑된 상세 정보들을 가져옵니다. 만약 캐싱된 게 없다면 DB의 business 테이블에서 가져와서 캐싱합니다.

 

b) 시설의 상세정보를 조회하는 경우

 

  1. 클라이언트는 조회할 시설의 id를 로드밸런서로 전송합니다.
  2. 로드밸런서는 URL을 보고 해당 요청을 시설 서비스 서버로 보냅니다.
  3. 시설 서비스 서버는 글로벌 캐시 서버로부터 해당 시설id에 대한 상세 정보를 가져옵니다. 만약 캐싱된 게 없다면 DB의 business테이블에서 가져와서 캐싱합니다.

 

시설 정보가 추가/갱신/삭제된 경우, 해당 정보가 실시간으로 반영될 필요는 없음을 처음에 가정했으므로 캐시에 보관된 시설 정보의 갱신은 밤 사이에 배치를 수행하는 방식으로 처리할 수 있습니다.

'INFRA & DEVOPS' 카테고리의 다른 글

docker-compose 훑어보기  (0) 2024.02.07

도커 컴포즈(Docker-Compose)란

쉽게 말하면, "여러 컨테이너를 하나의 서비스로 묶어주는 관리 환경을 제공해주는 도구"다.

 

도커는 웹 서버 - WAS - DB 등과 같은 3 tier service부터 MSA 등 분산된 컴포넌트를 실행 가능한 환경을 가지고 있으며, 각 컴포넌트들이 자신만의 컨테이너에서 실행되면서 도커가 표준 네트워크 프로토콜을 통해 이들을 엮어낸다. 도커 컴포즈를 통해 이렇게 여러 컨테이너를 통해 돌아가는 애플리케이션을 정의하고 관리할 수 있게 된다.

 

한 마디로 도커 컴포즈가 없다면 애플리케이션을 구성하는 컨테이너를 직접 하나하나 docker run을 때려가며 구동시켜야 했지만, 도커 컴포즈라는 툴 통해 컨테이너들을 하나로 묶어서 관리할 수 있다는 것.

 

앞서 말했듯, 도커 컴포즈는 도커에서 기본적으로 제공하는 "기능"이 아닌 여러 컨테이너를 엮고 관리하게 해주는 "툴"이기 때문에 별도의 설치가 필요하다. 도커 컴포즈는 docker-compose.yml파일을 작성한 후, docker-compose up 명령어를 통해 실행 가능하다.

 

 

docker-compose.yml

되새김질 차원에서 기존에 docker run으로 컨테이너를 돌리던 커맨드를 봐보면 다음과 같다.

docker run --name mongodb -v ~/data:/data/db -d -p 27017:27017 mongo

 

  • --name mongodb : mongodb라는 컨테이너명으로 돌리겠다
  • -v ~/data:/data/db : 호스트의 ~/data를 실행될 컨테이너 내부의 /data/db로 마운트하겠다
  • -d : 컨테이너를 백그라운드에서 실행하겠다(detached mode)
  • -p 27017:27017 : 호스트의 27017 포트를 컨테이너의 27017포트로 포워딩하겠다
  • mongo : mongo라는 docker image를 사용하겠다

 

이를 docker-compose.yml파일로 옮겨 적으면 다음과 같이 적을 수 있다

version: '3'

services:
  mongodb:
    image: mongo
    volumes:
      - ~/data:/data/db
    ports:
      - "27017:27017"
    restart: always

 

본 예제에서는 컨테이너를 하나만 적어주긴 했으나, 보다시피 도커 컴포즈 파일은 애플리케이션의 모든 컴포넌트가 실행되고 있을 때 어떤 상태여야 하는지를 기술하는 파일이다. docker run으로 컨테이너를 돌릴 때 사용하던 옵션들을 한 곳에 모아둔 단순한 형식이며, version에 도커 컴포즈 파일 형식의 버전을 작성하고, services 아래로 엮어줄 컨테이너들의 정보를 작성하는 형태이다(위 예시에선 몽고디비 컨테이너만 기재했으나, 당연히 다른 컨테이너들도 쭈루룩 쓸 수 있는 것이다)

 

재밌는 점은, 도커 컴포즈는 이렇게 단순히 컴포넌트들을 동시 실행하도록 해주는 것 뿐만 아니라 컨테이너 간의 의존관계도 정해줄 수 있다는 것이다. 다음 docker-compose.yml파일을 보자.

 

version: '3'

services:
  mongodb:
    image: mongo
    volumes:
      - ~/data:/data/db
    ports:
      - "27017:27017"
    restart: always
    
  ray:
    image: ray/ray-ray-image
    ports:
      - "8010:80"
    depends_on:
      - mongodb

 

docker-compose는 컨테이너를 구동할 때 docker-compose.yml에 작성된 순서대로 컨테이너를 돌리지 않고, 병렬로 즉 동시에 실행시키려고 한다. 이 때 위 예시처럼 depends_on을 활용하여 컨테이너 간 의존성을 명시해 컨테이너가 구동되는 순서를 맞춰줄 수 있다. ray컴포넌트는 mondodb컨테이너가 구동된 뒤 실행되게 된다.

 

 

 

 

 

 

이전에 로컬에서 도커 이미지를 빌드하고 이를 도커 허브에 올린 뒤 ec2에서 이미지를 풀하고 컨테이너를 돌리는 식으로 배포해봤었는데, 새로운 기능을 추가하고 수정도 하면 이 과정을 하나하나 하기는 귀찮다. 그래서 jenkins라는 오픈소스를 활용해 빌드하고 테스트, 배포하는 과정을 자동화해본다.

 

또한, 나름대로의 코드 품질 개선과 혹시 모를 보안 위협에 대비하기 위해 sonarqube라는 코드 정적 분석 툴을 사용할 건데, 이는 sonarcloud라는 서비스를 통해 간단하게 도입해볼 것이다.

 

그림으로 시퀀스를 나타내면 다음과 같다.

  1. pull request가 생기면 sonarqube(sonarcloud)에서 코드 정적 분석을 한다.
  2. main브랜치에 푸시가 발생하면(merge) 도커로 띄운 젠킨스로 웹훅(webhook)을 보낸다
  3. 웹훅을 수신하면, jenkins쪽에서 지정된 깃헙 리포지토리의 코드를 땡겨온다 (+ application.properties 설정)
  4. 이를 gradle을 통해 빌드하고(jar)
  5. 이를 docker image로 빌드한 뒤 도커 허브에 푸쉬.
  6. 실제 서비스가 돌아갈 ec2에서 도커 허브에 푸쉬된 이미지를 땡겨와 돌리게끔 하면 된다

 

이 때 단순히 gradle로 빌드한 jar를 서비스를 돌릴 ec2로 보낼 수도 있지만(scp등으로) 멘토님 가라사대 요즘은 도커 모르면 안 된다라고 하셔서, 도커에 익숙해질 겸 도커 이미지를 만들고 이를 전달하는 식으로 가기로 했다. 그러면 도커로 띄운 젠킨스에서 도커 이미지를 만들어내야 해서, 도커 내에 도커를 설치해야 하니 이 점 유의

 


0. sonarcloud 설정

깃헙 계정으로 가입해서 설정만 딸깍딸깍 누르면 된다. sonarcloud를 설치할 레포지토리들을 선택하면 지가 알아서 설치되고, 이후 pull request를 날리면 지가 알아서 다음과 같이 분석 결과를 남겨준다

 

 

1. ec2 띄우고 docker 설치 및 swap 메모리 설정

참고로 ec2는 public subnet에 띄웠다. private subnet에 띄우면 깃헙 쪽에서 웹훅을 보낼 때 직접 받을 수 없기 때문에, 편의를 위해 그리 한 것임.

 

아 그리고 EBS볼륨을 20GB로 해줬다! 기본인 8GB로 해줬다가 용량이 없다고 하는 문제가 생겼었기 때문.

 

나는 ec2는 ubuntu로 띄웠고, 공식 매뉴얼에 나온 대로 도커를 설치했다

https://docs.docker.com/engine/install/ubuntu/

 

Install Docker Engine on Ubuntu

Jumpstart your client-side server applications with Docker Engine on Ubuntu. This guide details prerequisites and multiple methods to install.

docs.docker.com

 

그 뒤 swap 메모리를 설정해줬다. 나같은 경우 ec2를 프리티어로 해서 젠킨스를 띄울 건데, 이 때 프리티어는 메모리가 부족해서 멈추는 경우들이 있다고 한다. swap메모리를 설정해 이를 방지할 수 있다고 해서, 설정해줬다

(swap 메모리 : 메모리의 부족한 부분을 하드디스크로 대체하는 것)

 

아래 공식문서 글을 참고하면 된다

https://repost.aws/ko/knowledge-center/ec2-memory-swap-file

 

스왑 파일을 사용하여 Amazon EC2 인스턴스의 스왑 공간으로 메모리 할당

Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스에서 스왑 파일로 사용할 메모리를 할당하려고 합니다. 어떻게 해야 하나요?

repost.aws

 

 

2. jenkins 이미지 풀 땡기고 컨테이너로 돌리기

sudo docker pull jenkins/jenkins:lts

 

docker run --name jenkins -p 8080:8080 -p 50000:50000 -d -v /var/run/docker.sock:/var/run/docker.sock -v jenkins_home:/var/jenkins_home -u root jenkins/jenkins:lts
  • --name : 컨테이너의 이름을 지정(여기서는 jenkins라는 이름으로)
  • -p : 컨테이너를 띄우는 호스트의 포트와 컨테이너의 포트를 매핑하는 것(즉 호스트의 8080과 컨테이너의 8080이 매핑) 참고로 jenkins 웹 인터페이스가 8080으로 열리고, jenkins 슬레이브 에이전트가 50000으로 열린다
  • -d : 컨테이너를 백그라운드에서 실행. 이를 통해 터미널로 다른 작업이 가능
  • -v : 호스트와 컨테이너를 볼륨 마운트. 이를 통해 컨테이너와 호스트 머신 간에 데이터를 공유할 수 있음
  • -v /var/run/docker.sock:/var/run/docker.sock -> 컨테이너 내부에서 호스트머신의 docker 데몬과 상호작용하게끔 하는 것.
  • -v jenkins_home:/var/jenkins_home -> jenkins 설정과 데이터를 영구적으로 저장하기 위함
  • -u root : 컨테이너 내에서 jenkins를 root사용자로 실행

 

그 뒤 다음 커맨드를 통해 docker.sock파일의 권한을 수정해준다(다른 사용자도 접근 가능하게끔). 참고로 컨테이너 밖에서 실행해준다.

sudo chmod 666 /var/run/docker.sock

참고로 docker.sock은 도커 데몬과 컨테이너 간의 통신을 위한 소켓 파일이다.(그래서 ls -l로 보면 파일타입이 s가 나옴) 이를 통해 컨테이너 내부에서도 호스트 머신의 docker데몬과 통신 가능하며, 이를 통해 컨테이너 내부에서도 docker커맨드를 쓸 수 있게 되는 것. 컨테이너 내에서도 docker데몬에 대한 권한을 부여하기 위해 위 커맨드를 통해 권한을 주는 거라고 생각하면 된다.

 

 

3. 컨테이너 내 도커 설치

엇? 컨테이너 내에서도 호스트의 도커 데몬과 통신할 수 있게끔 한 거면 도커를 설치할 필요가 없지 않나? (나는 도알못이기 때문..) 왜 도커를 설치하지?

 

이럴 거면 docker run을 할 때 굳이 -v를 해서 docker.sock을 컨테이너로 마운트한 이유가 없지 않나? 그냥 컨테이너에서 도커 깔았으면 거기서 자체적으로 도커 데몬 쓰면 되지..(참고로 이를 Docker in Docker, DinD라고 부르고 소켓파일 마운트해서 호스트의 도커 데몬을 쓰는 방식을 Docker ouf of docker, Dood라고 부른다)

 

간단하다. 제 아무리 호스트 꺼여도, 어찌됐건 도커 데몬과 통신하려면 도커 클라이언트가 필요하기 때문에 도커를 설치하는거다. 다만 데몬을 호스트 꺼를 사용한다는 것. (그냥 DinD로 컨테이너 내에서 자체 도커 데몬을 써도 되나, 보안상 이를 권하지 않는다고 한다. 무려 도커공식피셜임. 참고로 도커를 요로코롬 컨테이너 내에 깔아도 docker.sock은 호스트와 마운트된 걸 사용하게 된다)

 

그림으로 표현하면 아래와 같은 형태가 되겠다.

 

찾아보니 나는 젠킨스 이미지를 바로 컨테이너로 띄우면서 docker.sock을 마운트하고 컨테이너 내에 도커를 설치한 반면, 젠킨스 이미지를 베이스로 도커를 설치하는 도커이미지를 만든 다음(즉 젠킨스를 한 번 더 포장한 이미지인데 도커가 설치돼있는 이미지가 된다) 컨테이너 띄울 때 docker.sock을 마운트하는 방식도 있다.

 

암튼.. 컨테이너 내에 도커를 설치해본다

 

우선 docker exec을 해서 컨테이너 내부로 들어간 다음, 위에서 한 것과 똑같은 가이드북대로 설치했다.

 

근데 중간에 apt-get update를 하니 The repository 'https://download.docker.com/linux/ubuntu bookworm Release' does not have a Release file이라는 오류가 났었다. 이는 아래 글을 참고해 /etc/apt/sources.list.d/docker.list를 지운 뒤 실행함으로써 해결했다

 

(/etc/apt/sources.list.d : 이 디렉토리에 있는 파일들은 APT 패키지 관리자에게 소프트웨어 패키지 저장소를 추가하는 데 사용됨)

(docker.list : Docker 패키지를 제공하는 저장소를 가리키며, 이 저장소는 Docker 엔진과 관련된 패키지를 제공)

 

https://dct-wonjung.tistory.com/entry/apt-update-404-not-found

 

apt update 404 Not Found 에러 해결 방법 - The repository does not have a Release file.

TmaxOS21에서 docker를 설치하려고 했더니 설치는 안되고 apt update를 할 때마저 아래와 같은 에러가 발생했다. $ sudo apt update 기존:1 http://tos-repo.tmaxos.com/tmax nabi InRelease 기존:2 http://tos-repo.tmaxos.com/tmax t

dct-wonjung.tistory.com

 

근데 또 이번엔 sudo apt-get install docker-ce docker-ce-cli containerd.io (이하 생략) 커맨드에서 Package docker-ce is not available, but is referred to by another package. This may mean that the package is missing, has been obsoleted, or is only available from another source라는 메시지와 함께 오류가 났다. 아까 docker.list를 삭제했던게 원인인 듯 한데.. 문제는 얘를 삭제안하면 다른 오류가 계속 나던 상황이라.. 해결방법을 찾아보다가, docker.list를 지운 상태에서 아래 커맨드를 실행하니 설치가 잘 됐다.

 

curl -fsSL get.docker.com -o get-docker.sh

bash get-docker.sh // 다운받은 쉘 스크립트 파일 실행

이렇게 하니 설치가 됐다. curl을 통해 get.docker.com으로부터 도커를 설치하는 스크립트를 get-docker.sh란 파일로 다운받은 뒤 이를 실행하는 커맨드다.

 

 

4. application.properties 설정

내 프로젝트는 .gitignore를 통해 application.properties를 깃 관리 대상에서 제외했다. 따라서 나중에 젠킨스에서 깃헙 리포지토리에 있는 코드를 땡겨가도 그래도 빌드해 사용한다면 문제가 생길 것이다. application.properties를 주입하는 방법은 여러 가지가 있겠지만, 나는 컨테이너의 /home/env밑에 application.properties를 두고 나중에 젠킨스에서 이걸 가져다 쓰는 식으로 구성했다.

 

docker exec -it jenkins bash // 컨테이너 내부로 들어가기

cd /home

mkdir env // /home밑에 env 디렉토리 생성

cd env

echo "{application.properties 내용}" >> application.properties

 

이런 방식으로 만들었었다.

 

여까지 했다면, 이제 젠킨스라는 툴을 사용해 ci cd 파이프라인을 구축할 준비가 된 거다. 젠킨스 설정은 젠킨스에서 제공하는 웹 인터페이스(즉 사이트)를 통해 가능하니, 웹사이트로 들어가 설정을 진행해본다.

 

5. 젠킨스 웹사이트 접속 (ec2의 퍼블릭ip:8080)

docker exec -it {컨테이너명} bash 같은 걸로 컨테이너 안에 들어가서 저 경로로 cat찍으면 패스워드 볼 수 있으니 그리 하면 된다. (사실 엄밀히 말하면 들어간다기보다는 컨테이너에서 작업할 수 있게 해주는 커맨드라고 보는게 맞다)

 

추천해주는 플러그인 설치한 뒤, 회원가입 페이지(?)같은 게 나온다. 자신의 플젝에 맞게 설정하면 된다.

 

6. 젠킨스 설정  - jdk, gradle, github & dockerhub token

jar 빌드를 위해 jdk와 gradle을 설정해줄 필요가 있고, 깃허브 리포지토리에 있는 코드들을 땡겨와야 하기 때문에 github token이 필요하며, 도커 이미지를 도커허브에 올리기 위해 dockerhub token도 필요하다. 우선 jdk와 gradle설정부터 시작해본다

 

프로젝트에서 jdk11버전을 썼다면 모를까, 나는 17버전을 쓰기 때문에 따로 설정을 해줘야 한다.

jenkins관리 - tools로 가서 jdk를 설정해준다.

이 설정은 아래 글을 참고했었다.

https://royleej9.tistory.com/entry/Jenkins-jdk-%EC%84%A4%EC%A0%95

 

[Jenkins] pipeline jdk 설정

[Jenkins] pipeline jdk 설정 작업 순서 JDK 설정 pipeline에서 특정 버전의 JDK 사용하기 1. JDK 설정 Jenkins 관리 > Global Tool Configuration > JDK 항목 > Add JDK https://stackoverflow.com/questions/55243120/jenkins-add-jdk-11-to-jdk-l

royleej9.tistory.com

 

download url : 내가 원하는 버전의 jdk를 다운받는 경로

https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8.1%2B1/OpenJDK17U-jdk_x64_linux_hotspot_17.0.8.1_1.tar.gz

 

subdirectory : url을 통해 다운된 파일을 압축 해제 했을 때 나오는 폴더 이름

jdk-17.0.8.1+1

 

gradle 설정도 jenkins관리 - tools에서 해주며, 플젝에서 쓰는 spring boot에서 사용중인 버전과 똑같은 놈으로 하면 된다.

본인 스프링부트 프로젝트의 gradle > wrapper > gradle-wrapper.properties에서 확인 가능하다

 

그리고 jenkins관리 - Credentials로 가서 깃허브 토큰을 등록한다. 깃허브 토큰을 발급받는 과정은 생략한다.

이 때 kind를 secret text가 아닌 username with password로 해준다. git이 뭐 지원 안한다고 함

username : 깃허브 계정 (이메일)

password : 깃헙 토큰

id : 본인이 식별할 수 있는 거 암거나 작성

 

마찬가지로 dockerhub token도 발급받은 뒤 똑같이 username with password로 등록해주면 된다. username은 도커허브 계정(아이디) 넣으면 되고, password에 토큰값 넣어주면 되고, id도 본인이 식별가능한 거 써주면 된다

 

7. 젠킨스 설정 - ssh 관련 설정

도커 이미지를 도커 허브에 푸시한 뒤, ssh로 서비스를 배포할 ec2에 접속해 도커허브로부터 이미지를 땡기는 작업을 하게 된다. 이를 위해 ssh를 사용하는데, 예전엔 Publish Over SSH라는 걸 썼던 듯 하나 지금은 못 쓰는 듯 하다. (새로운 openssh에서 ssh-rsa키 유형이 비활성화됐다고 한다. https://github.com/jenkinsci/publish-over-ssh-plugin/issues/247#issuecomment-1153768171)

 

그러면 이제 젠킨스가 띄워진 ec2에 ssh키를 물리적으로 둘 수도 있을 텐데, 이건 사실상 현관문 바로 앞에 키를 두는 방식이니 좋지 않다. 찾아보니 ssh agent플러그인을 젠킨스에서 다운받아 쓰는 방법이 있어, 이를 쓰기로 한다.

 

jenkins관리 -> 플러그인 -> available plugins로 가서 ssh agent를 install한다

난 이미 설치해서 안 뜬다

 

그 다음 아까 깃헙&도커허브 토큰을 등록했을 때처럼 ssh key정보를 등록한다. 

cat으로 ec2의 ssh key(pem파일)을 본 다음, 내용물을 적어주면 된다.

 

8. 깃허브 웹훅 설정

리포지토리 세팅 -> Webhooks로 가서 http://젠킨스서버ip:8080/github-webhook/을 등록한다

그리고 깃허브 쪽에서 보내는 웹훅을 젠킨스서버가 잘 받기 위해, 젠킨스 서버에 대한 보안그룹에서 깃허브 쪽에서 오는 인바운드들이 열려 있어야 한다.

 

https://api.github.com/meta

요기에서 현재 깃허브에서 웹훅을 보낼 때 쓰는 ip들을 확인 가능하다. 현재

 

"hooks": [
    "192.30.252.0/22",
    "185.199.108.0/22",
    "140.82.112.0/20",
    "143.55.64.0/20",
    "2a0a:a440::/29",
    "2606:50c0::/32"
  ],

 

요 ip들에서 8080으로 오는 인바운드들을 다 열어주면 된다.

 

 

9. 파이프라인 작성

새로운 아이템 - 파이프라인을 선택해주고 이름을 지어준다

General - GitHub project에 리포지토리 url(.git으로 끝나는..)을 입력한다.

 

그리고 build triggers에서 다음과 같이 체크해준다

 

그리고 파이프라인 스크립트를 작성한다.

난 다음과 같이 작성해줬다.

pipeline {
    environment { 
        repository = "{도커허브계정}/{레포지토리 이름}" // 이메일말고 아이디
        DOCKERHUB_CREDENTIALS = credentials('등록해둔 도커허브토큰id') // jenkins에 등록해 놓은 docker hub credentials 이름
        dockerImage = '' 
    }
    
    tools {
        jdk "{jdk이름}" //아까 jdk등록할 때 name으로 준 값
    }
    
    agent any 
    
    stages {
        stage('GitHub Repository Clone') { 
            steps { // url의 main branch 내용을 credential을 통해 땡겨온다
              
                git branch: 'main', credentialsId: '{등록해둔 깃헙토큰id}', url: '{깃헙 리포지토리 주소(.git으로 끝나는)}'
              
            }
        }
        
        stage('Set Application.properties') {
            steps {
                sh '''
                    if [ ! -d ./src/main/resources ]; then
                        mkdir ./src/main/resources
                    else
                        echo "resources directory alreay exist"
                    fi
                    
                    if [! -e ./src.main/resources/application.properties ]; then
                        rm ./src/main/resources/application.properties
                    fi
                    
                    cp /home/env/application.properties /var/jenkins_home/workspace/{지어준 파이프라인 이름}/src/main/resources
                    
                    cat ./src/main/resources/application.properties
                    '''
                
            }
        }
        
        stage('Build jar') {
            steps{
                sh '''
                    echo 'start bootJar'
                    echo $JAVA_HOME
                    ./gradlew clean bootJar
                    '''
            }
        }
        
        stage('Docker build') {
            steps {
                sh '''
                    docker build -t $repository:latest .
                    '''
            }
        }
        
        stage('Dockerhub Login'){
            steps {
                sh '''
                    echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin
                    '''
            }
        }
        
        stage('Push Docker image') {
            steps {
                sh '''
                    docker push $repository:latest
                    '''
            }
            
            post {
                success {
                    echo 'push success'
                }
                
                failure {
                    echo 'push failure'
                }
            }
        }
        
        stage('Run New Docker Container'){
            steps {
                sshagent(credentials: ['ssh-key']) {
                    sh '''
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} 'sudo docker ps -a'
                    
                    // 기존에 돌아가던 컨테이너 삭제
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} '
                        if [ "$(sudo docker ps -q --filter name={삭제할 컨테이너명}" ]; then
                            sudo docker rm -f $(sudo docker ps -aq --filter name={삭제할 컨테이너명})
                        else
                            echo "no there containers"
                        fi
                    '
                    
                    // 기존 이미지 삭제
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} '
                        if [ "$(sudo docker images -q)" ]; then
                            sudo docker rmi $(sudo docker images -q)
                        else
                            echo "no there images"
                        fi
                    '
                    
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} 'sudo docker pull {아까 푸시한 이미지명}'
                    ssh -o StrictHostKeyChecking=no ubuntu@{서비스 띄울 ec2 ip} 'sudo docker run -d --name={컨테이너명} -p 8080:8080 {풀받은 이미지명}'
                    
                    '''
                }
            }
        }
        
        stage('Clean Docker Image') {
            steps {
                sh '''
                    docker rmi $repository:latest
                    '''
            }
        }
    }
}

참고 : docker image -q 또는 docker ps -q -> 아이디값만 보게 해준다.

 

이후 깃헙에서 푸시를 해보니..

 

다음과 같이 파이프라인이 잘 돌아가는 걸 확인할 수 있다.


어찌보면 남들 다 하는 ci cd 구성이지만.. 그래도 처음 해보면서 삽질 여러가지 하면서 배운 게 많은 것 같다. DinD나 DooD도 있고, 특히 쉘 스크립트 작성을 꽤나 해보게 됐다ㅋㅋㅋㅋ.. jar로 빌드하고, 도커 이미지 빌드하고, 도커허브에 푸쉬하고, 서비스 띄울 ec2에선 기존 컨테이너랑 이미지 삭제한 뒤에 새로 풀받아서 돌리고.. 한 스텝 한 스텝 성공할 때마다 왠지 모를 희열이 느껴졌다. 

 

다만 아직 고려해야할 부분들이 더 많다. 당장 도커 이미지만 봐도 일단 전부다 latest를 달아서 하게끔 했는데, 이것보다는 빌드별로 번호를 매겨서 붙여주는게 더 나을 거다. 또한 지금 구축한 것대로라면 기존에 돌아가던 컨테이너를 내리고 새로 이미지를 받은 뒤 컨테이너를 띄우는 동안은 서비스가 중단된다!! 즉 무중단 배포도 고려해야 할 것이다.

 

기능 개발을 해야 할 게 남아 있는 만큼, 그리고 https 구축도 해야 하는 만큼 일단 그것들에 먼저 신경을 쓰고 해야 할 듯 하다..

'INFRA & DEVOPS > CI & CD' 카테고리의 다른 글

[GitHub Action] 플러터 프로젝트 CI 구축하기  (0) 2023.07.09

public subnet에 띄운 ec2에 고정된 공인ip, 즉 elastic ip를 붙이고 이를 route 53에서 구입한 도메인과 연결해본다


Elastic IP 생성 및 ec2와 연결

1. Elastic IP를 생성한다. 생성하는 모습은 생략함

 

2. 생성한 elastic ip를 선택하고 작업 탭을 눌러 "탄력적 IP 주소 연결"을 누른다

리소스 유형 = 인스턴스고르고 연결할 ec2 고르면 된다. 프라이빗 주소는 그 인스턴스의 사설ip를 고르면 된다

 

3. 그러면 해당 ec2의 상세화면에서 public ip로 elastic ip가 연결된 걸 볼 수 있다 

 


route53에서 구입한 도메인과 elastic ip연결

1. 구입한 도메인의 호스팅 영역에 들어가서 "레코드 생성"을 누른다

 

2. 다음과 같이 생성해주면 끝

값으로는 아까 생성한 elastic ip를 주면 된다

 

3. 이렇게 하고 나면, www.petdori.com으로 트래픽을 보내면 ec2로 가진다. 

소마 프로젝트를 진행하면서, 인프라 쪽을 내가 어느 정도 담당하고 있어서(사실 담당한다고 하기에도 민망할 정도로 기본적인 부분들만 담당하긴 하지만) RDS를 띄워보기로 했다. Private Subnet에 RDS를 띄우기로 했는데, 문제는 "로컬에서 Private Subnet에 어떻게 접근하는가?" 였다.

 

Private Subnet은 외부와 단절된 네트워크로, 외부에서 직접 접근이 불가능하다. NAT Gateway를 둬서 Private Subnet에서 외부로 트래픽을 보낼 순 있어도, 외부에서 먼저 Private Subnet으로 트래픽을 보낼 순 없다. VPN등을 통해 직접 로컬에서 Private Subnet으로 연결할 순 있으나, 비용이 무시무시하다는 소문이 들려서 다른 방법을 찾아보기로 했다

 

당장 떠오르는 방법은 Public Subnet에 EC2를 둔 후, ssh로 EC2에 원격으로 연결한 다음 거기서 Private Subnet으로 접근하는 것. 같은 VPC안의 애들끼리는 서로 통신이 가능하기 때문에 이 방법을 쓸 수 있을 것이다. 이렇게 외부에서 내부 네트워크로의 안전한 접근을 제어하기 위해 사용되는 중간 서버를 Bastion Host라고 부른다.

 

로컬에서 ssh로 Public Subnet의 EC2에 원격으로 연결한 뒤, 거기서 Private Subnet의 RDS로 직접 쿼리를 날릴 순 있다.

 

이렇게 하면 로컬에서 Private Subnet에 있는 RDS로 create등의 쿼리를 작성해 날릴 수 있다. 그러나 "얘를 로컬에서 돌리고 있는 스프링부트와 어떻게 연결하는가"가 숙제. 스프링부트에서 만들어지는 쿼리를 터미널에 인풋으로 주도록 내가 직접 구축을 해야 하나..? 그건 너무 에바같은데.. 

 

이 때 ssh 포트 포워딩(다른 말로는 ssh 터널링)을 통해 이 문제를 해결할 수 있다. ssh 포트포워딩이란 ssh를 사용해 로컬 머신과 원격 서버 사이에서 포트 전달을 설정하는 것으로, 이를 통해 로컬에서 특정 포트로 트래픽을 보내면 ssh터널(ssh를 통해 연결된 통로라고 생각)을 통해 원격서버의 특정 포트로 보낼 수가 있다. 즉 내가 로컬의 3306포트와 원격 서버의 3306포트를 포트포워딩하면, 내가 로컬의 3306포트로 보내는 트래픽을 원격 서버의 3306포트로 보낼 수가 있는 거다. (참고로 이 예시는 로컬 포트 포워딩에 해당하며, 리모트 포트 포워딩을 하면 원격 서버에서 특정 포트로 들어오는 트래픽을 내 로컬 머신의 포트에서 받을 수 있다)

 

이런 로컬 포트 포워딩은 다음과 같이 설정해 사용 가능하다

ssh -L [로컬에서 사용할 포트]:[최종적으로 접근할 곳(ip:port형태)] [SSH Server 주소]

예시로, aws에서 지급받은 pem키와 함께 사용하려면 다음과 같이 하면 된다.

ssh -i [pem키 경로] -L 3306:[RDS 엔드포인트]:3306 [사용자명(ex:ubuntu)]@[Bastion Host public ip]

 

이렇게하면 로컬의 3306포트와 Private RDS의 3306이 연결됐으니 이제 스프링부트에서도 datasource를 내 로컬의 3306으로 한 뒤 쓰면 된다. 물론 각각 보안그룹들이 다 설정돼있어야 한다!(RDS는 Bastion으로부터 3306포트로 오는 인바운드 허용, Bastion은 22번 인바운드와 3306아웃바운드 허용 등..) 이 때 Public Subnet에 둔 EC2는 점프호스트로 쓰는 거라고 볼 수 있다

 

 

하지만! SSM이란 서비스를 사용하면 좀 더 쉽게 Private Subnet접근이 가능하다.

 

 

SSM이란?

AWS System Manager. Simple System Manager라고도 불리며 이의 줄임말이 SSM이다. 다양한 기능들을 제공하는데, Session manager라는 걸 통해 EC2접속을 할 수 있는 서비스도 제공한다.

 

https://aws.amazon.com/ko/blogs/architecture/how-wego-secured-developer-connectivity-to-amazon-relational-database-service-instances/

https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html

 

AWS Systems Manager Session Manager - AWS Systems Manager

AWS Systems Manager Session Manager Session Manager is a fully managed AWS Systems Manager capability. With Session Manager, you can manage your Amazon Elastic Compute Cloud (Amazon EC2) instances, edge devices, on-premises servers, and virtual machines (

docs.aws.amazon.com

 

Session Manager를 사용하면 기존에 했던 것처럼 Bastion Host를 둘 필요 없이 Private Subnet에 있는 EC2인스턴스에 직접 접근이 가능하다(당연히 퍼블릭도 가능). ssh로 연결할 때 사용하던 pem키 관리 및 EC2인스턴스들에 22번 포트를 여는 인바운드 설정도 할 필요가 없다. 즉 더 안전하게 사용 가능한 방법이라고도 볼 수 있다. 또한 포트포워딩도 지원하기 때문에 로컬에서 Private Subnet에 있는 RDS로의 접근 역시 가능하다!

(Session Manager를 통해 EC2들에 대한 직접 액세스는 가능하나 RDS등은 직접 액세스는 안 돼서 포트포워딩을 거쳐야 함. 즉 EC2라는 관리 인스턴스를 "점프 호스트"로 사용해 RDS로 접근하는 것이며, ssh 포트포워딩을 통해 public에 있던 EC2를 점프호스트로 쓰던 것과 같은 원리이다.)

 

참고로 이때 SSM을 통해 직접 액세스하게 되는 EC2는 22번 인바운드는 열릴 필요가 없으나, 443번 아웃바운드는 열려있어야 한다. 해당 EC2에서 AWS SSM에게 지속적인 폴링을 통해 연결을 유지하기 때문이다. 따라서 보안그룹 설정시 꼭 주의할 것. 관련된 내용은 아래 링크의 [엔드포인트에 연결] 섹션을 참고하면 된다

 

https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-prerequisites.html

 

1단계: Session Manager 사전 조건 완료 - AWS Systems Manager

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

그럼 본격적으로 SSM으로 포트포워딩을 설정해 로컬에서 Private Subnet의 RDS에 접속하는 법을 알아보자.

 


1. 로컬환경에 aws cli 설치하기

https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions

 

최신 버전의 AWS CLI 설치 또는 업데이트 - AWS Command Line Interface

이전 버전에서 업데이트하는 경우 unzip 명령을 실행하면 기존 파일을 덮어쓸지 묻는 메시지가 표시됩니다. 스크립트 자동화와 같은 경우에 이러한 프롬프트를 건너뛰려면 unzip에 대한 -u 업데이

docs.aws.amazon.com

 

2. aws IAM을 만든 다음, Access Key와 Secret Access Key를 발급받는다.

 

3. 로컬에서 터미널을 띄우고 aws configure를 입력해 구성한다

$ aws configure
AWS Access Key ID [None]: {각자에게 주어진 Access Key}
AWS Secret Access Key [None]: {각자에게 주어진 Secret Access Key}
Default region name [None]: ap-northeast-2
Default output format [None]: json

 

4. 로컬에 Session Manager Plugin을 설치한다

https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html

 

용 Session Manager 플러그인 설치 AWS CLI - AWS Systems Manager

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

5. EC2를 띄우고, 그 EC2에 AmazonSSMManagedInstanceCore라는 정책이 있는 역할을 부여한다

참고로 EC2에 SSM agent가 설치돼있어야 한다. aws에서 제공하는 AMI로 만들어진 EC2들은 ssm agent가 기본적으로 깔려있는 애들이 많다! 다음 링크의 [SSM Agent의 상태 확인] 섹션을 참고해 SSM agent가 설치돼있는지 살펴보고, 안돼있으면 설치해야한다. 설치된 상황이라면 해당 역할을 부여한 후 SSM agent를 재시작해준다.

 

재시작하는 커맨드는 상태 확인 커맨드에서 status만 restart로 바꾸면 된다.

 

https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ami-preinstalled-agent.html

 

Amazon Machine Images(AMIs), SSM Agent 사전 설치 - AWS Systems Manager

SSM Agent가 이 목록에 없는 AWS 관리형 AMIs에 사전 설치되어 있을 수 있습니다. 이 경우 일반적으로 운영 체제(OS)에서 모든 Systems Manager 기능이 완벽하게 지원되지 않습니다. 또는 SSM Agent가 AWS Marketp

docs.aws.amazon.com

 

6. 터미널에 다음 커맨드를 입력한다

aws ssm start-session --target {EC2 인스턴스의 id} \
                       --document-name AWS-StartPortForwardingSessionToRemoteHost \
                       --parameters '{"portNumber":["3306"],"localPortNumber":["3306"],"host":["{RDS 엔드포인트}"]}'

 

그러면 ssh 포트포워딩을 했을 때처럼 로컬의 3306포트와 RDS의 3306포트가 연결된다.

 

 

 

 

 

참고한 글

https://blog.naver.com/PostView.naver?blogId=alice_k106&logNo=221364560794 

 

150. [SSH, Network] SSH 포트 포워딩(SSH 터널링)의 개념 및 사용 방법

이번 포스트에서는 'SSH 포트 포워딩' 또는 'SSH 터널링' 이라고 불리는 것에 대해서 설명한다. 1. ...

blog.naver.com

https://musma.github.io/2019/11/29/about-aws-ssm.html

 

AWS SSM으로 EC2 인스턴스에 접근하기 (SSH 대체)

목차 서론 들어가기: 더 좋은 방법 대상 독자 SSM: AWS Systems Manager 원격 호스트 접속 방법 비교: SSH (기존) vs. SSM (개선) S...

musma.github.io

https://hbase.tistory.com/328

 

[Linux] ssh 터널링(ssh port forwarding) - Local / Remote / Dynamic Tunneling

sh는 Secure SHell의 줄임말로 원격 호스트에 접속하기 위해 사용되는 보안 프로토콜이다. 당연하게도 ssh는 원격 호스트로 접속하기 위해 가장 많이 사용된다. 그런데 ssh는 원격 호스트로의 접속과

hbase.tistory.com

https://hwan-shell.tistory.com/382

 

AWS Session manager 란? (설명 및 설정 방

EC2를 접속할 때 기본적으로 SSH를 통해서 접속합니다. SSH를 통해서 접속하려면 SSH key가 필요하고 EC2 Inbound 22 port를 허용해 줘야 합니다. SSH를 사용하게 되면 Key 없이는 접속할 수 없고, Key를 분실

hwan-shell.tistory.com

https://aws.amazon.com/ko/about-aws/whats-new/2022/05/aws-systems-manager-support-port-forwarding-remote-hosts-using-session-manager/

 

AWS Systems Manager, Session Manager를 사용한 원격 호스트로의 포트 포워딩 지원 발표

AWS Systems Manager가 Session Manager를 사용한 원격 호스트로의 포트 포워딩 지원을 발표합니다. AWS Systems Manager는 AWS 애플리케이션 및 리소스를 위한 운영 허브이며, 하이브리드 클라우드 환경에 대해

aws.amazon.com

 

+ Recent posts