Slack을 이용하여 AWS EC2 Instance On/Off 만들기

처음에는 아주 단순한 이유로 출발했다.

AWS EC2 Instance 는 유동 아이피 이기 때문에, 서버가 꺼졌다가 켜지면 IP가 변한다.
그래서 서버의 IP를 확인하고자, 서버 목록을 slack 에서 출력하기로 했다.

!서버 중요 정보가 많아서 다 블라인드 처리;;

해당 기능을 가볍게(?) 개발하기 시작한다. 기존에 만들어둔 slackBot 에다가 !서버를 추가하고, 해당 할 일을 작성하기 시작했다.

aws ec2 describe-instances --query "Reservations[].Instances[].[Tags[?Key=='Name'] | [0].Value, PublicIpAddress, InstanceId, State.Name]  
Example Result  
[
    [
        "InstanceName",
        "54.xxx.xxx.xxx",
        "i-1234567890",
        "running"
    ],
    [
        ...
        반복
        ...
    ]
]

해당 내용을 가지고, SlackBot을 구현. Botkit을 가지고 구현했는데, nodejs 를 사용하다보니, nodejs 안에서 다른 process를 실행하는 것을 찾다가 child_process 를 발견하고, 아래 코드로 진행했다.

var Botkit = require('botkit');  
var controller = Botkit.slackbot();  
var bot = controller.spawn({  token: "TOKENVALUE" })

const { exec } = require('child_process');


....
....
....

controller.hears(["!서버"],["direct_message","direct_mention","mention","ambient"],function(bot,message) {  
    exec('aws ec2 describe-instances --query "Reservations[].Instances[].[Tags[?Key==\'Name\'] | [0].Value, PublicIpAddress, InstanceId, State.Name]"', (err, stdout, stderr) => {
      if (err) {
        return;
      }
      if (stderr) {
        bot.reply(message, stderr);
        return;
      }
      if (stdout) {
        var json = JSON.parse(stdout);

        // json parsing
        // 추가 처리 진행

        var reply_format = {
            "text": txt,
            "mrkdwn" : true
        };

        bot.reply(message, reply_format);  
      }
    }); 
});

이렇게 !서버는 구현을 완료했다.

여기서 더 나아가서 요구사항이 하나 더 생겼다. 특정 팀에서 GPU 서버를 사용하게 해달라고 하는데, 비싼 서버라서 on/off 하면서 사용하겠다는 내용이었다. aws console login을 하게 해서 사용 방법을 알려주면 그만인 것인데, IAM 설정해야 하고, 권한 확인하고, 그리고 일단 이것저것 설명해야 하는 것이 귀찮았다.

생각난김에 !서버를 완성했으니 서버의 on/off도 만들 수 있을 것 같아서 도전해보았다. 아니 이렇게 만들어서 사용 방법을 알려주는게 더 귀찮은 것 아닌가??
botkit에서 hears 를 이용하여 chatbot 만드는 기능으로도 기능을 만들 수 있을 것 같았는데, 뭔가 arguments 를 받는 내용이 아직인 것 같아서 빠르게 개발을 진행하려고 Slack Command 기능을 이용하기로 결정했다. slack command를 이용하면 arguments 를 받아서 쓸 수 있다.

/start-instance/stop-instances 를 slack command에 등록하고, 그 해당 slack command 의 Request가 들어오면 실행되고 있는 API 서버에서 받아서 Response를 날려주는 기능으로 개발했다.

slack aws ec2 instance on/off

해당 기능도 slackBot 개발한 것처럼 빠르게 만드려고 nodejs를 가지고 기본 http 기능을 이용해서 빠르게 만들었다.

const util = require('util');  
const http = require('http');  
var  uri = require('url');  
const { exec } = require('child_process');

const commandToken = { start : "TOKENVALUE" , stop : "TOKENVALUE" };

var Botkit = require('botkit');  
var controller = Botkit.slackbot();  
var bot = controller.spawn({  
  token: "TOKENVALUE" 
})

http.createServer((request, response) => {  
    const { headers, method, url } = request;
    request.params = params(request);

    let body = [];
    request.on('error', (err) => {
        console.error(err);
    }).on('data', (chunk) => {
        body.push(chunk);
    }).on('end', () => {
        body = Buffer.concat(body).toString();

        response.on('error', (err) => {
            console.error(err);
        });

        response.statusCode = 200;

        // Empty value pass
        if (!Object.keys(request.params).length) {
            response.write ("start!");
            response.end();
            return;
        }

        //console.log(request.params);

        var req_command = request.params.command || '';
        var req_text = request.params.text || '';

        // Valid Token
        var flag = isValidToken(request.params.token || '');
        if (!flag) {
            console.error("bad request");
            response.write("bad request");
            response.end();
        }

        if (req_command.length && req_text.length)
        {
            // Execute Command
            var execCommand = util.format('aws ec2 %s --instance-id %s' , req_command.replace('%2F', ''), req_text);
            console.log("execCommand ", execCommand);

            exec(execCommand, (err, stdout, stderr) => {
              if (err) {
                  console.error(err);
                  bot.say({text : err.toString(), channel: '@'+ request.params.user_id });
                return;
              }
              if (stderr) {
                console.error(stderr);
                bot.say({text : stderr.toString(), channel: '@'+ request.params.user_id });
                return;
              }
              if (stdout) {
                  var json = JSON.parse(stdout);
                    var firstKeyName = Object.keys(json)[0];
                    var explains = json[firstKeyName][0];

                    var msg = {
                        text : " ",
                        attachments: [
                            {
                                "color": firstKeyName.indexOf('Stop') > -1 ? "warning" : "good",
                                "title": firstKeyName,
                                "text": "InstanceId : "+ explains.InstanceId,
                                "fields": [
                                    {
                                        "title": "CurrentState",
                                        "value": explains.CurrentState.Name,
                                        "short": true
                                    },
                                     {
                                        "title": "PreviousState",
                                        "value": explains.PreviousState.Name,
                                        "short": true
                                    }
                                ],
                                "footer": util.format('[%s] %s' , request.params.channel_name, request.params.user_name)
                            }
                        ],
                        channel : '@'+ request.params.user_id
                    }             
                bot.say(msg);   
                console.log(stdout);
              }
            }); 

            response.write ("Okay. wait a second..");
            response.end();
        }   
    });
}).listen(8080);


var isValidToken = function(token) {  
    if (!token) return false;

    var isValid = false;
    for( var key in commandToken)
    {
        isValid = token == commandToken[key];
        if (isValid) break;
    }
    return isValid;
};


var params=function(req){  
    let q=req.url.split('?'),result={};
    if(q.length>=2){
      q[1].split('&').forEach((item)=>{
           try {
             result[item.split('=')[0]]=item.split('=')[1];
           } catch (e) {
             result[item.split('=')[0]]='';
           }
      })
    }
    return result;
}

역시 slack은 UI 만드는게 가장 힘들고 어려운 작업이다. -_-a

이렇게 만들고나니까, 누가 on/off를 하는지 모른다는 단점이 있길래, slack command log를 남기는 #channel 을 만들어서 명령어가 들어올 때마다 해당 #channel 로 포스팅 하도록 log append 해버렸다.

만들고 나니까 별건 아닌데, 서버에 대한 개념이 없는 사람이 편하게 사용할 수 있고, 요청한 분도 아주 쉽게 이용하고 계시니까 만족스러운 작업이었다.

Ssemi

Read more posts by this author.