unity | webGL Game on Kubernetes

유니티 챌린지로 데모 게임(김치런)을 만들고 webGL Build 후 unity에 업로드 및 unity에서 제공하는 링크로 데모게임 까지 테스트 완료하였습니다. 하지만 개인 테스트 K8S에 업로드 하고 싶어서 이것저것 테스트 해보고 업로드 하게 되었습니다.

완성된 데모게임

https://play.unity.com/en/games/40499aa7-3656-45cf-ab52-60a0e0c10a82/demo2sdjo

Unity 설정 변경

에디터 종속을 제외하기위한 설정으로 압축방식을 바꿀겁니다.

https://docs.unity.cn/kr/2023.2/Manual/webgl-deploying.html

https://stackoverflow.com/questions/72453065/unable-to-parse-build-build-framework-js-br

Build 압축방식 변경

Edit > Project Settings > Player > Publishing Settings
Compression Format 값을 Gzip으로 변경합니다.

다음 빌드를 진행하고 나면 Build 폴더의 압축 파일들의 확장자가 *.gz로 변경된것을 확인 할 수 있습니다.

Docker Build

webGL로 Build된 것을 바로 Docker Build해서 띄워보려고했는데 http://localhost:5000번이 출력되었습니다.
그래서 0.0.0.0:5000로 빌드해보았는데 이것도 잘안되서 nginx 빌드로 하기로 하였습니다.

chatGPT의 도움과 몇가지 직접 손보면서..GPT가 가끔씩 헛소리를…해서 좀 고생하긴했습니다.

nginx.conf

몇번의 수정 끝에 성공한 환경 설정 파일입니다.
(gpt 뿐만 아니라 google검색도…gpt 맹신은 금물…)

wasm type에러가 계속 발생하는데 해당 부분을 잘 수정합니다.
https://github.com/buildstar-online/unity-webgl-nginx/blob/main/docker/nginx.conf

server {
    listen 80;
    server_name localhost;

    # Unity WebGL 빌드 파일의 기본 경로
    root /usr/share/nginx/html;

    index index.html;

    # 기본 파일 제공
    location / {
        try_files $uri $uri/ /index.html;
        expires max;
        add_header Cache-Control "public, max-age=31536000";
    }

    # Gzip으로 압축된 WebAssembly 파일 처리
    location ~ \.wasm\.gz$ {
        gzip_static always;
        add_header Content-Encoding gzip;
        add_header Cache-Control "public, max-age=31536000";
        default_type application/wasm;
        try_files $uri =404;
    }

    # Gzip으로 압축된 JavaScript 파일 처리
    location ~ \.js\.gz$ {
        gzip_static always;
        add_header Content-Encoding gzip;
        add_header Content-Type application/javascript;
        add_header Cache-Control "public, max-age=31536000";
        try_files $uri =404;
    }

    # Gzip으로 압축된 CSS 파일 처리
    location ~ \.css\.gz$ {
        gzip_static always;
        add_header Content-Encoding gzip;
        add_header Content-Type text/css;
        add_header Cache-Control "public, max-age=31536000";
        try_files $uri =404;
    }

    # Gzip으로 압축된 HTML 파일 처리
    location ~ \.html\.gz$ {
        gzip_static always;
        add_header Content-Encoding gzip;
        add_header Content-Type text/html;
        add_header Cache-Control "public, max-age=31536000";
        try_files $uri =404;
    }

    # Gzip으로 압축된 데이터 파일 처리
    location ~ \.data\.gz$ {
        gzip_static always;
        add_header Content-Encoding gzip;
        add_header Content-Type application/octet-stream;
        add_header Cache-Control "public, max-age=31536000";
        try_files $uri =404;
    }

    # 404 에러 페이지 처리
    error_page 404 /index.html;
}

Dockerfile 내용

# Nginx 기반 Docker 이미지
FROM nginx:alpine

# 기본 Nginx 설정 제거
RUN rm /etc/nginx/conf.d/default.conf

# Unity WebGL 빌드 파일 복사
COPY ./Build /usr/share/nginx/html/Build
COPY ./TemplateData /usr/share/nginx/html/TemplateData
COPY ./index.html /usr/share/nginx/html/

# Nginx 커스텀 설정 복사
COPY ./nginx.conf /etc/nginx/conf.d/default.conf

# 포트 공개
EXPOSE 80

# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]

샘플 테스트

빌드와 도커실행을 해볼 차례입니다.

docker build -t harbor.icurfer.com/unity/kimchirun-demo:0.1 .
docker run --name unity -p 8080:80 --name unity harbor.icurfer.com/unity/kimchirun-demo:0.1

잘 동작하는 것을 확인 할 수 있습니다.
그런데 화면 크기가 문제가 좀 있어보입니다…그래서 페이지를 채울수 있도록 index.html수정을 좀해보았습니다.

index.html 수정

