[DevOps] Hubot 적용기

안녕하세요? 정리하는 개발자 워니즈입니다. 이번시간에는 hubot 적용기에 대해서 정리를 해보도록 하겠습니다. 필자는 DevOps업무를 하다보니, 주로 여러개의 개발팀으로부터의 요청들 (CI/CD설정, 네트워크, 인프라, 모니터링 등)에 대해서 처리를 해주는 업무를 합니다.

요청양도 최근들어 꽤나 늘어났고, 요청의 범주도 굉장히 다양하기 때문에 Slack W/F를 통해서 모든것으르 제어하기는 어려운 시점이 왔습니다.

JIRA의 티켓을 수동으로 등록하여 관리를 하고있었는데, 이부분을 자동화 시키고 슬랙 Thread상에서 일감을 처리하면 자동 종료까지 되는 부분으로 업무의 프로세스를 개선하고 싶었습니다.

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을 이 모듈이 응답한 것이다. 그 외 timeecho도 있다.
  • 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-imageshubot-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들을 참고 할 수도 있습니다.

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가 위치하는 영역

이번에 정리된 기준으로는 Jira의 Ticket을 생성하고 종료하는 내용이였습니다. 추후에는 기능을 좀더 강화 할 예정입니다.

6. 마치며..

이번시간에는 Slack W/F와 Hubot을 연계하여 업무를 자동화했던 내용을 정리해보았습니다. DevOps업무를 하면서 자동화 처리를 통하여 좀더 효율적으로 일하는 문화를 배울 수 있었고, 특히나 업무 자체를 코드화 시키고, 프로세스화 시키니까 처리하기가 좀더 수월했던 것 같습니다.

다음시간에는 Hubot의 좀더 강화된 기능을 사용하는 내용으로 찾아뵙겠습니다.

2 Replies to “[DevOps] Hubot 적용기”

  1. 좋은 내용 감사합니다.
    그런데 code 에서 ‘->’ 가 모두 escape 처리되어 `-&gt` 로 출력되고있는거 같습니다.

    1. 앗, 초안을 먼저 다른프로그램으로 작성하고 복사/붙여넣기 하면서 검토를 못했네요!! 감사합니다 건님!

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다