web-technical-blog

web開発に関する技術メモ

コマンドプロンプト(windows10)でlinuxコマンドを使用できるようにする

virtualboxの共有フォルダの設定について

# ls -l /media
drwxrwx--- 1 root vboxsf 0 12月 11 17:09 2016 sf_vbox_share

このままだと権限がないので、 rootかvboxsfグループのユーザーしかアクセスできないため、 既存ユーザーをvboxsfグループに追加しておく。有効にするため一度再起動したら、 共有できるようになっているはず。単に共有さえできればいいならここまでで設定は終わり。

# gpasswd -a vagrant vboxsf
# reboot

http://itemy.net/?p=1355

windows10の場合はシンボリックリンクを設定が必要 everyoneを設定する

https://tokibito.hatenablog.com/entry/2018/02/28/012014

digdagServerの構築メモ

Centos7にdigdagサーバーを構築した際のメモ

久しぶりに自分のローカルPCにCentos7でサーバーを構築しようとしたが、 vagrant up しようとするとエラーが発生。。

下記組み合わせだとvagrant upがエラーする

  • virtual box:バージョン 5.2.6 r120293 (Qt5.6.2)
  • vagrant:Vagrant 2.0.4
D:\vagrant\labo>vagrant up
The version of powershell currently installed on this host is less than
the required minimum version. Please upgrade the installed version of
powershell to the minimum required version and run the command again.

  Installed version: 2
  Minimum required version: 3

上記エラーメッセージからPowerShellのバージョンを2系から3系以上に変更したが 次にWMF5.0(Windows Management Framework 5.0)をインストールする必要もあった。 詳細は以下のリンクから確認できる。

WMFのインストールが必要

digdagServerを構築する際にpostgreSQL

  • postgres9.6のインストールは以下のURLを参考にしてインストール
  • CREATE EXTENSIONしないとエラーになるので注意
digdag_db=> CREATE EXTENSION "uuid-ossp";
ERROR:  could not open extension control file "/usr/pgsql-9.6/share/extension/uuid-ossp.control": No such file or directory

## 拡張モジュールのインストールが必要
# yum -y install postgresql96-contrib.x86_64

## postgresユーザーでログインして
psql -U postgres -h 127.0.0.1 -d digdag_db

CREATE EXTENSION "uuid-ossp";

postgre9.6 install

digdag/embulkのインストール

  • java1.8がインストールされていることが前提
$ java -version
openjdk version "1.8.0_151"
OpenJDK Runtime Environment (build 1.8.0_151-b12)
OpenJDK 64-Bit Server VM (build 25.151-b12, mixed mode)

# インストールされていなければいれる
$ yum -y install java-1.8.0-openjdk
# Embulkの導入
$ curl --create-dirs -o ~/bin/embulk -L https://dl.embulk.org/embulk-latest.jar
$ chmod +x ~/bin/embulk

# Digdagの導入
$ curl --create-dirs -o ~/bin/digdag -L "https://dl.digdag.io/digdag-latest"
$ chmod +x ~/bin/digdag

# OSX環境の場合は次のように.bash_profileを編集する
$ cat << 'EOF' >> ~/.bash_profile
# User specfic environment and startup programs
export PATH=$PATH:$HOME/bin
EOF
$ source ~/.bash_profile

digdagはまりポイント

https://qiita.com/toyama0919/items/142d290c8dcb2c86851c

# vi /usr/lib/systemd/system/digdag.service
[Unit]
Description=digdag

[Service]
Type=simple
PIDFile=/run/digdag.pid
ExecStart="/usr/local/bin/digdag server -b 0.0.0.0 --config /opt/digdag/server.properties -O /opt/digdag/logs/tasklogs -A /opt/digdag/logs/accesslogs -L /opt/digdag/logs/server.log"
User=root
Group=root
WorkingDirectory=/opt/digdag
Restart=always
RestartSec=5
KillMode=process
TimeoutStopSec=1200
SyslogIdentifier=digdag

