ようへいの日々精進XP

よかろうもん

いまさらで恐縮ですが cloud-init について勉強する(1)

追記

各モジュールの動作確認には以下のようなシェルスクリプトを作って行うと捗る。こちら参考になりました。ありがとうございます。

#!/bin/sh

rm -rf /var/lib/cloud/*
cloud-init init --local
cloud-init init
cloud-init modules --mode config
cloud-init modules --mode final

まず、/var/lib/cloud/ 以下をバサッと消す。その後で...

  • cloud-init init --local
  • cloud-init init
  • cloud-init modules --mode config
  • cloud-init modules --mode final

を実行する。これは /etc/rc3.d/ 以下の起動スクリプトの順番に準拠している。

[root@centos-01 cloud]# ls -l /etc/rc3.d/*cloud*
lrwxrwxrwx 1 root root 20 Nov 13 21:38 /etc/rc3.d/K50cloud-init -> ../init.d/cloud-init
lrwxrwxrwx 1 root root 26 Nov 13 21:38 /etc/rc3.d/K50cloud-init-local -> ../init.d/cloud-init-local
lrwxrwxrwx 1 root root 22 Nov 13 21:38 /etc/rc3.d/S50cloud-config -> ../init.d/cloud-config
lrwxrwxrwx 1 root root 21 Nov 13 21:38 /etc/rc3.d/S51cloud-final -> ../init.d/cloud-final

tl;dr

いまさらで恐縮ですが cloud-init に迫ってみる

普段、何気なく EC2 を起動していてもあまり意識していなかった cloud-init について、面と向かわないといけないことがあったので、これを機会に cloud-init について勉強してみたいと思う。既に cloud-init については数多くの知見が出回っているが、実際に手を動かして cloud-init に迫ってみたい。

参考


cloud-init を自分なりに紐解いていく

cloud-init とは

cloud-init とは」について詳しく書かれている資料を見つけることが出来なかったが、こちらのページの冒頭に...

Package provides configuration and customization of cloud instance.

とあるので、(ディストリビュージョンを超えた)クラウドインスタンスの構成やカスタマイズを行うことが出来るパッケージという認識ですすめる。

また、ドキュメントには以下のように書かれている。

  • Setting a default locale(デフォルトロケールの設定)
  • Setting a instance hostname(インスタンスのホスト名の設定)
  • Generating instance ssh private keys(SSH プライベートキーの生成)
  • Adding ssh keys to a users .ssh/authorized_keys so they can log in(ユーザーの SSH キーを authorized_keys に追加)
  • Setting up ephemeral mount points(マウントポイントの設定)

また、

Cloud-init's behavior can be configured via user-data.

cloud-init の振る舞いは user-data で設定することが出来るとのことで、ドキュメントには EC2 インスタンスを起動する際に利用する例として以下のように記載されている。

This is done via the --user-data or --user-data-file argument to ec2-run-instances for example.

うんちくは程々に

cloud-init の設定ファイルである cloud-init について自分なりに紐解いていく。紐解くにあたっては予算が少ないので Ubuntu 14.04 上に LXC で起動した CentOS 6.7 コンテナを利用する。

$ sudo apt-get install lxc yum
$ sudo lxc-create -n centos-01 -t centos
$ sudo lxc-start -d -n centos-01

CentOS release 6.7 (Final)
Kernel 3.13.0-55-generic on an x86_64

centos-01 login:

尚、LXC についてはこちらの特集記事が大変参考になる。

CentOS コンテナが起動したらログインして cloud-init をインストールする。

# yum install cloud-init

これで準備完了。

cloud-init のディレクトリ構成

前述の設定ファイルではなく、cloud-init によって実行されるスクリプト等が展開されるディレクトリは以下のようになっている。

# tree --charset=x /var/lib/cloud/ -L 2
/var/lib/cloud/
|-- data
|   |-- instance-id
|   |-- previous-datasource
|   |-- previous-hostname
|   |-- previous-instance-id
|   |-- result.json
|   `-- status.json
|-- handlers
|-- instance -> /var/lib/cloud/instances/iid-datasource-none
|-- instances
|   `-- iid-datasource-none
|-- scripts
|   |-- per-boot
|   |-- per-instance
|   |-- per-once
|   `-- vendor
|-- seed
`-- sem
    `-- config_scripts_per_once.once

このディレクトリについてはドキュメントにて言及されているので、ざっくりと拝借させて頂く。

ディレクトリ 詳細
/var/lib/cloud cloud-init 固有のサブディレクトリを含むメインディレクトリで変更することも可能
data/ インスタンス ID やデータソース、ホスト名等の情報が含まれている(以下で全てのファイルを展開してみる)
handlers/ part-handler コードを置いておく、ファイル名は part-handler-${ハンドラ番号}(ドキュメントでは XYZ で表現)とする
instance/ instances のサブディレクトリのアクティブなインスタンスディレクトリのシンボリックリンクになっている
instances/ 同一のインスタンスイメージから作成されたインスタンスの識別子ディレクトリが作成され、アクティブなインスタンスが instance にリンクしている
scripts/ ダウンロードしたり作成したスクリプトを配置する
seed/ TBD = 未確定
sem/ このディレクトリにはインスタンス ID に関連付けられていないセマフォファイルが含まれており、これらのファイルは各モジュールが確実に実行する為に使用される

data/ 以下に保存されているファイルを全て展開すると以下のような内容となっている。

# for i in `ls /var/lib/cloud/data/*`; do echo "====== $i ======"; cat $i; echo ""; done
====== /var/lib/cloud/data/instance-id ======
iid-datasource-none

====== /var/lib/cloud/data/previous-datasource ======
DataSourceNone: DataSourceNone

====== /var/lib/cloud/data/previous-hostname ======
# Created by cloud-init v. 0.7.5 on Thu, 12 Nov 2015 14:30:25 +0000
HOSTNAME=centos01.localdomain

====== /var/lib/cloud/data/previous-instance-id ======
iid-datasource-none

====== /var/lib/cloud/data/result.json ======
{
 "v1": {
  "errors": [
   "'NoneType' object is not iterable",
   "'NoneType' object is not iterable"
  ],
  "datasource": null
 }
}

====== /var/lib/cloud/data/status.json ======
{
 "v1": {
  "init": {
   "start": 1447339866.7182679,
   "finished": 1447340129.1867039,
   "errors": [
    "'NoneType' object is not iterable"
   ],
   "end": null
  },
  "datasource": null,
  "modules-config": {
   "start": 1447340129.37832,
   "finished": 1447340129.4275589,
   "errors": [
    "'NoneType' object is not iterable"
   ],
   "end": null
  },
  "modules-final": {
   "start": 1447368909.2942469,
   "finished": 1447368909.3693061,
   "errors": [],
   "end": null
  },
  "init-local": {
   "start": 1447339866.4216521,
   "finished": 1447339866.5258591,
   "errors": [],
   "end": null
  },
  "stage": null
 }
}

instance ディレクトリには以下のようなファイルが展開されている。

# tree --charset=x /var/lib/cloud/instance -L 1
/var/lib/cloud/instance
|-- boot-finished
|-- cloud-config.txt
|-- datasource
|-- handlers
|-- obj.pkl
|-- scripts
|-- sem
|-- user-data.txt
|-- user-data.txt.i
|-- vendor-data.txt
`-- vendor-data.txt.i

sem ディレクトリには以下のようにファイルが展開されている。

# for i in `ls /var/lib/cloud/sem/*`; do echo "====== $i ======"; cat $i; echo ""; done
====== /var/lib/cloud/sem/config_scripts_per_once.once ======
375: 1447338625.91

cloud-init 設定ファイルのディレクトリ構成

cloud-init をインストールすると以下のように設定ファイルが展開される。

# tree --charset=x /etc/cloud/
/etc/cloud/
|-- cloud.cfg
|-- cloud.cfg.d
|   |-- 05_logging.cfg
|   `-- README
`-- templates
    |-- chef_client.rb.tmpl
    |-- hosts.debian.tmpl
    |-- hosts.redhat.tmpl
    |-- hosts.suse.tmpl
    |-- resolv.conf.tmpl
    |-- sources.list.debian.tmpl
    `-- sources.list.ubuntu.tmpl

2 directories, 10 files

cloud-init の挙動を制御するのは cloud.cfg となるが、cloud.cfg.d 以下に切り出すことも可能。但し、切り出しについては行わず cloud.cfg のみを見てみる。また、templates ディレクトリには hosts ファイルや resolv.conf のテンプレートとなるファイルが保存されている。例えば、hosts.redhat.tmpl は以下のような内容になっている。

# cat hosts.redhat.tmpl
#*
    This file /etc/cloud/templates/hosts.redhat.tmpl is only utilized
    if enabled in cloud-config.  Specifically, in order to enable it
    you need to add the following to config:
      manage_etc_hosts: True
*#
# Your system has configured 'manage_etc_hosts' as True.
# As a result, if you wish for changes to this file to persist
# then you will need to either
# a.) make changes to the master file in /etc/cloud/templates/hosts.redhat.tmpl
# b.) change or remove the value of 'manage_etc_hosts' in
#     /etc/cloud/cloud.cfg or cloud-config from user-data
#
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 ${fqdn} ${hostname}
127.0.0.1 localhost.localdomain localhost
127.0.0.1 localhost4.localdomain4 localhost4

# The following lines are desirable for IPv6 capable hosts
::1 ${fqdn} ${hostname}
::1 localhost.localdomain localhost
::1 localhost6.localdomain6 localhost6

また、興味深いのが chef_client.rb.tmpl で展開すると以下のようになっている。

# cat chef_client.rb.tmpl
#*
     This file is only utilized if the module 'cc_chef' is enabled in
     cloud-config. Specifically, in order to enable it
     you need to add the following to config:
       chef:
         validation_key: XYZ
         validation_cert: XYZ
         validation_name: XYZ
         server_url: XYZ
*#
log_level              :info
log_location           "/var/log/chef/client.log"
ssl_verify_mode        :verify_none
validation_client_name "$validation_name"
validation_key         "/etc/chef/validation.pem"
client_key             "/etc/chef/client.pem"
chef_server_url        "$server_url"
environment            "$environment"
node_name              "$node_name"
json_attribs           "/etc/chef/firstboot.json"
file_cache_path        "/var/cache/chef"
file_backup_path       "/var/backups/chef"
pid_file               "/var/run/chef/client.pid"
Chef::Log::Formatter.show_time = true

このテンプレートを展開して OS 起動時に Chef を使ってプロビジョニングも出来たりするのだろう。(今回は試さないけど)

cloud.cfg について

cloud-init インストール直後の cloud.cfg は以下のようになっている。(長いので gist に...)

cloud.cfg のフォーマットは YAML となっており、各キーに関しての解説がこれまた見つからない...ので以下のファイルのコメントが参考になった。

github.com

上記のファイルを参考にさせて頂いて以下のようにコメントを付けてみた。

# インスタンス上に作成するユーザーを定義する。以下の場合には default_user を参照する。
users:
 - default

# root でのログインを無効にする定義。以下の場合には root でのログインを有効にしている。
disable_root: 1
# SSH のパスワードログインを許可する、許可しないの定義。以下の場合には SSH のパスワードログインを有効にしている。
ssh_pwauth:   0

# locale の設定ファイルのパスを定義
locale_configfile: /etc/sysconfig/i18n
# fstab の各フィールドを定義
mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2']
# ???
resize_rootfs_tmp: /dev
# SSH キーを削除する定義。パブリックなイメージについては有効にしておく必要がある。以下の例では有効にしている。
ssh_deletekeys:   0
# 生成する SSH キーのタイプを定義する
ssh_genkeytypes:  ~
# syslog のパラメータを定義する
syslog_fix_perms: ~

# 起動時に実行されるモジュールを定義する(※S50cloud-init-local/S51cloud-init)
cloud_init_modules:
 - migrator
(snip)

# 起動時に実行されるモジュールを定義する(※S52cloud-config なので S50cloud-init-local/S51cloud-init の後に実行される)
cloud_config_modules:
 - mounts
(snip)

# 起動時に実行されるモジュールを定義する(※S53cloud-final なので S50cloud-init-local => S51cloud-init => S52cloud-config => S53cloud-final)
cloud_final_modules:
 - rightscale_userdata
(snip)

# システム固有の設定等を定義する(???)
system_info:
  # 冒頭の users > default から参照されるユーザー定義
  default_user:
    name: centos
    lock_passwd: true
    gecos: Cloud User
    groups: [wheel, adm]
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
    shell: /bin/bash
  distro: rhel
  paths:
    cloud_dir: /var/lib/cloud
    templates_dir: /etc/cloud/templates
  ssh_svcname: sshd

# vim:syntax=yaml

ひとまず今日はここまで。


おわり

cloud-init

  • 奥深い
  • EC2 使っていれば漏れ無く使っているはずなんだけど...情報が少ない気がするのは自分だけかな...

まだまだ

  • 全然使いこなせていないよ...(汗

(おまけ)cloud-init ヴィデオ

www.youtube.com

(超メモ)CloudWatch Logs Subscriptions → Lambda → Twitter メモ

超メモシリーズ。

何がしたいのか?

CloudWatch Logs Subscriptions でフィルタされたレコードを Lambda で Twitter に呟かせたい。


メモ

Lambda にはどのような状態でレコードが入ってくるのか?

CloudWatch Logs Subscriptions でフィルタされたレコードは Kinesis ストリームを介して Lambda の event オブジェクトには以下のような JSON で入ってくる。

{
    "awslogs": {
        "data": "H4sIAAAAAAAAAJ2RTY+CMBCG/wrpXgXb0lLgZrJqTPbjoDdjTJWJNlIgpega43/fAbMfpz3sqZN3Ju887/RGLLStPsDq2gDJyfNkNdm+TpfLyXxKRqS+VOBQ5pIxSdM4SShFuawPc1d3DXaKen8CF6Ly0JfegbbYaGunrbYQNqaB0lSw5ZTJkNGQKRxtu127d6bxpq5mpvTgWpKvyYu2u0I/TLb+AuDJZvCdnqHy/ciNmALtY865SngsOE0TJWjMleI8TZhgMhNKZkpKpSjPUFY8EzLFkUT09N5gZq8t4jMhJMNUTCSKj75ugfaLUbD+5k1XNMuFzGMaSUFxY/DEkk2weJu9B2EY5MHR+yYfjxvLZeTROzJVfdJOR/vajk/Xrr1245/00dHbEjngwzu991DMDJQFZruRQnvo8fqT/7m9v2DDcSwfKoZvGPY/cB7g+wqGEhnJ72D/Qb3fN/dPf1Js6SkCAAA="
    }
}

Python で処理しようとする場合には以下のように JSON がパースされた状態で event オブジェクトに入ってくる。

f:id:inokara:20151018195837p:plain

data キーから値を取り出してレコードを解析(Base64 をデコードして解凍)する。解析すると以下のような状態の JSON になる。

{
  "logEvents": [
    {
      "extractedFields": {
        "message": "http://pm25.test.inokara.com/kyusyu/2015-10-17.html",
        "lev": "INFO",
        "lv": "I,",
        "sp1": "--",
        "sp2": ":",
        "datetime": "2015-10-18T09:45:30.540420 #16"
      },
      "message": "I, [2015-10-18T09:45:30.540420 #16] INFO -- : http://pm25.test.inokara.com/kyusyu/2015-10-17.html",
      "timestamp": 1445136614672,
      "id": "32227623420867403277228614159475975577029772729458032640"
    }
  ],
  "subscriptionFilters": [
    "LambdaStream_tweet"
  ],
  "logStream": "soramame-pipeline_2015-10-17",
  "logGroup": "docker-log",
  "owner": "xxxxxxxxxxxxxxx",
  "messageType": "DATA_MESSAGE"
}

最終的には logEventsextractedFields.message にある URL を取り出して Twitter に呟かせたい。

ということで

以下のような Lambda ファンクションとなった。

# -*- coding: utf-8 -*-

import sys, json
import datetime
import ConfigParser
import twitter
import base64
import gzip
from StringIO import StringIO

def extract_url(record):
    # Base64 デコードして解凍してデータを取得する
    decoded_data = record.decode("base64")
    json_data = json.loads(gzip.GzipFile(fileobj=StringIO(decoded_data)).read())
    for data in json_data['logEvents']:
        return data['extractedFields']['message']

def lambda_handler(event, context):
    # config.ini から Twitter API の Credential 情報を取得
    c = ConfigParser.SafeConfigParser()
    c.read("./config.ini")
    # Twitter API を初期化
    api = twitter.Api(
        consumer_key        = c.get('tw','consumer_key'),
        consumer_secret     = c.get('tw','consumer_secret'),
        access_token_key    = c.get('tw','access_token_key'),
        access_token_secret = c.get('tw','access_token_secret'),
        )
    # event オブジェクトからレコードを取得する
    url = extract_url(event['awslogs']['data'])
    d = datetime.date.today() - datetime.timedelta(1)
    d = u"%s 年 %s 月 %s 日 PM2.5 の状況(九州地方のみ): " % (d.year, d.month, d.day)
    print d + url
    print api.PostUpdate(d + url)

早速...呟かせてみる

以下のように呟かせてみた。

#
# テストログを用意
#
% cat test.log
[
  {
    "timestamp": 1445136614672,
    "message": "I, [2015-10-18T09:45:30.540420 #16] INFO -- : http://pm25.test.inokara.com/kyusyu/2015-10-17.html"
  }
]

#
# ログを put する
#
% aws --region ap-northeast-1 logs put-log-events \
--log-group-name docker-log \
--log-stream-name ${stream_name} \
--log-events file://test.log \
--sequence-token ${token}

おお、きた。

f:id:inokara:20151018201043p:plain


後で...

も少し手順をまとめよう。

以上。

(超メモ)Python で Base64 と gzip で固められたデータを復元するメモ(CloudWatch Logs Subscriptions で Kinesis ストリームに入ったレコードを復元する)

何がしたいの?

CloudWatch Logs Subscriptions で Kinesis に入ったレコードを取得すると以下のように gzip で固められて Base64エンコードされたデータになっている。

H4sIAAAAAAAAAJ2RTY+CMBCG/wrpXgXb0lLgZrJqTPbjoDdjTJWJNlIgpega43/fAbMfpz3sqZN3Ju887/RGLLStPsDq2gDJyfNkNdm+TpfLyXxKRqS+VOBQ5pIxSdM4SShFuawPc1d3DXaKen8CF6Ly0JfegbbYaGunrbYQNqaB0lSw5ZTJkNGQKRxtu127d6bxpq5mpvTgWpKvyYu2u0I/TLb+AuDJZvCdnqHy/ciNmALtY865SngsOE0TJWjMleI8TZhgMhNKZkpKpSjPUFY8EzLFkUT09N5gZq8t4jMhJMNUTCSKj75ugfaLUbD+5k1XNMuFzGMaSUFxY/DEkk2weJu9B2EY5MHR+yYfjxvLZeTROzJVfdJOR/vajk/Xrr1245/00dHbEjngwzu991DMDJQFZruRQnvo8fqT/7m9v2DDcSwfKoZvGPY/cB7g+wqGEhnJ72D/Qb3fN/dPf1Js6SkCAAA=

この文字列を元のデータに Python で復元したい。


どうするの?

StringIO と gzip モジュールで解決

以下のように StringIO モジュールと gzip モジュールを利用することで復元出来た。

#!/usr/bin/env python

import base64
import gzip
from StringIO import StringIO

event = "H4sIAAAAAAAAAJ2RTY+CMBCG/wrpXgXb0lLgZrJqTPbjoDdjTJWJNlIgpega43/fAbMfpz3sqZN3Ju887/RGLLStPsDq2gDJyfNkNdm+TpfLyXxKRqS+VOBQ5pIxSdM4SShFuawPc1d3DXaKen8CF6Ly0JfegbbYaGunrbYQNqaB0lSw5ZTJkNGQKRxtu127d6bxpq5mpvTgWpKvyYu2u0I/TLb+AuDJZvCdnqHy/ciNmALtY865SngsOE0TJWjMleI8TZhgMhNKZkpKpSjPUFY8EzLFkUT09N5gZq8t4jMhJMNUTCSKj75ugfaLUbD+5k1XNMuFzGMaSUFxY/DEkk2weJu9B2EY5MHR+yYfjxvLZeTROzJVfdJOR/vajk/Xrr1245/00dHbEjngwzu991DMDJQFZruRQnvo8fqT/7m9v2DDcSwfKoZvGPY/cB7g+wqGEhnJ72D/Qb3fN/dPf1Js6SkCAAA="

decoded_data = event.decode("base64")
print gzip.GzipFile(fileobj=StringIO(decoded_data)).read()

結果

以下のように JSON で格納されていることが解った。

% python test.py | jq .
{
  "logEvents": [
    {
      "extractedFields": {
        "message": "http://pm25.test.inokara.com/kyusyu/2015-10-17.html",
        "lev": "INFO",
        "lv": "I,",
        "sp1": "--",
        "sp2": ":",
        "datetime": "2015-10-18T09:45:30.540420 #16"
      },
      "message": "I, [2015-10-18T09:45:30.540420 #16] INFO -- : http://pm25.test.inokara.com/kyusyu/2015-10-17.html",
      "timestamp": 1445136614672,
      "id": "32227623420867403277228614159475975577029772729458032640"
    }
  ],
  "subscriptionFilters": [
    "LambdaStream_tweet"
  ],
  "logStream": "soramame-pipeline_2015-10-17",
  "logGroup": "docker-log",
  "owner": "xxxxxxxxxxxxx",
  "messageType": "DATA_MESSAGE"
}

ちなみに、logEventsextractedFields.message だけ取得したい場合には以下のように書けば取得出来る。

decoded_data = event.decode("base64")
json_data = json.loads(gzip.GzipFile(fileobj=StringIO(decoded_data)).read())
for data in json_data['logEvents']:
    print data['extractedFields']['message']

以下のように出力される。

% python test.py
http://pm25.test.inokara.com/kyusyu/2015-10-17.html

参考

有難うございます。

以上。

(超メモ)AWS SDK で Signature not yet current: 20151018T000614Z... というエラーが出た際の対処

何が起きた?

Ubuntu サーバー上で動かしていた Docker 1.9.0-dev で Log Driver に CloudWatch Logs を利用している際に以下のようなエラーに遭遇。

$ docker run --rm -i -t \
>   --log-driver=awslogs \
>   --log-opt awslogs-region=ap-northeast-1 \
>   --log-opt awslogs-group=docker-log \
>   --log-opt awslogs-stream=hello-world \
> hello-world
Error response from daemon: Cannot start container 5ec548413d2d1b24c34b3cf552d4616130f5da0552f623785eb403983d3a6270: Failed to initialize logging driver: InvalidSignatureException: Signature not yet current: 20151018T000614Z is still later than 20151018T000104Z (20151017T235604Z + 5 min.)
        status code: 400, request id: []

原因と対処

原因

  • 時刻のズレ

対処

  • 時刻のズレを直す
# 直す前
$ date
Sun Oct 18 09:07:27 JST 2015

# 直す
$ sudo ntpdate ntp.ubuntu.com
18 Oct 08:59:45 ntpdate[18694]: step time server 91.189.94.4 offset -610.069478 sec

対処後

$ docker run --rm -i -t \
>   --log-driver=awslogs \
>   --log-opt awslogs-region=ap-northeast-1 \
>   --log-opt awslogs-group=docker-log \
>   --log-opt awslogs-stream=hello-world \
> hello-world

Hello from Docker.
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/userguide/

CloudWatch Logs にも。

f:id:inokara:20151018091637p:plain

ヨカタ。


参考

stackoverflow.com

有難うございます。

ラムダこりゃ(Amazon Lambda チュートリアル 4)〜 Lambda と Python で「赤い霊柩車シリーズ」の過去の作品名をTwitter につぶやかせる 〜

引き続き Lambda を触ってみたいと思います。

tl;dr

Python の勉強を兼ねて Wikipedia の「赤い霊柩車シリーズ」ページから過去の作品のタイトルを取得して作品名と放映年を Twitter に呟かせる bot を Lambda で作ってみた。


赤い霊柩車シリーズとは

Wikipedia より抜粋。

『赤い霊柩車シリーズ』(あかいれいきゅうしゃシリーズ)は、フジテレビ系の2時間ドラマ「金曜プレミアム」(毎週金曜日21:00 - 22:52)で1992年から放送されているテレビドラマシリーズ。主演は片平なぎさ。(略)推理作家・山村美紗の作品「葬儀屋社長 石原明子シリーズ」を原作とする。片平なぎさの代表的作品であり、2012年までの29作の平均視聴率が17.0%という人気シリーズである。

片平なぎさもさることながら、脇役として山村美紗の娘、山村紅葉が出演しており、大村崑との掛け合いなどが楽しめる、サスペンスなのにあまり怖くなくて家族で楽しめる作品だと思う。


メモ

Lambda Function Python

github.com

以下、Function を作成するにあたってのメモ。

  • HTML の解析には BeautifulSoup というモジュールを利用することで簡単に HTML を解析することが出来た
  • ConfigParser が便利だけど "' までも展開されるので注意する
  • コメント行が日本語だと Lambda 上で実行すると怒られたので # -*- coding: utf-8 -*- を追加

あと、ローカルで実行している時には気づかなったが Lambda 上で実行する場合に変数を雑に設定していると怒られた。

    # JSON を解析
    json = json.loads(htmldata.read())

上記のように書いていると...

UnboundLocalError: local variable 'json' referenced before assignment

上記のようなエラーで怒られてれしまう。

パッケージング

作成した Function をアップロードするにあたって外部モジュールを利用している場合にはあらかじめダウンロードしておいて Function のスクリプトと合わせて ZIP で固める必要がある。ドキュメントに従って以下のように固めるテンプルした。

# MacOS X 環境で homebrew でインストールした Python を利用している場合には必要
% cat << EOT >> ~/.pydistutils.cfg
[install]
prefix=
EOT

# パッケージ用のディレクトリを作成
% mkdir lambda-korya-twitter-akaireikyusha

# ディレクトリ以下に必要なモジュールをインストール
% pip install BeautifulSoup - t lambda-korya-twitter-akaireikyusha
% pip install twitter - t lambda-korya-twitter-akaireikyusha

# Function もディレクトリに含める
% ls -l lambda-korya-twitter-akaireikyusha/lambda_function.py
-rwxr-xr-x  1 kappa  staff  1736 Oct 10 19:09 lambda-korya-twitter-akaireikyusha/lambda_function.py

# zip で固める
% cd lambda-korya-twitter-akaireikyusha
% zip -r your_function.zip *

# 消しておく
% rm -f ~/.pydistutils.cfg

create-function

以下のように Lambda Function を作成する。

aws lambda --region us-east-1 \
  create-function \
    --function-name akai-reikyusha \
    --runtime python2.7 \
    --role arn:aws:iam::1234567890123:role/lambda_basic_execution \
    --handler lambda_function.lambda_handler \
    --timeout 10 \
    --zip-file fileb://your_function.zip

update-function-code

以下のようにデバッグした Function をアップロード、デバッグ、アップロードを繰り返す...。

# アップロード
% aws lambda --region us-east-1 update-function-code --function-name akai-reikyusha --zip-file fileb://your_function.zip

Scheduled event

前の記事と同様に Scheduled event を利用したいので以下のように設定。

f:id:inokara:20151010203919p:plain

しかし、以下のようにエラー。

f:id:inokara:20151010095818p:plain

ん、東京リージョンではリソースが不足しているのかしら...ということで、ヴァージニア(US-EAST)で試したところ登録することが出来た。

つぶやき

f:id:inokara:20151010192013p:plain

緊張感のあるタイトルである。

f:id:inokara:20151010214857p:plain

ログも上記のように。


ということで...

Python で作成する Lambda Function

  • Lambda にアップロードする際の手順に慣れが必要(virtualenv というのを使うことも出来る)
  • ローカルのデバッグってどうするのか引き続き調べる必要がある

一日に一回

一日に一回だけ呟かせるようにした。

f:id:inokara:20151010204756p:plain

ラムダこりゃ(Amazon Lambda チュートリアル 3)〜 Lambda 「だけで」 Twitter につぶやかせる 〜

おはげようございます。

追記

5 分ごとに処理をさせたい場合には以下のように設定するのが一番簡単だった。

f:id:inokara:20151010185344p:plain

最初からこの設定でやっておけば良かった...。


tl;dr

これまた re:Invent 2015 にて Lambda で Scheduled Events という機能をサポートとの発表があった。

aws.typepad.com

この Scheduled Event というのは個人的解釈でざっくり言うとクーロン、そう、Cron 。つまり、従来 Lambda は SNS や S3 や CloudWatch Logs 等のイベントを検知して Lambda Function が実行されていたものが、Lambda 内のスケジュールでも Function が実行出来るようになったということ!(なんだなという認識)


ためそ

教材

github.com

スケジューリング

今回、重要になるのは Scheduled Event の設定。例えば、5 分毎に Function を実行させたい場合には以下のように設定する。

f:id:inokara:20151010095510p:plain

Schedule expression には Cron フォーマットで記載することが出来るとのことなので、当初は敢えて以下のように記述してみたが...

cron(*/5 * * * * )

