Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

pkg로 Node.js 애플리케이션의 하나의 바이너리로 만들기

pkgzeit에서 만든 Node.js 바이너리 컴파일러이다. 보통 Node.js로 애플리케이션을 만들면 실행 머신에 애플리케이션에 맞는 Node.js가 설치되어 있어야 하고 npm installl로 관련 모듈을 설치하고 npm start나 다른 실행방식으로 애플리케이션을 실행하거나 사용해야 한다. 사용자가 다 Node.js 개발자라면 큰 문제가 아니지만 타 언어 개발자나 일반 사용자를 생각하면 편한 환경은 아니라고 할 수 있다. pkg가 Node.js 컴파일러라는 의미는 Node.js까지 내장해서 하나의 실행 파일로 다른 의존성 없이 실행할 수 있게 만들어 준다.

이런 방식이 편하다고 느끼기 시작한 건 Go 언어로 만들어진 프로그램을 사용하면서부터다. 나 같은 경우는 HashiCorp에서 만든 Vault, Consul, Terraform 등을 사용하면 이런 방식이 편하다고 생각하게 되었다. 내 맥북에 Go 언어 환경이 없어도 바이너리 파일 하나만 다운로드 받아서 실행하면 되었고 하나의 파일로 서버와 클라이언트를 모두 사용할 수 있어서 꽤 편했다. 보통 다른 언어의 프로그램을 다운받아서 사용하려면 한두 번씩은 의존성 문제나 버전이 일치하지 않는 문제로 고생했던 터라 더 편하게 느껴진 것 같다.

pkg를 위한 환경 구성

pkg를 사용하기 위한 간단한 예제를 만들어 보자. 애플리케이션의 종류는 여기서 크게 중요하지 않으므로 Express.js의 제너레이터를 사용해서 기본 Express 앱을 만들어 보자.

$ npx express .

  warning: the default view engine will not be jade in future releases
  warning: use `--view=jade' or `--help' for additional options


   create : .
   create : ./package.json
   create : ./app.js
   create : ./public
   create : ./routes
   create : ./routes/index.js
   create : ./routes/users.js
   create : ./views
   create : ./views/index.jade
   create : ./views/layout.jade
   create : ./views/error.jade
   create : ./bin
   create : ./bin/www
   create : ./public/stylesheets
   create : ./public/stylesheets/style.css

   install dependencies:
     $ cd . && npm install

   run the app:
     $ DEBUG=pkg-test:* npm start

   create : ./public/javascripts
   create : ./public/images

바이너리를 만들기 전에 의존성을 설치되어 있어야 하므로 npm install로 의존성을 설치한다.

pkg를 사용하기 위해 npm install --save-dev pkg로 설치한다. pkg는 빌드용 모듈이므로 devDependencies로 추가했다.

{
  ...
  "bin": {
    "app": "./bin/www"
  },
  "scripts": {
    "build": "pkg ."
  },
  ...
}

pkg 명령어를 사용하려고 package.json의 스크립트로 추가했다. 이렇게 하면 npm run buildpkg . 명령어를 실행할 수 있다.(요즘은 npm으로 전역 모듈은 거의 설치하지 않는 편이다) 그리고 pkg가 바이너리를 만들기 위한 엔트리 포인트를 지정해야 하는데 bin 프로퍼티를 이용해서 지정한다. 여기서 express 애플리케이션의 실행하는 파일, 즉 시작 파일이 ./bin/www 이므로 이 파일을 지정했다. 이 시작 파일을 기준으로 pkgrequire()를 추적하고 필요한 파일을 찾아서 컴파일한다.

추가로 Express 예제 같은 경우 템플릿으로 Jade를 사용하고 있다. 이 뷰 엔진 같은 경우는 require()로 불러오는 것이 아니라 아래와 같이 사용하게 된다.

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

