이 글은 우아한테크코스 백엔드 6기 초코칩, 명오, 냥인에 의해 작성되었습니다.


운영 환경 아키텍처

현재 크루루 서비스의 개발 환경 아키텍처입니다.

Public subnet에 위치한 EC2 인스턴스에 Nginx 서버, Spring 서버, DB 서버가 띄워져 있습니다.

위 아키텍처를 운영 환경에 적용하기에는 몇 가지 문제가 있습니다.

  1. DB 서버가 Public subnet에 있어 보안에 취약함.
  2. Nginx와 Spring 서버가 하나의 인스턴스에 존재하여, 확장성에 제약이 있음.
  3. 여러 서버가 하나의 인스턴스에 존재하여, SPOF(Single Point of Failure) 발생 가능성이 있음.

이러한 점을 개선하여 운영 환경은 아래와 같이 설계하였습니다.

Production CD

해당 운영 환경 인프라에 지속적인 배포와 업데이트의 일관성을 유지하기 위해 Production CI/CD 과정을 구축하였습니다.

CD 파이프라인은 Github Actions를 사용하여 애플리케이션의 Docker 이미지를 빌드하고, 이를 운영 환경에 배포하는 과정을 자동화합니다.

  1. 코드 변경 감지 및 빌드 시작: be/main 브랜치에 코드가 푸시되거나 수동으로 워크플로우가 트리거되면, CI/CD가 시작됩니다.
  2. 애플리케이션 빌드: Java와 Gradle 환경을 설정한 후, 애플리케이션을 prod profile로 JAR 파일을 빌드합니다.
  • Docker 이미지 빌드 및 푸시: 빌드된 JAR 파일을 사용해 Docker 이미지를 생성하고, 이 이미지를 Docker Hub에 업로드합니다. 이 Docker 이미지는 나중에 프로덕션 환경에서 사용할 수 있도록 prod 태그를 활용하여 업로드됩니다.
  • 배포 준비: 빌드 작업이 완료되면, 프로덕션 환경에서 실행될 docker-compose 파일이 다운로드되고, 배포에 필요한 환경 변수들이 .env 파일로 설정됩니다.
  • 기존 컨테이너 종료 및 새로운 컨테이너 배포: 현재 실행 중인 애플리케이션 컨테이너가 있다면 이를 종료하고, 새로운 Docker 이미지를 사용해 애플리케이션을 다시 배포합니다. 이 과정에서 새로운 애플리케이션 버전이 프로덕션 환경에 적용됩니다.
  • 애플리케이션 실행: docker-compose up 명령어를 통해 새로운 컨테이너가 배포된 후, 애플리케이션이 프로덕션 환경에서 실행됩니다.

운영 서버 구축

EC2 인스턴스

운영 서버를 구축하기 위해 제일 먼저 해야 하는 일은 EC2 인스턴스를 생성하는 것입니다. 스펙을 설정하면서 고려한 점은 아래와 같습니다.

  • 외부에서 접근이 가능해야 하기 때문에, Public subnet으로 생성하였습니다.
  • 인스턴스 유형은 테스트를 진행한 개발 서버와 동일하게 생성하였습니다. 인스턴스 유형은 추후 변경이 가능하기 때문에 리소스가 부족하다고 판단되면 변경하는 것을 염두에두고 있습니다.
  • 추후 모니터링 서버와 DB 서버와의 연결을 고려하여 같은 VPC 영역에 생성하였습니다.

저희는 Docker와 Github Actions로 CD pipeline을 구축했기 때문에, Java를 설치할 필요는 없습니다. 대신 CD 과정에서 생성된 docker-compose를 실행하기 위한 docker와, Action가 배포 과정을 실행할 환경을 제공해주기 위해 self-hosted runner를 적용해야 합니다.

Docker

docker 설치 명령어는 sudo apt install docker 입니다.

만약 기억나지 않는다면, 서버에 접속하여 아무 docker 명령어나 사용해보면 됩니다.

사진에서 보이듯, docker command가 존재하지 않는다고 말해 주면서 친절하게 docker 설치 명령어도 알려줍니다. 첫 명령어인 sudo snap install docker를 사용하면 docker가 설치됩니다.

두 명령어 모두 docker를 설치해 주지만, 패키지 관리자가 snap인지 apt인지가 다릅니다.

Self-Hosted Runner

self-hosted runner가 인스턴스 내에서 실행이 되면 지정된 github action을 listen하고 있게됩니다. 언제 Actions가 구동될지 모르기 때문에, 안정적인 실행을 위해 runner는 인스턴스 환경에서 항상 켜져있어야합니다.

Actions가 구동될 때 workflow에서 지정한 job을 집어가 runner가 실행되고 있는 인스턴스 환경에서 해당 job에 정의된 step들을 구동합니다.

runner는 Github repo에서 settings → actions → runners → new self-hosted runner를 통해 설치할 수 있습니다.

runner image와 Architecture를 설정하면, 아래에 self-hosted runner를 설치-설정-실행하는 명령어를 친절하게 설명해줍니다.


DB EC2 인스턴스 구축

EC2 인스턴스 생성

먼저 DB 서버로 사용할 EC2 인스턴스를 생성합니다.

팀 크루루는 아래와 같은 스펙으로 생성하였습니다.

  • OS: Ubuntu Server 24.04 LTS
  • 인스턴스 아키텍처: arm64
  • 인스턴스 유형: t4g.small
  • 네트워크, 서브넷, 보안 그룹
    • 우아한테크코스에서 AWS 리소스를 제공하는 바 해당 사항을 적용하였습니다.
    • 서브넷의 경우, WAS와 같은 가용 영역 내 private 서브넷에 위치시켰습니다.

