본문 바로가기메뉴 바로가기


kakao의 오픈소스 Ep7 – CMUX: CLI에 날개를 달자!

“카카오의 오픈소스를 소개합니다” 일곱 번째는 jon.kwon과 동료들이 개발한 CMUX입니다.

CMUX는 Cloudera Manager 기반의 하둡 클러스터를 관리하는데 필요한 대화형 커맨드라인 인터페이스 도구들을 제공합니다.

CMUX의 아이디어를 참고해 보세요. 여러분의 커맨드라인에 날개를 달 수 있을 것입니다.

카카오의 하둡 엔지니어링 파트에서는 CMUX를 이렇게 사용합니다.

수천 대의 하둡 클러스터의 정보를 빠르게 검색하여 필요한 정보를 조회하기도 하고,

cmux lh

특정 조건으로 검색한 노드에 SSH 로그인하여 병렬 작업을 하거나,

cmux ssh

특정 조건으로 검색한 관리자 웹페이지를 열어보기도 합니다.

cmux websvc
cmux webcm

특정 조건으로 검색한 노드에만 명령어를 실행할 수도 있습니다.

cmux scmagent

필요하면 외부 툴을 실행할 수도 있죠.

cmux hri

조금만 여유가 있다면 Rolling restart role처럼 아주 복잡한 유지 보수 작업이나 배치 작업을 만들어서 사용할 수도 있습니다.

지금부터 이런 것들을 가능하게 한 CMUX의 아이디어를 소개해 드리겠습니다.

CMUX의 작업 흐름

CMUX는 크게 4단계의 작업 흐름으로 구성된 명령어 집합입니다.

입력 단계

입력 소스는 파일, 파이프라인, API 등 여러분이 상상할 수 있는 다양한 형태가 될 수 있습니다. CMUX에서는 Cloudera Manager의 REST API와 카카오의 인프라 자원을 조회하는 API를 주로 사용합니다.

검색 / 필터 단계

CMUX의 꽃에 해당되는 부분입니다. 빔(Vim)신이라고 불리는 카카오의 jg.choi이 만드신 FZF라는 커맨드라인 검색 도구를 사용하여, 원하는 조건으로 입력을 검색하고 결과를 걸러냅니다.

명령어 생성 단계

걸러진 결과를 바탕으로 명령어 또는 명령어 집합을 생성합니다. CMUX는 Ruby로 작성되어 있기 때문에 당연히 Ruby 코드로 생성하겠죠.

명령어 실행 단계

생성한 명령어 또는 명령어 집합을 실행합니다. 이때 명령어 집합을 병렬로 실행할 수 있는 인터페이스가 필요할 수 있는데, CMUX는 이를 위해 TMUX를 사용합니다.

그럼, 이해를 돕기 위해 간단한 예제 코드를 통해 이 과정을 구현해 보겠습니다.

구현 예제

준비

CMUX의 아이디어를 구현하기 위해 필요한 두 가지 도구인 FZFTMUX를 설치합니다. OS X 유저라면 homebrew로 간단하게 설치할 수 있습니다.

brew install tmux fzf

CMUX는 Ruby로 작성되어 있지만, 이 예제에서는 쉘 스크립트로 간단하게 구현해 보겠습니다. 그리고 쉘에서 JSON 포맷으로 제공되는 Cloudera Manager REST API를 편하게 활용할 수 있도록 jq를 설치하겠습니다.

brew install jq

자 이제 준비가 되었으니, 데이터 입력 부분부터 구현해 볼까요?

입력

Cloudera Manager REST API를 활용하여, Cloudera Manager에 생성되어 있는 클러스터 정보를 취득하겠습니다.

$ CM="test.kakao.cm"
$ CM_PORT="7180"
$ CM_API_VER="v14"
$ CM_USER="hadoop"
$ CM_USER_PWD="doopy"
$ RESOURCE="/clusters"
$ BASE_URL="$CM:$CM_PORT/api/$CM_API_VER"
$ URL="$BASE_URL$RESOURCE"
$ curl -f -s -u "$CM_USER":"$CM_USER_PWD" "$URL"

대략 이런 모습의 결과를 확인할 수 있을 것입니다. Cloudera Manager가 없는 환경이라고 낙담하지 않으셔도 됩니다. 다음 단계부터는 아래의 내용을 clusters.txt로 저장했다고 가정하고 진행하겠습니다.

