XPath と Lens で XML を扱う

はじめに

OpenGL の仕様が XML で書かれていてこれについて調べたりしてました。 XML 扱う機会があまりなくてほとんど使ったことがなかったんですが調べてみると色々と XML 使われているところはあるのだなと感じました。 XML から検索したりコード生成したりしたくて調べたことをまとめておきます。

XPath

developer.mozilla.org

ja.wikipedia.org

XPathXML Path Language の略です。 XPath を使って XML から必要な情報を検索したりできるようです。

JSON でいうところの JMESPath や jq などに近いものだと思います。

XPath は HTML にも使えるようで JavaScript やクローラでも使われたりする例を見かけました。 XPath 3.1 までバージョンが出てるらしいんですが、新しいものは対応しているツールがあまり無いようでした。

CLI から XPath を扱うことができるツールがあって XMLStarlet や xmllint が使えるようです。

xmlstar.sourceforge.net

xmlsoft.org

xmlstarlet では XML の更新も行える機能があるようなのでこちらを使ってみてます。

問い合わせの場合は sel のサブコマンドを使います。 ドキュメントはこちらです。

xmlstar.sourceforge.net

Lens

XPath を使って CLI で検索したりはできたんですが Haskell などから扱う場合にどうするのかなと調べていたところ Lens を使うと似たようなことができそうでした。

Lens についてはあまりよくわかっていないので詳しくは他の情報を参照してください。 とりあえず使うだけであれば Getter や Setter として使えるというぐらいの理解でもいいような気がします。

XML を扱う Haskell のライブラリはいくつかあるんですがあまり更新されてないものもあってどれを使うか迷いました。 XML 自体の仕様が変わっているわけでは無いので最近更新されていなくてもそんなに問題なく使えるような気もします。

hackage.haskell.org

xml-conduit は比較的更新されていそうだったためこれを使ってみています。

hackage.haskell.org

xml-conduit の Lens ライブラリとして xml-lens がありました。

hackage.haskell.org

Lens 自体のライブラリはこれです。

なお JSON にも同じように aeson というパーサーのライブラリと lens-aeson という Lens のライブラリがあります。

XPath と Lens の対応

<?xml version="1.0" ?>
<animals>
    <animal id="1">
        <name>rat</name>
    </animal>
    <animal id="2">
        <name>cow</name>
    </animal>
    <animal id="3">
        <name>tiger</name>
    </animal>
    <animal id="4">
        <name>rabbit</name>
    </animal>
</animals>

上のような XML をサンプルとして扱う場合の一部の例です。

XPath Lens
ルートの要素 / doc ^.? root
要素の階層を指定して取得 /animals/animal/name doc ^.. root . el "animals" ./ el "animal" ./ el "name"
属性に条件を付けて取得 /animals/animal[@id = "2"]/name doc ^.. root . el "animals" ./ el "animal" . attributeIs "id" "2" ./ el "name"
doc ^.. root . el "animals" ./ el "animal" . filtered ((Just "2" ==) . (^? attr "id")) ./ el "name"
子の要素に条件を付けて属性を取得 /animals/animal[name = "cow"]/@id doc ^.. root . el "animals" ./ el "animal" . filtered ((== Just "cow") . (^? plate . el "name" . text)) . attr "id"
名前が一致する要素を取得 //animal doc ^.. root . entire . named "animal"

データ型に変換する

ほとんどの場合は xml-conduit の結果の型そのままだと使いにくいと思います。 ここから自分で使いたいデータ型に変換して使うことになると思うのでデータ型に変換するあたりのコード例を書いておきます。 Maybe モナドや Either モナドでパーサーと似たような処理を書いて変換できました。

import Data.Text
import qualified  Text.XML as XML
import Text.XML.Lens

data Animal = Animal
    { animalId :: !Int
    , animalName :: !Text
    } deriving (Show)

parseAnimal :: XML.Element -> Maybe Animal
parseAnimal a = do
    aid <- a ^? attr "id"
    aname <- a ^? el "name" . text
    return (Animal aid aname)

parseAnimals :: XML.Document -> Maybe [Animal]
parseAnimals doc = mapM parseAnimal elems
    where
    elems = doc ^.. root . el "animals" ./ el "animal"

ECS で JMeter を動かしてみる

ECS を使った負荷テストについてです。 負荷テストでは十分な負荷を発生させることができれば 実行環境はある程度自由に選択できるので Docker で色々なツールを実行する というのも選択肢の一つになると思います。

Locust など新しめのツールを ECS で使うという記事は他にも書かれているのを見かけたのですが JMeter はあまり見かけないので書いてみます。 JMeter をそんなに使いたいというわけではないのですが、すでに作ったシナリオを利用したいなど使うことはあるかなと思います。