[Install]
WantedBy=multi-user.target

ExecStartは別ファイルにしないとエラーになるので注意

シェルは別ファイルにしないとエラーする

[Unit]
Description=digdag

[Service]
Type=simple
PIDFile=/run/digdag.pid
ExecStart=/opt/digdag/start.sh
User=root
Group=root

[Install]
WantedBy=multi-user.target

https://qiita.com/cmwig65/items/3386a061aeb3d2f81b81 https://qiita.com/toyama0919/items/142d290c8dcb2c86851c https://qiita.com/bwtakacy/items/ec3151644512ca65f6b6

Go言語でJSONのデコード処理を検証してみた

GOでJSONをデコードする際の方法

GoでJSONを処理する最も一般的な方法

  • Goの標準ライブラリには、JSONを扱うためのパッケージ(encoding/json)が含まれていて
    エンコード(構造体から文字列)およびデコード(文字列から構造体)の両方をサポートしている
package main

import "encoding/json"

type Message struct {
    Name string
    Body string
    Time int64
}

func main() {
    b := []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)
    var m Message
    json.Unmarshal(b,&m)
    println(m.Name) // Alice
    println(m.Body) // Hello
    println(m.Time) // 1294706395881547000
}
  • bとして定義されたJSONjson.Unmarshalでデコード
  • ポイントは、デコード結果を受け取るために構造体を定義している点
  • 対象のJSONデータの構造を確認しつつ、実装時に構造体の定義を決める必要がある

複雑な構造のJSON処理で直面する問題

  • JSONデータの構造が複雑な場合、具体的には多重の入れ子を含むケースだと実装がだんだん大変になってくる

  • (例)Elasticsearchの検索APIは処理結果として複雑なJSONを返してくる

{
    "took": 1,
    "timed_out": false,
    "_shards":{
        "total" : 1,
        "successful" : 1,
        "failed" : 0
    },
    "hits":{
        "total" : 1,
        "max_score": 1.3862944,
        "hits" : [
            {
                "_index" : "twitter",
                "_type" : "tweet",
                "_id" : "0",
                "_score": 1.3862944,
                "_source" : {
                    "user" : "kimchy",
                    "message": "trying out Elasticsearch",
                    "date" : "2009-11-15T14:12:12",
                    "likes" : 0
                }
            }
        ]
    }
}

上記JSONデータをデコードする処理を、単純にコーディングすると

package main

import (
    "encoding/json"
    "net/http"
)

type ResultShards struct {
    Total      int `json:"total"`
    Successful int `json:"successful"`
    Failed     int `json:"failed"`
}

type ResultHitSource struct {
    User    string `json:"user"`
    Message string `json:"message"`
    Date    string `json:"date"`
    Likes   int    `json:"likes"`
}

type ResultHit struct {
    Index  string          `json:"_index"`
    Type   string          `json:"_type"`
    Id     string          `json:"_id"`
    Score  float32         `json:"_score"`
    Source ResultHitSource `json:"_source"`
}

type ResultHits struct {
    Total    int         `json:"total"`
    MaxScore float32     `json:"max_score"`
    Hits     []ResultHit `json:"hits"`
}

type Result struct {
    Took     int          `json:"took"`
    TimedOut bool         `json:"timed_out"`
    Shards   ResultShards `json:"_shards"`
    Hits     ResultHits   `json:"hits"`
}

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result Result
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        panic(err)
    }
    println(result.Hits.Total)
}
  • JSON入れ子構造になっているケースでは、結果を受け取る構造体も入れ子に定義する必要がある
  • 実装にかなり苦労する
  • 別のAPIを呼ぶたびにそのAPIの出力結果に応じた構造体群を定義する必要がある

デコード結果格納までのコードの見通しをよくする

  • 上記の例では、Goの平易な言語仕様しか用いていないために、見通しの悪い実装となっているのでリファクタリングする
  • Goは構造体をネスト定義することが可能
  • さらにネスト定義する構造体には型名が必要ないので、これも削除する