<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Unity Web Player | demo</title>
    <link rel="shortcut icon" href="TemplateData/favicon.ico">
    <link rel="stylesheet" href="TemplateData/style.css">
    <style>
      #unity-container {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        overflow: hidden;
      }

      #unity-canvas {
        width: 100%;
        height: 100%;
        display: block;
      }
    </style>
  </head>
  <body>
    <div id="unity-container" class="unity-desktop">
      <canvas id="unity-canvas" tabindex="-1"></canvas>
      <div id="unity-loading-bar">
        <div id="unity-logo"></div>
        <div id="unity-progress-bar-empty">
          <div id="unity-progress-bar-full"></div>
        </div>
      </div>
      <div id="unity-warning"></div>
      <div id="unity-footer">
        <div id="unity-logo-title-footer"></div>
        <div id="unity-fullscreen-button"></div>
        <div id="unity-build-title">demo</div>
      </div>
    </div>
    <script>
      var canvas = document.querySelector("#unity-canvas");

      function resizeCanvas() {
        var container = document.querySelector("#unity-container");
        canvas.width = container.clientWidth;
        canvas.height = container.clientHeight;
      }

      window.addEventListener("resize", resizeCanvas);
      resizeCanvas();

      var buildUrl = "Build";
      var loaderUrl = buildUrl + "/demo3.loader.js";
      var config = {
        arguments: [],
        dataUrl: buildUrl + "/demo3.data.gz",
        frameworkUrl: buildUrl + "/demo3.framework.js.gz",
        codeUrl: buildUrl + "/demo3.wasm.gz",
        streamingAssetsUrl: "StreamingAssets",
        companyName: "DefaultCompany",
        productName: "demo",
        productVersion: "1.0",
        showBanner: function (msg, type) {
          var warningBanner = document.querySelector("#unity-warning");
          function updateBannerVisibility() {
            warningBanner.style.display = warningBanner.children.length ? "block" : "none";
          }
          var div = document.createElement("div");
          div.innerHTML = msg;
          warningBanner.appendChild(div);
          if (type === "error") div.style = "background: red; padding: 10px;";
          else {
            if (type === "warning") div.style = "background: yellow; padding: 10px;";
            setTimeout(function () {
              warningBanner.removeChild(div);
              updateBannerVisibility();
            }, 5000);
          }
          updateBannerVisibility();
        },
      };

      var script = document.createElement("script");
      script.src = loaderUrl;
      script.onload = () => {
        createUnityInstance(canvas, config, (progress) => {
          document.querySelector("#unity-progress-bar-full").style.width = 100 * progress + "%";
        }).then((unityInstance) => {
          document.querySelector("#unity-loading-bar").style.display = "none";
          document.querySelector("#unity-fullscreen-button").onclick = () => {
            unityInstance.SetFullscreen(1);
          };
          resizeCanvas();
        }).catch((message) => {
          alert(message);
        });
      };

      document.body.appendChild(script);
    </script>
  </body>
</html>

빌드 및 실행을 해봅니다.

docker build -t harbor.icurfer.com/unity/kimchirun-demo:0.2 .
docker run --name unity -p 8080:80 --name unity harbor.icurfer.com/unity/kimchirun-demo:0.2

여기까지 했으면 container image 생성이 완료 된 것입니다.
Private Registry를 갖고있어서 docker-hub가 아닌 harbor로 image를 저장하고 kubernetes로 배포 해보았습니다.

Kubernetes 배포 사전 설정

쿠버네티스는 container 이미지 생성단계만 지나면 무난히 할수 있으므로 설명은 생략합니다.
아래 내용은 제가 참고하려고 적어놓은 내용입니다.

dns레코드 추가.

nhncloud에있는 dns plus를 사용중입니다. ingress로 향하는 cname레코드를 추가해주었습니다.

HAProxy 설정

외부 노출 HAProxy 에 해당 도메인 접근시 테스트 환경 HAProxy로 넘겨주도록 설정.
테스트 환경 HAProxy에서 쿠버네티스 인그레스쪽으로 넘겨주도록 설정.

Manifests Yaml 파일

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kimchirun-dp
  namespace: unity-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kimchirun
  template:
    metadata:
      labels:
        app: kimchirun
    spec:
      containers:
        - name: kimchirun-con
          image: harbor.icurfer.com/unity/kimchirun-demo:0.1
          imagePullPolicy: Always
          ports:
            - containerPort: 80
      imagePullSecrets:
        - name: harbor-icurfer-private
---
apiVersion: v1
kind: Service
metadata:
  name: kimchirun-svc
  namespace: unity-web
spec:
  selector:
    app: kimchirun
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
# ssl-offloading 환경에서 ingress 설정 샘플.
piVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kimchirun-ing
  namespace: unity-web
spec:
  ingressClassName: nginx
  rules:
  - host: unity.icurfer.com
    http:
      paths:
      - backend:
          service:
            name: kimchirun-svc
            port:
              number: 80
        path: /
        pathType: Prefix

기타

게임플레이가 동작을하니 확실히 리소스가 증가하는게 체감이 되는 것도 확인 해볼 수 있었습니다.