커뮤니티 플랫폼 프로젝트

[AWS]백엔드 배포 흐름 정리-1

gotopm 2025. 3. 17. 20:13
본 포스트는 단계별로 상세히 기술한 것이 아니라 흐름별로 정리하여
처음 배포할 때 따라하기엔 부적합합니다.
배포를 한 번 완료해 본 상태에서 흐름 정리용으로 보실 것을 권합니다.

전체적인 시스템 흐름도

0. 먼저 백엔드의 CORS 관련 코드에 프론트 배포된 도메인을 추가한다.

CORS는 어떤 Origin에서 온 요청을 허용할 지 판단하는 것으로 여기에 도메인을 추가해준다.

1. AWS에서 EKS, RDS, ElastiCache(Redis OSS)를 생성한다.

1-1.EKS

백엔드 배포는 EKS 클러스터를 통해 진행하므로 EKS를 먼저 생성한다.

생성시에 태그값을 입력하는 칸이 있는데 EC2 AutoScailing까지 염두해두고 아래와 같은 태그값을 붙였다.

- 키:k8s.io/cluster-autoscaler/enabed,값:true -> 클러스트 오토스케일러가 이 클러스터의 노드 그룹을 감지하고 자동으로 스케일링을 조정한다.

- 키:k8s.io/cluster-autoscaler/클러스터 이름, 값:true -> 이 노드 그룹은 특정 클러스터에 귀속되어 있다는 의미로 , cluster autoscaler가 이 노드 그룹이 어떤 클러스터에 속하는 지 판단할 때 필요한 태그다.

 

EKS 클러스터가 생성되면 컴퓨팅-노드그룹에 들어가 노드를 추가해준다.

그러면 자동으로 워커노드(EC2)가 생성되게 된다.

 

그리고 추가로 ECR(Elastic Container Registry)도 생성해주는데 우리가 스프링을 도커이미지로 감싸서 ECR에 올릴 것 이기 때문이다.

 

1-2.RDS

DB 서버가 될 RDS도 생성한다.

생성 후, 수정 탭에 들어가 '퍼블릭 액세스' 불가능 -> '퍼블릭 액세스' 가능으로 바꿔준다.

퍼블릭 액세스 가능은 RDS 데이터베이스 인스턴스를 인터넷에서 접근 가능하도록 하는 것인데

사실 EC2와 같은 VPC안에 존재하기 때문에 퍼블릭 액세스 불가능이어도 통신에는 문제가 없다.

우리가 가능으로 바꾸는 것은 워크벤치(DB클라이언트 툴) 사용을 위해 가능으로 열어둔 것이다.

 

워크벤치로 들어가 나중에 스프링이 돌아갈 스키마를 미리 create 해둔다.

 

1-3.ElastiCache(Redis OSS)

ElastiCache를 생성할 땐, 고급설정에서 '전송 중 암호화' 에 체크 해제를 한다.

전송 중 암호화는 클라이언트와 레디스 서버 간에 주고 받는 데이터를 암호화하는 것이다.

레디스는 초당 수십만 건 이상의 빠른 처리가 강정인 인메모리 데이터베이스인데,

암호화 기능을 하면 성능이 다소 저하되기 때문이다.

어차피 레디스와 서버는 같은 VPC 내에서 통신하기 때문에 기본적으로 보안이 강화되어있어 전송 중 암호화를 체크해제 해주는 경우가 많다.

 

2. 백엔드 코드를 도커 이미지로 빌드한다.

백엔드 서버를 실행하는데 필요한 환경(코드+설정+라이브러리)등을 통째로 하나의 패키지(이미지)로 만들어야한다.

먼저 도커 이미지로 빌드하기전에 application.yml에 하드코딩된 중요 정보값들을 환경 변수로 대체하고 

프로그램 실행시 혹은 배포시에 시스템 환경 변수에서 값을 불러올 수 있도록 한다.

(이 환경 변수는 클러스터의 secret에서 불러오도록 할 것이다)

 

2-1.application.yml파일 수정

