Container/Kubernetes

GKE에서 프런트/백엔드 (포드)통신 구조 만들기

제이널 2023. 1. 16. 02:16

배경

GKE 쿠버네티스 환경에서 프런트엔드 앱이 백엔드에 요청하는 걸 테스트하고 싶었지만, 요청 URL이 localhost인지 개별 IP인지 같은 외부 IP를 사용해야 할지 도무지 감이 안 왔습니다. 우선 쿠버네티스의 네트워크 방식을 이해해야 했고, 아래 글을 통해 Ingress 오브젝트에 대해 많이 배우게 되었습니다.

https://bcho.tistory.com/1263

 


Ingress 구조

 

보안을 위해서 비공개 클러스터로 생성했기 때문에 외부로부터 들어오는 트래픽을 수신하려면 따로 설정해주어야 하는데, 여러 방법이 있겠지만 경험 상으로는 LoadBalancer 타입의 서비스를 생성해 외부 주소를 생성해주는 방법과, Ingress를 사용해 외부 트래픽을 라우팅 해주는 방법이 있었습니다. 이 글에선 백엔드 REST API를 구현하기 위해 Ingress 오브젝트를 사용해 라우팅해 줄 것입니다.

 

실제 구현하다보니 알게된 것은 Ingress를 사용하기 위해선 몇 가지 조건이 있다는 것이었습니다.

  1. Ingress Controller가 필수이며 Controller 없이 Ingress 혼자 동작하지 않습니다. 인그레스 컨트롤러는 여러 유형이 있는데, 이 글에선 가장 많이 사용되는 Nginx Ingress를 사용했습니다.
  2. Pod의 서비스는 NodePort 타입이어야 합니다.

 

아래 그림은 Ingress 구현에 대한 대략적인 구조를 보여줍니다.

 

 

Ingress Controller가 사용자의 요청을 받고, Ingress 오브젝트에 정의된 URL 규칙에 따라 서비스 오브젝트로 요청을 전달하고, 이를 서비스 자신과 연결되어 있는 무작위 포드에게 전달합니다. 무작위 포드로 전달하는 이유는 포드의 장애에 대비해서 한 애플리케이션의 포드를 여러 개 띄울 수 있기 때문입니다.

 

이제 위 같은 구조를 GKE 환경에서 직접 구현해 보겠습니다.

 


목차

  1. 프런트엔드 준비
  2. 백엔드 준비
  3. 도커 파일 빌드 / 푸시
  4. Ingress Controller 설치 / Ingress 배포
  5. 프런트엔드 / 백엔드 배포

1. 프런트엔드 준비

이 글에선 Nextjs 기본 앱을 사용했습니다. 프로젝트를 생성하려면 아래 명령어를 입력해 주세요.

npx create-next-app frontend && cd frontend
npm i axios
  • 타입스크립트를 사용하려면 프로젝트 생성 명령어 뒤에 --typescript를 붙여주시면 됩니다.

 

다음으로 /pages/index.js에 백엔드로 요청을 보내는 간단한 코드를 작성합니다.

import axios from 'axios'

export default function Home() {
  const data = axios('/api/blue');

  return (
    <>
      <div>{JSON.stringify(data)}</div>
    </>
  )
};

 

다음으로 도커 파일로 빌드하기 위한 Dockerfile과 k8s에서 배포에 사용할 yaml 파일을 프로젝트 루트 위치에 생성해줍니다.

 

Dockerfile

# 공식 레포에서 지원해주는 걸 사용
# Install dependencies only when needed
FROM node:16-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi


# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN yarn build

# If using npm comment out above and use below instead
# RUN npm run build

# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

 

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-prod
  # Deployment 오브젝트의 라벨 정보
  labels:
    app: frontend-prod
    tier: frontend
    environment: production
spec:
  selector:
    # Deployment 오브젝트가 파드를 관리할 때 사용하는 매핑 정보
    matchLabels:
      app: frontend-prod
  template:
    metadata:
      # 생설될 파드의 라벨 정보
      labels:
        app: frontend-prod
    spec:
      containers:
      - name: frontweb-lesson-prod-app
        image: gcr.io/YOUR/PATH:frontend
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: front-prod-svc
spec:
  # 서비스를 연결할 포드 라벨 정보
  selector:
    app: frontend-prod
  type: NodePort
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 3000
  • 참고: 이 글에선 도커 이미지를 GCR에 푸시해서 사용합니다.

Deployment 오브젝트를 만들어 한 개의 Pod를 생성해주고, template의 라벨 항목인 app을 서비스 오브젝트에서 selector로 연결해주었습니다. Nextjs의 서버는 3000번 포트에 띄워지고, 80번으로 들어온 요청을 컨테이너의 3000번 포트로 포워딩 해줍니다.

 

이렇게 하면 프런트엔드는 준비가 완료됩니다.

 

2. 백엔드 준비

백엔드 폴더를 원하는 곳에 새로 생성해주고, index.js 파일을 만들어 줍니다. 저는 편의를 위해 server 폴더를 Nextjs 프로젝트 안에 생성해주었습니다.

 

index.js

let os = require('os');

let http = require('http');
let handleRequest = function(request, response) {
  response.writeHead(200);
  response.end("Hello World! I'm User server "+os.hostname() +" \n");
}