그래서 pkg가 이 views 디렉터리를 자동으로 바이너리 파일에 포함하지 않는다. 이런 경우 바이너리 파일에 넣어줄 파일(다른 수정 없이)을 직접 지정해 주어야 하는데 package.json에 다음과 같이 지정할 수 있다.

{
  ...
  "pkg": {
    "assets": "views/**/*"
  }
}


pkg로 바이너리 생성하기

$ npm run build

> pkg-test@0.0.0 build /Users/outsider/example
> pkg .

> pkg@4.3.1
> Targets not specified. Assuming:
  node8-linux-x64, node8-macos-x64, node8-win-x64

기본 타겟 플랫폼인 Linux와 macOS, Windows를 위한 바이너리가 생성되었다.

.
├── [ 40M]  example-linux
├── [ 40M]  example-macos
└── [ 28M]  example-win.exe

위처럼 생성된다. 기본 express 예제는 포함된 의존성이 많지 않음에도 Node.js가 바이너리 안에 포함했기 때문에 용량이 적지는 않다. 여기서 example이라는 이름은 package.jsonname에서 온 것이다.

실제 이 파일이 Linux에서 다른 의존성 없이도 실행될 수 있는지 확인해 보자. Docker로 Ubuntu에서 실행해 보려고 Dockerfile을 다음과 같이 만들었다.

FROM ubuntu:18.04

ADD example-linux ./

ENV DEBUG example:server

CMD ./example-linux

이 도커 이미지를 만들어서 실행하면 다음과 같이 잘 실행되는 것을 알 수 있다.

$ docker build -t pkg-example .

$ docker run --rm -it -p 3000:3000 pkg-example
  example:server Listening on port 3000 +0ms

당연히 여기서는 ubuntu:18.04 도커 이미지를 사용했으므로 Node.js나 다른 환경은 전혀 설정되어 있지 않으므로 pkg로 컴파일한 바이너리 하나만으로 Express 애플리케이션을 실행할 수 있는 걸 알 수 있다.(사실 Windows에서는 직접 테스트해보지 않았다.)

pkg 옵션

위에서는 pkg .로 컴파일할 위치만 지정했지만 몇 가지 옵션을 지정할 수 있다.

$ pkg . --out-path dist

위처럼 --out-path를 지정하면 dist 디렉토리 하에 바이너리가 생성된다.

$ pkg . --targets node8-linux-x64,node8-macos-x64,node8-win-x64

타겟 플랫폼을 지정하고 싶다면 --targets 옵션을 사용하면 된다. 타겟 플랫폼의 지정 방식은 node8-linux-x64처럼 node${n}로 Node.js 버전, freebsd, linux, macos, win의 플랫폼 이름, x64, x86, armv6, armv7로 아키텍처를 지정해 주면 된다.

직접 서버에서 실행하는 경우에는 이렇게 컴파일하는 것에 큰 이점이 없지만 배포하는 프로그램의 경우에는 사용자가 쉽게 사용할 수 있어서 충분히 장점이 된다고 생각한다. Node.js가 포함되어 용량이 큰 것은 좀 문제이지만 상황에 따라서는 그 부분을 무시할 수 있는 이점을 준다고 본다.

2018/05/30 02:25 2018/05/30 02:25

Terraform으로 Google Cloud Platform의 GKE 클러스터 생성하기

AWS만 사용해 보다가 GCP(Google Cloud Platform)를 써보기 시작한 게 사실은 Kubernetes를 테스트해보기 위함이다. GCP에서는 구글이 완전히 관리해주는 GKE(Google Kubernetes Engine)를 제공하고 있어서 따로 Kubernetes를 설치하고 관리할 필요 없이 사용할 수 있다. Kubernetes는 이제 막 배우는 단계이지만 인프라를 거의 Terraform으로 관리하고 있어서 GKE 설정도 Terraform을 이용했다. GCP에 대한 기본 설정은 Google Cloud Platform에 Terraform 설정하기를 참고하면 된다.

GKE 클러스터 생성