spring :
  config:
    activate:
      on-profile: prod
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://${DB_HOST}:3306/TTT
    username: admin
    password: ${DB_PW}
  jpa:
    database: mysql
    database-platform: org.hibernate.dialect.MariaDBDialect
    generate-ddl: true
    hibernate:
      ddl-auto: create
    show_sql: true
  redis:
    host: ${REDIS_HOST}
    port: 6379
  rabbitmq:
    host: rabbit-service
    port: 5672
    username: guest
    password: guest
    virtual-host: /
  servlet:
     multipart:
      max-file-size: 10MB
      max-request-size: 20MB
logging:
  level:
    root: info
    
jwt:
  #  be11-2nd-4dollorExit-TikTakTalkbe11-2nd-4dollorExit-TikTakTalkbe11-2nd-4dollorExit-TikTakTalk
  secretKey: YmUxMS0ybmQtNGRvbGxvckV4aXQtVGlrVGFrVGFsa2JlMTEtMm5kLTRkb2xsb3JFeGl0LVRpa1Rha1RhbGtiZTExLTJuZC00ZG9sbG9yRXhpdC1UaWtUYWtUYWxr
  expiration: 3000 #3000?
#  tttisthecommunitysiteforstudentofhanhwatttisthecommunitysiteforstudentofhanhwatttisthecommunitysiteforstudentofhanhwa
  secretKeyRt: dHR0aXN0aGVjb21tdW5pdHlzaXRlZm9yc3R1ZGVudG9maGFuaHdhdHR0aXN0aGVjb21tdW5pdHlzaXRlZm9yc3R1ZGVudG9maGFuaHdhdHR0aXN0aGVjb21tdW5pdHlzaXRlZm9yc3R1ZGVudG9maGFuaHdh
#  200일
  expirationRt: 288000


cloud:
  aws:
    credentials:
      access-key: ${AWS_KEY}
      secret-key: ${AWS_SECRET}
    region:
      static: ap-northeast-2
    s3:
      bucket: ttt-image

# 휴대폰 API 관련설정
coolsms:
  apiKey: ${APIKEY}
  apiSecret: ${APISECRET}
  fromNumber: ${PHNUMBER} # 발신번호

# oauth 로그인 설정
oauth:
  google:
    client-id: ${CLIENT_ID}
    client-secret : ${CLIENT_SECRET}
    redirect-url: https://www.jy1187.shop/oauth/google/redirect
  kakao:
    client-id: ${KCLIENT_ID}
    redirect-url: https://www.jy1187.shop/oauth/kakao/redirect

(JWT 토큰 관련 시크릿 키도 사실 환경변수로 대체하는 게 맞다...)

2-2. 루트 폴더 바로 하위에  dockerfile을 작성한다.

# 필요프로그램 설치
FROM openjdk:17-jdk-alpine as stage1
# 파일 복사
WORKDIR /app
COPY gradle gradle
COPY src src
COPY build.gradle .
COPY gradlew .
COPY settings.gradle .
# 빌드
RUN chmod 777 gradlew
RUN ./gradlew bootJar