package main

import (
    "encoding/json"
    "net/http"
)

type Result struct {
    Took     int  `json:"took"`
    TimedOut bool `json:"timed_out"`
    Shards   struct {
        Total      int `json:"total"`
        Successful int `json:"successful"`
        Failed     int `json:"failed"`
    } `json:"_shards"`
    Hits struct {
        Total    int     `json:"total"`
        MaxScore float32 `json:"max_score"`
        Hits     []struct {
            Index  string  `json:"_index"`
            Type   string  `json:"_type"`
            Id     string  `json:"_id"`
            Score  float32 `json:"_score"`
            Source struct {
                User    string `json:"user"`
                Message string `json:"message"`
                Date    string `json:"date"`
                Likes   int    `json:"likes"`
            } `json:"_source"`
        } `json:"hits"`
    } `json:"hits"`
}

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result Result
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        panic(err)
    }
    println(result.Hits.Total)
}
  • ネストに定義することで、本来のJSONの出力形式に見た目が近くなり、かつコード量も減る

発展的な実装テクニック

  • 出力されるJSONの形式を完全に構造体でカバーする必要がなく、参照が必要なオブジェクトだけを構造体の定義でおさえるようにしても、デコード処理に支障は出ない
  • ヒットしたドキュメントのリストのみを参照した場合は以下のように実装することができる
package main

