Private Subnet & Public Subnet 구축 및 aws parameter store를 이용한 CI/CD 구축 도전기(1)

지누·2024년 7월 31일
2

아키텍처를 대충 그려보면, 위와 같은 구조를 만들고 있었다.
하나의 VPC 내부에 두 개의 서브넷을 생성한다.
public subnet 내부에는 스프링부트 서버를 구동한다.
private subnet 내부에는 mysql 서버를 구동한다.

두 개의 서브넷이 private인지, public인지 구분하는 것은 vpc의 라우팅 테이블이다.

1. IAM Role과 AWS parameter store를 이용한 환경변수 관리.

스프링부트 서버 내에서 s3 라든지, parameter store 라든지 aws에서 운영하는 서비스를 사용하려면, 자격증명(사용자에 대한 인증)이 반드시 필요하다.

인증을 하려면 account의 아이디와 패스워드를 이용하여 로그인을 하든가, 권한을 가진 useraccess keysecret access key 값을 필요로 한다.

지금까진, 해당 값을 관리하기 위해 두 가지 방법을 사용 해 왔다,

  1. application.yml에서 .gitignore로 관리하여 gradle build 단계에서 yml을 주입 해 줌.
  2. 서버 인스턴스의 환경변수로 관리하여 jar를 실행할 때 환경변수 주입

두가지 방법 모두 나름 안전한 방식일 수 있으나 어느정도 문제점이 존재한다.

  1. 누군가 빌드 과정을 열어보거나 jar파일을 까보는 경우, 환경변수가 그대로 노출됨
  2. 누군가 서버 인스턴스에 접속한 경우, 환경변수가 그대로 노출됨.
    -> 모든 권한을 가지는 aws user를 통해 ec2외에, 모든 aws service가 악용되어버림.

따라서, 이번 프로젝트에서 모든 권한을 가지고 있는 user access keysecret access key코드나 인스턴스 내부의 환경 변수로 관리하지 않는 것을 목표로 했다.

aws service에 접근하는 권한을 user의 role을 활용하기로 하였다.

IAM Role을 활용하는 자세한 부분은
이전 글을 참고하도록 하자.

2. VPC, public subnet, private subnet 생성

VPC 생성

CIDR을 10.0.0.0/16로 가지는 VPC를 생성한다.

subnet 2개 생성

ip대역은 크게 중요하지 않다.
10.0.1.0/2410.0.2.0/24 대역의 서브넷을 각각 생성했다.

첫 번째 서브넷은 public subnet으로, 두 번째 서브넷은 private subnet으로 만들 것이다.

3. 두개의 ec2와 보안그룹 생성, IAM Role 주입

까먹었을까봐, 나는 위 그림처럼 두 개의 ec2 인스턴스를 각각의 보안그룹으로 감쌀 예정이다.

3.1. public Security Group & 스프링부트 서버 ec2

public subnet에 포함되고 스프링부트 서버가 돌아갈 보안그룹이다.
(보안그룹은 public/private 개념이 없지만 편의상 저렇게 부르겠다.)
외부의 요청을 받야아하므로, 보안그룹의 인바운드 설정에서 80번, 443번, 8080번의 포트를 열어둔다.

ec2를 생성 할 때, 위의 보안그룹에 포함되게 만든다.

그리고 나는 IAM Role을 이용하여 ec2내에서 aws service에 대한 권한을 얻으므로, IAM 인스턴스 프로파일을 설정 해 주도록 하자.

해당 role에는 S3FullAccessSSMFullAccess 정책이 부여되었다.

3.2. private Security Group & 데이터베이스 서버 ec2

private subnet에 포함되고, 데이터베이스 서버가 돌아갈 보안그룹이다.

해당 서브넷에서는 스프링부트 서버 ec2에서 들어오는 요청만을 받아야 한다.
따라서 보안그룹의 인바운드 설정에 22번, 3306번 포트를 열어두고, 0.0.0.0/0이 아닌 3.1의 public security group에서 오도록 소스를 설정하자.

4. Internet Gateway 생성

subnet을 만든 것 만으로는 public인지 private인지 구분할 수 없다.
해당 subnet이 외부(VPC 외부)와 통신이 가능해야 public subnet이 되는 것이다.

따라서, VPC에 외부와 통신 가능한 인터넷 게이트웨이(IGW)를 생성한다. 그리고 우리가 만든 VPC에 연결 해 준다.

5. public Routing table 생성

vpc에는 여러개의 라우팅 테이블이 존재 가능하다. 그리고 vpc내부에 존재하는 subnet의 라우팅 요청을 처리할 수 있는 적절한 라우팅 테이블을 연결 해 주어야 한다.

