AWS는 인스턴스의 생성과 삭제, S3로의 이미지 업로드, DynamoDB로의 스트림 업데이트, 이미지 분석, 문서 변환등 다양한 서비스를 제공한다. AWS 기반으로 서비스를 개발할 경우, AWS의 자원 변동상황을 트리거 해서 적절한 작업을 수행해야 한다. S3에 이미지가 업로드가 끝나면 섬네일을 만들거나, 이미지 업로드를 기다리는 다른 사람에게 이벤트를 발생하는 등의 기능이 그것이다.
보통은 별도의 서버를 만들어서 이런 일들을 하기 마련인데, 예를 들어 셈네일의 경우
파일 업로드가 끝나면
작업해야 할 파일의 이름을 message queue에 적재
이미지 처리 프로그램이 message queue로 부터 파일이름을 읽어와서 섬네일을 제작
섬네일 처리가 끝나면 다시 메시지를 만들어서 유저에게 전송
하는 방식으로 작동한다. AWS 람다 서비스를 이용해서 이 과정을 아래와 같이 단축할 수 있다.
S3에 def A라는 함수를 등록 한다.
def A는 이미지 업로드 완료 이벤트가 발생하면, 업로드된 파일을 읽어서 섬네일로 만든다.
섬네일 만드는 작업이 끝나면, 유저에게 작업 완료 메시지를 전송한다.
IFTTT 등 다른 서비스와 연동해서 트위터, 구글메일, 페이스북 등에 메시지를 보낼 수도 있다.
다른 추가적인 서버의 준비 없이 이런 일을 할수 있다. 코드를 정의하는 것으로 자유로운 확장이 가능하다는 장점이 있다. 특히 코드를 정의 할 수 있다는 점이 맘에 든다. IFTTT와 같은 서비스를 구축할 수 있지 않을까 ?
Lambda를 이용한 IFTTT 서비스 구축
내가 관심있는 건 AWS 람다 서비스가 아니다. 람다 서비스의 IoT 응용이다. IoT 환경에서 기기들은 다른 기기, 유저, 웹 서비스와 연결 될 수 있다. IFTTT와 비슷한 서비스라고 보면 되겠다. 기기에서 어떤 이벤트가 발생하면, 내가 올린 코드를 실행하게 하는 방식.
인프라 구축등 해결해야 할 문제들이 많을 건데, 여기에서는 "개발 언어"에 대한 고민을 해보려고 한다. 일반사용자(그게 파워 유저라고 하더라도) 루비나 파이슨 코드를 올려라고 할 수 없는 노릇 아니겠는가.
DSL
루비 DSL을 이용해서 이 문제를 풀 수 있는지 확인해 보려 한다.
DSL은 Domain Specific Language 의 줄임말이다. 범용적으로 사용하는 언어가 아닌 특수한 (좁은)영역에 사용하는 언어를 통칭한다. 별로 사용하지 않는 언어라고 생각할 수 있겠는데, 이미 여러 영역에서 사용하고 있다. awk, 빌드 환경을 정의 하기 위한 make, ant, 스프레드쉬트에 내장된 매크로등이 DSL이다.
이들 DSL은 C와 같은 또 다른 언어를 이용해서 만드는데, 루비를 이용하면 좀 더 쉽게 DSL을 만들 수 있다. 실제 루비를 이용한 DSL은 여러 소프트웨어에서 응용되고 있다. Chef는 시스템관리를 위한 특수한 목적에 사용하는 언어인데, 시스템관리 목적에 맞는 DSL을 개발해서 사용하고 있다. 아래는 디렉토리를 관리하기 위해서 사용하는 chef 문법이다.
directory "/tmp/folder" do
owner 'root'
group 'root'
mode '0755'
action :create
end
directory 뿐만 아니라 유저, 패키지, 파일, http 클라이언트, 로그, raid, git 관리 등 모든 시스템자원에 대해서 아래의 문법 형식을 사용한다.
type "name" do
attribute "value"
action :type_of_action
end
루비기반의 경량 웹 프레임워크인 sinatra도 DSL을 이용한다. 아래는 시나트라 애플리케이션 예제다.
get '/hi' do
"Hello World!"
end
post '/user/login' do
......
end
HTTP 메서드와 라우터가 자연스럽게 표현된다. Python의 plask라면 아래와 같이 코딩해야 한다.
파이선의 문법 규칙을 따라야 하기 때문에, 표현에 제한이 생긴다. 루비 시나트라가 훨씬 직관적임을 알 수 있다.
DSL 적용 대상이 정해지면, 해당 대상을 묘사할 수 있는 제한된 스펙을 가지는 언어의 개발이 가능해지기 때문에, 개발자는 직관적으로 소프트웨어를 개발할 수 있다.
IFTTT도 특수한 목적의 서비스 이므로 DSL로 IFTTT의 서비스 엔진을 구현할 수 있지 않을까 ? ()현재 정리가 덜 된 생각이라서 물음표를 붙였다. 될 수도 있을 것 같은데, 세부적인 기술문제로 안될 수도 있다. 혹은 부분적인 기능만 구현이 가능할 수도 있고 아직은 잘 모르겠다.
작동 방식 정의
먼저 어떤 방식으로 작동할지를 정의 해야 겠다. 단순한 전구를 기반으로 시나리오를 만들어 본다. 이 전구는 On, Off의 기능만을 가진다. 나는 On, Off 이벤트가 발생 했을 때, 어떤 코드를 실행하려고 한다. IF Then은 아래와 같이 구성할 수 있을 것이다.
전구 서비스를 사용하고 있다고 가정하자.
전구가 on 했을 때, gmail로 메일을 보내고 싶다.
IF는 "ON" 이고, Then은 "action sendmail"이다.
전구가 켜지면 "ON" 메시지가 발생하고, action sendmail을 실행한다.
시간도 IF 조건이 될 수 있다. 10분 후에 불을 켜라! 라거나 (산업용이라면) 매일 저녁 7시가 되면 불을 키고, 새벽 6시가 되면 불을 켜라. 라는 식으로 사용할 수 있을 거다. 일종의 스케쥴러인 셈이다. 스케쥴러는 독립적으로 만들어서 timer 이벤트를 만들도록 하면 될테다. 그러면 user:service_id:event:timer를 만들고, 여기에 대한 Then을 등록하면 된다. 스케쥴러 만드는 것도 재미있긴 하겠는데, 일단은 IF Then 만들어 보려고 한다.
IF에 해당하는 원본은 어딘가 데이터베이스 시스템에 저장할 테고, 실제 운영에는 REDIS를 사용하도록 한다. IF가 Key Then이 Value인 자료구조를 만들면 되겠다. Then에 매개변수만 두고, 실행 할 때는 recipe와 조합해서 코드를 만드는 방법도 있겠다. 이렇게 하면 공간을 절약할 수 있겠는데, 지금은 테스트가 목적이니 그냥 통째로 코드를 입력한다.
DSL 개요
아래는 DSL 예제다.
require 'delegate'
class Action
def self.build(name, &block)
config = new
delegator = eval "#{name}Recipe.new(config)"
delegator.instance_eval(&block)
config
end
class GmailRecipe < SimpleDelegator
def to receiver
puts "To : #{receiver}"
end
def from sender
puts "from : #{sender}"
end
def message msg
puts "Message : #{msg}"
end
def action!
puts "Gmail 전송"
end
end
class SmsRecipe < SimpleDelegator
def to num
puts "phone num: #{num}"
end
def message msg
puts "SMS Message :#{msg}"
end
def action!
puts "Sms 메시지 전송"
end
end
end
레시피들은 개발자들이 만들어서 등록하며, 서버 어딘가에 놓이게 될 거다. 일반 유저는 DSL 형식에 따라서 매개변수만 설정하면 된다. 전구가 켜졌을 때, Gmail로 메시지를 보내기 원한다면
Action.build "Gmail" do
from :"blub"
to :"yundream@gmail.com"
message :"전등 켜졌다는"
action!
end
같은 메시지를 SMS로 보내기 원한다면
Action.build "Sms" do
to :"010-0000-0000"
message :"전틍 켜졌다는"
action!
end
이런 코드들을 값을 REDIDS에 저장한 후, eval로 실행하면 된다. 대략 아래와 같은 느낌이 될테다.
# REDIS에서 읽어온 값의 형태는 아래와 같다.
code = %{
Action.build "Gmail" do
from :"blub"
to :"yundream@gmail.com"
message :"전등 켜졌다는"
action!
end
}
# eval로 실행한다.
eval code
유저 입장에서 프로그래밍이라고 해봤자 "매개변수만 지정하는" 것이라서 쉽게 사용할 수 있다. 웹 인터페이스를 제공해 주면, 몇 번의 마우스 클릭만으로 레시피를 사용할 수 있을 것이다.
물론 준비해야 할 다른 많은 것들이 있다.
제대로 하려면 고민해야 할 것들이 많다.
메시징 인프라는 있어야 한다.
메시징 인프라는 IoT 메시지를 받아서, IF에 등록된 값인지를 search 해야 한다. 수많은 기기로 부터 메시지들 발생 할거다. 효과적인 search를 위한 구성이 필요하다.
스케쥴 이벤트 발생기도 필요하다.
이벤트 조건도 다양해 질 수 있다.
DSL을 이용해서 이런 요소들을 매개변수로 표현하는 건 그리 어려울 것 같지는 않다. 하지만 통계같은 경우를 처리해야 한다면 문제가 어려워 질 수 있다. 뭐, 이런 서비스에 쓸데없이 복잡한 기능이다라고 생각할 수도 있겠고 굳이 구현하려면 빅데이터 영역으로 넘겨서 배치처리하는게 맞는 방향이겠지만..
IFTTT 서비스 구현
프로토타이핑 수준으로 구현한다.
구성
전구 Device에서 /sensor/001/on 으로 메시지를 전송한다. 001은 디바이스 이름이다. 그렇다고 전구 디바이스를 만들 건 아니고, 그냥 MQTT 클라이언트로 메시지를 전송하는 것으로 대신한다.
IFTTT 엔진은 /sensor/001/on에 이벤트를 기다린다.
이벤트가 발생하면, Ruby DSL 코드를 읽어와서
실행한다. Gmail로 메일을 전송 하는 것 까지 구현해 볼 생각이다.
REDIS 서버, IFTTT 서버, MQTT 서버를 구성한다. 우분투리눅스 14.10 서버 버전을 기준으로 하며, VirtualBox로 올린다.
ifttt server
루비로 구현 한다. mqtt와 redis라이브러리를 설치했다.
# gem install redis mqtt
프로그램의 이름은 ifttt.rb다. /sensor/001/on과 /sensor/001/off 두개의 토픽을 subscribe 한다. 데이터가 오면, REDIS로 부터 DSL 코드를 읽어서 실행한다.
recipes.rb : 위의 코드를 그대로 사용한다. 실제 email을 전송하도록 코드를 수정한다.
require 'delegate'
class Action
def self.build(name, &block)
config = new
delegator = eval "#{name}Recipe.new(config)"
delegator.instance_eval(&block)
config
end
class GmailRecipe < SimpleDelegator
def to receiver
puts "To : #{receiver}"
end
def from sender
puts "from : #{sender}"
end
def message msg
puts "Message : #{msg}"
end
# 이부분을 수정해서 Email을 전송하도록 한다.
def action!
puts "Gmail 전송"
end
end
class SmsRecipe < SimpleDelegator
def to num
puts "phone num: #{num}"
end
def message msg
puts "SMS Message :#{msg}"
end
def action!
puts "Sms 메시지 전송"
end
end
end
ifttt.rb : ifttt 엔진 부분. 우선 단순하게 만든 다음, 수정한다.
require 'mqtt'
require 'redis'
require 'pp'
require './recipes'
class IFTTT
@mqtt = nil
@redis = nil
def initialize
@mqtt = MQTT::Client.connect('192.168.56.3')
@mqtt.subscribe '/sensor/001/#'
@redis = Redis.new(:host => '192.168.56.3')
end
def run
@mqtt.get do |topic, message |
event = topic.split('/').last
if (code = @redis.get("user.001.event:#{event}")) == nil
puts "Not found"
else
execute code
end
end
end
def execute code
eval code
end
end
ifttt = IFTTT.new
ifttt.run
redis 서버에 gmail 레시피를 밀어 넣었다.
# cat gmail
Action.build "Gmail" do | m |
to :'yundream@gmail.com'
from :'localhost'
message :'Hello World'
action!
end
# redis-cli -x SET user.001.event:on < gmail
eval은 데이터를 코드로 바꿔서 실행한다. 메타 프로그래밍을 위한 중요한 요소이긴 하지만, 데이터를 코드로 변환하는 과정에서 평가가 이루어지기 때문에 성능이 이슈가 될 수 있다. 성능을 개선하기 위한 방법을 찾아보자.
코드 Cache
유저가 만든 코드는 지속적으로 실행 될 거다. 캐쉬를 할 수 있다면, 성능을 크게 개선할 수 있을 것이다. 간단해 보이지만 (대량의 데이터 처리를 위해서) 클러스터링 시스템을 구축할 경우, 캐쉬하기가 애매모호해 진다. REDIS등의 네트워크 캐시 시스템을 이용할 수도 있겠지만, 네트워크 지연이 걸림돌이다.
Contents
AWS Lambda 서비스
Lambda를 이용한 IFTTT 서비스 구축
DSL
작동 방식 정의
DSL 개요
물론 준비해야 할 다른 많은 것들이 있다.
IFTTT 서비스 구현
구성
ifttt server
고민 거리들
eval
코드 Cache
Jruby
인프라 구성
Recent Posts
Archive Posts
Tags