GKE를 사용하려면 사용할 프로젝트에서 API를 활성화 해야 한다. "API 및 서비스"에서 Compute Engine API, Kubernetes Engine API를 찾아가서 사용 설정을 한다.

Kubernetes Engine API 설정

GKE를 만들려면 google_container_cluster 리소스를 사용하면 된다.

resource "google_container_cluster" "practice" {
  name               = "practice"
  zone               = "${data.google_compute_zones.available.names[0]}"
  initial_node_count = 2

  node_version       = "1.10"
  min_master_version = "1.10"

  additional_zones = [
    "${data.google_compute_zones.available.names[1]}",
  ]

  master_auth {
    username = "${var.username}"
    password = "${var.password}"
  }

  node_config {
    oauth_scopes = [
      "https://www.googleapis.com/auth/compute",
      "https://www.googleapis.com/auth/devstorage.read_only",
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
    ]
  }
}

Terraform 문서를 참고한 기본 설정이다. 클러스터 인스턴스를 실행할 zoneadditional_zones는 리전에서 사용할 수 있는 available zone을 가져오도록 했다. 이 이름을 하드 코딩하지 않으려고 variables.tf파일에서 다음과 같이 data 설정을 추가했다.

data "google_compute_zones" "available" {}

여기서 가져온 availabe zone 배열에서 첫 번째 존을 기본 존으로 설정하고 additional_zones에 다른 존을 추가했다. additional_zones에는 여러 값을 넣어도 되지만 연습이므로 하나만 추가했다. initial_node_count는 클러스터의 노드 개수다. 이 숫자에는 마스터 노드는 포함되지 않는다.(아직 다 파악 못했지만 마스터 노드는 GKE가 따로 관리해 주는 것으로 보인다. 인스턴스 목록에도 나오지 않는다) 그래서 initial_node_count를 2로 설정하면 존마다 2개씩 생성되므로 여기서는 존을 총 2개 설정했으므로 노드는 4개가 실행된다.

node_versionmin_master_version은 노드와 마스터의 Kubernetes 버전이다. GEK의 버전은 문서에서 참고할 수 있는데 1.X, 1.X.Y, 1.X.Y-.*, latest처럼 정의할 수 있고 이후에는 별도로 업데이트 가능한 것 같지만 처음 생성할 때는 node_versionmin_master_version의 버전이 같아야 한다.

master_auth는 마스터에 접근할 인증 정보이다. 여기에 하드 코딩하면 아이디와 비밀번호가 노출되므로 환경변수로 처리하려고 변수로 분리했다. variables.tf에 변수를 추가한다.

variable "username" {}
variable "password" {}

그리고 환경변수로 TF_VAR_usernameTF_VAR_password를 지정하면 Terraform을 사용할 때 이 환경변수의 값이 이 변수에 자동으로 할당되어서 Terraform 설정 파일에서는 이 값을 노출하지 않을 수 있다.

node_config는 아직 자세히 몰라서 Terraform 문서에 나온 대로 설정했다.

