地図タイルをs3へ低冗長化でアップロード

作成した地図タイルをs3に低冗長化ストレージでアップする方法です。
冗長化にしたのは、料金を安くしたいのと、元データは手元にあるので消えても大丈夫だからです。

S3へアクセスするためのアクセスキーを設定

cd ~/
emacs .aws

export AWS_ACCESS_KEY_ID=hogehoge
export AWS_SECRET_ACCESS_KEY=fugafuga

source ~/.aws

s3-parallel-putと依存するbotoをインストール
冗長化ストレージでアップするようにプログラムを変更

pip install boto
git clone https://github.com/twpayne/s3-parallel-put
cd s3-parallel-put
emacs s3-parallel-put
-key.set_contents_from_string(content, headers, md5=value.md5, policy=options.grant)
+key.set_contents_from_string(content, headers, md5=value.md5, policy=options.grant,reduced_redundancy=True)

タイルのある場所に移動して実行
更新ではないのでstupidでスピード優先

~/github/s3-parallel-put/s3-parallel-put --bucket_region=ap-northeast-1 --bucket=ecoris --processes=5 --put=stupid --content-type="guess" --prefix=tiles/veg67 veg67 --log-filename=/home/ubuntu/log.txt

公開設定などは、こちらのエントリを参照
http://d.hatena.ne.jp/tm33/20130329/p1

料金

保管料

タイル1セット約5Gなので毎月かかる料金は
スタンダードで$5(500円)、低冗長化だと$0.4(40円)

使用料

データアクセスでの料金はこちら参照
http://qiita.com/kawaz/items/07d67a851fd49c1c183e

確認事項

状況

PostGIS→out of memory
SQLite→missing

対策

PostGIS

  • SWAP作成
  • パラメータ変更
  • tilemill のプログラム分析、どこでOut of Memoryになるのか?
  • グリッドあり、なしで作成

SQLite

  • tilemillプログラム分析、なぜmissingが発生するのか?
  • tilemill の最新版をビルドして試す tm2
  • mbcheckで確認
  • missingをあとから追加できないか
  • gridのmbcheck もしくは手動で確認

植生図をタイルにする(第6,7回版)

準備

以下をインストールしておく

OSGeo4W :shpファイルのインポートに利用
cygwinシェルスクリプトの実行に利用
spatialite:sqliteの作成、操作に利用

データをダウンロード

http://www.vegetation.jp/index.html

veg_download.shを参照

解凍

解凍すると、a.直接ファイルが作られるもの、b.フォルダの中にファイルが作られるもの、c.なぜだか他のファイルが紛れ込んでいるもの があるので取り扱い注意。

unzip.exeをダウンロードしておく。
http://gnuwin32.sourceforge.net/packages/unzip.htm

unzip.shで解凍&移動

for file in `find -type f -name "*.zip"`
do
  ./unzip -o -q $file
done
for file in `find shp* -maxdepth 1 -type f -name "*"`
do
  mv $file vegdir
done
mv p*.* vegdir

spatilaiteにインポート

veg.bat vegdir\*.shp

SET SHAPE_ENCODING=CP932
for %%A in (%1) do ogr2ogr -gt 65536 -append -nlt polygon -s_srs epsg:4612 -t_srs epsg:3857 -f SQLite veg67.sqlite %%A -dsco SPATIALITE=YES  

それぞれファイル名と同じテーブル名に格納される。

vegテーブルにマージ

vegテーブルにマージするために以下のスクリプトsqlを作成

$ mergesql.sh > merge.sql

echo "CREATE TABLE veg (OGC_FID INTEGER PRIMARY KEY, GEOMETRY POLYGON, mesh2_c VARCHAR, hanrei_c VARCHAR, surv_year VARCHAR, org_no VARCHAR, zukaku_no VARCHAR, shoku_c VARCHAR, shoku_n VARCHAR, dai_c VARCHAR, dai_n VARCHAR, chu_c VARCHAR, chu_n VARCHAR, sai_c VARCHAR, sai_n VARCHAR, hanrei_n VARCHAR);"

for file in `find vegdir -maxdepth 1 -type f -name "*.shp"| sed -e "s/.*\/\(.*\)\..*/\1/g"`
do
  echo "INSERT INTO veg(GEOMETRY, mesh2_c, hanrei_c, surv_year, org_no, zukaku_no, shoku_c, shoku_n, dai_c, dai_n, chu_c, chu_n , sai_c , sai_n, hanrei_n)"
  echo "SELECT GEOMETRY, mesh2_c, hanrei_c, surv_year, org_no, zukaku_no, shoku_c, shoku_n, dai_c, dai_n, chu_c, chu_n , sai_c , sai_n, hanrei_n"
  echo "FROM $file;"
