1. hubot 소개
- Hubot 은 깃헙의 사내용으로 제작된 챗봇이지만, 많은 발전을 거듭하여 현재 오픈소스로 공개되어있습니다. Node 기반이며, Slack과 친화적입니다.
- 휴봇의 가장 큰 장점은 간단한 스크립트(CoffeeScript, JavaScript) 작성을 통해 강력한 기능을 추가할 수 있다는 점입니다.
- 특정 단어 혹은 문장에 따라 프로세스를 정의할 수 있습니다. 이미 구축된 스크립트들도 많이 공개 되어있어 손쉽게 스크립트를 추가하여 구현할 수 있습니다.
enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
leaveReplies = ['Are you still there?', 'Target lost', 'Searching']
module.exports = (robot) ->
robot.enter (res) ->
res.send res.random enterReplies
robot.leave (res) ->
res.send res.random leaveReplies
[출처] https://blog.hax0r.info/2017-05-14/slack-developer-kit-for-hubot/ [Hax0r blog]
스크립트를 통해 Local 혹은 Heroku를 통해 배포하여 슬랙과 연동할 수 있습니다.
- hubot-diagnostics: 간단한 기본기능들이 들어있다. 위에서 사용했던
ping을 이 모듈이 응답한 것이다. 그 외time과echo도 있다. - hubot-help: 현재 hubot의 명령어들을 표시해준다. script들의 # Commands 들을 가져와서 뿌려주는 역할을 한다.
- hubot-pugme: 설명을 보면은 가장 중요한 휴봇 스크립트라고 적혀있다. 기능은 퍼그 이미지 url을 랜덤으로 가져오는 것이다. 하지만 2년이 지나서 그런지 url이 유효하지 않다.
- hubot-rules: hubot의 룰을 설명한다.
> hubot rules으로 볼 수 있다. - hubot-shipit: 가지고 있는 이미지URL중 랜덤으로 하나를 보내준다.
hubot-pugme와 마찬가지로 유효한 URL이 별로 없다. - hubot-heroku-keepalive: 무료 heroku를 사용할 경우 하루 사용시간 제한이 있기 때문에 필요한 것 같다.
- hubot-redis-brain: hubot의 brain기능을 redis로 이용하는 것이다. hubot-google-images와 hubot-google-translate는 이름에서도 알 수 있듯이 구글의 API키를 받아서 구글 서비스를 사용할 때 필요하다. hubot-maps도 구글의 맵서비스를 이용하는 것이다.
2. hubot 설치 및 설정
hubot은 기본적으로 node기반으로 수행되는 어플리케이션입니다. 따라서 npm과 node가 설치되어있어야 설치가 가능합니다.- node version : v16.17.0
- npm version : 8.15.0
2-1. hubot 설치
$ npm install -g yo generator-hubot
여기서 yoman 이라는것을 같이 설치하게 되는데, 간단하게 말하면 구조를 어플리케이션의 구조를 잡아주는 도구라고 보시면 됩니다.
$ mkdir -p ~/apps/devops
$ cd ~/apps/devops
$ yo hubot --adapter=slack
여기서 몇가지 Interactive Question을 받게 되는데, 간단하게 입력을 하면됩니다.
2-2. hubot 설정
이제 간단하게 hubot 설치는 마쳤습니다. Hubot 기능중에 데이터 유지를 위해서 Redis module이 자동적으로 들어가있는데 이부분을 제거해야 합니다.# external-script.json
[]
external-script.json에는 hubot에 필요한 모듈들을 탑재할 수 있는데, 별도의 서버에서 구성을 진행하기 때문에 모든 내용들을 삭제해줘도 무방합니다. 밑의 내용은 필수적으로 삭제를 진행합니다.
- hubot-heroku-keepalive
- hubot-redis-brain
# ~/apps/devops/node_modules/hubot/src/hubot.js
...
const port = process.env.EXPRESS_PORT || process.env.PORT || 8083
...
port도 겹치지 않게 custom port로 변경을 해줍니다.
2-3. Slack 설정
Hubot 사용의 가장 큰 목적은 Slack을 통해서 ChatOps를 구현하기 위함입니다. 따라서 Slack에 앱을 추가하고 연동을 하는 작업이 필요합니다.
Slack 앱을 추가하면 Hubot 설정 페이지가 나오게 되고, 그곳에서 Hubot의 Token값을 얻을 수 있습니다.
2-4. Hubot 실행
$ HUBOT_SLACK_TOKEN=xoxb-... ./bin/hubot --adapter slack
hubot을 실행하게 되면, Slack상에 Hubot이 연결이 되면서 연결중으로 접속이 표시가 됩니다. 이렇게 되면 설치 & 설정이 마무리 된것이고, Hubot에 대한 Script를 작성해서 기능을 추가하면 됩니다.
3. hubot script 가이드
hubot은 script를 통해서 기능 확장이 가능하고, 현재 open source로 나와있는 여러가지 Script들을 참고 할 수도 있습니다.- Hubot Script Guide
- .coffee 혹은 .js 파일로 작성이 가능합니다.
3-1. send/ reply / emote
# 경로에 script 작성 /apps/devops/scripts
# send : room으로 왔으면 room으로 전달, DM으로 오면 DM으로 전달
# reply : thread에 댓글 형식으로 메시지 전달
# emote : room으로 메시지 전달
module.exports = (robot) ->
robot.hear /badger/i, (res) ->
res.send "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"
robot.respond /open the pod bay doors/i, (res) ->
res.reply "I'm afraid I can't let you do that."
robot.hear /I like pie/i, (res) ->
res.emote "makes a freshly baked pie"
3-2. messageRoom
# messageRoom 기능을 이용하여 지정된 방이나 사용자에게 메시지를 보낼 수 있습니다.
module.exports = (robot) ->
robot.hear /green eggs/i, (res) ->
room = "mytestroom"
robot.messageRoom room, "I do not like green eggs and ham. I do not like them sam-I-am."
robot.respond /I don't like Sam-I-am/i, (res) ->
room = 'joemanager'
robot.messageRoom room, "Someone does not like Dr. Seus"
res.reply "That Sam-I-am\nThat Sam-I-am\nI do not like\nthat Sam-I-am"
robot.hear /Sam-I-am/i, (res) ->
room = res.envelope.user.name
robot.messageRoom room, "That Sam-I-am\nThat Sam-I-am\nI do not like\nthat Sam-I-am"
3-3. Capturing
# 정규식에 대해 들어오는 메시지를 처리할 수 있습니다.
robot.respond /open the (.*) doors/i, (res) ->
doorType = res.match[1]
if doorType is "pod bay"
res.reply "I'm afraid I can't let you do that."
else
res.reply "Opening #{doorType} doors"
3-4. HTTP 호출하기
# Hubot은 3rd API들과 연계하기 위해서 HTTP 호출을 할 수 있습니다.
data = JSON.stringify({
foo: 'bar'
})
robot.http("https://midnight-train")
.header('Content-Type', 'application/json')
.post(data) (err, res, body) ->
# your code here
if err
res.send "Encountered an error : ( #{err}"
return
# your code here, knowing it was successful
if res.statusCode isnt 200
res.send "Request didn't come back HTTP 200 : ("
return
# RateLimit을 이용하여 호출량을 조절
rateLimitRemaining = parseInt res.getHeader('X-RateLimit-Limit') if res.getHeader('X-RateLimit-Limit')
if rateLimitRemaining and rateLimitRemaining < 1
res.send "Rate Limit hit, stop believing for awhile"
3-5. HTTP 수신기
# Hubot은 HTTP 요청을 처리하기 위한 익스프레스 웹 프레임워크에 대한 지원을 포함합니다.
# 해당 포트로 정적파일 제공도 가능합니다.
module.exports = (robot) ->
# the expected value of :room is going to vary by adapter, it might be a numeric id, name, token, or some other value
robot.router.post '/hubot/chatsecrets/:room', (req, res) ->
room = req.params.room
data = if req.body.payload? then JSON.parse req.body.payload else req.body
secret = data.secret
robot.messageRoom room, "I have a secret: #{secret}"
res.send 'OK'
# Curl을 이용하여 테스트
// raw json, must specify Content-Type: application/json
curl -X POST -H "Content-Type: application/json" -d '{"secret":"C-TECH Astronomy"}' http://127.0.0.1:8080/hubot/chatsecrets/general
// defaults Content-Type: application/x-www-form-urlencoded, must st payload=...
curl -d 'payload=%7B%22secret%22%3A%22C-TECH+Astronomy%22%7D' http://127.0.0.1:8080/hubot/chatsecrets/general
3-6. 기타 기능
# Randomize
lulz = ['lol', 'rofl', 'lmao']
res.send res.random lulz
# Detect Enter & Exit
enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
leaveReplies = ['Are you still there?', 'Target lost', 'Searching']
module.exports = (robot) ->
robot.enter (res) ->
res.send res.random enterReplies
robot.leave (res) ->
res.send res.random leaveReplies
# Brain
robot.respond /have a soda/i, (res) ->
# Get number of sodas had (coerced to a number).
sodasHad = robot.brain.get('totalSodas') * 1 or 0
if sodasHad > 4
res.reply "I'm too fizzy.."
else
res.reply 'Sure!'
robot.brain.set 'totalSodas', sodasHad+1
robot.respond /sleep it off/i, (res) ->
robot.brain.set 'totalSodas', 0
msg.reply 'zzzzz'
4. hubot 기능 구현
필자가 최초 생각했던 것처럼, Slack W/F와 연계하여 각 개발팀에서 DevOps를 통한 문의 및 요청들이 접수되면, 해당 내용을 기반으로 Hubot이 JIRA에 Ticket을 생성하고 해당 Ticket을 링크로 응답 합니다. 또한, 작업이 모두 완료된 이후로는 emoji를 설정하였을때, 작업을 종료하도록 구성하고자 합니다.4-1. JIRA 자동 등록
JIRA에 자동 등록을 하기 위해서는 JIRA의 API를 활용해야 합니다. Jira Rest API 문서 간단하게 JSON 구조를 만들어서 http request를 한 뒤, 결과를 parsing하여 massage로 다시 return해주는 구조입니다. 그렇게 되면, Slack의 Thread 상에서 티켓의 링크를 확인할 수 있습니다.module.exports = (robot) ->
robot.respond /create TICKET(.*)/i, (msg) ->
threadId = getThreadId(msg);
title = ''
for line in msg.message.text.split(/\r?\n/)
console.log (line)
if line.indexOf("*Summary : *") != -1
title = line.replace /\*Summary \:\*/, ""
if title == ''
msg.send 'Faild to get the subject'
return
json =
fields:
project:
key: "LNDO"
summary: title,
description: msg.message.text,
issuetype:
name: "_Task"
labels: ["help_devops_thread"]
json = JSON.stringify(json)
create_query = btsBaseUrl + "/rest/api/2/issue"
auth = btoa("#{user}:#{password}")
msg.http(create_query)
.headers(Authorization: "Basic #{auth}", 'Content-Type': 'application/json')
.post(json) (err, res, body) ->
issueName = undefined
if body
returnJson = JSON.parse(body)
if returnJson.hasOwnProperty('key')
issueName = returnJson.key
if err
console.log 'Error!'
console.log err
if issueName == undefined
console.log res
msg.send 'Error on creation issue on BTS'
else
link = ''
msg.send 'Create TICKET at ' + link
threadCache[threadId] = {'lastUpdate': Date.now(), 'fsUpdate': Date.now(), 'BTS': issueName}
fs.writeFileSync(threadDir + threadId, JSON.stringify(threadCache[threadId]))
4-2. JIRA 상태 업데이트
Slack을 통해서 일감 처리가 완료되면, 이모지(DONE)를 통해서 해당 요청이 종료되었다는 것을 표기하였습니다. 그러다보니 명확하게 티켓과 동기화가 되지 않았었습니다. 이러한 부분을 이모지를 인식해서 티켓의 상태를 업데이트(Resolve)처리를 하도록 구성했습니다.robot.respond /(.*)resolve TICKET(.*)/i, (msg) ->
threadId = getThreadId(msg);
getThreadCache(threadId);
json =
transition:
id: "21"
fields:
resolution:
name: "Done"
json = JSON.stringify(json)
BTS = threadCache[threadId]['BTS']
resolve_query = btsBaseUrl + '/rest/api/2/issue/' + BTS + '/transitions?expand=transitions.fields&transitionId=21'
auth = btoa("#{user}:#{password}")
msg.http(resolve_query)
.headers(Authorization: "Basic #{auth}", 'Content-Type': 'application/json')
.post(json) (err, res, body) ->
if body
returnJson = JSON.parse(body)
console.log(returnJson)
if err
console.log err
msg.send 'There is an error reolsve ticket!'
else
link = ''
msg.send 'Cloase BTS at ' + link
threadCache[threadId] = {'lastUpdate': Date.now(), 'fsUpdate': Date.now(), 'BTS': BTS}
fs.writeFileSync(threadDir + threadId, JSON.stringify(threadCache[threadId]))
5. hubot 구성도
전체적인 구성도는 위와 같습니다.
- Request Layer : Slack W/F를 통해서 요청하는 영역
- Slack - Hubot Layer : Slack의 W/F를 분석하여 Hubot 스크립트를 수행하는 영역
- InfraStructure Layer : Hubot과 연계되는 Tools가 위치하는 영역
6. 마치며..
이번시간에는 Slack W/F와 Hubot을 연계하여 업무를 자동화했던 내용을 정리해보았습니다. DevOps업무를 하면서 자동화 처리를 통하여 좀더 효율적으로 일하는 문화를 배울 수 있었고, 특히나 업무 자체를 코드화 시키고, 프로세스화 시키니까 처리하기가 좀더 수월했던 것 같습니다. 다음시간에는 Hubot의 좀더 강화된 기능을 사용하는 내용으로 찾아뵙겠습니다.함께 보면 좋은 글
- [DevOps] kubeadm upgrade를 통한 pod cidr 변경
- [DevOps] Nginx Rate Limit
- [DevOps] k8s monitoring with Datadog
- [DevOps] Istio virtualhost 사용법
새 글이 올라오면 Threads @wonizz.ai 에도 올립니다. 팔로우해 두면 피드에서 바로 볼 수 있습니다.
좋은 내용 감사합니다.
그런데 code 에서 ‘->’ 가 모두 escape 처리되어 `->` 로 출력되고있는거 같습니다.
앗, 초안을 먼저 다른프로그램으로 작성하고 복사/붙여넣기 하면서 검토를 못했네요!! 감사합니다 건님!