これだと...なぜかエラーが出てしまう...試行錯誤した結果、以下のように記述することで 5 分毎の実行が可能となった。

cron(*/5 * * * * ?)

6 番目の「?」は「年」を表現しているようで、この場合には「2015」が入るようだ。

トゥイート

f:id:inokara:20151010100223p:plain

おお、Lambda だけでトゥイート出来てる。

ログも確認してみる。

f:id:inokara:20151010100556p:plain

で、なんぼ?

ドキュメントによると Lambda のコスト計算をざっくり纏めると以下の要素で計算を行う。

  • リクエスト数
  • 処理に掛かった時間
  • 無料枠(Lambda 関数に割り当てたメモリ量によって無料利用枠で実行できる時間が異なる)

例えば、5 分ごとのツイートを 128MB メモリ量で 1 ヶ月間行った場合には以下のような計算式になる(と思われる)。

  • 24 時間 x 60 分 = 1440 分
  • 1440 分 ÷ 5 分毎 = 288 回(リクエスト)/day
  • 288 リクエスト/day ✕ 30 日 = 8640 回(リクエスト)
  • 1 回のファンクションの処理に 10 秒掛かると想定して 8640 リクエスト✕ 10 秒 = 86400 秒

128 メモリ量で無料枠で実行可能なリクエスト数は 3,200,000 秒なので無料枠内で収まる...はず。