{
  "items" : [{
    "name" : "cluster1",
    "displayName" : "Neo",
    "version" : "CDH5",
    "fullVersion" : "5.3.8",
    "maintenanceMode" : false,
    "maintenanceOwners" : [ ],
    "clusterUrl" : "https://www.kakaofriends.com/kr/products/groups/characters/8",
    "hostsUrl" : "https://www.kakaofriends.com/kr/products/groups/characters/8",
    "entityStatus" : "NONE"
  }, {
    "name" : "cluster2",
    "displayName" : "Ryan",
    "version" : "CDH5",
    "fullVersion" : "5.11.1",
    "maintenanceMode" : true,
    "maintenanceOwners" : [ "CLUSTER" ],
    "clusterUrl" : "https://www.kakaofriends.com/kr/products/groups/characters/23",
    "hostsUrl" : "https://www.kakaofriends.com/kr/products/groups/characters/23",
    "entityStatus" : "GOOD_HEALTH"
  }, {
    "name" : "cluster3",
    "displayName" : "Apeach",
    "version" : "CDH5",
    "fullVersion" : "5.3.8",
    "maintenanceMode" : true,
    "maintenanceOwners" : [ ],
    "clusterUrl" : "https://www.kakaofriends.com/kr/products/groups/characters/6",
    "hostsUrl" : "https://www.kakaofriends.com/kr/products/groups/characters/6",
    "entityStatus" : "GOOD_HEALTH"
  } ]
}

검색/필터

호출된 결과를 jq 명령어를 이용해서 특정 필드만 가져오도록 하겠습니다. 우리는 “클러스터 이름(name)”, “클러스터 표시 명(displayName)”, “클러스터 관리자 페이지 URL(clusterUrl)”에만 관심이 있습니다.

$ cat clusters.txt \
| jq -r '.items[] | [.name, .displayName, .clusterUrl]'

잘 걸러졌습니다.

[
  "cluster1",
  "Neo",
  "https://www.kakaofriends.com/kr/products/groups/characters/8"
]
[
  "cluster2",
  "Ryan",
  "https://www.kakaofriends.com/kr/products/groups/characters/23"
]
[
  "cluster3",
  "Apeach",
  "https://www.kakaofriends.com/kr/products/groups/characters/6"
]

이제, 특정 클러스터를 검색하고 필터링할 수 있도록 하겠습니다.

먼저, 위 결과를 클러스터별로 선택 가능하도록 jq의 join문으로 열을 결합합니다.

$ cat clusters.txt \
| jq -r '.items[] | [.name, .displayName, .clusterUrl] | join("\t")'
cluster1   Neo     https://www.kakaofriends.com/kr/products/groups/characters/8
cluster2   Ryan    https://www.kakaofriends.com/kr/products/groups/characters/23
cluster3   Apeach  https://www.kakaofriends.com/kr/products/groups/characters/6

참고

CMUX에서는 결과물을 잘 정렬된 테이블 형태로 출력할 수 있도록, 이 부분을 별도의 모듈로 구현하였습니다.

내가 원하는 결과를 검색하고 선택할 수 있도록 해 볼까요?

fzf 명령어를 파이프에 연결하겠습니다.

  • --multi: 다중 선택
  • --reverse: 역순 레이아웃, 기본값은 아래에서 위로 정렬합니다.
$ cat clusters.txt \
| jq -r '.items[] | [.name, .displayName, .clusterUrl] | join("\t")' \
| fzf --multi --reverse

위 스크립트를 실행하면 아래와 같이 선택 가능한 상태로 FZF가 활성화됩니다.

“Ryan”이라는 클러스터 이름을 검색하겠습니다. “Fuzzy Finder”라는 이름에 걸맞게 아래와 같이 느슨한 검색이 가능합니다. 다양한 검색 옵션이 있으니, fzf의 man 페이지를 읽어보시기 바랍니다.

이번엔 “Neo”와 “Apeach” 클러스터를 선택하겠습니다. 기본적으로 tab 키로 선택할 수 있습니다. 전체 선택, 전체 선택 해제 등 다양한 옵션이 있으니, 마찬가지로 man 페이지를 읽어보시기 바랍니다.

최종 선택한 결과는 아래와 같이 문자열로 반환됩니다.

cluster1  Neo     http://test.kakao.cm:7180/cmf/clusterRedirect/cluster
cluster3  Apeach  http://test.kakao.cm:7180/cmf/clusterRedirect/cluster2

명령어 생성

결과의 마지막 열인 “Cluster URL”을 기본 브라우저로 열 수 있도록 명령어를 만들겠습니다. OS X 환경이라는 가정하에 open명령어를 사용합니다.

$ cat clusters.txt \
| jq -r '.items[] | [.name, .displayName, .clusterUrl] | join("\t")' \
| fzf --multi --reverse \
| awk '{print "open "$NF}'
open https://www.kakaofriends.com/kr/products/groups/characters/8
open https://www.kakaofriends.com/kr/products/groups/characters/23

명령어 실행

위에서 생성한 명령어를 실행할 수 있도록 스크립트를 변경하겠습니다.

$ cat clusters.txt \
| jq -r '.items[] | [.name, .displayName, .clusterUrl] | join("\t")' \
| fzf --multi --reverse \
| awk '{print "open "$NF}' \
| { while read CMD; do
  eval "${CMD}"
done }