aws.amazon.com

ECS は去年少し触ってみてからあまり使う機会がなくてすでに EKS も使えるようになっていました。

aws.amazon.com

Kubernetes を負荷テストに使うという記事も見かけたことがあるので、Kubernetes 使う方が GCP などでも活用しやすいかもしれません。

cloud.google.com

今回は ECS まだあまりつかったことがなくてもう少し使い方を覚えたいということで ECS でやってみてます。 それから JMeter 以外のツールもこれから使うと思うのでいくらか触れています。

今回使ったテンプレートや Dockerfile などは下のリポジトリにおいています。

github.com

分散負荷テストのための構成

負荷テストツールでは大きな負荷をかけられるようにするため多くは分散テスト (複数のサーバーから負荷をかける) ができるようになっています。 ツールによってこの仕組みが色々違っているので使うものに合わせて実行環境を用意する必要があります。

JMeter

JMeter のウェブページはここです。

jmeter.apache.org

JMeter を動かすときの構成を図にしてみました。

f:id:tkaaad97:20190127193538p:plain

JMeter に外からアクセスする必要はないためプライベートサブネット内にクラスターをおいています。 図に書いてないですが ECR からのイメージダウンロードや負荷テスト時にはインターネットアクセスは必要なので NAT を使っています。 図ではマスターも同じ ECS クラスターで実行していますが、 マスターは手動でコマンドを色々変えて実行したいということもあると思います。 マスターだけパブリックサブネットに踏み台のような EC2 サーバーを用意してそこから実行するというやり方もあると思います。

JMeter ではまずスレーブをサーバーモードで立ち上げて マスターを実行するときにスレーブのIPアドレスとポートの一覧を指定して接続できるようにするという流れになります。 また JMeter の場合はマスターのプロセスを実行したときに負荷テストもスタートして計測結果がマスターに集められログのファイルに保存されます。

JMeter の場合は

  • スレーブの IP とポートの一覧を取得できるようにする
  • マスター実行時に結果をログに出力するか S3 などにアップロードする
  • マスター・スレーブ間で通信できるようにポートなど設定する

ということが必要そうです。

ECS上のタスクのIPとポートは ecs-cli から取得することができるようです。 色々取得方法があると思いますがサービスディスカバリに登録するようにして aws-cli から取得するということもできました。

github.com

上のリポジトリVPC、サブネット、NAT、ECS クラスター、オートスケーリンググループなどの CFn テンプレートの例が公開されていました。 これを真似して一通り構築することができました。

Locust

Locust のページはこちらです。

locust.io

Locust ではマスターを先に立ち上げ、スレーブの起動時にマスターのIPとポートを指定して接続することになります。 Locust のマスターはウェブサーバーとしても機能してウェブページから負荷テストをスタートさせられます。 負荷テスト結果もウェブページから見ることができます。

ECS を使った例を色々と探していて Locust の記事がいくつか見つかったため参考にさせてもらいました。

dev.classmethod.jp

Locust と ECS を使った負荷テストについてこちらの記事で詳しく解説されていて参考になりました。CFn テンプレートも記載されています。

マスターへのインターネットからアクセスが可能なようにマスターのみ Fargate モードでパブリック IP を付けて動作させているようです。

マスターとスレーブはそれぞれタスク定義を分けて別のサービスにしているようです。

現在ではサービスディスカバリも利用できるようになっているのでスレーブからマスターへのアクセスは サービスディスカバリで行うこともできると思います。

inokara.hateblo.jp

こちらの記事も同じく ECS と Locust を使った例です。

こちらの記事では docker-compose.yml でマスター・スレーブ構成を定義して使うようにしていました。

ecs-cli では docker-compose.yml の定義をタスクとして実行するという機能があるようです。

docs.aws.amazon.com

docker-compose を使うことができるとローカルでも同じ構成で確認できて便利だと思います。

ただし一つのタスク定義内に入れられるコンテナ数は10以下に制限されるという点には注意する必要があると思います。

大きな負荷が必要な場合にはタスク定義を分けておいたほうがいいと思います。

Fargate と EC2 どちらを使うか

構成は Fargate の方が簡単になるとは思います。 アカウント毎の Fargate タスク数の制限が前はデフォルト20まででしたが今は50になっていて少し使いやすくなっています。 料金も値下げされたようです。

ただ EC2 で動かす場合もクラスター構築の CFn テンプレートなどを用意しておけばある程度簡単にセットアップできると思います。 また EC2 の場合はスポットインスタンスを使って安く使うことができる場合があります。 状況によってどちらがいいかというのは変わりそうなので EC2 での手順も用意しておいて Fargate で簡単に対応できる要件であれば Fargate でやるなど選択することになるかなと思います。

