Clojure 앱에서 환경변수를 사용할 때는 어떻게 해야 할지 생각해봤다. Node.js에서는 dotenv 라이브러리를 사용해서 중요한 변수들을 코드로부터 분리하고 노출되는 것을 막을 수 있었는데 Clojure에서도 당연히 그런 라이브러리가 있을 거라는 생각에 검색을 해보니 weavejester/environ을 많이 쓰는 것 같았다. 그런데 좀 더 살펴보니 yogthos/config 라는 것도 괜찮을 것 같아 사용해봤다. 


Node.js에서 dotenv 사용

https://iamcool.tistory.com/7?category=779855


weavejester/environ

https://github.com/weavejester/environ


yogthos/config

https://github.com/yogthos/config


yogthos/config 라이브러리를 사용하면 jar 외부의 config.edn 파일을 사용할 수도 있고 jar 파일 내부에 config.edn, prod.edn 파일을 포함시킬 수도 있다. 이번에는 두 번째 방법을 이용해 런타임에서 환경변수에 접근했다. 


프로젝트 생성

lein을 이용해서 새로운 프로젝트를 생성한다.

lein new app env-app


라이브러리 및 프로파일 추가

아래와 같이 yogthos/config 라이브러리를 project.clj 파일에 추가한다. 그리고 dev, prod에서 각각 사용하게 될 config.edn 파일의 위치를 profiles에 추가한다. 

1
2
3
4
5
6
7
8
9
10
11
12
(defproject env-app "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.9.0"]
                 [yogthos/config "1.1.1"]]
  :main ^:skip-aot env-app.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}
             :prod {:resource-paths ["config/prod"]}
             :dev  {:resource-paths ["config/dev"]}})
cs

config 폴더 생성

project.clj 파일의 profiles에 추가한 config.edn 파일을 생성하기 위해 아래와 같이 폴더 및 파일들을 생성한다. 

config

  - dev

    - config.edn

  - prod

    - config.edn


config.edn 파일 생성

config 폴더에 생성한 config.edn 파일에 아래와 같은 내용을 입력한다. 파일이 로드되면 :runtime 키워드로 접근할 수 있으며 MySQL, Redis 서버 정보를 포함하고 있다. 

1
2
3
4
5
6
7
8
9
{:runtime {:name "dev"
           :db {:dbtype "mysql"
                :host "localhost"
                :port 3306
                :user "user"
                :password "password"
                :dbname "dbname"}
           :redis {:host "localhost"
                   :port 6379}}}
cs
config/dev/config.den 파일


1
2
3
4
5
6
7
8
9
{:runtime {:name "prod"
           :db {:dbtype "mysql"
                :host "prod_host"
                :port "prod_port"
                :user "prod_user"
                :password "prod_password"
                :dbname "prod_dbname"}
           :redis {:host "prod_host"
                   :port "prod_port"}}}
cs

config/prod/config.edn  파일


core.clj  파일 변경

앱의 entry point인 core.clj 파일에 로드한 환경변수를 출력할 수 있는 코드를 추가한다. 

1
2
3
4
5
6
7
8
9
10
11
(ns env-app.core
  (:gen-class)
  (:require [config.core :refer [env]]))
 
(defn -main [& args]
  (let [{name :name
         db :db
         redis :redis} (:runtime env {})]
    (println name "runtime variables for MySQL:", db)
    (println name "runtime variables for Redis:", redis))
  (println "App start!!"))
cs

yogthos/config 라이브러리의 core로부터 불러온 env를 통해서 project.clj 파일의 profiles에서 정의한 파일로부터 로드한 환경변수에 접근할 수 있는데, config에서 정의한 값을 읽기 위해 :runtime을 키워드를 이용한다. 그리고 MySQL, Redis 값을 출력했다. 


테스트

앱을 실행하여 dev 환경변수를 출력한다. 

$ lein deps

$ lein run

dev runtime variables for MySQL: {:dbtype mysql, :host localhost, :port 3306, :user user, :password password, :dbname dbname}

dev runtime variables for Redis: {:host localhost, :port 6379}


App start!!


prod 환경변수를 출력하기 위해 아래 명령을 실행한다. 

lein with-profile prod run