이 코드를 실행하면 여러분이 선택한 클러스터의 URL이 기본 웹브라우저에서 열릴 것입니다. 이 예제에서는 이해를 돕기 위해 각 카카오 프렌즈의 상품페이지로 이동하도록 URL을 편집해 두었습니다. ^^;

이번엔 조금 더 복잡한 구현을 도전해 볼까요?

웹 브라우저로 각 URL을 열기 전에 TMUX로 선택한 URL의 수만큼 윈도우를 분할한 후 URL 정보를 화면에 출력하도록 스크립트를 보강하겠습니다.

$ cat clusters.txt \
| jq -r '.items[] | [.name, .displayName, .clusterUrl] | join("\t")' \
| fzf --multi --reverse \
| awk '{print "echo \""$1": " $NF"\" && open "$NF";echo -n Press enter to finish.; read"}' \
| { while read CMD; do
 tmux -2 split-window "${CMD}"
 tmux -2 select-layout tiled
done } && tmux setw synchronize-panes on && exit

위와 같이 실행하면 여러분의 터미널 윈도우가 아래와 같이 분할된 후 URL에 출력되는 것을 확인할 수 있을 것입니다.

참고

CMUX에서는 이 부분이 tws 라는 별도의 명령어로 보다 정밀한 화면 제어가 가능하도록 구현되어 있습니다.

맺음말

CMUX를 개발한 후, 지난 1년여 동안 내부적으로 사용하면서 버그를 수정하고 기능을 보강하고 있지만 부끄러울 정도로 부족한 면이 많습니다. 하지만, CLI에서도 이런 것도 할 수 있다는 아이디어를 공유하고 싶었으며, 더 기발한 아이디어로 CLI가 풍부해지길 기대하면서 오픈 소스로 공개하게 되었습니다.

마지막으로 CMUX를 위해 FZF에 여러가지 기능을 탑재해 주신 jg.choi님께 감사의 말씀을 드립니다.

jon.kwon
jon.kwon
Top Tag
adaptive-hash-index
adt
agile
agilecoach
ai
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
android
angular
anycast
applicative
Architecture
arena
async
aurora
Backend
bgp
ble
blind-recruitment
block
blockchain
bluetooth
brian
cahtbot
cd
ceph
certificate
certification
cgroup
ci
cite
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
coding
competition
component
conference
consul
container
contest
couchbase
COVID-19
cpp
Data
DB
deep-learning
developer
developers
devops
digitalization
digitaltransformation
dns
docker
employeecard
eslint
Featured
friendstime
front-end
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
go
graphdb
graphql
growth
ha
hadoop
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
id
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
json
kafka
kakao
kakao-commerce
kakao-games
kakaoarena
kakaocon
kakaok
kakaokey
kakaokrew
kakaomap
kakaotalk
KCDC
khaiii
kubernetes
l3dsr
l4
links
load-balancing
machine-learning
marathon
meetup
melon
mesos
Messaging
microservice
mobil
monad
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
nomad
ocp
open
opensource
openstack
OpenWork
page
parallel
PBA
programming-contest
pycon
python
quagga
reactive-programming
reactor
recommendation
recruitment
redis
redis-keys
redis-scan
rest
rubics
ruby
rxjs
s2graph
scala
scalaz
server
service
sharding
shopping
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
support
System
talk
talkchannel
tcp
tech
test
Thread-Debugging
time-wait
tmux
update
vim
vim-github-dashboard
vim-plugin
vue
web-cache
webapp
WebSocket
weekly
All Tag
adaptive-hash-index
adt
agile
agilecoach
ai
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
android
angular
anycast
applicative
Architecture
arena
async
aurora
Backend
bgp
ble
blind-recruitment
block
blockchain
bluetooth
brian
cahtbot
cd
ceph
certificate
certification
cgroup
ci
cite
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
coding
competition
component
conference
consul
container
contest
couchbase
COVID-19
cpp
Data
DB
deep-learning
developer
developers
devops
digitalization
digitaltransformation
dns
docker
employeecard
eslint
Featured
friendstime
front-end
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
go
graphdb
graphql
growth
ha
hadoop
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
id
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
json
kafka
kakao
kakao-commerce
kakao-games
kakaoarena
kakaocon
kakaok
kakaokey
kakaokrew
kakaomap
kakaotalk
KCDC
khaiii
kubernetes
l3dsr
l4
links
load-balancing
machine-learning
marathon
meetup
melon
mesos
Messaging
microservice
mobil
monad
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
nomad
ocp
open
opensource
openstack
OpenWork
page
parallel
PBA
programming-contest
pycon
python
quagga
reactive-programming
reactor
recommendation
recruitment
redis
redis-keys
redis-scan
rest
rubics
ruby
rxjs
s2graph
scala
scalaz
server
service
sharding
shopping
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
support
System
talk
talkchannel
tcp
tech
test
Thread-Debugging
time-wait
tmux
update
vim
vim-github-dashboard
vim-plugin
vue
web-cache
webapp
WebSocket
weekly

위로