ということで...

  • ちょっとしたサービスなら Lambda だけで運用出来てしまいそうな気がした
  • crontab フォーマットっぽい書き方が出来るのは解りやすいので嬉しい(但し、6 桁だったりしたのが戸惑った)
  • 次は Python で Function 作ってみたいなあ

以上。

ほぼスクショで CloudWatch Dashboard チュートリアル

おはげようございます。

tl;dr

CloudWatch にて選択したメトリクスをペタペタ貼り付けてオリジナルのダッシュボードを作成する機能がサポートされたとのことなのでこれまた試してみた。

f:id:inokara:20151010064830p:plain

こんな感じになる。


試してみた

参考

ダッシュボード作成

ダッシュボード作成の入り口は...

f:id:inokara:20151010065847p:plain

か...

f:id:inokara:20151010065946p:plain

から。メトリクスを選択した状態から Create Dashboard する以下のような画面となる。

f:id:inokara:20151010070141p:plain

適当な名前を付けて始めよう。Add to Dashboard でダッシュボードに追加される。

ウィジェット

CloudWatch Dashboard ではメトリクスとテキストをウィジェットという形でダッシュボードに貼り付けていく。

f:id:inokara:20151010070449p:plain

ウィジェットの配置やサイズはほぼ自由に配置することが出来る。

