최근 서버 환경을 쿠버네틱스로 변경했기 때문에 지속적 배포 방법을 변경해야 합니다. 오늘은 Github Action으로 구현해 볼 것입니다.

쿠버네틱스에 배포하는 방법은 간단합니다. 원하는 배포사항을 YAML 파일로 작성해 kubectl로 등록해주면 됩니다. 이후에 Github Action에서 SSH로 접속해 YAML 파일을 전달하고 kubectl을 실행해줄 것입니다.

도커 빌드 및 레지스트리 저장

레포지토리명과 브랜치 이름을 바탕으로 이미지 이름을 정할 것입니다. Github Secret 에는 도커 허브 로그인에 사용할 DOCKER_USERNAME와 DOCKER_PASSWORD를 등록하면 됩니다.

스크립트는 다음과 같습니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle>

name: Spring/Kotlin Coverage Test on main

on:
  push:
    branches: [ "main","dev","qa" ]

permissions:
  contents: write

jobs:
  Deploy-docker-image:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Build
        run: ./gradlew build

      - name: Create Test Coverage Report
        run: ./gradlew jacocoTestReport

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3.1.1
        with:
          file: ./build/reports/jacoco/test/jacocoTestReport.xml

      - name: Unpack JAR
        run: |
          mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

      - name: Set lowercase repository name without owner environment variable
        run: |
          echo "REPOSITORY_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV

      - name: Get lowercase branch name
        run: |
          echo "BRANCH_NAME=$(echo $GITHUB_REF_NAME | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Get docker image name
        run: |
          echo "IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:${{ env.BRANCH_NAME }}" >> $GITHUB_ENV

      - name: Build docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} .
          docker tag ${{ env.IMAGE_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Push docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker push ${{ env.IMAGE_NAME }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

Plain JAR 생성은 억제해야 합니다. 코틀린의 경우 build.gradle.kts에 아래와 같이 입력합니다.

tasks.getByName<Jar>("jar") {
    enabled = false
}

자바인 경우 build.gradle에 아래와 같이 입력합니다.

jar{
	enabled = false
}

이렇게 하면 dev 브랜치에 새로운 커밋이 생성될 때마다 그 커밋을 기준으로 도커 이미지를 빌드하고 푸시합니다. 그럼 latest와 dev 두 개가 만들어져 있을 것입니다. 꼭 latest를 만들어야 하는 것은 아닙니다.

EC2에 SSH로 접속하기

이제 SSH에 접속해보겠습니다. ssh-action을 사용하면 상당히 쉽습니다.

접속할 때는 ec2를 생성할 때 적용한 .pem 키 파일을 사용할 예정입니다.

1. 깃헙 시크릿 등록하기

깃헙 시크릿에는 총 3개를 등록해야 합니다.

  1. 사용자 이름 (AWS_USERNAME)
  2. EC2 호스트 주소(AWS_HOST)
  3. 시크릿 키(AWS_SECRET_ACCESS_KEY)

사용자 이름은 기본적으로 ec2-user 입니다.

호스트의 경우 ip 주소 또는 이를 가리키는 도메인을 의미합니다.

시크릿 키는 .pem 파일 값입니다.

메모장 또는 cat 명령어로 .pem 파일 내부의 값을 출력해 복사 후 시크릿으로 등록합니다.

2. 배포 스크립트 작성

k8s에 배포를 위한 스크립트를 작성합니다.

서비스와 배포에 대한 명세를 deploy.yml에 작성합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-demo-deploy
  labels:
    app: spring-demo
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-demo
  template:
    metadata:
      labels:
        app: spring-demo
    spec:
      containers:
        - name: spring-demo
          image: 'lee01042000/beamworks-backend:dev'
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: spring-service
spec:
  type: NodePort
  selector:
    app: spring-demo
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
      nodePort: 30007

3. Github Action k8s 배포 스크립트 작성

이제 deploy.yaml을 서버에 전달해 kubectl로 적용하는 스크립트를 작성합니다.

scp, ssh 액션을 사용하면 쉽게 가능합니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle>

name: Spring/Kotlin Coverage Test on main

on:
  push:
    branches: [ "main","dev","qa" ]

permissions:
  contents: write

jobs:
  Deploy:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Build
        run: ./gradlew build

      - name: Create Test Coverage Report
        run: ./gradlew jacocoTestReport

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3.1.1
        with:
          file: ./build/reports/jacoco/test/jacocoTestReport.xml

      - name: Unpack JAR
        run: |
          mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

      - name: Set lowercase repository name without owner environment variable
        run: |
          echo "REPOSITORY_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV

      - name: Get lowercase branch name
        run: |
          echo "BRANCH_NAME=$(echo $GITHUB_REF_NAME | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Get docker image name
        run: |
          echo "IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:${{ env.BRANCH_NAME }}" >> $GITHUB_ENV

      - name: Build docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} .
          docker tag ${{ env.IMAGE_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Push docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker push ${{ env.IMAGE_NAME }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Transport deploy.yaml to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.AWS_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          overwrite: true
          source: ./deploy.yaml
          target: ~/

      - name: Connect to AWS EC2 with SSH
        uses: appleboy/ssh-action@v0.1.8
        with:
          host: ${{ secrets.AWS_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          script: kubectl apply -f deploy.yaml

4. 배포 및 확인

지금까지 작성한 커밋을 dev 브랜치에 커밋하면 Github Action이 실행됩니다.

위와 같이 배포가 잘 이루어지면 서버에 가서 상태를 확인합니다.

아래처럼 새로운 파드가 돌아가면 성공입니다.

쿠버네틱스에 배포하는 방법은 간단합니다. 원하는 배포사항을 YAML 파일로 작성해 kubectl로 등록해주면 됩니다. 이후에 Github Action에서 SSH로 접속해 YAML 파일을 전달하고 kubectl을 실행해줄 것입니다.

도커 빌드 및 레지스트리 저장

레포지토리명과 브랜치 이름을 바탕으로 이미지 이름을 정할 것입니다. Github Secret 에는 도커 허브 로그인에 사용할 DOCKER_USERNAME와 DOCKER_PASSWORD를 등록하면 됩니다.

스크립트는 다음과 같습니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle>

name: Spring/Kotlin Coverage Test on main

on:
  push:
    branches: [ "main","dev","qa" ]

permissions:
  contents: write

jobs:
  Deploy-docker-image:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Build
        run: ./gradlew build

      - name: Create Test Coverage Report
        run: ./gradlew jacocoTestReport

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3.1.1
        with:
          file: ./build/reports/jacoco/test/jacocoTestReport.xml

      - name: Unpack JAR
        run: |
          mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

      - name: Set lowercase repository name without owner environment variable
        run: |
          echo "REPOSITORY_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV

      - name: Get lowercase branch name
        run: |
          echo "BRANCH_NAME=$(echo $GITHUB_REF_NAME | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Get docker image name
        run: |
          echo "IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:${{ env.BRANCH_NAME }}" >> $GITHUB_ENV

      - name: Build docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} .
          docker tag ${{ env.IMAGE_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Push docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker push ${{ env.IMAGE_NAME }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

Plain JAR 생성은 억제해야 합니다. 코틀린의 경우 build.gradle.kts에 아래와 같이 입력합니다.

tasks.getByName<Jar>("jar") {
    enabled = false
}

자바인 경우 build.gradle에 아래와 같이 입력합니다.

jar{
	enabled = false
}

이렇게 하면 dev 브랜치에 새로운 커밋이 생성될 때마다 그 커밋을 기준으로 도커 이미지를 빌드하고 푸시합니다. 그럼 latest와 dev 두 개가 만들어져 있을 것입니다. 꼭 latest를 만들어야 하는 것은 아닙니다.

EC2에 SSH로 접속하기

이제 SSH에 접속해보겠습니다. ssh-action을 사용하면 상당히 쉽습니다.

접속할 때는 ec2를 생성할 때 적용한 .pem 키 파일을 사용할 예정입니다.

1. 깃헙 시크릿 등록하기

깃헙 시크릿에는 총 3개를 등록해야 합니다.

  1. 사용자 이름 (AWS_USERNAME)
  2. EC2 호스트 주소(AWS_HOST)
  3. 시크릿 키(AWS_SECRET_ACCESS_KEY)

사용자 이름은 기본적으로 ec2-user 입니다.

호스트의 경우 ip 주소 또는 이를 가리키는 도메인을 의미합니다.

시크릿 키는 .pem 파일 값입니다.

메모장 또는 cat 명령어로 .pem 파일 내부의 값을 출력해 복사 후 시크릿으로 등록합니다.

2. 배포 스크립트 작성

k8s에 배포를 위한 스크립트를 작성합니다.

서비스와 배포에 대한 명세를 deploy.yml에 작성합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-demo-deploy
  labels:
    app: spring-demo
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-demo
  template:
    metadata:
      labels:
        app: spring-demo
    spec:
      containers:
        - name: spring-demo
          image: 'lee01042000/beamworks-backend:dev'
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: spring-service
spec:
  type: NodePort
  selector:
    app: spring-demo
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
      nodePort: 30007

3. Github Action k8s 배포 스크립트 작성

이제 deploy.yaml을 서버에 전달해 kubectl로 적용하는 스크립트를 작성합니다.

scp, ssh 액션을 사용하면 쉽게 가능합니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle>

name: Spring/Kotlin Coverage Test on main

on:
  push:
    branches: [ "main","dev","qa" ]

permissions:
  contents: write

jobs:
  Deploy:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Build
        run: ./gradlew build

      - name: Create Test Coverage Report
        run: ./gradlew jacocoTestReport

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3.1.1
        with:
          file: ./build/reports/jacoco/test/jacocoTestReport.xml

      - name: Unpack JAR
        run: |
          mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

      - name: Set lowercase repository name without owner environment variable
        run: |
          echo "REPOSITORY_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV

      - name: Get lowercase branch name
        run: |
          echo "BRANCH_NAME=$(echo $GITHUB_REF_NAME | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Get docker image name
        run: |
          echo "IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:${{ env.BRANCH_NAME }}" >> $GITHUB_ENV

      - name: Build docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} .
          docker tag ${{ env.IMAGE_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Push docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker push ${{ env.IMAGE_NAME }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Transport deploy.yaml to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.AWS_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          overwrite: true
          source: ./deploy.yaml
          target: ~/

      - name: Connect to AWS EC2 with SSH
        uses: appleboy/ssh-action@v0.1.8
        with:
          host: ${{ secrets.AWS_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          script: kubectl apply -f deploy.yaml

4. 배포 및 확인

지금까지 작성한 커밋을 dev 브랜치에 커밋하면 Github Action이 실행됩니다.

위와 같이 배포가 잘 이루어지면 서버에 가서 상태를 확인합니다.

아래처럼 새로운 파드가 돌아가면 성공입니다.

이전 시간에는 미니큐브에서 스프링 프로젝트와 MySQL을 실행하고, 포트포워딩을 사용하여 외부에서 접속하는 방법을 배웠습니다. 그러나 이 방법으로는 하나의 서비스만 접속할 수 있습니다. 만약 여러 개의 서비스를 운영하고 있다면, 파이썬 기반의 플라스크나 장고, 자바 기반의 스프링, 그리고 자바스크립트 기반의 노드제이에스 등을 함께 사용하는 경우 라우팅이 필요합니다. 이번 시간에는 쿠버네티스에서 라우팅할 수 있는 방법을 배우겠습니다.

domain.com/spring → spring project

domain.com/flask → flask project

1. 샘플 프로젝트 만들기

  1. 스프링의 경우, 이전 시간에 만들었던 것을 그대로 활용할 예정입니다.
  2. 파이썬 프로젝트를 만들기 싫다면, 스킵하고 아래 flask-deploment.yaml 에 명시된 샘플 이미지를 그대로 사용할 수 있습니다.
  3. 플라스크의 경우, 다음과 같이 코드를 작성합니다.
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, Flask!'

if __name__ == '__main__':
    app.run(host="0.0.0.0",port=80, debug=True)

  1. Dockerfile 작성
# Use an official Python runtime as the base image
FROM python:3.9

WORKDIR /app

COPY . .

RUN pip install Flask

EXPOSE 80
# Run the app.py file
CMD ["python", "app.py"]

  1. 빌드 (ec2를 x86으로 생성해줬으니 맞춰서 이미지를 만들어줄것입니다. )
docker build -t lee01042000/flask-test --platform linux/amd64 .

  1. 실행
docker run -it -p 8080:80 lee01042000/flask-test

  1. 크롬에서 8080 포트로 테스트해보세요. 만약 "Hello, Flask!"가 잘 나오면 끝입니다!
  2. 쿠버네틱스가 이미지를 가져오게 하기 위해 도커 허브에 푸쉬합니다. 만약 로그인이 안되어 있다면 로그인을 하고 진행하시면 됩니다.
docker push lee01042000/flask-test

2. Python Deployment & Service 작성하기

이전 포스트에서 스프링 프로젝트를 실행한 것처럼, 서비스와 배포를 명시한 python-deploy.yaml을 작성할 것입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-deployment
  labels:
    app: flask
spec:
  replicas: 1
  selector:
    matchLabels:
      app: flask
  template:
    metadata:
      labels:
        app: flask
    spec:
      containers:
      - name: flask
        image: 'lee01042000/flask-test'
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: flask-service
spec:
  type: NodePort
  selector:
    app: flask
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 80
      nodePort: 30008

kubectl apply -f python-deploy.yaml 을 이용해 쿠버네틱스에 적용하면 배포와 서비스를 실행할 수 있습니다.

minikube ip 명령어를 사용하여 IP 주소를 찾은 다음, curl {minikubeip}:30008을 실행하여 "Hello, Flask!"가 출력되면 성공입니다.

2. Ingress로 라우팅 구현하기

쿠버네틱스에서는 ingress를 사용하여 서비스 라우팅이 가능합니다.

이렇게 ingress는 요청을 먼저 받아들여 적절한 서비스로 라우팅할 수 있습니다.

URL 디렉터리를 기반으로 하거나, 요청 헤더에 포함된 호스트명을 가지고도 라우팅이 가능합니다.

L7 스위치라고 생각하시면 이해하기 쉽습니다.

2.1 Nginx ingress controller 설치

ingress 룰을 적용하기 위해선 룰을 구현하는 ingress controller가 필요합니다. 공식 홈페이지에서는 다양한 ingress controller를 찾아볼 수 있지만, 이 문서에서는 nginx를 사용해보겠습니다.

https://kubernetes.io/ko/docs/tasks/access-application-cluster/ingress-minikube/

  1. NGINX 인그레스 컨트롤러를 활성화하기 위해 다음 명령어를 실행합니다.
  2. minikube addons enable ingress

2.2 ingress 룰 작성

참고로 nginx.ingress.kubernetes.io/rewrite-target 옵션을 설정하지 않으면 http://minikubeip/spring 으로 요청시 /spring이 url에 포함되서 포워딩이 되니 / 로 명시해야합니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - http:
      paths:
      - path: /spring
        pathType: Prefix
        backend:
          service:
            name: spring-service
            port:
              number: 8080
      - path: /flask
        pathType: Prefix
        backend:
          service:
            name: flask-service
            port:
              number: 80

  1. kubectl apply -f ingress.yaml 명령어로 적용합니다.
  2. curl {minikubeip}/flask & curl {minikubeip}/spring을 쳤을 때 사이 좋게 나오면 성공입니다!

3. 외부 접속 연결하기

아직은 ec2 내부에서만 접속이 가능합니다. 즉, client → server — x → ingress → service → pod인 상태입니다.

그럼 이제 서버포트랑 미니큐브 사이만 연결해주면 됩니다.

이를 포트포워딩이라고 합니다.

가장 친숙한 방법은 nginx를 사용하는 것입니다.

3.1 EC2에 Nginx를 설치

sudo amaxon-linux-extras install nginx1

3.2 EC2에 라우팅 정책 설정

minikube ip 가 192.168.49.2 인 경우 아래와 같이 server.location을 설정합니다.

# For more information on configuration, see:
#   * Official English Documentation: <http://nginx.org/en/docs/>
#   * Official Russian Documentation: <http://nginx.org/ru/docs/>

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See <http://nginx.org/en/docs/ngx_core_module.html#include>
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        error_page 404 /404.html;
        location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }

	location / {
            proxy_pass         http://192.168.49.2/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
        }
    }

# Settings for a TLS enabled server.
#
#    server {
#        listen       443 ssl http2;
#        listen       [::]:443 ssl http2;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        ssl_certificate "/etc/pki/nginx/server.crt";
#        ssl_certificate_key "/etc/pki/nginx/private/server.key";
#        ssl_session_cache shared:SSL:1m;
#        ssl_session_timeout  10m;
#        ssl_ciphers PROFILE=SYSTEM;
#        ssl_prefer_server_ciphers on;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        error_page 404 /404.html;
#            location = /40x.html {
#        }
#
#        error_page 500 502 503 504 /50x.html;
#            location = /50x.html {
#        }
#    }

}

3.3 재설치 및 재실행

sudo systemctl restart nginx

4. 테스트

이제 EC2서버에 접속해보겠습니다.

아래와 같이 잘 출력되면 성공입니다!

들어가며

지난글 (https://beoks.tistory.com/86)에선 API를 이용해서 AWS Lex와 대화할 수 있는 기능을 구현해보았습니다. 이번에는 발화 의도(Intent)를 업로드할 수 있는 API를 개발해보려고 합니다. 지난 글과 상당히 유사한 부분이 많기 때문에 이전 글을 많이 인용할 예정입니다.

1. 역할 생성

AWS Lex API 를 관리하기 위한 역할과 정책을 정의합니다. 여러 서비스를 이용하는 경우 각 서비스 마다 목적에 따라서 권한을 나누어 관리하는 것이 좋습니다. 권한을 정의할 정책과 정책을 적용한 역할을 만들어봅시다.

  1. IAM Console(https://console.aws.amazon.com/iam/) 에 루트 사용자로 로그인합니다.
  2. 엑세스 관리 > 역할 부분에서 역할 만들기를 선택합니다.

  1. 엔티티 선택에서 Lambda를 선택하고 다음을 클릭합니다.

  1. 권한 추가 단계는 생략합니다.
  2. 역할 생성이후 별도의 인라인 권한을 명시할 것이기 때문입니다.
  3. 역할 이름을 명시한 후 역할 생성 버튼을 클릭합니다.
  4. IAM 콘솔 > 엑세스 관리 > 역할에서 생성했던 역할 이름을 검색합니다.
  5. 권한 추가 > 정책 연결을 클릭합니다. (인라인으로 별도의 정책을 선언해 연결 할 수도 있습니다.)

여기에서 필요한 권한을 정책으로 등록할 수 있습니다. 지금은 편의를 위해 이미 존재하는 AmazonLexFullAccess 권한을 추가하겠습니다. 권한 정책 검색에서 해당 권한을 추가하고 체크 후 정책연결을 클릭해주세요.
 

2. 람다 함수 생성

AWS Lex 와 통신하기 위한 람다 함수를 생성합시다. 여기에서 이전에 정의한 역할을 사용합니다.

  1. AWS Lambda Console에 접속해서 함수 생성을 클릭합니다.
  2. 함수 이름 등 기본정보를 입력합니다. 여기서는 NodeJS를 사용해보겠습니다.
  3. 기본 실행 역할 변경에서 이전에 생성했던 역할을 선택합니다.

3. 람다 함수 코드 업로드

이제 Lex를 다루기 위한 람다 함수 코드를 작성해보겠습니다. Lex V2 Models SDK (https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-lex-models-v2/index.html) 에서 Lex V2 를 관리하기 위한 API 를 확인할 수 있습니다. 

1. npm init을 통해 새로운 NodeJS 프로젝트를 생성합니다.

2. npm install @aws-sdk/client-lex-models-v2 를 입력해 필요한 패키지를 설치합니다.

3. 이 패키지를 이용해 질문을하면 간단하게 초기 응답을 실행하는 의도를 생성하는 코드를 index.js에 작성합니다. 이 때, 의도의 이름(intentName)은 영어 또는 숫자를 사용해야만합니다. 저는 별도의 아이디를 생성하고 이를 질문과 응답과 함께 API에 전달한다고 가정하고 아래와 같이 코드를 작성했습니다. ( API 요청 형식이 궁금하다면 아래 5.테스트를 참고해주세요)

const { LexModelsV2Client, CreateIntentCommand } = require("@aws-sdk/client-lex-models-v2");

exports.handler = async (event, context, callback) => {
	try {
		const client = new LexModelsV2Client({ region: 'us-east-1' });
		const qnAList=event
		let response
		for(let i=0;i<qnAList.length;i++){
			const question=qnAList[i].question
			const answer=qnAList[i].answer
			const dataId=(qnAList[i].id).toString()
			const initialResponseSetting ={
				initialResponse:{
					messageGroups:[
						{
							message:{
								plainTextMessage: {
									value:answer
								}
							}
						}
					]
				}
			}
			const input = {
				'botId':process.env.BOT_NAME,
				'botVersion': process.env.BOT_VERSION,
				'intentName': dataId,
				'sampleUtterances':[{
					utterance : question
				}],
				'initialResponseSetting':initialResponseSetting,
				'localeId' : "ko_KR"
			};
			const command = new CreateIntentCommand(input);
			response = await client.send(command);
		}
		callback(null,response.messages)
	} catch(e) {
		console.log(e);
		callback(e);
	}
};

4. 코드를 작성했다면, 람다에 업로드하기 위해 압축을 진행합니다.

zip -r project.zip . #For Mac user

4. 람다 함수 환경 변수 설정

코드를 보면 일부 파라미터가 process.env 로 정의된 것을 알 수 있습니다. 이는 환경변수를 참조한다는 뜻입니다. 따라서 이에 맞는 환경변수를 입력해야 합니다.

  1. 선택한 람다 함수에서 → 구성 → 환경변수에 진입합니다.

  1. BOT_NAME 은 연결하려는 봇의 아이디를 의미합니다. Lex Console 에서 봇을 선택하면 봇 ID를 확인할 수 있습니다.

3. BOT_VERSION은 봇의 특정 버전을 의미합니다. 의도를 업로드하고자 하는 봇의 버전을 선택합니다. 초안 버전의 경우 DRAFT를 입력하면 됩니다.

5. 테스트

이제 전체적인 람다 함수에서 테스트를 해볼 수 있습니다.

  1. 람다 함수 테스트 → 테스트 이벤트에서 새로운 이벤트를 생성합니다.실행 결과를 확인해 테스트가 잘 수행되는것을 확인할 수 있습니다.

6. API Gateway 연결

이제 람다 함수를 API Gateway 와 연결해 HTTPS 로 접근할 수 있도록 합시다.

  1. API Gateway 콘솔에서 REST API 생성을 클릭합니다.
  2. 리소스 → 작업 → 메서드 생성 → POST Method 를 생성합니다.
  3. 이전에 생성한 람다 함수와 연결합니다.
  4. 설정이 완료되면 메서드를 테스트 할 수 있습니다.

7. API 배포

API를 등록하고 테스트를 완료했다면 외부에서 접속하는 것을 허락하기 위해 배포를 진행할 수 있습니다.

  1. 리소스 → 작업 → 배포를 선택합니다.

  1. 적절한 스테이지를 설정합니다. 스테이지는 dev, test, QA, prod 등의 배포 단계를 의미합니다.)

  1. 스테이지 편집기에서 외부에서 API를 호출할 수 있는 URL을 흭득할 수 있습니다.

스테이지에 표시된 URL을 통해 요청을 수행하면 발화 의도를 업데이트하는 API 완성입니다!

들어가며

오늘은 회사에서 새로운 프로젝트를 시작하기 위해 리액트 프로젝트를 자동으로 AWS에 배포하는 과정을 진행했고 마주쳤던 이슈에 대해서 정리해보고자 합니다. 이 포스트는 아래와 같이 단계별로 구성되어 있습니다. 

1. S3로 배포하기

2. CloudFront로 배포 강화하기 - 예정

3. Github Action으로 배포 자동화하기 - 예정

 

기존 배포 과정과 문제점

기존에는 AWS 서비스에 대해서 잘 몰라 Nginx를 이용해서 정적 파일을 제공하도록 설계하고 이를 EC2에 올려 배포했습니다. 여기서 문제점은 단순히 정적 파일을 제공하는데 EC2라는 자원은 상당히 비싸다는 것입니다. 웹 페이지의 경우 특정 시간에는 접속이 낮은 경우가 많은데 EC2로 배포를 진행하게 될 경우 시간당 과금이 발생하기 때문에 불필요한 과금이 지속되기 때문입니다. 또한 단순히 정적 파일을 제공하는데 연산에 필요한 자원들은 거의 필요가 없기에 EC2의 자원낭비가 심했습니다.

 

문제 정의

해결방법을 모색하기 위해 지금 프로젝트에 필요한 요소들을 우선순위 기준으로 정의했습니다.

1. 정적 파일을 HTTP 기반으로 제공가능한 기능

2. 부하 분산 기능

3. TLS를 적용 기능

4. API 요청의 경우 리버스 프록시 기능

 

솔루션 탐색

AWS에서 제공하는 서비스 중에 위를 만족하는 서비스는 다음과 같았습니다.

1. 정적 파일을 HTTP 기반으로 제공 가능한 기능 : S3

2. 부하 분산 기능 : CloudFront

3. TLS를 적용 기능 : CloudFront

4. API 요청의 경우 리버스 프록시 기능 : CloudFront

탐색 결과, 운이 좋게 대부분의 요구사항이 CloudFront에서 제공하는 것을 알게 되었습니다. 

 

S3에 프로젝트 배포

1. S3 버킷 생성

S3 콘솔로 접속 후, 왼쪽 메뉴에서 버킷을 선택하고 버킷 만들기를 클릭합니다.

버킷 이름은 원하는 서비스 이름을 입력합시다. 여기서는 react-test-project라고 입력하겠습니다.

객체 소유권의 경우 ACL 활성화를 선택합니다. 

모든 퍼블릭 액세스 차단을 해제하고 확인을 클릭합니다. 우리는 정적 웹 사이트 호스팅에 이를 사용할 것이기 때문에 퍼블릭 액세스를 허용해야 합니다. 나머지 옵션 설정은 생략하고 버킷 만들기를 클릭합니다.

2. 파일 업로드

버킷을 생성했다면 해당 버킷에 들어가봅시다. 비어있는 객체 칸을 처음으로 확인할 수 있습니다. 여기에 배포할 파일을 업로드할 수 있습니다. AWS CLI를 이용하면 쉽게 업로드할 수 있습니다. 콘솔을 이용해도 되지만, 우리는 개발자니까 한번 시도해봅시다. 초기 설정은 어려울 수 있어도 이 시간이 지나면 편안함이 찾아옵니다. 우선 아래 링크를 통해 각자 환경에 맞게 AWS CLI를 설치합니다.

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

 

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

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

docs.aws.amazon.com

 

이제 IAM을 이용해서 배포 권한만 가지고 있는 사용자 계정을 만들어 보겠습니다. 루트 계정을 사용해도 배포를 할 수 는 있지만, 루트 계정의 인증 정보가 다른 곳에 유출되면 굉장히 위험합니다. 따라서 목적에 따라 최소한의 권한을 가진 계정을 생성해서 사용하는 것이 좋습니다. AWS IAM Console에 접속해봅시다. 콘솔 -> 사용자 -> 사용자 추가를 통해서 새로운 사용자를 만들 수 있습니다.

리액트 프로젝트를 배포할 TestReactDeployer라는 이름을 입력하고 CLI를 사용므로 자격 증명 유형은 액세스 키를 선택합니다. 

다음으로 사용자에게 줄 권한을 선택해봅시다. 권한은 AmazonS3FullAccess 에는 배포에 필요한 권한이 모두 들어있습니다. 하지만 필요 없는 권한도 있습니다. 세부적으로 정말 필요한 권한만 설정하면 좋겠지만 이 포스트의 목적과는 거리가 멀어 생략하고 AmazonS3 FullAccess를 사용하겠습니다. (힌트를 드리자면 세부적인 권한을 선택하기 위해선 IAM 정책을 활용할 수 있습니다. )

마지막으로 액세스 유형, 권한 을 확인하고 사용자를 생성합니다.

 생성한 사용자의 엑세스 키와 비밀 액세스 키를 확인할 수 있습니다. AWS CLI에서 인증할 때 필요한 정보이니 CSV를 소중히 다운로드해줍시다. 

 

이제 AWS CLI를 이용해 인증을 진행해봅시다.

프로파일을 설정하면 여러 자격증명 및 설정을 따로 관리할 수 있습니다. 위와 같이 커맨드를 입력하고 CSV에 포함된 키를 각각 입력합니다. 나머지 옵션도 위와 같이 입력해줍니다.

이제 업로드할 정적 파일로 이동합시다. 저는 Vite를 사용하고 있기 때문에 빌드를 하면 dist 디렉터리에 정적 파일이 생성됩니다. 일반적으로 build 디렉토리에 정적파일이 생성되니 상황에 따라 적절하게 이동하면 됩니다.

정적 파일 디렉터리로 이동하고 위와 같은 커맨드를 입력합니다. s3://[버킷 주소]를 입력하고 사용할 자격증명은 이전에 저장한 프로파일로 설정합니다. 이렇게 하면 한 줄의 커맨드로 아래와 같이 한 번에 필요한 파일들을 배포할 수 있습니다. 

3. 정적 웹 사이트 호스팅

자 이제 이 버킷을 정적 웹 사이트 호스팅용으로 변경해봅시다. 

선택한 버킷에서 속성을 선택합니다.

스크롤을 내리면 맨 아래 속성에서 정적 웹 사이트 호스팅을 확인해볼 수 있습니다. 편집을 누르고 정적 웹 사이트 호스팅을 활성화합니다. 

위와 같이 호스팅 유형을 설정하고 인덱스 문서를 버킷에 업로드한 파일 중 기본 페이지 파일 이름으로 설정합니다. 대부분 index.html 입니다:) 이제 변경사항을 저장합니다.

다시 속성을 확인하면 위와 같이 엔드포인트 주소를 확인할 수 있습니다. 저 링크로 접속해서 원하는 프로젝트에 접속이 가능하다면 성공입니다! 

다음

위 설정만으로 웹 사이트를 배포할 수 있습니다. 하지만 조금 아쉽습니다. HTTP가 아니라는 점, 접속자 수가 급증하면 부하 분산이 불가능하다는 점, 여러 지역에서 접속할 경우 거리마다 정적 파일이 제공되는 시간이 다르다는 점 그리고 API 서버와 연결하기 어렵다는 점 등 실제 서비스에 적용하기에는 부족한 점이 많습니다. 이 부족한 점을 CloudFront로 해결해보겠습니다. 

 

'Web' 카테고리의 다른 글

CSS - Element가 배치되는 방법들  (0) 2020.01.18
Servlet - Hellow World 출력하기(intellij)  (0) 2020.01.18
Web - Event & EventListener  (0) 2020.01.18
Ajax 통신 정의, 필요성, 예제  (0) 2020.01.18
JSP(Java Server Page) - 정의, 문법  (0) 2020.01.18

+ Recent posts