이때, VPC 대역의 바깥으로 하는 요청에 대해, IGW로 가도록 설정 해 주면, public subnet이 되는 것이다.

위 사진은 VPC에 생성한 라우팅 테이블이다. vpc의 ip 대역인 10.0.0.0/16이 아닌 요청을 전부 인터넷 게이트웨이로 보냄으로서 외부와 소통할 수있는 public vpc가 된 것이다.

지금까지의 과정은 아래의 블로그에 더 자세히 나와있으니, 사진 등은 참고하도록 하자.
https://growth-coder.tistory.com/169


[!] 데이터베이스 서버 인스턴스에 mysql 설치하기

이제 public subnet에 존재하는 ec2 인스턴스에 접속하여, 외부와 통신할 수 있게 되었다.
private subnet에 존재하는 ec2 인스턴스도, 22번 포트를 열어놓았으므로 ssh 연결을 통해 접속 가능할 것이다.

우리는 본 인스턴스를 데이터베이스로 사용 할 것이므로, mysql을 설치하도록 하자.

지금은private subnet을 외부 인터넷과 연결 가능하게 해서 사진이 없지만, 아마 네트워크에 연결할 수 없어서 mysql 설치가 거부 될 것이다.

간단하게, 외부 인터넷으로 요청을 하려면 public ip를 가져야 한다.
따라서 public subnet 내부에 NAT Gateway를 만들어 해당 요청을 Internet Gateway로 보냄으로서 private subnet에서 외부 인터넷으로 트래픽을 보낼 수 있다.

6. NAT Gateway와 private routing table 구성

public subnet 영역에 존재하는 NAT Gateway를 생성하고, elastic IP까지 할당 해 주도록 하자.

이후 mysql을 설치하면, 정상적으로 설치가 될 것이다!


인프라 구축 완료..?

여기까지는, 아래 이미지와 같은 서버 아키텍처를 구축하는 과정이었다.
중간중간 문제가 많았지만, 레퍼런스가 많아서 할 만했다.

7. dev.yml & prod.yml 분리

parameter store에서 값을 읽어올 수 있는 role이 부여된 곳은 public subnet 내에 존재하는 ec2이다. 따라서, 로컬에서는 parameter store에 접근하는 코드가 필요 없기 때문에, 개발 환경을 분리하였다.

application.yml

//application.yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        format_sql: true
    database-platform: org.hibernate.dialect.MySQLDialect
    open-in-view: false

  s3:
    bucket: ${S3_BUCKET}
  region:
    static: ${REGION}

application-dev.yml

//application-dev.yml
#

application-prod.yml

//application-prod.yml
config:
  type: aws-parameterstore:/config/caecae/

spring:
  config:
    import: ${config.type}

개발환경과 배포환경에서 공통적으로 쓰이는 코드는 application.yml에 넣고, 각각의 환경에 종속된 설정만 코드에 추가하기로 하였다. 현재까지는 개발환경에서만 쓰이는 설정이 없기 때문에 파일을 만들기만 하였다.

8. CI-CD workflow

CI-CD가 동작하는 과정에 대해서는 자세히 적지 않겠다. 다만 누군가에게 도움이 될 수 있으므로 기록만 남겨놓겠다.

//workflow/cicd.yml

name: Gradle Build

on:
  push:
    branches: [ "main", "develop" ]

jobs:
  build:

    runs-on: ubuntu-24.04
    permissions:
      contents: read

    steps:
    - uses: actions/checkout@v4
    
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'oracle'

    - name: Gradle Caching
      uses: actions/cache@v3
      with:
        path: |  
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew build
      shell: bash

    ###### CD ######
    - name: Docker build & push
      run: |
        docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
        docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE }} .
        docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE }}

    - name: Deploy
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ${{ secrets.EC2_USERNAME }}
        key: ${{ secrets.EC2_PRIVATE_KEY }}
        envs: GITHUB_SHA
        script: |
          sudo docker rm -f $(docker ps -qa)
          sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE }}
          sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE }}
          sudo docker image prune -f

그리고 이후 CI-CD 테스트를 하는 과정에서 알 수 없는 에러에 부딪혀 이틀동안 코드만 보고 있었는데, 해결한 과정을 적도록 하겠다 ^__^


아래는 인프라 구축 과정에서 참고한 블로그들이다! 이것 외에도 정말 많은 레퍼런스를 참고했지만, 크게 도움되었던 두 개의 블로그만 적도록 하겠다.

profile
열심히 살자😱

0개의 댓글