$ terraform apply
data.google_compute_zones.available: Refreshing state...

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + google_container_cluster.practice
      id:                                    <computed>
      additional_zones.#:                    "1"
      additional_zones.2275771321:           "asia-northeast1-b"
      addons_config.#:                       <computed>
      cluster_ipv4_cidr:                     <computed>
      enable_kubernetes_alpha:               "false"
      enable_legacy_abac:                    "false"
      endpoint:                              <computed>
      initial_node_count:                    "2"
      instance_group_urls.#:                 <computed>
      logging_service:                       <computed>
      master_auth.#:                         "1"
      master_auth.0.client_certificate:      <computed>
      master_auth.0.client_key:              <computed>
      master_auth.0.cluster_ca_certificate:  <computed>
      master_auth.0.password:                <sensitive>
      master_auth.0.username:                "outsider"
      master_version:                        <computed>
      min_master_version:                    "1.10"
      monitoring_service:                    <computed>
      name:                                  "practice"
      network:                               "default"
      network_policy.#:                      <computed>
      node_config.#:                         "1"
      node_config.0.disk_size_gb:            <computed>
      node_config.0.guest_accelerator.#:     <computed>
      node_config.0.image_type:              <computed>
      node_config.0.local_ssd_count:         <computed>
      node_config.0.machine_type:            <computed>
      node_config.0.oauth_scopes.#:          "4"
      node_config.0.oauth_scopes.1277378754: "https://www.googleapis.com/auth/monitoring"
      node_config.0.oauth_scopes.1632638332: "https://www.googleapis.com/auth/devstorage.read_only"
      node_config.0.oauth_scopes.172152165:  "https://www.googleapis.com/auth/logging.write"
      node_config.0.oauth_scopes.299962681:  "https://www.googleapis.com/auth/compute"
      node_config.0.preemptible:             "false"
      node_config.0.service_account:         <computed>
      node_pool.#:                           <computed>
      node_version:                          "1.10"
      private_cluster:                       "false"
      project:                               <computed>
      region:                                <computed>
      zone:                                  "asia-northeast1-a"


Plan: 1 to add, 0 to change, 0 to destroy.

이를 실행하면 GKE 클러스터가 생성된다.

생성된 GKE 클러스터 정보

생성된 GKE 클러스터 상세 정보

클러스터를 만들었으니 제대로 동작하는지 사용해보자.

클러스터 정보를 가져오려면 Google Cloud Shell을 먼저 설치해야 한다. gcloud init으로 로그인한 뒤 클러스터 정보를 가져오기 위해 설정한다.

$ gcloud config set core/project kubernetes-practice-204905
Updated property [core/project].

$ gcloud config set compute/zone asia-northeast1-a
Updated property [compute/zone].

$ gcloud config set container/cluster practice
Updated property [container/cluster].

설정한 정보는 gcloud config list로 확인할 수 있다.

$ gcloud config list
[compute]
region = asia-northeast1
zone = asia-northeast1-a
[container]
cluster = practice
[core]
account = YOUR_EMAIL
disable_usage_reporting = True
project = kubernetes-practice-204905

Your active configuration is: [default]

프로젝트와 생성한 GKE 클러스터 설정을 했으니 클러스터 인증 정보를 gcloud container clusters get-credentials PROJECT_NAME으로 가져올 수 있다.

$ gcloud container clusters get-credentials practice
Fetching cluster endpoint and auth data.
kubeconfig entry generated for practice.

성공적으로 실행되면 Kubenetes에서 사용할 수 있도록 ~/.kube/config에 클러스터 정보가 업데이트된다.

$ kubectl config get-contexts
CURRENT   NAME                                                        CLUSTER                                                     AUTHINFO                                                    NAMESPACE
          docker-for-desktop                                          docker-for-desktop-cluster                                  docker-for-desktop
*         gke_kubernetes-practice-204905_asia-northeast1-a_practice   gke_kubernetes-practice-204905_asia-northeast1-a_practice   gke_kubernetes-practice-204905_asia-northeast1-a_practice

gke_kubernetes-practice-204905_asia-northeast1-a_practice가 추가된 것을 볼 수 있고 현재 컨텍스트로 설정되어 있지 않다면 kubectl config use-context gke_kubernetes-practice-204905_asia-northeast1-a_practice로 변경할 수 있다.

클러스터가 정보를 가져와 보자.

$ kubectl cluster-info
Kubernetes master is running at https://35.200.104.117
GLBCDefaultBackend is running at https://35.200.104.117/api/v1/namespaces/kube-system/services/default-http-backend:http/proxy
Heapster is running at https://35.200.104.117/api/v1/namespaces/kube-system/services/heapster/proxy
KubeDNS is running at https://35.200.104.117/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://35.200.104.117/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

kubectl로 GKE 클러스터에 접속하는 것을 확인할 수 있다. 클러스터 사용에 관해서는 다음에...

2018/05/22 16:51 2018/05/22 16:51