EC2에 만들어둔 프로젝트 어플리케이션을 RDS DB와 연결해서 컨테이너로 올리는 실습을 해볼 것이다. 근데 이제 GitHub Actions를 통해 CI/CD를 곁들인.
RDS는 실행 준비되는 데 시간이 꽤 걸리므로 RDS를 먼저 생성해준 후, EC2를 생성하고 어플리케이션 컨테이너 실행에 필요한 세팅을 해주겠다.
지금은 별로 DB의 보안이 중요하지 않지만, 정말로 현업에서 고객의 정보가 담긴 DB라면 보안이 중요할 것이다. 그때는 현재의 나는 모르는 다른 설정을 통해 지켜줄 수도 있겠지만, 일단 현재의 내가 할 수 있는 선에서 보안을 지켜주기 위한 설정을 했다.
db-subnet에는 로컬 통신만 가능한 private 서브넷으로만 구성했고, 퍼블릭 액세스도 아니오로 선택했다.
해당 보안 그룹에 속한 EC2에서 오는 3306번 포트 요청만 허용하도록 보안 그룹을 설정했다.
EC2는 퍼블릭하게 만들 것이므로 public subnet에 위치시켰고, public IP도 자동 할당 활성화했다.
보안 그룹은 허용 포트를 이것저것 분산시켜놔서 여러 개 선택했다.
종합하면 HTTP/HTTPS 포트, 어플리케이션 실행을 위한 8080번 포트, DB를 위한 3306번 포트, SSH 연결을 위한 22번 포트를 열어놨다. 그리고 아까 private-rds-sg에서 소스로 설정한 first-sg 보안그룹은 꼭 선택해야 한다.
생성이 잘됐으면 EC2 내부에서 java 어플리케이션을 컨테이너로 올리기 위해 적합한 환경 세팅을 해줘야 한다.
sudo apt-get update
# JAVA 설치
sudo apt install -y openjdk-17-jdk
sudo vi /etc/environment
# JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
source /etc/environment
echo $JAVA_HOME
# Docker 설치
sudo apt install -y docker.io
sudo docker -v
echo $USER
tail /etc/group
sudo usermod -aG docker $USER
newgrp docker
tail /etc/group
세팅이 끝난 후 RDS가 사용 가능 상태가 되어 엔드포인트가 생긴다면, EC2에서 엔드포인트로 RDS 접근 가능한지 확인한다.
이 확인을 위해서는 EC2에 mysql을 설치해야 한다.
sudo apt install mysql-server-8.0
# 접근 가능한지 확인
mysql -u 사용자이름 -p -h RDS엔드포인트
접속이 잘된다. 나는 RDS 설정 시, 초기 데이터베이스 설정을 해놓아서 app이라는 데이터베이스를 확인할 수 있다.
확실히 하기 위해서 first-sg 보안그룹을 선택하지 않은 EC2에서 RDS에 접근이 가능한지 확인해보겠다.
비밀번호를 입력하고 한참을 반응이 없길래 그냥 멈춰주었다.
의도한 보안 설정이 잘 되었다!
DB로 RDS를 사용할 때 application.properties의
spring.datasource.url=jdbc:mysql://localhost:3306/app?serverTimezone=Asia/Seoul
localhost
를 RDS 엔드포인트로 바꿔줘야 하는데 이건 외부에 노출이 되면 안된다. 따라서 .gitignore로 application.properties를 제외시킨 후, ci 중에 application.properties 파일을 생성해줘야 한다.
이때 gitHub의 secrets 변수로 application.properties파일의 내용을 등록한다.
spring.application.name=demo
# DB
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://[RDS 엔드포인트]:3306/app?serverTimezone=Asia/Seoul
spring.datasource.username=[RDS 설정 시 만든 USERNAME]
spring.datasource.password=[RDS 설정 시 만든 PASSWORD]
# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
이것 외에도 EC2 관련 정보, DockerHub 관련 정보 모두 secrets 변수로 등록해야 한다.
CI/CD를 수행하는 yml 파일을 작성할 차례이다.
name: App With Github Actions with docker-compose CI/CD
on: push
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Make application-prod.properties
run: |
mkdir -p ./src/main/resources
cd ./src/main/resources
touch ./application.properties
echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./application.properties
cat ./application.properties
shell: bash
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew clean build -x test
- name: Naming jar file
run: |
ls ./build/libs/
mv ./build/libs/demo-0.0.1-SNAPSHOT.jar ./build/libs/app.jar
- name: Check jar file
run: ls ./build/libs
- uses: actions/upload-artifact@v4
with:
name: app
path: ./build/libs/app.jar
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/app-for-cicd-test:latest
cd:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Execute deployment script
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
sudo fuser -k -n tcp 8080 || true
docker stop app || true
docker rm app || true
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/app-for-cicd-test:latest
docker run -d -p 8080:8080 \
-e TZ=Asia/Seoul \
--name app \
${{ secrets.DOCKERHUB_USERNAME }}/app-for-cicd-test:latest
EC2에서 실행이 잘되는지 확인한다.
잘됐다!
요청에 대한 응답도 잘 온다!
이때 RDS를 연결할 때 주의할 점이 있다. RDS의 보안그룹이 소스를 특정 보안그룹으로 한정한 상태이므로, EC2 컨테이너를 별도의 독립적인 도커 네트워크를 생성해서 연결하면 RDS와 연결이 안된다.
새로 생성한 네트워크는 독립적인 네트워크 공간으로, EC2 인스턴스의 기본 네트워크 인터페이스와 격리되어 동작하므로, 컨테이너에서 나가는 네트워크 트래픽이 EC2 인스턴스의 기본 네트워크 인터페이스를 사용하지 않을 수 있다.
따라서, EC2 인스턴스의 네트워크 인터페이스에서 나오는 트래픽으로 간주되지 않으므로 RDS와 연결이 되지 않는다.
라고 생각했다....
분명히 오후에는 그랬다. 근데 저녁에 다시
"RDS를 private 서브넷에 위치시킨 상태에서도, RDS 보안그룹의 소스를 0.0.0.0/0 전체로 열어놓으면, 별도의 도커 네트워크에 연결한 어플리케이션 컨테이너에서 연결이 될까?" 를 실험해보았다.
내가 예상한 결과는 '안된다' 였다. 근데 되는 것이다.... 그리고 시험삼아 혹시나 해서 RDS 보안그룹의 소스를 특정 보안 그룹으로 다시 막아놨는데도 되는 것이다.... 뭐지? 반영이 늦나? 하고 삭제하고 새로 다시 만들었는데도 됐다.... 분명 오후에 했을 때는 안됐는데 그때는 다른 게 잘못됐었나 보다....ㅎㅎ
"EC2 인스턴스의 네트워크 인터페이스에서 나오는 트래픽으로 간주되지 않으므로" 이게 아니라 도커에서 생성된 사용자 정의 브리지 네트워크는 기본적으로 EC2 인스턴스의 네트워크 인터페이스를 게이트웨이를 설정한다고 한다. 사실 이게 좀 더 합리적인 방식이긴 하다. EC2 내부에서 도커 네트워크를 만들었으니 EC2 인스턴스의 네트워크 인터페이스를 통하는 게 정석적이다. 내가 도커 네트워크를 만들어서 컨테이너 올릴 때 연결이 안돼서 합리화하고 이해하기 위해서 주입식으로 잘못된 지식을 그런가보다 했다. 이제 사실을 알게 됐으니 되었다! 이 편이 이해하기도 더 쉽다!
사용한 docker-compose.yml 파일이다. 여기서 environment
설정은 있어도 되지만 application.properties에서 설정을 잘 해두었다면, 없어도 실행이 잘된다!
version: '3'
services:
backend:
container_name: backend-server
image: [dockerhub_username]/app-for-cicd-test:latest
ports:
- 8080:8080
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://[RDS 엔드포인트]:3306/app?serverTimezone=Asia/Seoul
SPRING_DATASOURCE_USERNAME: [설정한 username]
SPRING_DATASOURCE_PASSWORD: [설정한 password]
networks:
- app-network
networks:
app-network:
driver: bridge
정리 : 도커에서 생성된 사용자 정의 브리지 네트워크는 기본적으로 EC2 인스턴스의 네트워크 인터페이스를 게이트웨이를 설정한다. 따라서 컨테이너에서 외부로 나가는 트래픽은 EC2의 기본 네트워크 인터페이스를 통해 전달된다.
=> 사용자 정의 도커 네트워크에 연결된 컨테이너도 EC2의 보안 설정을 따른다!