f:id:inokara:20151010071029p:plain

あまり実用性が無いような配置も可能。

また、色々とメモ書きとか各ダッシュボードへのリンクを貼っておいたりするのに利用出来そうなテキストウィジェットは上図にもあるようにマークダウン形式で記載することが出来る。当然、画像も埋め込んだりすることが出来るので面白い利用方法を考えてみたい(個人的に)。

ウィジェットの追加、削除

ウィジェットの追加だけであれば Add widget ボタンでウィザード形式で追加するか、下図のように Action メニューから Add metrics graph を選択しても良さそう。

f:id:inokara:20151010071402p:plain

メトリクスを選択してウィジェットを作成する。

f:id:inokara:20151010071414p:plain

尚、削除については Action メニューから削除するか、ウィジェットの右上のメニューから削除も出来る。

f:id:inokara:20151010071803p:plain

ズーム、ズーム、ズーム

f:id:inokara:20151010072313p:plain

上図のようにメトリクスをドラッグすると...

f:id:inokara:20151010072430p:plain

指定した時間の範囲でズーム、ズーム、ズーム。ただ、これだけではありきたり、他のグラフも同じ時間の範囲でズーム、ズーム、ズームして欲しい場合には...

f:id:inokara:20151010072633p:plain

Link Graphs にチェックを入れておくことで...

