DynamoDBとGoogleスプレッドシートを相互同期するやつ

confdのDynamoDBをGoogleスプレッドシートで管理したかったので作りました。

docs.google.com

使い方

まず、以下のようにDynamoDBのテーブルを作ります。(confdのと同じ

aws dynamodb create-table \
    --region ap-northeast-1 --table-name hello \
    --attribute-definitions AttributeName=key,AttributeType=S \
    --key-schema AttributeName=key,KeyType=HASH \
    --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

ついでにddbcliを使ってテーブルにデータを入れておきます。

$ ddbcli
ap-northeast-1> insert into hello (key, value) values ('foo', 'bar');
// 1 row changed (0.29 sec)

ap-northeast-1> select all * from hello;
[
  {"key":"foo","value":"bar"}
]
// 1 row in set (0.07 sec)

したらば上記のスプレッドシートをコピーします。

コピーしたらスクリプトエディタを開きます。

f:id:winebarrel:20160901213655p:plain

config.gsというスクリプトがあるので、それを適当に編集します。

f:id:winebarrel:20160901213835p:plain

メニューからDynamoDBLoad from DynamoDBを選択すると、DynamoDBからスプレッドシートにデータが読み込まれます。

f:id:winebarrel:20160901214114p:plain

スプレッドシートを適当に編集して、DynamoDBSave to DynamoDBを選択すると、データがDynamoDBに保存されます。

f:id:winebarrel:20160901214244p:plain

$ ddbcli
ap-northeast-1> select all * from hello;
[
  {"key":"zoo","value":"baz"},
  {"key":"foo","value":"BAR"}
]
// 2 rows in set (0.05 sec)

参考にしたりフォークしたりしたやつ

適当なECSデプロイスクリプト

JSONYAMLプリプロセス出来ると、なおうれし。

{
  "containerDefinitions": [
    {
      "volumesFrom": [],
      "memory": 300,
      "portMappings": [
        {
          "hostPort": 0,
          "containerPort": 80,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "mountPoints": [
        {
          "containerPath": "/usr/local/apache2/htdocs",
          "sourceVolume": "vol"
        },
        {
          "containerPath": "/var/log/httpd",
          "sourceVolume": "log"
        }
      ],
      "name": "httpd",
      "environment": [],
      "image": "httpd:2.4",
      "cpu": 10
    },
    {
      "volumesFrom": [
        {
          "sourceContainer": "httpd"
        }
      ],
      "memory": 200,
      "portMappings": [],
      "essential": true,
      "entryPoint": [
        "sh",
        "-c"
      ],
      "mountPoints": [],
      "name": "testapp",
      "environment": [],
      "image": "busybox",
      "command": [
        "/bin/sh -c 'while true; do echo xxx >> /usr/local/apache2/htdocs/index.html; sleep 3; done'"
      ],
      "cpu": 10
    }
  ],
  "volumes": [
    {
      "name": "vol"
    },
    {
      "host": {
        "sourcePath": "/var/log/httpd"
      },
      "name": "log"
    }
  ],
  "family": "test"
}
  • ec-deploy.sh
#!/bin/bash
set -e

if [ -z "$1" -o -z "$2" ]; then
  echo "usage: $0 SERVICE TASK_JSON"
  exit
fi

SERVICE=$1
TASK_JSON=$2
TASK_NAME=$(jq -r .family $TASK_JSON)
TASK_ROLE=arn:aws:iam::...:role/...

function get_last_event() {
  EVENT=$(aws ecs describe-services --services $1 | jq -r '.services[0].events[0] | [.id, .message] | @tsv')
  EVENT_ID=$(awk -F '\t' '{print $1}' <<< "$EVENT")
  EVENT_MSG=$(awk -F '\t' '{print $2}' <<< "$EVENT")
}

get_last_event $SERVICE
LAST_EVENT_ID="$EVENT_ID"
LAST_EVENT_MSG="$EVENT_MSG"

aws ecs register-task-definition \
  --task-role-arn=$TASK_ROLE \
  --cli-input-json file://$TASK_JSON > /dev/null

aws ecs update-service --service $SERVICE --task-definition $TASK_NAME > /dev/null

while true; do
  get_last_event $SERVICE

  if [ "$EVENT_ID" != "$LAST_EVENT_ID" ]; then
    echo $EVENT_MSG

    if [[ "$EVENT_MSG" =~ has\ reached\ a\ steady\ state ]]; then
      break
    fi

    LAST_EVENT_ID="$EVENT_ID"
    LAST_EVENT_MSG="$EVENT_MSG"
  fi
done

デプロイ例

$  ecs-deploy.sh hello task.json
(service hello) has started 1 tasks: (task 2b3ab535-05f7-40be-a2a7-b661e56e9527).
(service hello) registered 1 targets in (target-group arn:aws:elasticloadbalancing:ap-northeast-1:...:targetgroup/default/afb88bc90fe30b1a)
(service hello) has begun draining connections on 1 tasks.
(service hello) has stopped 1 running tasks: (task 9cc4b054-0fa5-4083-a23d-e3a497400bd5).
(service hello) has reached a steady state.

graceful restart可能なHAProxyのDockerイメージ

ふつうに作れそうだったので作ってみた。 以下、関連ファイル。

  • Dockerfile
FROM ubuntu:latest
MAINTAINER Genki Sugawara <sgwr_dts@yahoo.co.jp>

RUN apt-get update
RUN apt-get install -y haproxy

ADD haproxy.cfg /

ADD supervisor.sh /
RUN chmod +x /supervisor.sh

CMD ["/supervisor.sh"]
  • supervisor.sh
#!/bin/bash
RELOAD=0

function set_reload() {
  RELOAD=1
}

trap set_reload USR1

haproxy -f /haproxy.cfg &

while true; do
  if [ $RELOAD -eq 1 ]; then
    haproxy -f /haproxy.cfg -sf `ps -o pid,command | awk '$2 == "haproxy" {print $1}'` &
    RELOAD=0
  fi

  sleep 1
done
  • haproxy.cfg
defaults
  timeout connect 5000ms
  timeout client 50000ms
  timeout server 50000ms

listen example
  mode http
  bind :80
  server example.com example.com:80

これをdocker build

$ docker build -t haproxy .
Sending build context to Docker daemon 4.096 kB
Step 1 : FROM ubuntu:latest
 ---> 3876b81b5a81
Step 2 : MAINTAINER Genki Sugawara <sgwr_dts@yahoo.co.jp>
 ---> Using cache
 ---> 249acbf0fe6a
Step 3 : RUN apt-get update
 ---> Using cache
 ---> f2e5cbbb164f
Step 4 : RUN apt-get install -y haproxy
 ---> Using cache
 ---> 3f518b918618
Step 5 : ADD haproxy.cfg /
 ---> Using cache
 ---> bf5df2e90b51
Step 6 : ADD supervisor.sh /
 ---> Using cache
 ---> 98ae226f35b8
Step 7 : RUN chmod +x /supervisor.sh
 ---> Using cache
 ---> 1ae6d30dca44
Step 8 : CMD /supervisor.sh
 ---> Using cache
 ---> eaae080bd487
Successfully built eaae080bd487

で、docker run

$ docker run -p 10080:80 --name haproxy haproxy
$ curl localhost:10080
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        <head>
            <title>404 - Not Found</title>
        </head>
        <body>
            <h1>404 - Not Found</h1>
        </body>
</html>

docker execでUSR1を投げる。

$  docker exec haproxy kill -USR1 1

するとgraceful restartされる

[WARNING] 241/130328 (6) : Stopping proxy example in 0 ms.
[WARNING] 241/130328 (6) : Proxy example stopped (FE: 1 conns, BE: 1 conns).
$ curl localhost:10080
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        <head>
            <title>404 - Not Found</title>
        </head>
        <body>
            <h1>404 - Not Found</h1>
        </body>
</html>

おけおけ。

他のシグナルのこととか、そのままではプロダクションには使えないけど、同じ戦略でいけそう。

Docker+confdはなかなかよいと思う…

Dockerでいろいろやりたいことがあったので、いろいろ調べてました。

具体的には

  • 設定ファイルのテンプレート化
  • 設定の動的な管理と、変更に伴う継続的な設定の反映
    • 要するに設定が変更されたら設定ファイルが更新されてミドルウェアがリロードされると

でまあEntrykitconfdを教えてもらったんですが、継続的な設定の更新を考えるとconfdがよかろうと。codepの機能は必要ですが、それだけならdumb-initで十分そうでした。

で、以下のようなファイルを用意。

  • Dockerfile
FROM nginx:latest
MAINTAINER Genki Sugawara <sgwr_dts@yahoo.co.jp>

ENV DUMB_INIT_VERSION 1.1.3
ENV CONFD_VERSION 0.11.0

RUN apt-get update
RUN apt-get install -y --force-yes curl

RUN curl -sOL https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_amd64.deb
RUN dpkg -i dumb-init_*.deb
RUN rm dumb-init_*.deb

RUN curl -sOL https://github.com/kelseyhightower/confd/releases/download/v${CONFD_VERSION}/confd-${CONFD_VERSION}-linux-amd64
RUN chmod +x confd-*
RUN mv confd-* /bin/confd
RUN mkdir -p /etc/confd/{conf.d,templates}
ADD nginx_default.conf.toml /etc/confd/conf.d/
ADD nginx_default.conf.tmpl /etc/confd/templates/

ADD init.sh /
RUN chmod +x /init.sh

ENV CONFD_ARGS -backend dynamodb -table confd

ENV AWS_REGION ap-northeast-1
ENV AWS_DEFAULT_REGION ap-northeast-1
ENV AWS_ACCESS_KEY_ID ...
ENV AWS_SECRET_ACCESS_KEY ...

CMD /init.sh
  • init.sh
#!/usr/bin/dumb-init /bin/sh
confd $CONFD_ARGS &
nginx -g 'daemon off;'
  • nginx_default.conf.toml
[template]
src = "nginx_default.conf.tmpl"
dest = "/etc/nginx/conf.d/default.conf"
keys = [
    "/nginx/statuscode"
]
check_cmd = "service nginx configtest"
reload_cmd = "service nginx reload"
  • nginx_default.conf.tmpl
server {
  listen       80 default_server;
  server_name  _;

  location / {
    return {{getv "/nginx/statuscode"}};
  }
}

バックエンドのdynamodbは以下の通り。

aws dynamodb create-table \
  --table-name confd \
  --attribute-definitions AttributeName=key,AttributeType=S \
  --key-schema AttributeName=key,KeyType=HASH \
  --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1
$ ddbcli
ap-northeast-1> show create table confd;
CREATE TABLE `confd` (
  `key` STRING HASH
) read=1 write=1

ap-northeast-1> select all * from confd;
[
  {"key":"/nginx/statuscode","value":"404"}
]
// 1 row in set (0.05 sec)

バックエンドが選べるのはなかなかよいですね。 このコンテナをローカルで動かそうと思ったら、-backend envで適当な値を突っ込めるし。

で、これをdocker buildしてdocker runするとcurlで404が返る。

$ docker build -t awesome-confd .
...
$ docker run --rm -p 10080:80 --name awesome-confd -it awesome-confd
2016-08-28T08:34:26Z 1f4823caba59 confd[8]: INFO Backend set to dynamodb
2016-08-28T08:34:26Z 1f4823caba59 confd[8]: INFO Starting confd
2016-08-28T08:34:26Z 1f4823caba59 confd[8]: INFO Backend nodes set to
2016-08-28T08:34:26Z 1f4823caba59 confd[8]: INFO DynamoDB table set to confd
2016-08-28T08:34:26Z 1f4823caba59 confd[8]: INFO /etc/nginx/conf.d/default.conf has md5sum 4dce452bf8dbb01f278ec0ea9ba6cf40 should be 1e1181119f8355e81d3b0a7de2859d5a
2016-08-28T08:34:26Z 1f4823caba59 confd[8]: INFO Target config /etc/nginx/conf.d/default.conf out of sync
2016-08-28T08:34:26Z 1f4823caba59 confd[8]: INFO Target config /etc/nginx/conf.d/default.conf has been updated
$ curl localhost:10080
<html>
<head><title>404 Not Found</title></head>
<body bgcolor="white">
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.11.3</center>
</body>
</html>

おけおけ。これで-watchオブションつければ設定の監視できるぞ、、と思ったら

2016-08-28T08:36:32Z 63120fea70d1 confd[7]: INFO Watch is not supported for backend dynamodb. Exiting...

残念(あたりまえか…)。

まあ、手動でreloadすればいいかと、CONFD_ARGSとinit.shを以下のように書き換え。

ENV CONFD_ARGS -backend dynamodb -table confd -onetime
#!/usr/bin/dumb-init /bin/sh
confd $CONFD_ARGS
nginx -g 'daemon off;'

再度、docker runしたあと、dynamodbの値を書き換え。

ap-northeast-1> update confd set value = '401' where key = '/nginx/statuscode';
// 1 row changed (0.08 sec)

condを実行。

$ docker exec -it awesome-confd confd -backend dynamodb -table confd -onetime
2016-08-28T08:40:44Z 4b3509b1fd93 confd[49]: INFO Backend set to dynamodb
2016-08-28T08:40:44Z 4b3509b1fd93 confd[49]: INFO Starting confd
2016-08-28T08:40:44Z 4b3509b1fd93 confd[49]: INFO Backend nodes set to
2016-08-28T08:40:44Z 4b3509b1fd93 confd[49]: INFO DynamoDB table set to confd
2016-08-28T08:40:44Z 4b3509b1fd93 confd[49]: INFO /etc/nginx/conf.d/default.conf has md5sum 1e1181119f8355e81d3b0a7de2859d5a should be 306f95f0ae59a29344ed21eb0c7886c9
2016-08-28T08:40:44Z 4b3509b1fd93 confd[49]: INFO Target config /etc/nginx/conf.d/default.conf out of sync
2016-08-28T08:40:44Z 4b3509b1fd93 confd[49]: INFO Target config /etc/nginx/conf.d/default.conf has been updated
$ curl localhost:10080
<html>
<head><title>401 Authorization Required</title></head>
<body bgcolor="white">
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.11.3</center>
</body>
</html>

おけおけ。

機密情報について

機密情報については上記DynamoDBとは別のとこに置くと思うんですよね。 VaultとかCredStashとか。

現状のconfdだと、そこから引っ張るのは難しいので、init.shとかで機密情報を引っ張ってきて環境変数に保存して利用とかですかねぇ。環境変数に機密情報を入れるのはもにょるんですが、rootしか見れないだろうし、まあいいかなぁ…

PR眺めてたら機密情報を出し入れするやつがありました。

github.com

ですが、この機能をconfd側持ってしまうのは微妙かな、と考えて任意のシステムコマンドを実行できるテンプレート関数を追加するPRを投げてみました。

github.com

なんとなくリジェクトされる気がする…。

本来的にはプラガブルになるといいんでしょうけど、そこまで工数さくほどかなー

MySQLのPASSWORD関数のRubyバインディング

一身上の都合によりMySQLのPASSWORD関数のRubyバインディングを作った。

github.com

実装はPASSWORD関数の実態であるmake_scrambled_password()を拡張ライブラリから呼び出しているだけです。

使い方は以下の通り。

require "mysql_make_scrambled_password"
include MysqlMakeScrambledPassword

# PASSWORD()
make_scrambled_password("FOOBARZOO")
#=> "*70B45D8E18771E93E011F30DB91E80D6306EA4C3"

# OLD_PASSWORD()
make_scrambled_password_323("FOOBARZOO")
#=> "6e35068701b1cc8b"

追記

go版も作りました

github.com

Cfdef: CloudFrontをコード化するやつ

github.com

こんなん。

export AWS_ACCESS_KEY_ID='...'
export AWS_SECRET_ACCESS_KEY='...'
cfdef -e -o CFfile  # export CloudFront
vi CFfile
cfdef -a --dry-run
cfdef -a            # apply `CFfile`
require 'other/cffile'

distribution "EXAMPLEID" do
  aliases do
    quantity 0
  end
  origins do
    quantity 2
    items do |*|
      id "Custom-ehxample.cpm"
      domain_name "example.cpm"
      origin_path ""
      custom_headers do
        quantity 0
      end
      custom_origin_config do
        http_port 80
        https_port 443
        origin_protocol_policy "http-only"
        origin_ssl_protocols do
          quantity 3
          items ["TLSv1", "TLSv1.1", "TLSv1.2"]
        end
      end
    end
    items do |*|
      id "S3-example"
      domain_name "example.s3.amazonaws.com"
      origin_path ""
      custom_headers do
        quantity 0
      end
      s3_origin_config do
        origin_access_identity ""
      end
    end
  end
  default_cache_behavior do
    target_origin_id "S3-example"
    forwarded_values do
      query_string false
      cookies do
        forward "none"
      end
      headers do
        quantity 0
      end
    end
    trusted_signers do
      enabled false
      quantity 0
    end
    viewer_protocol_policy "allow-all"
    min_ttl 0
    allowed_methods do
      quantity 2
      items ["GET", "HEAD"]
      cached_methods do
        quantity 2
        items ["GET", "HEAD"]
      end
    end
    smooth_streaming false
    default_ttl 86400
    max_ttl 31536000
    compress false
  end
  cache_behaviors do
    quantity 0
  end
  custom_error_responses do
    quantity 0
  end
  comment ""
  price_class "PriceClass_All"
  enabled true
  viewer_certificate do
    cloud_front_default_certificate true
    minimum_protocol_version "SSLv3"
    certificate_source "cloudfront"
  end
  restrictions do
    geo_restriction do
      restriction_type "none"
      quantity 0
    end
  end
  web_acl_id ""
end

Repol: ECRのレポジトリをコード化するやつ

github.com

こんなん。

export AWS_ACCESS_KEY_ID='...'
export AWS_SECRET_ACCESS_KEY='...'
repol -e -o Repolfile  # export Repository Policy
vi Repolfile
repol -a --dry-run
repol -a               # apply `Repolfile`
Help
require 'other/repolfile'

repository "my_ecr_repo" do
  {"Version"=>"2008-10-17",
   "Statement"=>
    [{"Sid"=>"PullOnly",
      "Effect"=>"Allow",
      "Principal"=>{"AWS"=>"arn:aws:iam::123456789012:root"},
      "Action"=>
       ["ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer"]}]}
end