EC2 Ubuntu에 MySQL 8.0 설치

생성한 EC2 인스턴스에 MySQL 8.0을 설치합니다.

  1. SSH 연결

    DB 서버는 private 서브넷에 위치해 있으므로, 콘솔에 접근하기 위해 기존의 public 인스턴스 ssh를 통한 프록시 점프를 이용하여 SSH 연결을 합니다.

    ssh -J {WAS_호스트명}@{WAS_public_ip} -i {SSH_키_파일} {DB_서버_호스트명}@{DB_서버_private_ip}

  2. mysql-server 설치

    sudo apt-get install mysql-server

    “Do you want to continue?”라고 물으면 y로 대답하면 됩니다.

  3. 설치 여부 확인

    설치 완료 후 MySQL 서비스가 자동으로 시작됩니다. 아래 명령어를 통해 MySQL 서버의 실행 여부를 확인할 수 있습니다.

    sudo systemctl status mysql

    MySQL의 버전을 확인하고 싶으면 아래 명령어를 실행하면 됩니다.

    mysql –version

  4. MySQL 서버 보안 설정

    MySQL의 디폴트 설정과 더불어 추가적인 보안 적용을 위해 아래 명령어를 사용합니다.

    sudo mysql_secure_installation

    • **비밀번호 유효성 검사 컴포넌트 설정 여부: ****y**

      사용자가 충분히 안전한 비밀번호만 설정할 수 있도록 합니다.

    • **비밀번호 유효성 검사 정책 레벨 설정: ****2**

      • low, medium, strong 세 가지 레벨 중 하나로 설정할 수 있습니다.
      • 참고로 mixed case는 대소문자 둘 다 섞어 사용하는 것을 의미합니다.
      • dictionary는 일반적으로 우리가 사용하는 언어의 단어나 구를 말합니다. password, welcome 같이 사전에 나오는 단어만으로 이루어졌는지 검사합니다.
    • **익명 사용자 삭제 여부: **y** **

      MySQL 설치하면 기본적으로 익명 사용자 계정이 생성됩니다. 이 익명 사용자 계정을 통해 사용자 계정을 생성하지 않고도 MySQL에 접근할 수 있게 합니다. 운영 환경에서는 보안상 문제가 되므로 삭제하는 것을 권장합니다.

    • **root 사용자의 원격 접속 허용 여부: ****n**

      보안상 root 계정은 로컬에서만 접속하도록 제한하는 게 일반적입니다.

    • **test database 삭제: **y** **

      test 데이터베이스는 MySQL 설치 시 기본적으로 생성되는 데이터베이스입니다. 아무런 인증 없이 접속할 수 있기 때문에 보안상 위험이 따른다고 합니다. 따라서 삭제하는 것을 권장합니다.

    • **privilege 테이블 reload: ****y**

      y를 입력하면 현재 권한 변경 사항을 즉시 적용합니다. n을 입력하면 MySQL 서버를 재시작해야 변경 사항이 반영됩니다.

  5. MySQL DB 로그인1

    초기에는 root 계정의 비밀번호가 설정되어 있지 않습니다.

    sudo mysql -u root -p

    root 계정의 비밀번호를 설정하려면, 아래 명령어를 차례대로 실행합니다(MySQL을 설치한 후, root 계정의 비밀번호를 처음 설정하는 경우 해당되는 내용입니다).

    • root 계정의 비밀번호 설정

      ‘new_password’에 원하는 비밀번호를 지정하면 됩니다.

        ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'new_password';
        
    • 권한 변경 사항 적용

        FLUSH PRIVILEGES;
        
  6. 계정 생성

    팀 크루루는 cruru라는 계정을 생성해 주었습니다.

    • 데이터베이스 변경

      user 테이블이 있는 mysql 데이터베이스를 선택합니다.

        use mysql;
        
    • 계정 생성

      운영 서버에서 DB 서버로 접속할 수 있도록 운영 서버의 private ip를 지정해주었습니다. 참고로 크루루는 현재 운영 서버와 DB 서버가 같은 가용 영역 내에 있어, 운영 서버에서 private ip로 DB 서버에 접근할 수 있습니다.

        create user 'cruru'@'운영-서버의-private-ip' identified by '비밀번호';
        
    • 계정 권한 부여

        GRANT ALL PRIVILEGES ON *.* TO 'cruru'@'운영-서버의-private-ip';
        
    • 권한 변경 사항 적용

        FLUSH PRIVILEGES;
        
    • 계정 조회

      MySQL의 user 테이블에는 DB 서버의 사용자 계정과 관련된 정보가 저장되어 있습니다. host 컬럼과 user 컬럼을 조회해 보겠습니다.

      • host: 사용자가 접속할 수 있는 호스트 또는 IP 주소
      • user: MySQL 사용자 계정의 이름
        select host, user from user;
        

    • my.cnf 파일의 bind-address 설정 변경

      모든 IP에서의 접속을 허용할 수 있도록 my.cnf 파일의 bind-address 설정을 0.0.0.0 으로 변경해 줍니다.

      sudo vim /etc/mysql/my.cnf

      위 화면에서 /etc/mysql/mysql.conf.d/에 들어가서 bind-address의 값을 0.0.0.0으로 변경해 줍니다.

    • mysql 재시작

      sudo service mysql restart


Reference