f:id:inokara:20151010072835p:plain

上図のように全てのメトリクスを横断してズーム、ズーム、ズーム出来る。

で、なんぼ?

こちら によると...

CloudWatch Dashboards are available now and you can start using them today in all AWS regions! You can create up to three dashboards (each with up to 50 metrics) at no charge. After that, each additional dashboard costs $3 per month.

  • 一つのダッシュボードに 50 個のメトリクスまで貼り付けることが出来る
  • 3 つのダッシュボードまでは無料!
  • 替え玉(追加)は 3 ドル/1 ダッシュボード/月

替え玉が極端に高い印象。

共有機能?

こちら によると...

I am looking forward to seeing examples of this feature in action. Take it for a spin and let me know what you come up with!

乏しい英語力では上記のセンテンスを読み取ることは出来なかったが、そのうちに提供されるのかもしれない。


ということで...

  • 調べきれていないが、せっかく作ったダッシュボードを共有する機能があると(あるかもしれないけど)嬉しい気がする
  • プライベートな URL 共有、もしくは時限的な URL 共有があれば必要なメトリクスだけを Dashboard に纏めて顧客に見せることも出来そう

ecs-cli チュートリアル

おはげようございます。来年の re:Invent は現地で...という目標を胸に英語の勉強を始めたいかっぱです。

tl;dr

re:Invent での発表にて ECS 専用のコマンドラインツールが利用出来るようになったとのことでザクっと試してみたい。

aws.typepad.com

尚、このコマンドラインツールは Docker Compose をサポートしているので Docker Compose で利用する YAML ファイルが直接利用出来るようになっている。(従来は ECS の Task Definition と YAML ファイルの相互変換ツールを利用する方法があった)


参考

docs.aws.amazon.com

github.com


試す

試した環境

% sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.1
BuildVersion:   15B22c

ecs-cli のインストゥール

ドキュメント通り

% sudo curl -o /usr/local/bin/ecs-cli https://s3.amazonaws.com/amazon-ecs-cli/ecs-cli-darwin-amd64-latest
% sudo chmod +x /usr/local/bin/ecs-cli

簡単。

ECS クラスタの起動

以下のように ECS クラスタを作成する。

% ecs-cli configure --region ap-northeast-1 --cluster my-cluster

開封の儀みたいなもの。

ECS インスタンスコンテナの起動

コンテナインスタンスecs-cli up で起動する。

% ecs-cli up --keypair ${ssh_key_name} \
  --capability-iam \
  --size 2 \
  --vpc vpc-xxxxxxxxx \
  --instance-type t2.micro \
  --subnets subnet-xxxxxxxx,subnet-xxxxxxxxx \
  --azs ap-northeast-1a,ap-northeast-1c
INFO[0000] Created cluster                               cluster=my-cluster
INFO[0001] Waiting for your cluster resources to be created 
INFO[0002] Cloudformation stack status                   stackStatus=CREATE_IN_PROGRESS
INFO[0062] Cloudformation stack status                   stackStatus=CREATE_IN_PROGRESS
INFO[0122] Cloudformation stack status                   stackStatus=CREATE_IN_PROGRESS
INFO[0183] Cloudformation stack status                   stackStatus=CREATE_IN_PROGRESS

以下の通り CloudFormation を利用してコンテナインスタンスが 2 台起動した。

% aws ecs list-container-instances --cluster my-cluster
{
    "containerInstanceArns": [
        "arn:aws:ecs:ap-northeast-1:123456789012:container-instance/094216a6-5a7a-4462-be4c-b25d64254a3f", 
        "arn:aws:ecs:ap-northeast-1:123456789012:container-instance/2b0b743a-9f60-4090-b61a-a24c957a7f7e"
    ]
}

尚、指定出来るオプションは以下の通り。利用の環境に応じて適宜設定する必要があると思ふ。

% ecs-cli up --help
NAME:
   up - Create the ECS Cluster (if it does not already exist) and the AWS resources required to set up the cluster.

USAGE:
   command up [command options] [arguments...]

OPTIONS:
   --keypair            Specify the name of an existing Amazon EC2 key pair to enable SSH access to the EC2 instances in your cluster.
   --capability-iam     Acknowledge that this command may create IAM resources.
   --size               [Optional] Specify the number of instances to register to the cluster. The default is 1.
   --azs                [Optional] Specify a comma-separated list of 2 VPC availability zones in which to create subnets (these AZs must be in the 'available' status). This option is recommended if you do not specify a VPC ID with the --vpc option. WARNING: Leaving this option blank can result in failure to launch container instances if an unavailable AZ is chosen at random.
   --security-group     [Optional] Specify an existing security group to associate it with container instances. Defaults to creating a new one.
   --cidr               [Optional] Specify a CIDR/IP range for the security group to use for container instances in your cluster. Defaults to 0.0.0.0/0 if --security-group is not specified
   --port               [Optional] Specify a port to open on a new security group that is created for your container instances if an existing security group is not specified with the --security-group option. Defaults to port 80.
   --subnets            [Optional] Specify a comma-separated list of existing VPC Subnet IDs in which to launch your container instances. This option is required if you specify a VPC with the --vpc option.
   --vpc                [Optional] Specify the ID of an existing VPC in which to launch your container instances. If you specify a VPC ID, you must specify a list of existing subnets in that VPC with the --subnets option. If you do not specify a VPC ID, a new VPC is created with two subnets.
   --instance-type      [Optional] Specify the EC2 instance type for your container instances.

-security-group に関しては特に指定しない場合には 80 番ポートのみ 0.0.0.0/0 向けに解放されたルールが適用される。又、--capability-iam を指定しておくと以下のような IAM ポリシールールが作成され EC2 インスタンスに適用される。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:CreateCluster",
        "ecs:DeregisterContainerInstance",
        "ecs:DiscoverPollEndpoint",
        "ecs:Poll",
        "ecs:RegisterContainerInstance",
        "ecs:StartTelemetrySession",
        "ecs:Submit*"
      ],
      "Resource": "*"
    }
  ]
}

Docker Compose を利用してコンテナを起動

以下のような Docker Compose 用の YAML ファイルを用意。

web:
  image: amazon/amazon-ecs-sample
  ports:
   - "80:80"