prod runtime variables for MySQL: {:dbtype mysql, :host prod_host, :port prod_port, :user prod_user, :password prod_password, :dbname prod_dbname}

prod runtime variables for Redis: {:host prod_host, :port prod_port}


App start!!


이 예제에서는 jar 파일 내부에 config.edn 파일을 추가했는데, 경우에 따라 외부로부터 입력을 받아야 할 필요가 있을 수 있다. 이 때에는 yogthos/config의 다른 예를 살펴보면 좋겠다. 


이 방식으로 CircleCI 통해서 Google Cloud에 앱을 올려봤다. 로컬이나 개발서버에서는 dev/config.edn 파일을 읽도록 했고 Google Cloud에 릴리즈할 때는 CircleCI 설정 파일에서 jar 파일을 만들 때 lein with-profile prod uberjar 커맨드를 실행하여 prod 환경변수를 읽도록 했다. 


소스코드

이것도 간단한 예제이지만 아래에 전체 소스코드를 올려놓았다. 


https://github.com/ksleeq21/clojure-env-example



Posted by 코딩새싹
,

Pedestal을 사용해 간단한 웹서버를 만들어 보고 쓸만하다 싶어서 그것으로 백앤드 구성해보려고 했다. 그런데 이런 저런 것들을 붙이려고 보니 생각보다 사용하기 불편했다. 그래서 다른 프레임워크들을 찾다가 Catacumba라는 것을 발견했다. 나온지 얼마 안된 것처럼 보였지만 내가 필요로 하는 기능들은 적당히 들어가 있는 것 같았다. 우선 가벼운 프레임워크이고 기능이 부족하지만 계속 추가되는 것 같고 문서가 쉽게 잘 정리되어 있다. 웹서버 기능을 제대로 할 수 있는지는 아직 잘 모르겠지만 백앤드를 구성하는 마이크로서비스를 만들기에는 부족함이 없어 보였다. 외부로 API를 노출시켜야 하는 서비스는 다른 것을 가져다 써도 되니까 지금 걱정할 필요는 없다. 오늘은 문서를 보면서 간단한 서버를 만들어봤다.


Catacumba

https://funcool.github.io/catacumba/latest


프로젝트 생성

우선 lein을 이용해서 프로젝트를 생성한다. 

lein new app catacumba-app 


다음은 project.clj 파일에 Catacumba를 추가한다. 

1
2
:dependencies [[org.clojure/clojure "1.8.0"]
               [funcool/catacumba "2.2.1"]]
cs


다음으로 서버 실행과 관련된 파일인 core.clj 파일을 살펴보자. 


네임스페이스 구성

1
2
3
4
(ns catacumba-app.core
  (:gen-class)
  (require [catacumba.core :as ct]
           [catacumba.handlers.misc :as misc]))
cs



우선 Catacumba 코어 라이브러리와 handlers.misc 라이브러리를 추가한다. handlers.misc/autoreloader 함수는 routes에서 사용되며 핸들러와 관련된 모든 네임스페이스의 변경을 감지해 릴로드해주는 기능을 한다. 정상적인 동작을 위해서는 반드시 핸들러의 var reference를 등록해야 한다.


Routes 등록