今回は手順を色々覚えようということで EC2 で試しました。

Dockerfile

JMeter を動かすため Docker イメージには JRE が必要になります。 OpenJDK のイメージが公開されていたのでこれを元にその他必要なものを入れました。 Java のイメージの作り方に詳しくないのでもっとイメージサイズを小さくする方法があるのかもしれません。

hub.docker.com

FROM openjdk:11-jre-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        awscli curl groff-base jq less unzip wget zip \
    && rm -rf /var/lib/apt/lists/*

RUN mkdir /jmeter \
    && cd /jmeter \
    && wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.0.tgz \
    && tar --strip-components=1 -xvf apache-jmeter-5.0.tgz \
    && rm apache-jmeter-5.0.tgz

ENV JMETER_HOME /jmeter

ENV PATH $JMETER_HOME/bin:$PATH

WORKDIR /work

ADD scenarios /work/scenarios
ADD start-slave.sh /usr/local/bin
ADD run-master.sh /usr/local/bin

ENTRYPOINT []
CMD ["start-slave.sh"]

スレーブ起動用のスクリプトは以下のようになっています。 java.rmi.server.hostname にプライベート IP を指定するため EC2 メタデータ API で取得しています。 この方法は Fargate の場合は使えないので Fargate で使う場合修正必要だと思います。

#!/bin/bash

set -x

SERVER_IP=$(curl -q http://169.254.169.254/latest/meta-data/local-ipv4)
SERVER_PORT=1099

jmeter -Dserver_port=$SERVER_PORT -Jserver.rmi.ssl.disable=true \
    -D"java.rmi.server.hostname=$SERVER_IP" \
    -j /dev/stdout -s "$@"

タスク定義

パラメーターなど省略しますがタスク定義の CFn テンプレートは下のような感じにしました。 ネットワークモードは最初デフォルトのブリッジで試していたのですがマスターとスレーブ間の通信が上手くいかずホストモードを使うようにしました。 1099のポートを固定で使用しているため、タスクの配置が制限されてしまうのでできれば動的ポートマッピングにしたかったんですが RMI 関連の知識がないため上手くやる方法を見つけられませんでした。

マスターのタスク起動時にスレーブのIP一覧や負荷をかけるホストのIPをオーバーライドのパラメーターで渡します。 下のような感じで cli から実行しました。

aws ecs run-task --cluster {クラスタ名} \
    --task-definition jmeter-master \
    --overrides '{"containerOverrides":[{"name":"jmeter-master","command":["run-master.sh","-GTARGET_HOST={ターゲットIP}","-GTARGET_PATH={ターゲットパス}","-R{スレーブIP一覧}"]}]}'

スレーブのタスクはサービスにして動かしたのですが スレーブごとにパラメーターを変えて動かしたい場合などもあるかもしれないので run-task で一つづつ起動するやり方でもいいと思います。

MasterTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
        Family: 'jmeter'
        Cpu: !Ref 'ContainerCpu'
        Memory: !Ref 'ContainerMemory'
        NetworkMode: 'host'
        ContainerDefinitions:
            - Name: 'jmeter-master'
                Cpu: !Ref 'ContainerCpu'
                Memory: !Ref 'ContainerMemory'
                Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepositoryName}'
                LogConfiguration:
                    LogDriver: awslogs
                    Options:
                        awslogs-region: !Ref 'AWS::Region'
                        awslogs-group: !Ref 'LogGroup'
                        awslogs-stream-prefix: !Ref 'AWS::StackName'
                EntryPoint:
                    - run-master.sh

SlaveTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
        Family: 'jmeter'
        Cpu: !Ref 'ContainerCpu'
        Memory: !Ref 'ContainerMemory'
        NetworkMode: 'host'
        ContainerDefinitions:
            - Name: 'jmeter-slave'
                Cpu: !Ref 'ContainerCpu'
                Memory: !Ref 'ContainerMemory'
                Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepositoryName}'
                PortMappings:
                    - ContainerPort: 1099
                      HostPort: 1099
                LogConfiguration:
                    LogDriver: awslogs
                    Options:
                        awslogs-region: !Ref 'AWS::Region'
                        awslogs-group: !Ref 'LogGroup'
                        awslogs-stream-prefix: !Ref 'AWS::StackName'
                EntryPoint:
                    - run-slave.sh

まとめ

色々と時間がかかってしまいましたが ECS で JMeter を動かすことができました。 CFn テンプレートなどで手順をまとめておくと色々な要件で使いまわすことができて気軽に試すことができると思います。 JMeter 以外のツールを使う場合も Docker イメージとタスク定義あたりを変えることで活用できると思います。

その他に負荷テストを提供しているサービスも色々あるようです。 CodePipeline とも連携している BlazeMeter というサービスでも JMeter などが使えるようです。 おそらくこうしたサービスだと環境の構築は自分でやらなくていいようになっているのだと思います。

aws.amazon.com

AWS S3 への匿名での Put

概要

AWS S3 に匿名で Put すると危ないという話についてです。

私もこれを間違ってやってしまってました。

AWS のドキュメントにも少し書かれていましたが間違って使ってしまっている人もいるんじゃないかと思います。

docs.aws.amazon.com

注記

バケットをパブリックにした場合 (非推奨)、認証されていないどのユーザーもバケットにオブジェクトをアップロードできます。これらの匿名ユーザーは AWS アカウントを持っていません。匿名ユーザーがバケットにオブジェクトをアップロードすると、Amazon S3 によって特殊な正規ユーザー ID (65a011a29cdf8ec533ec3d1ccaae921c) がそのオブジェクトの所有者として ACL で追加されます。

どのように危険か

匿名ユーザーで Put したオブジェクトの ACL 情報を取得してみると以下のようになっています。

{
    "Owner": {
        "ID": "65a011a29cdf8ec533ec3d1ccaae921c"
    },
    "Grants": [
        {
            "Grantee": {
                "ID": "65a011a29cdf8ec533ec3d1ccaae921c",
                "Type": "CanonicalUser"
            },
            "Permission": "FULL_CONTROL"
        }
    ]
}

ドキュメントに書かれているように匿名ユーザーがオーナーになっていて、このオーナーの匿名ユーザーに FULL_CONTROL の権限がついています。 つまり匿名ユーザーが読み込みも書き込みもできてしまう状態です。

この匿名ユーザーは実は特定のユーザーになっているので オブジェクトのリージョンとバケット、オブジェクトキーの情報が知られてしまうと誰でもアクセスできてしまいます。

バケットポリシーで特定の IP 以外は拒否などの設定が入っていれば制限できると思いますが バケットポリシーで IP 許可だけの設定だと、オブジェクトの権限だけで通ってしまうようです。

結構間違って使われているのでは...

匿名ユーザーの ID で検索すると list-objects の結果と思われるような情報が色々でてきます。 list-objects の結果情報にはオーナーとオブジェクトキーも含まれているので匿名ユーザーでこれを書き換えたりできてしまうかもしれません。

間違った場合

匿名で Put できないようにバケットポリシーで制限します。 それからすでに匿名ユーザーで作られてしまったオブジェクトのオーナーは更新できないようなので 一度ダウンロードして、別のユーザーでアップロードし直しオーナー変更します。 list-objects の結果にオーナーIDも含まれているので問題のあるオブジェクトを探すのに使えます。

S3のアクセスコントロールについて参考

docs.aws.amazon.com

dev.classmethod.jp

Android で朝起きるときに音楽を流したい

概要

スマート家電などを活用している人の話に影響されて 朝起きる時に音楽を流したり照明を自動で点けたりしたいと思い色々試してました。

スマートスピーカーがあるとアラームとして音楽を流すなど簡単にできそうですが スマートスピーカーは持ってないため手持ちの Android タブレットBluetooth スピーカーでやる方法を試しました。 Automate でアラームをトリガーにして音楽を流すというやり方で目的のことができました。

アラームアプリ

最初から入っていたアラームのアプリや Google が出している時計のアプリもあってこれで十分の場合もあると思います。

play.google.com

Spotify のアプリと連携してプレイリストを再生するなどもできるようです。

ただし最初から入っていたアラームアプリだと外部スピーカーに音声出力するという設定ができませんでした。 またできれば Amazon Music も使えるようにしたいですが今のところはできないと思います。

IFTTT で Android Device 連携 (上手くいかなかった)

ifttt.com

IFTTT (If this, then that) というサービスがあってスマート家電などと一緒によく使われているようです。 トリガーとなるサービスと、そのときに実行するアクションを組み合わせて色々なことができるようになっています。

ifttt.com

Android 向けのアクションとして Play music もありました。 時間をトリガーにしてこのアクションを使えば目的のことができそうだったんですが 機種の問題なのかわかりませんが上手く動きませんでした。

上手くいかなかったのは Android を長時間スリープ状態にしておくと トリガーを設定した時間になってもスリープ解除されず音楽も再生されないというところです。 トリガー時間をすぎて手動で電源を入れるとその時点から音楽が流れますが これでは目覚ましになりません。

それからどの音楽アプリを使うかというのが設定しにくくなっています。 最初にトリガーされたときにどの音楽アプリを使うか選択する画面がでてきますが これを毎回選択するようにすると目覚ましにはできませんし、固定にするとあとで変えたりできなくなります。 音楽アプリを新しく追加すると選択画面がまた出るようになりますが、 このせいで選択画面で止まってしまい次の朝に目覚ましとして動かないという感じでやはり使いづらいです。

Automate

play.google.com

Automate ⋅ everyday automation for Android ⋅ LlamaLab

Automate は Android 上での色々なタスクの自動化などに使えるアプリです。 IFTTT より複雑で一から使い方を覚えるのは大変そうですが 他のユーザーが作成したフローを真似して作れば一応は使えそうです。

Alarm ⋅ Automate ⋅ LlamaLab

Automate にはアラームをトリガーにするブロックがありました。 標準のアラームではスリープから復帰できていたので、アラームと同時に起動するようにすれば IFTTT のときのようにスリープ抜けないという問題は回避できるのではないかと思いました。

Amazon Music で検索するといくつかフローがあったため音楽を流すところを参考にして トリガー部分をアラームにしてフロー作成しました。フローは下のようになりました。

f:id:tkaaad97:20181111144250p:plain

アラーム側は無音でバイブレーションもなしで画面だけ明るくなるという設定にして動作させています。

まだ使って一週間ほどですが大体問題なく動いています。 (スリープのせいか起動が少し遅れたりはしますが)

CloudFormation のリソース仕様からコード生成してみる

はじめに

この間 AWS-CDK というプロジェクトが公開されていました。

aws.amazon.com github.com https://awslabs.github.io/aws-cdk/awslabs.github.io

まだ開発者プレビューという段階で正式な公開ではないようです。 中身はあまり詳しく見ていませんが、かなり便利に使えそうな印象がありました。 TypeScript, JavaScript, Java, .NET, Python などの言語で CloudFormation のリソースを記述して、これを元に色々なリソースを AWS 上に構築するのに使えるようです。

CloudFormation のテンプレートは YAMLJSON で書くことができますがなかなか大変です。 少し複雑なものを作ろうとするとリファレンスを参照しながら、トライアルアンドエラーでスタックを何度も立てたりして書いていた記憶があります。 色々な言語で IDE の補間やシンタックスチェックなどが効いた状態で書ければ便利になりそうです。 またプログラミング言語で書けるので、他のリソース情報を参照したりということもやりやすくなるのではないかと思います。

すこし調べていたところ CloudFormation のリソース仕様が公開されていると知りました。

docs.aws.amazon.com

cfn-lint のツールなどでもこの仕様が利用されているようです。 AWS-CDK の内部でもおそらくこの仕様からコード生成したりして利用されているのではないかと思います。

実際に使う場合には CDK などの公式のツールを使うのがいいと思いますが この仕様からコード生成するというのはそんなに難しくなくできそうなのと、ちょっと面白そうという気がしてコード生成するのを試してみました。

リソース仕様の構造

リソース仕様の構造についての詳しい説明はどこにあるかわかりませんでしたが、中身を見てみると意味はなんとなく理解できました。

一番上の階層に PropertyTypes, ResourceTypes, ResourceSpecificationVersion があります。

{
    "PropertyTypes": {...},
    "ResourceTypes": {...},
    "ResourceSpecificationVersion": "2.8.0"
}

ResourceTypes は色々なリソースの定義です。 PropertyTypes はリソースの中で使われているプロパティの定義のようです。 ResourceSpecificationVersion はこの仕様のバージョンだと思います。

基本の型など

基本の型 (PrimitiveType) として Boolean, Double, Integer, Long, String, Timestamp, Json があるようです。 このほかに MapTagPrimitiveType ではないですが定義なしで使われていて少し特殊な扱いのようでした。

Amazon API Gateway Deployment DeploymentCanarySettings - AWS CloudFormation

Map は例えば上のような部分で使われていました。キーと値が両方文字列の連想配列のようです。

AWS CloudFormation Resource Tags タイプ - AWS CloudFormation

Tag はキーと値の文字列のペアです。

それから List タイプがあります。List は配列です。List の場合は ItemType または PrimitiveItemType で要素の型が書かれていました。

プロパティタイプ定義

プロパティタイプ のドキュメントはここにありました。

リソースプロパティタイプのリファレンス - AWS CloudFormation

プロパティタイプ定義は例えば下のような構造になっています。

    ...
    "AWS::IAM::User.Policy": {
      "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html",
      "Properties": {
        "PolicyDocument": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html#cfn-iam-policies-policydocument",
          "PrimitiveType": "Json",
          "Required": true,
          "UpdateType": "Mutable"
        },
        "PolicyName": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html#cfn-iam-policies-policyname",
          "PrimitiveType": "String",
          "Required": true,
          "UpdateType": "Mutable"
        }
      }
    },
    ...

キーが "リソース名.プロパティ名" になっています。 内容はドキュメントのリンクとプロパティ一覧で、おそらくプロパティタイプのプロパティは基本の型か Map か Tag に限られています。

Required はプロパティが必須なものかどうかです。 UpdateType はリソース更新で変更できるかどうかという意味だと思います。Immutable, Mutable, Conditional のどれかが使われていました。

リソースタイプ定義

リソースタイプ のドキュメントはこれです。

AWS リソースプロパティタイプのリファレンス - AWS CloudFormation

リソースタイプ定義の例です。

    ...
    "AWS::IAM::User": {
      "Attributes": {
        "Arn": {
          "PrimitiveType": "String"
        }
      },
      "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html",
      "Properties": {
        "Groups": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-groups",
          "DuplicatesAllowed": true,
          "PrimitiveItemType": "String",
          "Required": false,
          "Type": "List",
          "UpdateType": "Mutable"
        },
        "LoginProfile": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-loginprofile",
          "Required": false,
          "Type": "LoginProfile",
          "UpdateType": "Mutable"
        },
        "ManagedPolicyArns": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-managepolicyarns",
          "DuplicatesAllowed": false,
          "PrimitiveItemType": "String",
          "Required": false,
          "Type": "List",
          "UpdateType": "Mutable"
        },
        "Path": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-path",
          "PrimitiveType": "String",
          "Required": false,
          "UpdateType": "Mutable"
        },
        "Policies": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-policies",
          "DuplicatesAllowed": true,
          "ItemType": "Policy",
          "Required": false,
          "Type": "List",
          "UpdateType": "Mutable"
        },
        "UserName": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html#cfn-iam-user-username",
          "PrimitiveType": "String",
          "Required": false,
          "UpdateType": "Immutable"
        }
      }
    }
    ...

プロパティタイプの定義とほぼ似た感じですがドキュメントリンクとプロパティ一覧以外に Attributes があります。 Attributes はリソース作成した時に付与される情報だと思います。 Attributes に使われるのは基本の型のみで RequiredUpdateType の指定はありません。 リソースタイプのプロパティは基本の型かプロパティタイプ定義で定義した型が使われています。

コード生成してみる

実験に使ったコードはこれです。

github.com

リソース定義を http-client で取得して JSON をパースし、テンプレートエンジンでコード生成という感じです。

仕様からコード生成するという流れはよくあるもののようで例えば Swagger や Protocol Buffers などでコード生成するというのを聞いたことがあります。 どちらもまだ使ったことがないのでこのような場合に使えるかわからないですが、また機会があればこの辺のツールも触ってみたいです。

生成したコードは下のような感じになりました。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module AWS.IAM.User
    ( Policy(..)
    , LoginProfile(..)
    , User(..)
    , resourceJSON
    ) where

import AWS (Map, Tag)
import Data.Text (Text)
import Data.Aeson ((.=))
import qualified Data.Aeson as DA (FromJSON(..), ToJSON(..), Options(..), Value, defaultOptions, object)
import qualified Data.Aeson.TH as DA (deriveJSON)

data Policy = Policy
    { _PolicyPolicyDocument :: DA.Value
    , _PolicyPolicyName :: Text
    } deriving (Show, Eq)

data LoginProfile = LoginProfile
    { _LoginProfilePassword :: Text
    , _LoginProfilePasswordResetRequired :: Maybe Bool
    } deriving (Show, Eq)

data User = User
    { _UserGroups :: Maybe [Text]
    , _UserPath :: Maybe Text
    , _UserLoginProfile :: Maybe LoginProfile
    , _UserUserName :: Maybe Text
    , _UserManagedPolicyArns :: Maybe [Text]
    , _UserPolicies :: Maybe [Policy]
    } deriving (Show, Eq)

$(DA.deriveJSON DA.defaultOptions { DA.fieldLabelModifier = drop 7 } ''Policy)
$(DA.deriveJSON DA.defaultOptions { DA.fieldLabelModifier = drop 13 } ''LoginProfile)
$(DA.deriveJSON DA.defaultOptions { DA.fieldLabelModifier = drop 5 } ''User)

resourceJSON :: User -> DA.Value
resourceJSON a = DA.object [ "Type" .= ("AWS::IAM::User" :: Text), "Properties" .= a ]

生成したコードを使って一応は Haskell のデータでリソースを定義して JSON を出力するということはできるんじゃないかと思います。 (実際に使うつもりはあまりなく実験してみたという感じです。)

まとめ

CloudFormation のリソース定義をつかってのコード生成をやってみました。 やろうと思えば色々なツールを作ったりもできるんじゃないかと思います。 仕様がプログラムから利用しやすい形で公開されているのはいいなと思いました。

この他にも IAM のアクションの仕様なども公開されるとうれしいと思います。 こちらのページで IAM アクションが検索できるサービスが公開されていました。

Complete AWS IAM Reference

これは非公式なページで AWS ドキュメントから情報を抜き出したりして更新しているようです。 IAM アクション仕様が提供されていればこうしたツールも作りやすくなると思います。

InfluxDB と Grafana で可視化して Puppeteer で PDF を生成

はじめに

計測データのグラフや表をまとめたいことが 何度かあったんですがちょうどいいツールを見つけられていませんでした。

エクセルでグラフを作ったりgnuplotでグラフを作ったりもできますが 見た目をかっこよくまとめるのは慣れないと難しそうに感じました。

Grafana は前に目にしたことがあったんですが 常時計測したデータの可視化用でちょっと使いたい用途には合わないかなと思ってました。

ただ docker-compose を使って CLI ツールのようにまとめることで やりたい用途にも使えそうだなと思い試してみました。

実験に使ったリポジトリはここに置いています。

github.com

やり方

InfluxDB, Grafana, Puppeteer のコンテナを docker-compose で連携させるというやり方です。 なんでも docker でやってみたくなっている感じが少しあります。 色々なツールを使っていて全部直接入れるのはちょっと気が引けるし、環境構築も大変なので実際こういう用途では便利だと思います。

InfluxDB

www.influxdata.com

InfluxDB は時系列データを扱うデータベースで Go 言語で書かれているようです。 Grafana とセットでよく使われているのを目にする気がします。 データの有効期限を設定できるようで常時計測しているデータの保存などに使うことが多いようです。

InfluxDB Line Protocol reference | InfluxData Documentation

Writing data with the HTTP API | InfluxData Documentation

Line Protocol の書き方と curl での API 呼び出し方の例を真似してデータを取り込むことができました。

公式のイメージが Docker Hub にありました。

https://hub.docker.com/r/library/influxdb/

Grafana

grafana.com

Grafana はデータの可視化用のウェブサーバーで GUIダッシュボードに色々なグラフを表示したりできます。 InfluxDB 以外にも色々なデータソースを扱うことができるようです。 Grafana もサーバー部分は Go 言語のようです。

Grafana も Docker Hub にイメージがありました。

https://hub.docker.com/r/grafana/grafana/

Puppeteer

github.com

Puppeteer は Chrome の開発用 API を利用できる Node ライブラリです。

ウェブページの PDF を作るのに他のツールも少し使ってみたのですが

表示が終わるのを待つなど細かい制御をするには Puppeteer を使ったほうがうまく行きそうだったので使ってみることにしました。

下のような感じで PDF の保存ができました。

const puppeteer = require('puppeteer');
const url = process.argv[2];
const output_path = process.argv[3];
const width = 1080;
const height = 1920;

(async() => {
    const browser = await puppeteer.launch({
        headless: true,
        executablePath: 'chromium-browser',
        args: ['--no-sandbox', '--disable-setuid-sandbox']
    });

    const page = await browser.newPage();
    await page.setExtraHTTPHeaders({
        Authorization: 'Basic ' + new Buffer('admin:admin').toString('base64')
    });

    await page.setViewport({
        width: width,
        height: height,
        isMobile: false
    });

    await page.goto(url, {waitUntil: 'networkidle2'});

    await page.pdf({
        path: output_path,
        width: width + 'px',
        height: height + 'px',
    });

    await browser.close();
})();

puppeteer/troubleshooting.md at master · GoogleChrome/puppeteer · GitHub

ここの Dockerfile の書き方を参考にイメージを作りました。

docker-compose 設定

docker-compose 設定は下のようになりました。 Grafana のダッシュボードとデータソースの設定は /etc/grafana/provisioning/ 以下に設定ファイルを置くと反映されるので 設定ファイルを置いたディレクトリをマウントするようにしています。

version: "3"
services:
  influxdb:
    image: influxdb:1.6.2
    ports:
      - "8086:8086"
  grafana:
    image: grafana/grafana:5.2.4
    ports:
      - "3000:3000"
    volumes:
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards
      - ./grafana/datasources:/etc/grafana/provisioning/datasources
    depends_on:
      - influxdb
  puppeteer:
    build:
      context: .
      dockerfile: Dockerfile.puppeteer
    volumes:
      - ./volume:/volume
    entrypoint:
      - ./run-puppeteer.sh
    depends_on:
      - grafana
      - influxdb

生成した PDF

一連の流れを試してみるのが目的だったので見栄えしないグラフ一つしかありませんが下のような感じで生成できました。 Grafana のダッシュボードは kiosk=true の GET パラメーターを付けるとメニューを非表示にできるようです。 やろうと思えば Puppeteer で少し見た目を加工するなどもできるのかなと思います。

f:id:tkaaad97:20180919004337p:plain

まとめ

InfluxDB にデータを取り込み Grafana で表示して Puppeteer で PDF 作成という一連の流れをdocker-compose で CLI 化することができました。 はじめて使うものが多かったので少し時間がかかりましたが今後も使う機会がありそうなので少しは慣れることができてよかったと思います。

Puppeteer で PDF 生成する前に Grafana のサーバーが立ち上がるのを少し待つ必要があるのを今回は10秒スリープするといういい加減な方法で対応しました。 こういう用途には wait-for-it や dockerise が使えるらしいです。

Compose の起動順番を制御 — Docker-docs-ja 17.06.Beta ドキュメント

Docker 内で GLFW のプログラムを動かすサンプル

はじめに

Docker の利用は CLI やサーバーサイドの利用が主なのかなと思っていましたが GUI のプログラムもある程度は動かせるようです。

軽いゲームなどを作る場合も使えるかもしれません。

実験用に GLFW のデモアプリを動かす Docker イメージを作ってみました。

Dockerfile などはここのリポジトリに置きました。

github.com

マウントしたり特権コンテナにしたりという設定があるため docker-compose を使っています。

Docker での GUI

以下のページなどを参考にさせてもらいました。

docker/Tutorials/GUI - ROS Wiki

https://unskilled.site/dockerコンテナの中でguiアプリケーションを起動させる/

https://tail-island.github.io/programming/2017/07/11/docker-for-development-container-on-linux.htmltail-island.github.io

medium.com

kunst1080.hatenablog.com

いくつか方法があるようです。

  • X11 ソケットを共有する
    • ホストの X サーバーとコンテナの X クライアントがソケット通信
  • コンテナで X サーバーを動かす
    • コンテナで X サーバーも X クライアントも動かす
  • ssh を使った X11 forwarding
    • コンテナで ssh サーバーを動かす
  • VNC を使う
    • コンテナで VNC サーバーを動かす

ホスト側も X ウィンドウシステムの場合は上の二つの方法が使えそうです。

自分ではホストも Ubuntu などで動かすと思うので一番簡単そうな X11 ソケットを共有する方法を試してみました。

Docker イメージ

前に作った haskell 用のイメージを元にして作っています。

開発用として使う想定なので他の言語ではそれぞれの開発環境が入ったイメージを元にして必要なパッケージをいれることになると思います。

FROM tkaaad97/haskell-docker:8.2.2

# install dev tools
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        make \
        pkgconf \
        xz-utils \
        xorg-dev \
        libgl1-mesa-dev \
        libglu1-mesa-dev \
        libxrandr-dev \
        libxinerama-dev \
        libxcursor-dev \
        libxi-dev \
        libxxf86vm-dev

WORKDIR /app/

ENTRYPOINT []
CMD ["bash"]

下は docker-compose 設定ファイルです。

/tmp/.X11-unix:/tmp/.X11-unix:rw の部分で X11 ソケットをマウントして共有しています。

command で GLFW-b-demo のパッケージをインストールして起動しています。

version: "2"
services:
  app:
    build: .
    command: bash -c 'stack install GLFW-b-demo && /root/.local/bin/GLFW-b-demo'
    working_dir: /app
    privileged: true
    environment:
      - DISPLAY=${DISPLAY}
    volumes:
      - .:/app
      - .stack:/root/.stack
      - /tmp/.X11-unix:/tmp/.X11-unix:rw

xhost

コンテナとホストで X11 ソケット共有して通信する必要があるのですが コンテナ上のユーザーとホスト側で接続しているユーザーが 違う場合にそのままでは通信が許可されません。 この場合に xhost を使って許可することができます。

xhost local:

これを実行するとローカルの接続であればユーザーが違っても許可されます。

ただしあまり安全な方法では無いらしくコンテナのユーザーとホストのユーザーを合わせる方法なども紹介されていました。

スクリーンショット

以下のようにデモアプリが表示されました。

f:id:tkaaad97:20180902224447p:plain