docker-compose.yml というファイル名で保存しておく。尚、docker-compose.yml というファイル名以外で作成した場合には、以下の ecs-cli compose up を実行する際に --file オプションを利用して YAML ファイルを指定することが出来る。

以下のように実行してコンテナを起動。

% ecs-cli compose up
INFO[0000] Using ECS task definition                     TaskDefinition=ecscompose-ecs-sample:1
INFO[0001] Starting container...                         container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web
INFO[0001] Describe ECS container status                 container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0013] Describe ECS container status                 container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0025] Describe ECS container status                 container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0037] Describe ECS container status                 container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0049] Describe ECS container status                 container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0061] Describe ECS container status                 container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0073] Describe ECS container status                 container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0080] Started container...                          container=5caf3c9d-efcb-48cc-82ba-757760f0df71/web desiredStatus=RUNNING lastStatus=RUNNING taskDefinition=ecscompose-ecs-sample:1

コンテナが起動していることを確認

ecs-cli ps にて確認。

% ecs-cli ps
Name                                      State    Ports                    TaskDefinition
5caf3c9d-efcb-48cc-82ba-757760f0df71/web  RUNNING  54.65.xxx.xxx:80->80/tcp  ecscompose-ecs-sample:1

ひとまずアクセスしてみる。

% curl -I 54.65.xxx.xxx
HTTP/1.1 200 OK
Date: Fri, 09 Oct 2015 15:34:49 GMT
Server: Apache/2.2.22 (Ubuntu)
X-Powered-By: PHP/5.3.10-1ubuntu3.15
Vary: Accept-Encoding
Content-Type: text/html

ブラウザからも...

f:id:inokara:20151010061644p:plain

確認。

コンテナをスケールさせてみる

この時点ではコンテナは 1 コンテナのみ起動しているので、もう一つコンテナを起動してみる。コンテナをスケールさせるには ecs-cli compose scale N を利用する。

% ecs-cli compose scale 2
INFO[0000] Starting container...                         container=1e0fc3d7-c9da-4853-bdf5-e7c64277e31d/web
INFO[0000] Describe ECS container status                 container=1e0fc3d7-c9da-4853-bdf5-e7c64277e31d/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0012] Describe ECS container status                 container=1e0fc3d7-c9da-4853-bdf5-e7c64277e31d/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0025] Describe ECS container status                 container=1e0fc3d7-c9da-4853-bdf5-e7c64277e31d/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0037] Describe ECS container status                 container=1e0fc3d7-c9da-4853-bdf5-e7c64277e31d/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0049] Describe ECS container status                 container=1e0fc3d7-c9da-4853-bdf5-e7c64277e31d/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition=ecscompose-ecs-sample:1
INFO[0055] Started container...                          container=1e0fc3d7-c9da-4853-bdf5-e7c64277e31d/web desiredStatus=RUNNING lastStatus=RUNNING taskDefinition=ecscompose-ecs-sample:1

確認。

% ecs-cli ps
Name                                      State    Ports                      TaskDefinition
1e0fc3d7-c9da-4853-bdf5-e7c64277e31d/web  RUNNING  54.178.xxx.xxx:80->80/tcp  ecscompose-ecs-sample:1
5caf3c9d-efcb-48cc-82ba-757760f0df71/web  RUNNING  54.65.xxx.xxx:80->80/tcp    ecscompose-ecs-sample:1

2 コンテナ起動しているようなので確認。

% curl -I 54.178.xxx.xxx
HTTP/1.1 200 OK
Date: Fri, 09 Oct 2015 15:34:41 GMT
Server: Apache/2.2.22 (Ubuntu)
X-Powered-By: PHP/5.3.10-1ubuntu3.15
Vary: Accept-Encoding
Content-Type: text/html

% curl -I 54.65.xxx.xxx
HTTP/1.1 200 OK
Date: Fri, 09 Oct 2015 15:34:49 GMT
Server: Apache/2.2.22 (Ubuntu)
X-Powered-By: PHP/5.3.10-1ubuntu3.15
Vary: Accept-Encoding
Content-Type: text/html

後片付け

一通り試したら全てのリソースを削除する必要がある。これを後片付けと呼ぶ。以下のように実行することで CloudFromation のスタックがザクっと削除されて、全てのリソースが削除される。尚、コンテナが稼働している場合には --force を付ける

% ecs-cli down --force
INFO[0001] Waiting for your cluster resources to be deleted 
INFO[0001] Cloudformation stack status                   stackStatus=DELETE_IN_PROGRESS
INFO[0062] Cloudformation stack status                   stackStatus=DELETE_IN_PROGRESS
INFO[0122] Deleted cluster                               cluster=my-cluster

当たり前だのクラッカーだが...コンテナインスタンス、ECS クラスタ、IAM ロールのポリシー等ザクっと消えてしまうので消し忘れ防止は嬉しいが、実行の際にはくれぐれもご注意を...。


ということで...

  • Docker Compose が利用出来るようになったのは嬉しい
  • コンテナインスタンスから一気通貫で構築出来るのは嬉しい(マネジメントコンソールを拝む必要が「ほぼ」無い)
  • 少し CloudFormation のスキルが必要かもしれない
  • ELB に繋いだりするのも ecs-cli から出来ると嬉しいと思ったり

以上。

Docker をソースコードからビルドする + α

追記

Logging Driver の件、追記した。

inokara.hateblo.jp

samuelkarp さん、本当に有難うございました。

tl;dr

思う所があり以下を試してみた。

  • Docker をソースコードからビルドして開発中のバージョンを利用する
  • Logging Driver に CloudWatch Logs が追加されそうなので試してみる
  • AWS SDK for Go を少し触ってみる

Docker をソースコードからビルドして開発版を利用する手順

概要

  • ビルド手順はコンテナを利用して行われる
  • 最新の Docker が必要
  • ビルド→バイナリ生成が完了すると bundles 以下にバイナリが生成されている

参考

ビルド済みのイメージを利用しても良いかもしれない。

動作確認環境

$ cat /etc/system-release
Amazon Linux AMI release 2015.09

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.2 LTS"

も一つは手元の VirtualBox 上の Ubuntu 14.04 を利用した。

最新の Docker をインストール

$ curl -sSL https://get.docker.com/ | sh

尚、Amazon Linux の場合には Docker 1.7.1 を利用した。

ソースコードの取得とビルド

git clone する。

$ git clone https://github.com/docker/docker.git
$ cd docker
$ make build

開発版バイナリの作成

$ make binary

開発版のバイナリを利用

Docker サービスが動いている場合には一旦停止した上でバイナリを置き換える。

# 既存 Docker バイナリをバックアップ
$ sudo mv /usr/bin/docker /usr/bin/docker.old

# 最新の Docker バイナリをコピー
$ sudo cp cp bundles/1.9.0-dev/binary/docker-1.9.0-dev /usr/bin/

# シンボリックリンクを /usr/bin/docker に張る
$ sudo ln -s /usr/bin/docker-1.9.0-dev /usr/bin/docker

開発版バイナリで Docker を起動する

$ sudo service docker start

バージョンの確認。

$ docker version
Client:
 Version:      1.9.0-dev
 API version:  1.21
 Go version:   go1.4.2
 Git commit:   ce092ed
 Built:        Wed Sep 23 20:52:39 UTC 2015
 OS/Arch:      linux/amd64

Server:
 Version:      1.9.0-dev
 API version:  1.21
 Go version:   go1.4.2
 Git commit:   ce092ed
 Built:        Wed Sep 23 20:52:39 UTC 2015
 OS/Arch:      linux/amd64

Logging driver for CloudWatch Logs の動作確認

トィウィッターを見ていたら...

おお、スゴイ!と思ったので試してみることにした。

注意

あくまでも開発中の機能の為、実際のリリースの際に提供される機能とは異なる場合があるのでご注意を...。お試しの際は自己責任で宜しくお願いいたします。

参考

上記 issue は Docker サービスそのもので Log Driver を利用されようとしているが、「コンテナ毎に Log Stream を指定するんだよ」とコメントされている。