let www = http.createServer(handleRequest);
www.listen(8080);

 

위 코드는 node.js 기반으로, 8080번 포트로 요청이 들어오면 문자열을 응답해주는 간단한 서버입니다.

프런트엔드와 마찬가지로 도커 파일과, yaml을 만들어 줍시다.

 

Dockerfile

From node:alpine
 
WORKDIR /usr/app
COPY ./ /usr/app
 
RUN npm install
 
CMD ["node", "index.js"]

 

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-prod
  # Deployment 오브젝트의 라벨 정보
  labels:
    app: backend-prod
spec:
  selector:
    # Deployment 오브젝트가 파드를 관리할 때 사용하는 매핑 정보
    matchLabels:
      app: backend-prod
  template:
    metadata:
      # 생설될 파드의 라벨 정보
      labels:
        app: backend-prod
    spec:
      containers:
      - name: backend-prod-app
        image: gcr.io/YOUR/PATH:backend
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: backend-prod-svc
spec:
  selector:
    app: backend-prod
  type: NodePort
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 8080

 

이제 백엔드 쪽도 준비가 완료됐습니다.

 

3. 도커 파일 빌드

두 프로젝트 폴더를 GCP 환경으로 옮겨서 도커 파일을 빌드하고 푸시하겠습니다. CI/CD를 사용하면 편리하지만 테스트 용이기 때문에 그냥 GCP Shell에서 git clone으로 코드를 내려받아서 진행했습니다.

 

GCP Shell에서 아래 명령어를 사용해 도커 이미지를 빌드/푸시해 주세요.

# 저는 그냥 Next 프로젝트에 백엔드도 모두 포함시켜서 한번에 옮겼습니다.
git clone YOUR_GIT_REPOSITORY_URL

cd YOUR_PROJECT_PATH
docker build --tag gcr.io/YOUR/PATH:frontend
docker push gcr.io/YOUR/PATH:frontend

cd YOUR_BACKEND_PATH
docker build --tag gcr.io/YOUR/PATH:backend
docker push gcr.io/YOUR/PATH:backend

 

4. Ingress Controller 설치 / Ingress 배포

이제 Ingress Controller 중 하나인 Nginx Ingress를 먼저 설치해줍니다.

 

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

 

이제 컨트롤러 서비스가 잘 돌아가는지 확인해봅니다.

 

kubectl get svc -n ingress-nginx

 

EXTERNAL-IP가 잘 생성되었는지 확인해주세요

 

다음으로 Ingress를 생성할 yaml 파일을 만들어서 배포해줍니다.

 

ingerss.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-test
spec:
  ingressClassName: nginx # nginx ingress controller와 연결에 필요
  rules:
  - http:
      paths:
      - path: /
        pathType: ImplementationSpecific
        backend:
          service:
            name: frontend-prod-svc
            port:
              number: 80
      - path: /api/blue
        pathType: Prefix
        backend:
          service:
            name: backend-prod-svc
            port:
              number: 80
  • IngressClassName으로 위에서 설치한 Ingress Controller와 연결해줍니다.
  • service:name 항목엔 아까 만들었던 프런트엔드와 백엔드의 서비스 이름을 적어 연결해줍니다.

 

위와 같이 생성하면, 80번 포트로 들어오는 요청 중 정확히 '/'와 일치하는 요청은 프런트엔드 서비스로 보내고, '/api/blue'로 시작하는 URL은 백엔드 서비스 쪽으로 보내주게 됩니다. 서비스들은 각자 연결된 앱에 요청을 전달해 주겠죠.

 

이제 ingress.yaml 파일을 배포해줍시다.

 

kubectl create -f ingress.yaml
kubectl get ing

 

여기서 오류가 난다면 아래 명령어를 실행하고 다시 배포해주세요.

kubectl delete -A ValidatingWebhookConfiguration ingress-nginx-admission

 

 

5. 프런트엔드 / 백엔드 배포

이제 프런트엔드와 백엔드의 deployment.yaml 파일을 배포하면 세팅은 완료됩니다.

아래 명령어로 배포해주세요.

 

kubectl create -f /YOUR_FRONTEND_PATH/deployment.yaml
kubectl create -f /YOUR_BACKEND_PATH/deployment.yaml

kubectl get svc
kubectl get pods

 

프런트엔드/백엔드의 서비스와 포드가 잘 표시되는지 확인해 주세요.

 


배포가 완료되었습니다!

이제 Ingress의 외부 IP로 접속해서 결과물을 확인해보죠.

 

외부 IP는 'kubectl get ing' 명령어 또는 'kubectl get svc -n ingress-nginx' 명령어를 통해 알 수 있습니다.

 

https://EXTERNAL_IP

 

배포된 Nextjs 앱이 백엔드에 요청을 보냈고, 백엔드가 응답한 결과를 프린트한 화면이 나오는 걸 확인할 수 있습니다.

 


마치며

단순하고 쉽게 보였지만 막상 구현하려고 하니 엄청난 삽질을 했었던 경험이었습니다. 쿠버네티스 네트워킹에 대해 아주 조금 이해할 수 있었네요. 그래도 실제로 시행착오를 겪으며 이것저것 시도해보니 이것저것 알게 되었고 한 걸음 더 나아간 기분이 들었습니다. 저 같은 분들께 도움이 되었으면 좋겠습니다.