1
2
3
4
(def app
  (ct/routes [[:any (misc/autoreloader)]
              [:all "" #'all]
              [:get "ping" #'ping]]))
cs

 

Catacumba의 라우팅은 백터와 키워드로 구성되는데 모든 루트를 위한 :any, 그리고 :get, :post, :patch, :delete를 루트 디렉티브로 사용할 수 있다. 앞서 설명한 것처럼 (misc/autoreloader)를 사용하여 코드 변경이 있을 때마다 앱을 릴로드하도록 했다. 그리고 각 루트에 대해 #' 매크로를 사용해 var references를 등록했다. 


서버 실행

1
2
3
(defn -main
  [& args]
  (ct/run-server app {:port 8080}))
cs


앱의 메인 엔트리 포인트는 run-server이다. 위에서 등록한 핸들러에 옵션을 추가해 서버를 실행한다. :port 외에도 :host, :debug, :thread 등을 옵션에 추가할 수 있다. 옵션 항목들은 여기에서 확인할 수 있다. 

https://funcool.github.io/catacumba/latest/#launching-the-server


위 코드들을 모두 합친 core.clj 파일은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(ns catacumba-app.core
  (:gen-class)
  (require [catacumba.core :as ct]
           [catacumba.handlers.misc :as misc]))
 
(defn all [context]
  "Hello World!!")
 
(defn ping [context]
  "pong")
 
(def app
  (ct/routes [[:any (misc/autoreloader)]
              [:all "" #'all]
              [:get "ping" #'ping]]))
 
(defn -main
  [& args]
  (ct/run-server app {:port 8080}))
cs


서버 실행

서버 실행을 위해 아래 명령을 실행한다. 

lein deps

lein run


테스트

테스트를 위해 cURL을 이용한다. 

$ curl -i "localhost:8080"

Clojure 1.9.0 - served from /about(py2_env) kwiff-mbp-klee:echo-service klee$ curl -i "localhost:8080"

HTTP/1.1 200 OK

content-type: text/plain;charset=UTF-8

content-length: 13

set-cookie: sessionid=AAABaE-nFJG53hgFY5oSqeWkMtfT0Yajcwe4beQSgoz2KQu6hCZ_miYDktpXlUF3; Max-Age=0; Expires=Tue, 15 Jan 2019 03:56:16 GMT; Path=/; HTTPOnly


Hello World!!


$ curl -i "localhost:8080/ping"

HTTP/1.1 200 OK

content-type: text/plain;charset=UTF-8

content-length: 4

set-cookie: sessionid=AAABaE-nd9yOBW5YlZ1RWSBTNkSTU0QU94wG7_VQaBIGJd7q9-6Rb1QMx7GWYFmE; Max-Age=0; Expires=Tue, 15 Jan 2019 03:56:41 GMT; Path=/; HTTPOnly


pong


소스코드

간단한 코드이지만 여기에 전체 소스코드를 올려 놓았다. 

https://github.com/ksleeq21/catacumba-app


Catacumba 문서를 보면 알겠지만 작고 간단한 프레임워크인데도 비동기 핸들러 추가, 클라이언트 요청을 처리할 쓰레드 설정, 개발에 편리한 오토 릴로드 설정과 같은 기본은 잘 갖춰져 있다. 쓰다보면 단점도 발견하게 되겠지만 이 만하면 좀 더 사용해볼 만 하다. 앞으로는 이 Catacumba를 이용해서 특정 로직을 수행하는 백앤드를 만들어 보려고 한다. 몇 개의 마이크로서비스를 설계하고 데이터, 컨트롤, 프로토콜 등을 정의해 직접 구현까지 해볼 계획이다. 지금 시간 있을 때 해봐야지 안그러면 영영 못한다.


Posted by 코딩새싹
,

Node.js로 서비스를 만들 때 환경변수를 어떻게 사용해야 할지 처음에는 막막했는데 이제는 고민도 없이 dotenv를 쓴다. 런타임 때 중요한 변수들을 코드로부터 분리하는 방법인데 .env라는 파일에 원하는 변수들을 선언하고 서비스가 시작될 때 읽어들여 필요한 곳에서 사용하는 방식이다. 


NPM 라이브러리 주소는 다음과 같다. 

https://www.npmjs.com/package/dotenv


내용은 많지만 실제로 한 번 사용해보고 정리가 되면 다른 것들은 필요 없다. 가장 자주 쓰는 코드는 다음과 같다.

1
require('dotenv').config()
cs

이 코드를 가장 먼저 실행되도록 적당한 파일의 적당한 위치에 넣기만 하면 끝이다. 라이브러리가 로드되면서 같은 위치에서 .env 파일을 찾아 process.env 변수에 저장한다. 로드하는 파일의 위치를 변경할 수도 있다. 


구현


간단히 프로젝트를 만들어서 dotenv 패키지를 설치한 뒤 index.js 파일을 추가했다.  

npm init 

npm install --save dotenv

touch index.js


위에서 만든 index.js 파일에 아래 코드를 추가한다. 이 파일은 아래의 .env 파일을 읽어 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { parsed, error } = require('dotenv').config()
 
if (error) {
  console.log('dotenv error', error)
  throw error
else {
  console.log('Loaded env variables')
  Object.keys(parsed).forEach(k => {
    console.log(`${k}: ${parsed[k]}`)
  })
  console.log('\nMachine-specific env variables')
  Object.keys(parsed).forEach(k => {
    console.log(`${k}: ${process.env[k]}`)
  })
  console.log('\nOverride ALREADY_SET_VAR with loaded variable')
  process.env.ALREADY_SET_VAR = parsed.ALREADY_SET_VAR
  console.log(`process.env.ALREADY_SET_VAR=${process.env.ALREADY_SET_VAR}`)
}
cs


다음은 .env 예제이다.

1
2
3
4
5
6
7
SERVER_ADDRESS=localhost
SERVER_PORT=8080
DB_ADDRESS=localhost
DB_USER=user
DB_PASSWORD=password
DB_DATABASE=db_user
ALREADY_SET_VAR=2
cs


테스트


파일을 실행하기 전에 다음과 같은 환경변수를 추가한다. 이는 .env 변수를 등록할 때 기존 변수의 유무에 따라 어떻게 동작하는지 보기 위해서이다. 

export ALREADY_SET_VAR=1


다음 명령으로 index.js 파일을 실행한다. 

node .


정상적인 경우 다음과 같은 결과가 출력된다.

Loaded env variables


SERVER_ADDRESS: localhost

SERVER_PORT: 8080

DB_ADDRESS: localhost

DB_USER: user

DB_PASSWORD: password

DB_DATABASE: db_user

ALREADY_SET_VAR: 2


Machine-specific env variables


SERVER_ADDRESS: localhost

SERVER_PORT: 8080

DB_ADDRESS: localhost

DB_USER: user

DB_PASSWORD: password

DB_DATABASE: db_user

ALREADY_SET_VAR: 1


Override ALREADY_SET_VAR with loaded variable


process.env.ALREADY_SET_VAR=2


dotenv.config() 함수가 호출되면 변수들을 모두 process.env에 등록하고 로딩된 변수들을 parsed라는 프로퍼티에 저장해 리턴한다. 정상적인 경우라면 위의 메시지를 볼 수 있는데, 한가지 흥미로운 것은 프로그램 실행 전 export 시킨 변수인 ALREADY_SET_VAR 값은 변경되지 않는다는 것이다. 코드에서처럼 process.env.ALREADY_SET_VAR 값에 로드한 값을 할당해서 사용할 수는 있으나 이 작업은 런타임에서만 유효하다. 


만약 파일이 존재하지 않거나 하여 에러가 발생하면 error 프로퍼티가 추가된다. 다음은 파일이 존재하지 않을 때 위의 index.js 파일의 코드에 의해 생성되는 결과이다.

/project_path/index.js:4

  throw error

  ^


Error: ENOENT: no such file or directory, open '/project_path/.env'


    at Object.fs.openSync (fs.js:646:18)

    at Object.fs.readFileSync (fs.js:551:33)

    at Object.config (/project_path/node_modules/dotenv/lib/main.js:85:29)

    at Object.<anonymous> (/project_path/index.js:1:107)

    at Module._compile (module.js:643:30)

    at Object.Module._extensions..js (module.js:654:10)

    at Module.load (module.js:556:32)

    at tryModuleLoad (module.js:499:12)

    at Function.Module._load (module.js:491:3)

    at Function.Module.runMain (module.js:684:10)


dotenv 라이브러리를 사용하면서 주의할 점은 .env 파일을 git에 등록하지 않아야 한다는 점이다. 위의 .env 예에서와 같이 데이터베이스 유저의 이름과 암호가 포함될 경우 노출의 우려가 있기 때문이다. 그래서 나는 보통 env.sample 파일을 만들어 커밋한다. 이 파일을 이용해서 각자 환경에 맞게 .env 파일을 생성할 수 있도록 해주기 위해서다.


마이크로서비스가 늘어나고 공통되는 변수와 아닌 변수들이 많이 늘어나면서 좀 더 편한 방식은 없는지 찾게 되더라. 하지만 아직은 dotenv로 무리 없이 운영하고 있다.


간단한 예제이지만 소스를 올려놓았다. 


https://github.com/ksleeq21/dotenv-example


Posted by 코딩새싹
,