必要なもの

  • 開発版 Docker(1.9.0-dev)
  • CloudWatch Logs にて最低でも Log Stream 作成、Log を PUT 出来る権限が適用された IAM role 又は同権限が付与された IAM User の Credential な情報(以下、ドキュメントより抜粋)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

また、事前に Log Group を作成しておく。

ただいま開発途上

VirtualBox 上の Ubuntu 14.04 にて以下のように実行したところ、正常にコンテナは起動してくれなかった。→ /etc/default/docker に Credential 情報を設定したら起動した。

$ export AWS_ACCESS_KEY_ID=AKXXXXXXXXXXXXXXXXXXXX
$ export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ export AWS_REGION=ap-northeast-1
$ docker run \
  --log-driver=awslogs \
  --log-opt awslogs-region=ap-northeast-1 \
  --log-opt awslogs-group=docker-log \
  --log-opt awslogs-stream=hello-world \
hello-world

以下のようなエラー。Credential 周りの設定が影響しているのか Log Stream が作成されない。→ /etc/default/docker に Credential 情報を設定したら起動した。

Error response from daemon: Cannot start container 964c628a7c4ca1264d538b3b24640130d2c4cb7a4801116a4b761853615c2944: Failed to initialize logging driver: NoCredentialProviders: no valid providers in chain

ちょっと残念だが、環境変数に設定した Credential な情報や $HOME/.aws/credential ファイルを見つけてくれていないのが原因のようなので...

場当たりパッチで動かしてみた→こんなパッチを当てる必要は無い

以下のようにその場しのぎで $HOME/.aws/credential ファイルを認証情報として利用するようにしてみた。

$ git diff
diff --git a/daemon/logger/awslogs/cloudwatchlogs.go b/daemon/logger/awslogs/cloudwatchlogs.go
index 23fc283..272508e 100644
--- a/daemon/logger/awslogs/cloudwatchlogs.go
+++ b/daemon/logger/awslogs/cloudwatchlogs.go
@@ -11,6 +11,7 @@ import (
 
        "github.com/Sirupsen/logrus"
        "github.com/aws/aws-sdk-go/aws"
+  "github.com/aws/aws-sdk-go/aws/credentials"
        "github.com/aws/aws-sdk-go/aws/awserr"
        "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
        "github.com/docker/docker/daemon/logger"
@@ -85,12 +86,15 @@ func New(ctx logger.Context) (logger.Logger, error) {
                        Region: aws.String(ctx.Config[regionKey]),
                })
        }
+
+  fmt.Println(config)
        containerStream := &logStream{
                logStreamName: logStreamName,
                logGroupName:  logGroupName,
-               client:        cloudwatchlogs.New(config),
+    client:        cloudwatchlogs.New(&aws.Config{Credentials: credentials.NewSharedCredentials("/home/vagrant/.aws/credentials", "default"), Region: aws.String("ap-northeast-1")}),
                messages:      make(chan *logger.Message, 4096),
        }