done

spatialiteを起動して、merge.sqlを読み込み実行

$ spatialite veg67.sqlite
> .read merge.sql shift_jis

確認
> .charset shift_jis
> select * from veg limit 5;

ジオメトリをリカバー
select RecoverGeometryColumn('veg','GEOMETRY',3857,'POLYGON','XY');
Spatial Index を作成
select CreateSpatialIndex('veg','GEOMETRY');
個別のテーブルを削除

$ dropsql.sh > drop.sql
$ spatialite veg67.sqlite
> .read drop.sql shift_jis

for file in `find vegdir -maxdepth 1 -type f -name "*.shp"| sed -e "s/.*\/\(.*\)\..*/\1/g"`
do
  echo "select DisableSpatialIndex('$file','GEOMETRY');"
  echo "drop table $file;"
  echo "drop table idx_${file}_GEOMETRY;"
done
echo "VACUUM;"

tilemillで色つけ

tilemillを使って色つけします。
style.mss

Map {
  background-color: #99ffff;
}
#veg{
  ::shape{
    [zoom <= 11]{
      line-width:0.1;
      polygon-opacity:1;
      [shoku_c='1']{polygon-fill:#fdf1ce;line-color:#fdf1ce;}
      [shoku_c='2']{polygon-fill:#997f60;line-color:#997f60;}
      [shoku_c='3']{polygon-fill:#a58f74;line-color:#a58f74;}
      [shoku_c='4']{polygon-fill:#178017;line-color:#178017;}
      [shoku_c='5']{polygon-fill:#5b9700;line-color:#5b9700;}
      [shoku_c='6']{polygon-fill:#003300;line-color:#003300;}
      [shoku_c='7']{polygon-fill:#004a00;line-color:#004a00;}
      [shoku_c='8']{polygon-fill:#ffff00;line-color:#ffff00;}
      [shoku_c='9']{polygon-fill:#8cd27d;line-color:#8cd27d;}
      [shoku_c='10']{polygon-fill:#868585;line-color:#868585;}
      [dai_n="植林地"]{polygon-fill:#697720;line-color:#697720;}
      [hanrei_n="開放水域"]{polygon-fill:#99ffff;line-color:#99ffff;}
 	}
    [zoom >= 12]{
      line-width:0.1;
      polygon-opacity:1;
      [shoku_c='1']{polygon-fill:#fdf1ce;line-color:#fdf1ce;}
      [shoku_c='2']{polygon-fill:#997f60;line-color:#997f60;}
      [shoku_c='3']{polygon-fill:#a58f74;line-color:#a58f74;}
      [shoku_c='4']{polygon-fill:#178017;line-color:#178017;}
      [shoku_c='5']{polygon-fill:#5b9700;line-color:#5b9700;}
      [shoku_c='6']{polygon-fill:#003300;line-color:#003300;}
      [shoku_c='7']{polygon-fill:#004a00;line-color:#004a00;}
      [shoku_c='8']{polygon-fill:#ffff00;line-color:#ffff00;}
      [shoku_c='10']{polygon-fill:#868585;line-color:#868585;}
      [dai_n="植林地"]{polygon-fill:#697720;line-color:#697720;}
      [dai_n="竹林"]{polygon-fill:#cccc20;line-color:#cccc20;}
      [dai_n="牧草地・ゴルフ場・芝地"]{polygon-fill:#69ff00;line-color:#69ff00;}
      [dai_n="耕作地"]{polygon-fill:#999662;line-color:#999662;}
      [hanrei_n="水田雑草群落"]{polygon-fill:#8cd27d;line-color:#8cd27d;}
      [hanrei_n="開放水域"]{polygon-fill:#99ffff;line-color:#99ffff;}
      [zoom >= 14]{line-width:0.5;line-color:#000000;}
    }
  }
  ::label[zoom >= 14]{
     text-face-name: "unifont Medium";
     text-fill:#000000;
     text-size:9;
     text-halo-fill:rgba(255,255,255,0.3);
     text-halo-radius:1;
     text-line-spacing:1;
     text-wrap-width:20;
     text-name: "[hanrei_n]";
	[zoom >= 15] {
      text-size:10;
    }
  }
}

project.mml

{
  "bounds": [
    122.9,
    20.4,
    154,
    45.6
  ],
  "center": [
    140.8314,
    40.7119,
    10
  ],
  "format": "png8:t=0:c=64:z=5",
  "interactivity": {
    "layer": "veg",
    "template_teaser": "{{{shoku_n}}}\n{{{dai_n}}}\n{{{hanrei_n}}}",
    "fields": [
      "shoku_c"
    ]
  },
  "minzoom": 1,
  "maxzoom": 15,
  "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over",
  "Stylesheet": [
    "style.mss"
  ],
  "Layer": [
    {
      "geometry": "polygon",
      "extent": [
        122.873495939729,
        24.004206926072346,
        153.99615896406743,
        45.66879613975789
      ],
      "Datasource": {
        "type": "sqlite",
        "file": "veg67.sqlite",
        "table": "veg",
        "attachdb": "",
        "extent": "",
        "id": "veg",
        "project": "veg67",
        "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"
      },
      "id": "veg",
      "class": "",
      "srs-name": "900913",
      "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over",
      "advanced": {},
      "name": "veg"
    }
  ],
  "scale": 1,
  "metatile": 2,
  "name": "vegetation67",
  "description": "vegetation67",
  "attribution": "",
  "legend": ""
}

mbtileを出力

shmmaxの修正

これをしないとkillされる。ec2 microだとメモリが足りなくてkillされる。

sudo sysctl -w kernel.shmmax=268435456
sudo sysctl -p
タイル作成
cd /usr/share/tilemill
sudo node index.js export veg67 /usr/share/mapbox/export/veg67.mbtiles --format=mbtiles --bbox=122.9,20.4,154,45.6 --minzoom=1 --maxzoom=15 --metatile=2 --scale=1 --scheme=pyramid --log --files=/usr/share/mapbox/

フォルダに展開

S3に転送

CloudFrontにドメイン設定

CloudFrontにドメインを設定する。

DNSの設定

ドメインNavi→DNSレコード設定→CNAME
好きなホスト名を付けて、CNAMEにCloudFront側のドメイン名を入力

CloudFrontの設定

設定画面でCNAMEを入力する欄に設定するホスト+ドメイン名を入力する。
S3のバケット名を入力する
DefaultRootObjectにindex.htmlを指定

※S3にドメインを設定する場合は、バケット名=ドメイン名にしておかないとダメらしいけど
CloudFrontを利用する場合は、S3のバケット名はなんでも大丈夫っぽい。

S3での利用料金解析

設定

インストール

ec2にインストール。やり方はここの通り
http://kazeburo.github.com/GrowthForecast/

起動

外からアクセスしたい場合は、ポートをec2で開けておく。データの保存場所を指定
growthforecast.pl --data-dir ~/gw --port 20008

データ保存

例えば、vegetation-billing-dailyのグラフに現時点の値=30を入力
curl -F number=30 http://localhost:20008/api/vegetation/billing/daily

データの確認、保存

ここからアクセス
http://hogehoge:20008/
もしくは、グラフだけ直接
http://hogehoge:20008/list/vegetation/billing?t=sh
daily.pngに保存
curl -o daily.png "http://localhost:20008/graph/vegetation/billing/daily?t=sh&gmode=gauge"

s3 billing csv との連携
  1. s3cmdでbilling csv を取ってくる。
  2. トータル料金を抜き出す
  3. GrowthForecastに値を送る
  4. グラフをpngで保存する
  5. pngをS3にs3cmdで戻す


billing.shで以下を保存して定期実行

s3cmd --force get s3://ecoris/399748833865-aws-billing-csv-2013-04.csv billing04.csv
data=$(tail -n 2 billing04.csv |sed -n '1p'|awk -F"," '{gsub("\"","",$29);print $29}'|cut -d '.' -f 1)
echo $data
curl -F number=${data} http://localhost:20008/api/vegetation/billing/daily
curl -o daily.png "http://localhost:20008/graph/vegetation/billing/daily?t=sh&gmode=gauge"
s3cmd put daily.png s3://ecoris/vegetation/html/

毎時0分に実行
sudo crontab -e

0 * * * * /home/ubuntu/test.sh
fluentd(td-agent?)

fluentdを使えば、上記のログを取ってきて、GrowthForcastへ出力の部分が汎用的にできるようになるが、S3からログを取ってくるプラグインが見つからないのと、pngをアップする部分は結局、別で実行しなくてはいけないので、すべてお手製スクリプトで作成

http://help.treasure-data.com/kb/installing-td-agent-daemon/installing-td-agent-for-debian-and-ubuntu
http://d.hatena.ne.jp/naoya/20130219/1361262854

ファウンディング

クラウドファウンディング
個人スポンサー
http://www.goodsmileracing.com/personal-sponsors.html

地図タイルのスピードテスト

地図タイルの読み込みスピードをjmeterを使って計測します。

jmeterの設定

jmeterではhtmlからajaxで呼び出される地図タイルの計測はできない(らしい)ので
タイルを直接呼び出す時間を計測する。

  • テスト計画→追加→Threds→スレッドグループ
    • スレッド数 5、ループ回数100
  • スレッドグループ→追加→サンプラー→HTTPリクエス
    • サーバ名 hoge.s3-website-ap-northeast-1.amazonaws.com
    • パス /hogehoge/tiles/${z}/${x}/${y}.png
  • スレッドグループ→追加→設定エレメント→CSV Data Set Config
    • filename data.csv
    • Variable Names z,x,y
  • スレッドグループ→追加→リスナ→結果を表で表示

設定ファイル(jmx)を適当な場所に保存する
data.csvを設定ファイル(jmx)と同じ場所に保存しておく

data.csvの作成

以下のスクリプトを実行して、読み込むタイルのリストdata.csvを作成する。
ランダムに作成される中心位置から各ズームレベルごとに5×4=20枚のタイルを作成する。この場合、ズームレベル11〜15の5段階なので5×20=100タイル

#!/usr/bin/env python
from math import pi,cos,sin,log,exp,atan
import sys
import random

DEG_TO_RAD = pi/180
RAD_TO_DEG = 180/pi


def minmax (a,b,c):
    a = max(a,b)
    a = min(a,c)
    return a

class GoogleProjection:
    def __init__(self,levels=18):
        self.Bc = []
        self.Cc = []
        self.zc = []
        self.Ac = []
        c = 256
        for d in range(0,levels):
            e = c/2;
            self.Bc.append(c/360.0)
            self.Cc.append(c/(2 * pi))
            self.zc.append((e,e))
            self.Ac.append(c)
            c *= 2
                
    def fromLLtoPixel(self,ll,zoom):
         d = self.zc[zoom]
         e = round(d[0] + ll[0] * self.Bc[zoom])
         f = minmax(sin(DEG_TO_RAD * ll[1]),-0.9999,0.9999)
         g = round(d[1] + 0.5*log((1+f)/(1-f))*-self.Cc[zoom])
         return (e,g)
     
    def fromPixelToLL(self,px,zoom):
         e = self.zc[zoom]
         f = (px[0] - e[0])/self.Bc[zoom]
         g = (px[1] - e[1])/-self.Cc[zoom]
         h = RAD_TO_DEG * ( 2 * atan(exp(g)) - 0.5 * pi)
         return (f,h)


def print_tiles(bbox, minZoom=1,maxZoom=18,tms_scheme=False):

    gprj = GoogleProjection(maxZoom+1) 

    ll0 = (bbox[0],bbox[3])
    ll1 = (bbox[2],bbox[1])
    
    for z in range(minZoom,maxZoom + 1):
        px0 = gprj.fromLLtoPixel(ll0,z)
        px1 = gprj.fromLLtoPixel(ll1,z)

        xnum=0
        for x in range(int(px0[0]/256.0),int(px1[0]/256.0)+1):
            if (x < 0) or (x >= 2**z):
                continue
	    xnum+=1
	    if(xnum > 5):
	 	break
            ynum=0
            for y in range(int(px0[1]/256.0),int(px1[1]/256.0)+1):
                if (y < 0) or (y >= 2**z):
                    continue
                if tms_scheme:
                    y = (2**z-1) - y
		
                print("%d,%d,%d" % (z,x,y))
		ynum+=1
		if(ynum > 4):
			break


if __name__ == "__main__":

    rnd=random.random()*10
    bbox = (135+rnd,35+rnd,136+rnd,36+rnd)
    print_tiles(bbox, 11, 15)

data_tiles.pyで保存して、以下コマンドで実行

python data_tiles.py

スピードテストの項目

  • S3
  • CloudFront + S3
  • EC2(micro) + tilestream
  • EC2(2xlarge) + tilestream
  • CloudFrare + S3
  • さくらVPS
  • レベル11〜15のgrids読み込み有り
  • レベル11〜15のgrids読み込み無し

結果

  • CloudFrontを利用すると、およそ2倍のはやさ

(でも、いまいち体感速度は変わらないような。。。
CloudFrion使えばS3のバケット名が任意でOKなので、使う方向で)

  • tilestream + micro は CloudFrontに比べて100倍おそい

S3で地図タイルのアクセス制御

S3に保存した地図タイルへの直呼び出しのアクセス管理ができないか検討

アクセス制御方式

http://www.tdn.co.jp/techblog/201207/55/
s3でのアクセス制御の方法は4種類らしい。
ACLbucket policy、IAM、Query String Request

自分なりの解釈
ACL:ファイルパーミッション
bucket policy:.htaccessのようなもの
IAM:ユーザーグループのようなもの
Query String Request:Oauth認証のようなもの

地図タイルへのアクセス制限

目的:地図タイルへの直アクセスを制御したい。

  • 直アクセスできるのは、許可したサイト、アプリ、ソフトから

方法:

  1. Refererを参照してアクセス解析。申請してもらったサイト以外からアクセスが頻発する場合は、bucket policyでブロック
  2. 申請してもらったサイトをbucket policyで許可
  3. Query String Requestのキーを発行して、それを利用してアクセス

Refererによるアクセス制御

http://www.idupree.com/dreams/s3-referer-hotlink
s3のbucket policyに以下を設定
refererが無い場合(index.htmlの読み出し)と、許可した所。※下の不具合でもっとシンプルな設定が動作しないと勘違いしていたかもしれないので、そっちを試した方がいいかも。あとで。

{
	"Version": "2008-10-17",
	"Id": "refererGuard",
	"Statement": [
		{
			"Sid": "1",
			"Effect": "Allow",
			"Principal": {
				"AWS": "*"
			},
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::ecoris/*",
			"Condition": {
				"StringNotLike": {
					"aws:Referer": "*"
				}
			}
		},
		{
			"Sid": "2",
			"Effect": "Allow",
			"Principal": {
				"AWS": "*"
			},
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::ecoris/*",
			"Condition": {
				"StringLike": {
					"aws:Referer": "http://ecoris.s3-website-ap-northeast-1.amazonaws.com/*"
				}
			}
		}
	]
}
不具合?

ACLでタイルをpublicにしていると、上記のRefererを設定してもアクセス制御できないみたい。
なので、以下コマンドでACLを削除。(全部まとめて変更はkillされたので、レベルごとに実行した)
s3cmd --recursive setacl --acl-private s3://ecoris/vegetation/tiles/1/

アプリ、ソフトからのReferer

アプリ、ソフトから地図タイルを呼び出した場合、No Refererになるっぽい。
それだとアクセス制御できないので、なんとかしたい。
Google Analytics SDKを使ってもらえば、refererが付く?
http://murapong.hatenablog.com/entry/20111220/1324379177

Refereは偽装できるので、うーん。

Query String Requestのテスト

http://www.tdn.co.jp/techblog/201207/55/
アクセスキーの発行とかは、管理が面倒そうなので現実的ではないけど、一応テスト

<html>
<head>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/hmac-sha1.js"></script>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/components/enc-base64-min.js"></script>

<script>
    var AWSAccessKeyId='hoge';
    var secretAccessKey='hogehoge';
    var host = "https://s3-ap-northeast-1.amazonaws.com";
    var filePath = "/ecoristest/test/test.png";

    function myinit(){
     
     var dateTime = Math.floor(new Date().getTime()/1000) + 300; 
     var stringToSign = "GET\n\n\n" + dateTime + "\n" + filePath;
     var signature = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA1(stringToSign, secretAccessKey));
    
     var url = host + filePath + "?AWSAccessKeyId=" + AWSAccessKeyId + "&Signature=" + encodeURIComponent(signature) + "&Expires=" + dateTime;
     document.getElementById("AuthenticationURL").innerHTML = "<a href='" + url + "'>access</a>";
    }

</script>
</head>
<body onload="myinit()">
<div id="AuthenticationURL"></div>
</body>
</html>

結論

他サイトからの直アクセスはRefererで対応。ただしReferer偽装できる。
アプリからの直アクセスは、Refererを設定してもらう。

一定ズーム以上をprivateにしておき、許可を与えたところからだけアクセス可にするなら
Query String Requestを設定してもいいかも。