lsyncdでローカルファイルの更新をS3 バケットに同期する

2016/5/13 JAWS-UG アーキテクチャ専門支部 ハイブリッドクラウド分科会 CDP議論会 #5 の、発表枠番外編で紹介したもの。

lsyncd は、デフォルトでは rsync を使ってファイルのコピーを行いますが、じつは中で Lua の処理理系が動いていて、設定ファイルは Lua のスクリプトになっています。ということは、inotify で通知を受け取ったら実行したい処理を Lua で書けば、好きなことができます。いくつか[サンプル](https://github.com/axkibe/lsyncd/tree/master/examples)も用意されています。

というわけで、書いてみたのがこんなファイル。

AWS CLI をつかって `aws s3 cp` を実行します。3回トライしてダメだったら `aws sns publish` で通知します。

Lua から外部コマンドとして、`aws s3 cp` を呼んで終了コードを参照してリトライ…なんてことができればよいのですが、単純に `aws s3 cp` を呼ぶと、終了コードが非`0`で返って来ると lsyncd ごと終了してしまうので、シェルのワンライナーで処理しています。

Lua の文と、文字列として埋め込まれたシェルのワンライナーが混じってわかりにくいですが、`local runcmd =` がワンライナーを組み立てている部分です。

ワンライナーの最後の `|| :` は終了コードを上書きするためのコメント文です。

~~~
source_dir = "/home/lsync/upload"
s3bucket = "my-s3-bucket"
prefix = "my-prefix"

sns_topic_arn = "SNS_TOPIC_ARN"
msg_subject = "SNS_MSG_SUBJECT"
snscmd = "aws sns publish --topic-arn '" .. sns_topic_arn .. "' --subject '" .. msg_subject .. "'"

settings {
    logfile    = "/home/lsync/lsyncd.log",
    statusFile = "/home/lsync/lsyncd.status",
    nodaemon = false,
    statusInterval = 1,
    delay = 5,
}

cp = function(event)
  local src_path = event.sourcePathname
  local dst_path = event.targetPathname

  if (string.sub(event.source, -1, -1) == "/") then
      src_path = string.sub(event.source, 1, -2) .. event.pathname
  end
  if (string.sub(event.target, -1, -1) == "/") then
      dst_path = string.sub(event.target, 1, -2) .. event.pathname
  end
  local s3cmd = "aws s3 cp '" .. src_path .. "' '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"

  local runcmd = "rc=0 && [ -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .. " || :"
  spawnShell(event, runcmd)
end

rm = function(event)
  local src_path = event.sourcePathname
  local dst_path = event.targetPathname

  if (string.sub(event.source, -1, -1) == "/") then
      src_path = string.sub(event.source, 1, -2) .. event.pathname
  end
  if (string.sub(event.target, -1, -1) == "/") then
      dst_path = string.sub(event.target, 1, -2) .. event.pathname
  end

  local s3cmd = "aws s3 rm '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"
  local runcmd = "rc=0 && [ ! -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .. " || :"
  spawnShell(event, runcmd)
end

mv = function(event)
  local src_path = event.o.targetPathname
  local dst_path = event.d.targetPathname

  if (string.sub(event.o.target, -1, -1) == "/") then
      src_path = string.sub(event.o.target, 1, -2) .. event.o.pathname
  end
  if (string.sub(event.d.target, -1, -1) == "/") then
      dst_path = string.sub(event.d.target, 1, -2) .. event.d.pathname
  end
  local s3cmd = "aws s3 mv '" .. src_path .. "' '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"

  local runcmd = "rc=0 && [ -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .." || :"
  spawnShell(event, runcmd)
end

s3sync = {
    maxProcesses = 1,
    onCreate = cp,
    onModify = cp,
    onDelete = rm,
--  onMove = mv,
}
 
sync {
    s3sync,
    source = source_dir,
    target = "s3://" .. s3bucket .. "/" .. prefix,
}
~~~