+
        err := containerStream.create()
        if err != nil {
                return nil, err

改めて...

$ docker run \
  --log-driver=awslogs \
  --log-opt awslogs-region=ap-northeast-1 \
  --log-opt awslogs-group=docker-log \
  --log-opt awslogs-stream=hello-world \
hello-world

以下のように出力された。

Hello from Docker.
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/userguide/

CloudWatch Logs のコンソールでもログを確認することが出来た。

f:id:inokara:20150925151347p:plain

IAM role を適用した EC2 の場合

ちなみに、IAM role を適用した EC2 で試したところ、上記のような場当たり対応不要で CloudWatch Logs にコンテナのログが飛んでいくのを確認している。

# Docker バージョンの確認
[ec2-user@ip-xx-x-x-xxx ~]$ docker version
Client:
 Version:      1.9.0-dev
 API version:  1.21
 Go version:   go1.4.2
 Git commit:   ce092ed
 Built:        Wed Sep 23 20:52:39 UTC 2015
 OS/Arch:      linux/amd64

Server:
 Version:      1.9.0-dev
 API version:  1.21
 Go version:   go1.4.2
 Git commit:   ce092ed
 Built:        Wed Sep 23 20:52:39 UTC 2015
 OS/Arch:      linux/amd64

# IAM role を確認
[ec2-user@ip-xx-x-x-xxx ~]$ curl http://169.254.169.254/latest/meta-data/iam/info
{
  "Code" : "Success",
  "LastUpdated" : "2015-09-25T06:17:41Z",
  "InstanceProfileArn" : "arn:aws:iam::1234567890123:instance-profile/ec2",
  "InstanceProfileId" : "AIPAIZV2B4SWPBULEILCS"
}

# 適用されている role のポリシーを確認
[ec2-user@ip-xx-x-x-xxx ~]$ saws
No resource cache found
Refreshing resources...
  Refreshing instance ids...
You must specify a region. You can also configure your region by running "aws configure".
  Refreshing instance tags...
You must specify a region. You can also configure your region by running "aws configure".
You must specify a region. You can also configure your region by running "aws configure".
  Refreshing bucket names...

A client error (AccessDenied) occurred when calling the ListBuckets operation: Access Denied
Done refreshing
Version: 0.2.1
Theme: vim
saws> aws iam get-role-policy --role-name ec2 --policy-name cloudwatch-logs 
{
    "RoleName": "ec2", 
    "PolicyDocument": {
        "Version": "2012-10-17", 
        "Statement": [
            {
                "Action": [
                    "logs:CreateLogStream", 
                    "logs:PutLogEvents"
                ], 
                "Resource": "*", 
                "Effect": "Allow"
            }
        ]
    }, 
    "PolicyName": "cloudwatch-logs"
}

saws>  

コンテナを起動。

[ec2-user@ip-xx-x-x-xxx ~]$ docker run -d \
  --publish 80:80  \
  --log-driver=awslogs   \
  --log-opt awslogs-region=ap-northeast-1   \
  --log-opt awslogs-group=docker-log   \
  --log-opt awslogs-stream=nginx
nginx

アクセスしてみる。

[ec2-user@ip-xx-x-x-xxx ~]$ curl -I localhost
HTTP/1.1 200 OK
Server: nginx/1.9.5
Date: Fri, 25 Sep 2015 06:32:52 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 22 Sep 2015 16:10:55 GMT
Connection: keep-alive
ETag: "56017d8f-264"
Accept-Ranges: bytes

ログを確認してみると以下のように記録されている。

f:id:inokara:20150925153418p:plain


AWS SDK for Go の Credential 設定について

モチベーション

上記の通りコンテナから CloudWatch Logs にログが飛ばない(Log Stream が作成されない)件を調査の流れから...。

参考

よく解ってないけど書いてみた

ドキュメントの読み方すらよく理解していないけど、以下のように書いてみた。S3 のバケット一覧を出力するやつ。

package main

import (
    "os"
    "log"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/service/s3"
)

func main() {

  credentialsProvider := credentials.NewChainCredentials(
  []credentials.Provider{
    &credentials.EnvProvider{},
    &credentials.SharedCredentialsProvider{Filename: "", Profile: "default"},
  })

  svc := s3.New(&aws.Config{Credentials: credentialsProvider, Region: aws.String(os.Getenv("AWS_REGION"))})
  var params *s3.ListBucketsInput
  result, err := svc.ListBuckets(params)
  if err != nil {
      log.Println(err.Error())
      return
  }
  log.Println(result)
}

とりあえず実行。

$ go run s3-test.go 
2015/09/25 05:57:15 {
  Buckets: [
    {
      CreationDate: 2015-01-17 06:46:12 +0000 UTC,
      Name: "foo"
    },
    {
      CreationDate: 2015-07-13 12:44:01 +0000 UTC,
      Name: "bar"
    },
(snip)

    {
      CreationDate: 2014-03-30 16:53:07 +0000 UTC,
      Name: "baz"
    }
  ],
  Owner: {
    DisplayName: "hagepika",
    ID: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}

おお。

ポイント

以下の部分がポイントかと。

  credentialsProvider := credentials.NewChainCredentials(
  []credentials.Provider{
    &credentials.EnvProvider{},
    &credentials.SharedCredentialsProvider{Filename: "", Profile: "default"},
  })

credentials.EnvProvider{}&credentials.SharedCredentialsProvider{Filename: "", Profile: "default"}環境変数又は $HOME/.aws/credentialsdefault に定義されている情報を利用出来るようにした credentialsProvider を定義している。

そして S3 への接続(クライアントの作成)時に以下のように Credentials:credentialsProviderRegion: には環境変数 AWS_REGION から読み取った情報を利用する。

// 以下の AWS_REGION は String となるので注意
svc := s3.New(&aws.Config{Credentials: credentialsProvider, Region: aws.String(os.Getenv("AWS_REGION"))})

以上。

Supercharged AWS CLI = SAWS メモ(2)〜リソース補完を追加してみる〜

tl;dr

前回の続き。 触ってみただけではつまらないのでリソース補完を追加してみたのでメモ。


DynamoDB のテーブル名を補完してみる

追加実装の概要

  1. リソースリストを取得、管理する resources.py に取得したいリソース方法を記述する(aws dynamodb list-tables --query TableNames --output text を実行する)
  2. コマンドの補完機能が実装されている completer.py に対象のリソースとどのオプションがある場合にリソースリストを出力するかを定義する(--list-table オプションを利用した際にリソース(テーブル)一覧を表示する)

実際の修正

  • resources.py(リソースの取得、管理)
--- resources.py.bk  2015-09-23 11:14:46.000000000 +0900
+++ resources.py  2015-09-23 11:50:49.000000000 +0900
@@ -67,13 +67,14 @@
         """
 
         INSTANCE_IDS, INSTANCE_TAG_KEYS, INSTANCE_TAG_VALUES, \
-            BUCKET_NAMES = range(4)
+            BUCKET_NAMES = range(4) 
 
     def __init__(self,
                  log_exception,
                  refresh_instance_ids=True,
                  refresh_instance_tags=True,
-                 refresh_bucket_names=True):
+                 refresh_bucket_names=True,
+                 refresh_dynamodb_table_names=True):  # added by inokappa
         """Initializes AwsResources.
 
         Args:
@@ -94,23 +95,32 @@
         self.instance_tag_values = set()
         self.bucket_names = []  # TODO: Make this 'private'
         self.s3_uri_names = []  # TODO: Make this 'private'
+        self.dynamodb_table_names = [] # added by inokappa
+
         self.refresh_instance_ids = refresh_instance_ids
         self.refresh_instance_tags = refresh_instance_tags
         self.refresh_bucket_names = refresh_bucket_names
+        self.refresh_dynamodb_table_names = refresh_dynamodb_table_names # added by inokappa
+
         self.INSTANCE_IDS_MARKER = '[instance ids]'
         self.INSTANCE_TAG_KEYS_MARKER = '[instance tag keys]'
         self.INSTANCE_TAG_VALUES_MARKER = '[instance tag values]'
         self.BUCKET_NAMES_MARKER = '[bucket names]'
+        self.DYNAMODB_TABLE_NAMES_MARKER = '[dynamodb table names]' # added by inokapp
+
         self.INSTANCE_IDS = '--instance-ids'
         self.EC2_TAG_KEY = '--ec2-tag-key'
         self.EC2_TAG_VALUE = '--ec2-tag-value'
         self.EC2_STATE = '--ec2-state'
         self.BUCKET = '--bucket'
         self.S3_URI = 's3:'
+        self.DYNAMODB_TABLE = '--table-name'
+
         self.QUERY_INSTANCE_IDS_CMD = 'aws ec2 describe-instances --query "Reservations[].Instances[].[InstanceId]" --output text'
         self.QUERY_INSTANCE_TAG_KEYS_CMD = 'aws ec2 describe-instances --filters "Name=tag-key,Values=*" --query Reservations[].Instances[].Tags[].Key --output text'
         self.QUERY_INSTANCE_TAG_VALUES_CMD = 'aws ec2 describe-instances --filters "Name=tag-value,Values=*" --query Reservations[].Instances[].Tags[].Value --output text'
         self.QUERY_BUCKET_NAMES_CMD = 'aws s3 ls'
+        self.QUERY_DYNAMODB_TABLE_NAMES_CMD = 'aws dynamodb list-tables --query TableNames --output text' # added by inokapp
         self.log_exception = log_exception
 
     def refresh(self, force_refresh=False):
@@ -150,6 +160,9 @@
             if self.refresh_bucket_names:
                 print('  Refreshing bucket names...')
                 self.query_bucket_names()
+            if self.refresh_dynamodb_table_names:
+                print('  Refreshing DynamoDB table names...')
+                self.query_dynamodb_table_names()
             print('Done refreshing')
         try:
             self.save_resources_to_file(file_path)
@@ -224,6 +237,21 @@
                 except:
                     # Ignore blank lines
                     pass
+   
+    # added by inokappa
+    def query_dynamodb_table_names(self):
+        """Queries and stores DynamoDB table names from AWS.
+
+        Args:
+            * None.
+
+        Returns:
+            None.
+        """
+        output = self.query_aws(self.QUERY_DYNAMODB_TABLE_NAMES_CMD)
+        if output is not None:
+            output = re.sub('\n', ' ', output)
+            self.dynamodb_table_names = output.split()
 
     def add_bucket_name(self, bucket_name):
         """Adds the bucket name to our bucket resources.
@@ -317,3 +345,7 @@
             fp.write(self.BUCKET_NAMES_MARKER + '\n')
             for bucket_name in self.bucket_names:
                 fp.write(bucket_name + '\n')
+            # added by inokapp
+            fp.write(self.DYNAMODB_TABLE_NAMES_MARKER + '\n')
+            for dynamodb_table_name in self.dynamodb_table_names:
+                fp.write(dynamodb_table_name + '\n')

実際のリソース取得は...

        self.QUERY_DYNAMODB_TABLE_NAMES_CMD = 'aws dynamodb list-tables --query TableNames --output text' # added by inokapp

上記のように AWS CLI を叩いているのが解る。これが saws 起動時、F5 でリフレッシュされる度に実行されてリソースリストが生成、更新される。

  • completer.py(コマンドオプションの補完機能)
% diff -u completer.py.bk completer.py
--- completer.py.bk     2015-09-23 16:20:43.000000000 +0900
+++ completer.py        2015-09-23 16:20:50.000000000 +0900
@@ -112,13 +112,15 @@
                                       self.resources.EC2_TAG_VALUE,
                                       self.resources.EC2_STATE,
                                       self.resources.BUCKET,
-                                      self.resources.S3_URI],
+                                      self.resources.S3_URI,
+                                      self.resources.DYNAMODB_TABLE],
                                      [self.resources.instance_ids,
                                       self.resources.instance_tag_keys,
                                       self.resources.instance_tag_values,
                                       self.ec2_states,
                                       self.resources.bucket_names,
-                                      self.resources.s3_uri_names]))
+                                      self.resources.s3_uri_names,
+                                      self.resources.dynamodb_table_names]))
 
     def refresh_resources(self, force_refresh=False):
         """Convenience function to refresh resources for completion.

補完されてるの図

f:id:inokara:20150923163301p:plain

キタキタ。とりあえず、動いている。

取得されたリソース情報はテキストファイルに保存されているので確認してみる。

% cat /opt/boxen/homebrew/lib/python2.7/site-packages/saws/data/RESOURCES.txt | grep -A 5 dynamo 
[dynamodb table names]
soramame
soramame-2015-09-22

ちゃんとテキストファイルにも記録されている。


おわり

Pull Request してみたいけど

試しにリソース補完機能を拡張してみたが、以下の点が気になったのでメモ。

  • 追加サービスのリソース取得の実装を全て resources.py に書くのは大変そう
  • resources.py と completer.py の 2 つのファイルを修正するのも大変そう

も少し調べたりして Pull Request or Issue をチャレンジしてみたい。