# 두 번째 스테이지
FROM openjdk:17-jdk-alpine
WORKDIR /app
COPY --from=stage1 /app/build/libs/*.jar app.jar

# 컨테이너 내부 시간 한국시간으로 맞추는 설정.
RUN apk update && apk add --no-cache tzdata
RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
RUN echo "Asia/Seoul" > /etc/timezone

# 실행 : CMD 또는 ENTRYPOINT를 통해 컨테이너를 배열 형태의 명령어로 실행
ENTRYPOINT ["java", "-jar", "app.jar"]

 

"docker build -t 이미지이름:버전 ." 을 통해 이미지로 빌드한다.

이때 이미지이름은 본인 ECR레포지토리 경로로한다.
ex. docker build -t 211125660657.dkr.ecr.us-east-1.amazonaws.com/ttt:latest .

 

그러면 만들어진 이미지를 이제 ECR로 푸시해야한다.

그런데 먼저 도커가 내 ECR에 대한 접근을 가능하도록 ECR로그인 처리를 해놓아야 푸시가 가능하다.

 

-aws configure 명령어 입력-> 액세스,시크릿 키를 입력한다(= AWS 리소스에 접근할 수 있는 인증정보를 설정하는 것이다.)

- aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 211125660657.dkr.ecr.us-east-1.amazonaws.com(본인 ECR레포지토리 주소 com까지만) 명령어를 입력하여 도커가 ECR에 로그인한다.

 

이후, docker push 211125660657.dkr.ecr.us-east-1.amazonaws.com하면 ECR에 이미지가 올라간다.

 

3. 이제  ECR에서 내려받을 이미지가 생겼으니, 파드와 서비스를 만드는 스크립트를 작성한다.

아래 코드를 보면 스프링에 대한 deployment와 service, 래빗엠큐에 대한 deployment와 service를 만드는 것을 알 수 있다.

container의 이미지를 보면 우리가 ECR에 올려둔 이미지라는 것을 알 수 있다.

그리고 env 설정을 통해 application.yml의 변수처리 해두었던 값들을 jy-app-secrets(쿠버네티스 클러스터에 만들 secret)에서 꺼내오는 것을 알 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ttt-backend
  namespace: ttt-ns
spec:
#  replicas: 2 hpa에게 관리 맡기기 위해 주석처리로 변경
  selector:
    matchLabels:
      app: ttt-pod
  template:
    metadata:
      labels:
        app: ttt-pod
    spec:
      volumes:
        - name: tz-seoul
          hostPath:
            path: /usr/share/zoneinfo/Asia/Seoul
      containers:
        - name: ttt-container
          image: 211125660657.dkr.ecr.ap-northeast-2.amazonaws.com/ttt:latest
          ports:
            - containerPort: 8080
          resources:
            #컨테이너가 사용할 수 있는 리소스의 최대치를 지정
            limits:
              cpu: "1"
              memory: "500Mi"
            #컨테이너가 시작될 때 보장받아야 하는 최소 리소스 양을 지정
            #쿠버네티스는 이 요청을 만족할 수 있는 노드에 컨테이너를 스케줄링
            requests:
              cpu: "0.5"
              memory: "250Mi"
          volumeMounts:
            - name: tz-seoul
              mountPath: /etc/localtime
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: prod
            - name: DB_HOST
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: DB_HOST
            - name: DB_PW
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: DB_PW
            - name: REDIS_HOST
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: REDIS_HOST
            - name: AWS_KEY
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: AWS_KEY
            - name: AWS_SECRET
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: AWS_SECRET
            - name: APIKEY
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: APIKEY
            - name: APISECRET
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: APISECRET
            - name: PHNUMBER
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: PHNUMBER
            - name: CLIENT_ID
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: CLIENT_ID
            - name: CLIENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: CLIENT_SECRET
            - name: KCLIENT_ID
              valueFrom:
                secretKeyRef:
                  name: jy-app-secrets
                  key: KCLIENT_ID
          # 컨테이너 상태 확인
          readinessProbe:
            httpGet:
              # healthcheck 경로
              path: /ttt/user/check
              port: 8080
            # 컨테이너 시작 후 지연
            initialDelaySeconds: 20
            # 확인 반복 주기
            periodSeconds: 10
            # 요청이 완료되어야 하는 시간
            timeoutSeconds: 1
            # 연속 성공 횟수
            successThreshold: 1
            # 연속 실패 횟수
            failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
  name: ttt-service
  namespace: ttt-ns
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: ttt-pod

---
#MQ
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rabbit
  namespace: ttt-ns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rabbit
  template:
    metadata:
      labels:
        app: rabbit
    spec:
      containers:
        - name: rabbitmq
          image: rabbitmq:management
          ports:
            - containerPort: 5672
            - containerPort: 15672
---
apiVersion: v1
kind: Service
metadata:
  name: rabbit-service
  namespace: ttt-ns
spec:
  type: ClusterIP
  ports:
    - name: amqp
      port: 5672
      targetPort: 5672
    - name: management
      port: 15672
      targetPort: 15672
  selector:
    app: rabbit

 

그러면 , 실행시에 jy-app-secrets라는 곳에서 중요값들을 주입받도록 설정해놓았으니 jy-app-secrets에 저 값들을 넣어놓는다.

 

예시.

kubectl create secret generic jy-app-secrets --from-literal=REDIS_HOST=my-redis-011.ab2rc6.0201.kan2.cache.amazonaws.com --from-literal=DB_HOST=database-1.ct2fake7t3.ap-northeast-2.rds.amazonaws.com --from-literal=DB_PW=Wdvv23PkWydsZpjjYNxv --from-literal=AWS_KEY=AFAKEFAKE5G75DUHR --from-literal=AWS_SECRET=TGOI5jfakeFAKELIElielNaddbe+Odsdsd22Cq --from-literal=APIKEY="NCASDSDEWEQWESDW" --from-literal=APISECRET="SDJIOSDJKOSJDKLSDJKLSJDDSDSVEQTXOSA" --from-literal=PHNUMBER="01012345678" --from-literal=CLIENT_ID=9232323232323234-bjdsdsdsdsdsdsdlvbg.apps.googleusercontent.com --from-literal=CLIENT_SECRET=GASDSADSADjh6DdsdsdsSDSDSDSs1mQr_ --from-literal=KCLIENT_ID=d4ASDasdasdasdcdwewewewe8cd9 -n ttt-ns

 

그런다음 해당 스크립트를 kubectl apply -f 스크립트 이름 하여 스크립트에 작성된 Deployment와 service를 생성해준다.

4.Ingress Controller와 서버도메인 생성.

그리고 외부에서 서비스 접근할 때 효율적인 라우팅을 담당하는 Ingress Controller를 생성한다.

ingress는 어디서 들어온 요청을 어느 서비스로 보내는 지 정리한 규칙이고,

ingress controller는 이 ingress에 따라 실질적인 라우팅을 실행하는 주체다. 

ingress를 생성하기 이전에 아래 명령어를 통하여 클러스터 내에  ingress controller를 생성해주어야한다.

 

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.1/deploy/static/provider/aws/deploy.yaml  

 

이때 자동으로 ingress controller역할을 하는 로드밸런서가 생성된다.

그리고 클라이언트가 사이트에 접속하는 도메인(www~) 외에,

프론트단에서 백엔드 API로 통신하기 위한 별도의 도메인을 route53에 

아래와 같이 생성해준다. 값은 자동으로 만들어진 로드밸런서의 엔드포인트!

(이제 배포환경에서는 백엔드도 local에 있는 것이 따로 클라우드 서버(쿠버네티스 클러스터)에 존재한다.

따라서 프론트엔드에서 백엔드에 접근하려면 접근 가능한 고유 주소 (=도메인)이 필요하기 때문에)생성한 것이고,

거기에 로드밸런서의 주소값을 넣으면 ingress controller역할을 하는 로드밸런서로 요청이오고 이 요청을 ingress에 따라 service로 보내고 service는 다시 pod로 보내어 pod안에 있는 스프링이 데이터를 받을 수 있게 되는 것)

 

그리고 프론트에서 서버로 보내는 요청을 VUE_APP_API_BASE_URL로 공통화 해두고 localhost:8080으로 보내던 걸,

위에서 설정한 서버도메인으로 바꾼다. 

 

 

5.Ingress와  HTTPS 처리를 담당할 cert manager 리소스 스크립트도 작성한다.

 

우리는 앞선 프론트엔드 배포에서 클라이언트(브라우저)에서 CloudeFront로 보내는 통신구간이 HTTPS암호화 처리 되었다.

그러면 CloudeFront를 거쳐 Vue 앱이 백엔드 API 호출을 하게 되는데, 즉 Vue에서 백엔드로 넘어올 때도 HTTPS를 유지해야 클라이언트에서 부터 백엔드까지 통신이 안전하게 암호화가 되는 것이다. 따라서 이 과정(Vue->백엔드)에서 HTTPS처리를 쉽게 하기 위해 담당하는 것이 아래의 Cluster issuer(어디서 인증서를 발급받을 지 요청하는 설정파일)와 certificate(어떤 도메인에 대해 요청을 받는지 정의하는 실질적인 발급요청서)다.

 

먼저 아래 스크립트에 정의된 cluster issuer와 certificate리소스를 사용하기 위해선, 아래 두 명령어로 

첫번째 명령어는 cert-manager에서 사용하는 리소스 타입(cluster issuer,certificate 등)을 쿠버네티스 클러스터에 등록하는 작업

두번째 명령어는 cert-manager 프로그램을 클러스터에 설치하고 실행하는 단계.(cert-manager가 cluster issuer와 certificate를 기반으로 인증서를 발급받는다)

 

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.5.0/cert-manager.crds.yaml

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.5.0/cert-manager.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ttt-ingress
  namespace: ttt-ns
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1
    cert-magager.io/cluster-issuer: my-issuer
spec:
  tls:
    - hosts:
        - "server.jy1187.shop"
      secretName: server-jy1187-com-tls
  rules:
    - host: server.jy1187.shop
      http:
        paths:
          - path: /(.*)
            pathType: Prefix
            backend:
              service:
                name: ttt-service
                port:
                  number: 80

---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: my-issuer
  namespace: ttt-ns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: rifkehtm862@gmail.com
    privateKeySecretRef:
      name: my-issuer
    solvers:
      - http01:
          ingress:
            class: nginx
---
# 3.ClusterIssue를 사용하여 Certificate 리소스 생성 : Certificate리소스 생성시에 인증서 발급
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: server-jy1187-com-tls
  namespace: ttt-ns
spec:
  secretName: server-jy1187-com-tls
  duration: 2160h #90day
  renewBefore: 360h #before 15day
  issuerRef:
    name: my-issuer
    kind: ClusterIssuer
  commonName: server.jy1187.shop
  dnsNames:
    - server.jy1187.shop

 

역시 kubectl apply -f 스크립트이름 하여 ingress와  cluster-issuer, certificate를 만든다.

 

따라서 최종적으로 사용자가 www도메인을 입력하면 cloudeFront를 통해 s3에 있는 웹호스팅 서버로 라우팅.

거기서 요청을 보내면 프론트에서 ingress contoller가 받고 ingress controller는 이 요청을 다시 service에 보내고 service는 해당 파드로 연결하여 데이터 통신이 완성되어진다.

 

+자동화

루트 폴더 바로 하위에 .github/workflows에 아래 스크립트.yml파일을 추가한다.

아래 스크립트에 보면 필요한 액세스,시크릿 값은 깃 레포지토리 secret에 저장해둔다.

스크립트를 자세히 보면, 위에서 했던 과정이 적혀있는 것을 알 수 있다.

name: deploy order order-backend

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: checkout github
        uses: actions/checkout@v2

      - name: install kubectl
        uses: azure/setup-kubectl@v3
        with:
          version: "v1.25.9"
        id: install

      - name: configure aws
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_KEY }}
          aws-secret-access-key:  ${{ secrets.AWS_SECRET }}
          aws-region: ap-northeast-2

      - name: update cluster infomation
        run: aws eks update-kubeconfig --name my-cluster --region ap-northeast-2

      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: build and push docker image to ecr
        env:
          REGISTRY: 211125660657.dkr.ecr.ap-northeast-2.amazonaws.com
          REPOSITORY: ttt
          IMAGE_TAG: latest
        run: |
          docker build \
          -t $REGISTRY/$REPOSITORY:$IMAGE_TAG \
          -f Dockerfile .
          docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG

      - name: eks kubectl apply
        run: |
          kubectl apply -f ./k8s/tttdepl.yml
          kubectl apply -f ./k8s/tttingress.yml
          kubectl rollout restart deployment ttt-backend -n ttt-ns

 

728x90