import (
    "encoding/json"
    "net/http"
)

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result struct {
        Hits struct {
            Hits     []struct {
                Source struct {
                    Title       string `json:"title"`
                    Description string `json:"description"`
                    ImageUrl    string `json:"image_url"`
                    Url         string `json:"detail_url"`
                } `json:"_source"`
            } `json:"hits"`
        } `json:"hits"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        panic(err)
    }
    for _, hit := range result.Hits.Hits {
        println(hit.Source.Title)
        println(hit.Source.Description)
        println(hit.Source.ImageUrl)
        println(hit.Source.Url)
    }
}
  • JSONデータに含まれるオブジェクトには、エラー情報のように「もしかしたら返却されるかもしれない」オブジェクトというものもあるので、こういった場合は対応するフィールドをポインタ型で用意するう

構造体定義の自動生成

  • デコード結果を受け取る構造体の定義は、JSONデータから自動生成するWebアプリが公開されている

json-to-go

interfaceを利用して局所的に参照する

  • 必要なオブジェクトをピンポイントに取り出す
  • 各オブジェクトへのアクセスには型アサートを利用する
    • 注意点としては数値型はすべてfloat64型として変換される
    • JSON上に整数値が入っていても、それをint型として変換しようとするとエラーになる
    • JSONデータ上での型とGo側の型の対応ドキュメント
package main

import (
    "encoding/json"
    "net/http"
)

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result interface{}
    decoder := json.NewDecoder(resp.Body)
    if err := decoder.Decode(&result); err != nil {
        panic(err)
    }
    n, _ := result.(map[string]interface{})["hits"].(map[string]interface{})["total"].(float64)
    println(int(n))
    // 359
    // (int にキャストしなかった場合:+3.590000e+002)
}
  • JSONデータ内に検索結果として格納されている、ヒット件数の値を取り出しています
  • 整数型はすべてfloat64として格納されているので、取得後にintにキャストする操作を行っている

Jasonライブラリ

  • JasonというGitHub上で個人開発されているパッケージ
  • GoのJSONライブラリの中では比較的なメジャーなもの
  • 入れ子のオブジェクトを辿るためのインタフェイスも提供されていて、Jason を利用しない場合、型アサートを繰り返し記述することになりますが、これを簡略化して実装できる
package main

import (
    "github.com/antonholmquist/jason"
    "net/http"
)

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    v, err := jason.NewObjectFromReader(resp.Body)
    if err != nil {
        panic(err)
    }

    n, err := v.GetInt64("hits", "total")
    if err != nil {
        panic(err)
    }
    println(n)
}

参考URL

DecoderとUnmarshalの使い分け

  • データがio.Readerストリームからのものである場合、またはデータストリームから複数の値をデコードする必要がある場合は、json.Decoderを使用します。
  • すでにJSONデータがメモリにある場合は、json.Unmarshalを使用します。

Use json.Decoder if your data is coming from an io.Reader stream, or you need to decode multiple values from a stream of data.

Use json.Unmarshal if you already have the JSON data in memory.

Amazon S3に保存されているバケット毎のオブジェクト容量、オブジェクト数を知る方法は大きく分けて下記の2つの方法がある

  • CloudWatchのメトリクスであるS3ストレージメトリクスのBucketSizeBytes、NumberOfObjectsを参照する
  • AWS CLIでS3のバケット配下のオブジェクトに対して再帰的に容量と数を集計する。
    • CloudWatchのメトリクスは自動的に集計してくれるというメリットの一方で1日1回の集計結果しか表示されないというデメリットがある
    • リアルタイムで容量やオブジェクトを知りたい場合は使用できない

www.magtranetwork.com

AWS SAMを利用してGolangなLambdaをデプロイする

AWS CLIでデプロイ(Windows環境)

$ GOOS=linux go build -o main
$ zip deployment.zip main
$ aws lambda create-function \
--region us-west-2 \
--function-name HelloFunction \
--zip-file fileb://./deployment.zip \
--runtime go1.x \
--tracing-config Mode=Active \
--role arn:aws:iam::account_id:role/role_name \
--handler main

aws lambda create-functionのオプション

https://docs.aws.amazon.com/cli/latest/reference/lambda/create-function.html

depoly方法

https://github.com/aws/aws-lambda-go https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/deploying-lambda-apps.html


AWS SAMを利用してLambdaをデプロイする(Mac環境)

  • template.ymlのroleは指定する

テンプレート

$ cat template.yml

AWSTemplateFormatVersion: "2010-09-09"
Transform: 'AWS::Serverless-2016-10-31'
Resources:
  App:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: lambda-go-sample # ファイル名
      Runtime: go1.x
      CodeUri: build # ビルドファイルの設置ディレクトリを設定
      Role: arn:aws:iam::account_id:role/role_name # Roleを設定する
      Timeout: 1

デプロイ

$ GOARCH=amd64 GOOS=linux go build -o build/lambda-go-sample

$ aws cloudformation package \
    --template-file template.yml \
    --s3-bucket <bucket-name> \
    --s3-prefix lambda-go-sample \
    --output-template-file .template.yml

$ aws cloudformation deploy \
    --template-file .template.yml \
    --stack-name lambda-go-sample \
    --capabilities CAPABILITY_IAM

実行結果

$ aws cloudformation describe-stack-resources --stack-name lambda-go-sample

実行

$ aws lambda invoke --function-name lambda-go-sample-App-xxxx --payload '"Lambda"' out.txt

https://qiita.com/ikeisuke/items/3c0c422888ae8ae09831

Docker compose

  • npmコマンドが使用できる必要あり
  • docker-composeコマンドが使用できる必要あり

docker-compose.ymlファイルとは

docker-compose.ymlファイルは以下のようにyaml形式でDockerコンテナに関する起動オプション(buildオプションも含まれることもある)を記述したファイル

docker-compose.ymlサンプル

web:
  build: .
  ports:
    - "5000:5000"
  volumes:
    - .:/code
  links:
    - redis
  redis:
    image: redis

# yaml の記載方法については下記を参照
# https://docs.docker.com/compose/compose-file/

Docker Compose概要

Docker composeとは複数のコンテナから成るサービスを構築・実行する手順を自動的にし、管理を用意にする機能 Docker composeでは、composeファイルを用意してコマンドを1回実行することで、そのファイルから設定を読み込んで全てのコンテナサービスを起動することができる

Docker Composeを使うまでの主なステップ

  • それぞれのコンテナのDockerfieを作成する(既にあるイメージを使う場合は不要)
  • docker-compose.ymlを作成し、それぞれ独立したコンテナの起動定義を行う(場合によっては構築定義も含まれる)
  • docker-compose upコマンドを実行してdocker-compose.ymlで定義したコンテナを開始する

動作確認を行う環境について

(Working dir)
+- docker-compose.yml
+- app-server/
    +- Dockerfile
    +- src/
        +- app.js

Docker composeを使用した簡単なサンプル

Docker composeを使用した複数コンテナでひとつのサービスを作成するサンプルを実施してみる redisを使用してアクセス数をカウントする簡単なアプリケーションを作成してみる

application側の作成

アプリケーション側はnode.jsを使用する nodeの公式レポジトリを使用するための、Dockerfileは以下のように簡単なもの

app-server/Dockerfile

FROM node:5
RUN npm -g install redis
ENV NODE_APTH /usr/local/lib/mode_modules

ENTRYPOINT ["node", "app.js"]

npmはnode.jsのpackageを管理するためのツール ENTRYPOINT ["node", "app.js"]は nodeコマンドの引数にapp.jsを渡している

Redis側の作成

Redis側のコンテナの作成を行うが、公式のRedisイメージを使用するため、Dockerfileは不要

Docker composeファイルの作成

アプリ側のnodeのアプリケーションとredis側のアプリケーションをbuild,runするための定義をdocker-coposeファイルに記載する

docker-compose.yml

nodeapp:
  build: "./app-server"
  container_name: "nodeapp"
  working_dir: "/usr/src/app"
  ports:
   - "10080:10080"
  volumes:
   - "$PWD/app-server/src:/usr/src/app"
  links:
   - "noderedis"
noderedis:
  image: "redis:3"
  container_name: "noderedis"

nodeのアプリケーションが乗るコンテナは、Dockerfileからイメージをbuildしてそこからnodeappという名前のコンテナを起動する redisが乗るコンテナについては、DockerfileからイメージをせずDocker hubからpullしてきた公式イメージを利用し、そこからnoderedisという名前のコンテナを起動する

docker-compose.ymlファイルのあるディレクトリ移動し、docker-compose upコマンドを実行する

$ docker-compose up

動作確認

$ curl http://localhost:10080
You accessed here 1 times.
$ curl http://localhost:10080
You accessed here 2 times.

docker-composeコマンドで作成されるイメージ名

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
wk_nodeapp          latest              c36a03206379        15 minutes ago      649MB
redis               3                   256639e384de        8 weeks ago         99.7MB
node                5                   12b4a63115bc        17 months ago       648MB

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
9fa382e87f70        wk_nodeapp          "node app.js"            15 minutes ago      Up 15 minutes       0.0.0.0:10080->10080/tcp   nodeapp
dba91dafd516        redis:3             "docker-entrypoint.s…"   15 minutes ago      Up 15 minutes       6379/tcp                   noderedis

docker-compose.yml

nodeapp:                            # <- サービス名
  build: "./app-server"             # <- Dockerfile のあるファイルの場所(Dockerfile のある場所。git リポジトリのURL も指定可能)
  container_name: "nodeapp"         # <- コンテナ名。指定しなかった場合はDocker compose で勝手に決められる
  working_dir: "/usr/src/app"       # <- コンテナ内のワーキングディレクトリ。docker run コマンドの-w/--workdir に相当
  ports:                            # <- Expose するポート。docker run コマンドの-p/--publish に相当
   - "10080:10080"
  volumes:                          # <- Bind mount するディレクトリ。volume。docker run コマンドの-v/--volume に相当
   - "$PWD/app-server/src:/usr/src/app"
  links:                            # <- 他のコンテナと接続するときのコンテナ名。docker run コマンドの--link に相当
   - "noderedis"
noderedis:
  image: "redis:3"                  # <- イメージIDとtag
  container_name: "noderedis"

参考URL https://qiita.com/TsutomuNakamura/items/7e90e5efb36601c5bc8a