production.log

株式会社リブセンスでエンジニアをやっている星直史のブログです。

AWS IAM ユーザー各自のMFA設定ポリシーの登録

概要

AWS IAMでは、特定のグループにポリシー(権限)を設定し、そのグループにユーザーを追加することで、権限管理が容易になります。
設定できるポリシーは、AWSが用意しているポリシーと、独自に設定できるポリシーの2パターンあります。
今回は、AWS IAM ユーザー各自のMFA設定ポリシーの登録の方法について書いていきます。

背景

AWSのユーザー登録を行う場合、基本的にはIAMユーザーを作成し、操作を行うことになります。
この場合、権限が高ければ高いほど、コア機能の変更ができる権限を付与する設計になることが多いです。
また、開発組織が少なければ少ないほど、エンジニア一人あたりの権限の範囲が広くなりやすいです。
そのため、もし認証情報が漏れた場合の影響が計り知れないため、セキュリティ対策としてMFAを設定してもらうことになります。

先述の通り、AWS IAMのポリシーは、AWSが用意しているポリシーと、独自に設定できるポリシーがあるのですが、MFA設定に限定されたポリシーは、AWSが用意していないため独自に作成する必要があります。
Snapmart(およびPIXTA)の開発でも、このMFA設定を行うのですが、毎度毎度「あれ、どう書くんだっけ?」となってしまうため、備忘録として残しておきたいと思い、書きました。

設定

ポリシーの設定は下記の通りです。
下記JSONのうち、AWS_ACCOUNT_NOは、ご自分のAWSアカウントの番号をハイフンを除外した数値となります。(012345678901234みたいな数値)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:ListUsers"
            ],
            "Resource": [
                "arn:aws:iam::AWS_ACCOUNT_NO:user/"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:ListVirtualMFADevices"
            ],
            "Resource": [
                "arn:aws:iam::AWS_ACCOUNT_NO:mfa/"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:EnableMFADevice",
                "iam:DeactivateMFADevice",
                "iam:ResyncMFADevice",
                "iam:ListMFADevices"
            ],
            "Resource": [
                "arn:aws:iam::AWS_ACCOUNT_NO:user/${aws:username}"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:DeleteVirtualMFADevice",
                "iam:CreateVirtualMFADevice"
            ],
            "Resource": [
                "arn:aws:iam::AWS_ACCOUNT_NO:mfa/${aws:username}"
            ]
        }
    ]
}

SnapmartにPull Pandaを導入しました

概要

6月18日(火)にGitHubがPull Pandaを買収したと話題になったので、早速Snapmartにも導入しました。
今回は簡単にPull Pandaの紹介をします。

Pull Pandaとは

pullpanda.com

Pull Pandaとは、GitHubのPull Requestにおいて下記の機能を提供しています。

  • PRのリマインド
  • PRの分析
  • レビュアーの自動アサイン

PRのリマインド

PRのリマインドはSlackと連携し、特定のチャンネルや、DMで通知を行うことができます。
また、時間、曜日、フィルターの設定もできます。
設定は画面はこんな感じです。

特定のチャンネル宛てへのリマインド設定

DM設定

PRの分析

PRの分析は下記の画面が用意されています。

  • Review turnaround
  • Reviewer workload
  • Open PRs
  • PR merge time
  • PR throughput
  • PR size
  • Code churn

Code churn画面

これらは時間の経過と共に価値が上がる情報だと思うので、導入したタイミングの数値がどうこうというよりは、定期的に振り返って確認して整理するといった使い方になるのかと思います。

レビュアーの自動アサイン

レビュアーの自動アサインは、GitHub Appsで提供されます。
設定は、アプリをインストールし、自動アサインを有効化したいリポジトリを選択するだけです。

レビュアーの自動アサイン

所感

レビュアー自動アサインと、PRリマインドのReal-time messagesやDMの設定により、下記やりとりがbotで解消できそうです。人間による温かみのあるメッセージのやりとりがなくなるのは非常に残念です

  • レビューお願いします
  • レビューしました
  • lgtm!!

分析については、導入直後なので正直そこまで使用感がわからなかったのですが、少し時間を置いて見てみようと思います。

Linuxでduコマンドとdfコマンドを使って空き容量を増やす方法

概要

サービスを運用していると、サーバー内のディスク空き容量が枯渇してしまうことがしばしばあると思います。
以前AWS EC2のルートボリューム(EBS)をダウンタイム0で拡張する方法について書きましたが、対応の一つとして格納されているファイルを整理することで空き容量を増やすことも可能です。
今回は、Linuxでduコマンドとdfコマンドを使って空き容量を増やす方法について書きます。

コマンドの説明

dfコマンドとは

dfコマンドとは、サーバー内で使用中のディスクの量と使用可能量をファイルシステム毎に表示するコマンドです。
使用できるオプションは様々ですが、基本的にはdf -hで事足ります。

実行すると下記の通り、ファイルシステム、全体のサイズ、使用量、空き容量などが出力されます。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
devtmpfs         1.9G   56K  1.9G    1% /dev
tmpfs            1.9G     0  1.9G    0% /dev/shm
/dev/nvme0n1p1    32G   22G  9.7G   70% /

duコマンドとは

duコマンドとは、ディレクトリやファイル毎のディスク使用状況を出力するコマンドです。 実行すると下記の通り、ディレクトリやファイル毎にディスクの使用状況が出力されます。

$ du -sch ./*
5.8G    ./OUTPUT_DATA
76K ./OUTPUT_TABLE
12K ./bin
4.0K    ./date.txt
4.0K    ./nohup.out
32K ./test.log
4.0K    ./tmp
5.8G    合計

ディスク容量の整理の仕方

dfコマンドとduコマンドを使い方としては

  • dfコマンド: サーバー内で使用しているファイルシステム全体の使用状況を把握
  • duコマンド: 個別のディレクトリ、ファイルの使用状況を把握

といったように、duコマンドで狙いを定めて、容量を圧迫しているファイルを特定していくという使い方になります。

先述したduコマンドの結果をみると、OUTPUT_DATAというファイルが(カレントディレクトリに おいては)巨大なファイルであることがあります。
また、圧迫しているものがディレクトリの場合は、さらにduコマンドで深掘りし、ファイルを特定していくことで整理をしていきます。

ReactNative + Expo製アプリをiOS / Android(Genymotion)エミュレーターで動作させる手順

概要

業務やプライベートでReactNative + Expo製アプリをpullしてきた場合、コードを読むだけではなく、サクッとエミュレーターで動かしたいとことが多くあると思います。
今回は、ReactNative + Expo製アプリをiOS / Android(Genymotion)エミュレーターで動作させる手順を紹介します。
iOSのエミュレーターはXcodeを使用し、AndroidはGenymotionを使用していきます。

XcodeとGenymotionとは

Xcodeとは

XcodeとはApple社が開発している、アプリ開発ツールです。
Xcodeではエディタ、エミュレータ、リリースまで行うことができます。
ReactNative + Expo製アプリにおいては、Xcodeが内包しているエミュレーターを使用するといった具合です。

Genymotionとは

Genymotionとは"爆速で動作するAndroidエミュレーター"です。
Android StudioのAVDと比較すると起動や動作が軽快です。 また、エミュレーターとして動作するデバイスのインストールと設定が非常にシンプルです。 ホストPCのリソース割り当ての設定もできます。

ツール類のインストール

iOS(Xcode)

XcodeはApp StoreからDLするだけです。

Android(Genymotion)

GenymotionはVirtualBoxGenymotionをインストールしていきます。 ダウンロードはそれぞれ下記サイトから行います。

デバイスのインストール

VirtualBoxGenymotionのインストールが完了したら、Genymotionを起動し、エミュレートするデバイスをインストールします。

Genymotionのデバイスインストール

サイドバーのForm factorとAndroid APIを選択し、任意のデバイスをInstallします。 割り当てるリソースについては、Detailsから設定を行います。

動作確認

ターミナルからexpo startを実行し、画面にQRコードが表示されたら下記キーを押下すると、それぞれエミューレーターが立ち上がります。
Androidの場合はエミュレーター起動後でなければ実行ができないことに注意が必要です。

  • a: Androidエミュレーターの起動
  • i: iOSエミュレーターの起動

SnapmartのWebサーバーにmonitを導入してunicornを不死鳥にした

概要

Webサービスを運営していると、夜中にWebサーバーのプロセスが突然死してしまい、朝に気づくということが多々あります。
流石に人間が24時間365日監視しているのはツラいので、ある特定のプロセスが落ちたら自動で検知と再起動という処理を行いたいです。
今回はSnapmartのWebサーバーにmonitを導入してunicornを不死鳥にしたので、その導入について書きます。

monitとは

冒頭でも述べましたが、ざっくりいうとmonitはサーバーのある特定のプロセスを監視し、プロセスがなくなったことを検知、自動再起動を行うOSSです。 monitが監視する条件に合致した場合に再起動を行うだけなので、原因の根本解決などは人間が行う必要があります。

導入手順

手順は大きく4つあります。

  1. monitをinstall
  2. unicornの起動スクリプトを書く
  3. unicorn用のmonitの設定を記述
  4. monitの再起動と動作確認

monitをinstall

monitはyumでinstallすることができます。

sudo yum install monit

unicornの起動スクリプトを書く

monitが自動で再起動させるためのスクリプトを書きます。 このスクリプトは/etc/init.d/unicornに配置します。 スクリプトの内容は下記の通りです。

#!/bin/sh
NAME="Unicorn"
ENV=production

RAILS_DIR="/awesome_your_app"
PID="${RAILS_DIR}/log/unicorn/unicorn.pid"
CONF="${RAILS_DIR}/config/unicorn.rb"

start()
{
    if [ -e $PID ]; then
        echo "already started"
        exit 1
    fi
    echo "start unicorn"
    su - ec2-user -c "cd ${RAILS_DIR} && bundle exec unicorn_rails -c ${CONF} -E ${ENV} -D"
}
stop()
{
    if [ ! -e $PID ]; then
        echo "${NAME} already stopped"
        exit 1
    fi
    echo "stop ${NAME}"
    kill -QUIT `cat ${PID}`
}

restart()
{
    stop
    sleep 3
    start
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        restart
        ;;
    *)
    echo "Syntax Error"
    exit 2
    ;;
esac

上記のファイルを作成すると、/etc/init.d/unicorn start|stop|restartを行うことができます。 これを用いてmonitがプロセスの再起動を行うわけです。
※併せてchkconfigを設定するのも良いと思います。今回は割愛

unicorn用のmonitの設定を記述

monit.conf

まずmonit自体の設定を行います。設定ファイルは/etc/monit.confです。
こちらのファイルで重要なのは、Include /etc/monit.d/*.monitrcです。 これは、/etc/monit.d/配下にある*.monitrcをIncludeする設定です。この設定を行い、特定のモジュールに限定した設定ファイルを用意することで、管理が容易になります。

set daemon 60
set mailserver localhost
set eventqueue
    basedir /var/monit
    slots 100
set logfile syslog
Include /etc/monit.d/*.monitrc
Include /etc/monit.d/logging
set httpd port 2812 and use the address localhost
  allow localhost

/etc/monit.d/unicorn.monitrc

続いて、monitのunicorn監視用の設定を書いていきます。

check process unicorn with pidfile /awesome_your_app/log/unicorn/unicorn.pid
  group ec2-user
  start program = "/etc/init.d/unicorn restart"
  stop program = "/etc/init.d/unicorn stop"
  if 5 restarts within 5 cycles then unmonitor

設定はとてもシンプルです。

  • start, stopをどのgroupで、どのコマンドを実行するか
  • 何サイクルで何度失敗したら監視をやめるかを設定しています。

monitの再起動と動作確認

monitの再起動

これまでの設定を再読み込みしたいので、monitを再起動します。

sudo monit reload

動作確認

最後に動作確認をします。

  • monitがunicornプロセスを監視しているのか
  • unicornプロセスが落ちた場合に自動で再起動するか

上記2点が満たされれば良しとします。

monitがunicornプロセスを監視しているのか

monitの状況はsudo monit statusコマンドで確認できます。

Process 'unicorn'
  status                            running
  monitoring status                 monitored
  pid                               3941
  parent pid                        1
  uptime                            5h 0m 
  children                          5
  memory kilobytes                  204080
  memory kilobytes total            1670284
  memory percent                    5.3%
  memory percent total              44.0%
  cpu percent                       0.0%
  cpu percent total                 0.0%
  data collected                    Thu May 23 04:08:47 2019

バッチリですね!

unicornプロセスが落ちた場合に自動で再起動するか

monitのログを確認するために、sudo tail -f /var/log/monitでログを見守ります。 別のプロセスで/etc/init.d/unicorn stopを行います。

[UTC May 23 00:59:41] error    : 'unicorn' process is not running
[UTC May 23 00:59:41] info     : 'unicorn' trying to restart
[UTC May 23 00:59:41] info     : 'unicorn' start: /etc/init.d/unicorn
[UTC May 23 01:00:42] info     : 'unicorn' process is running with pid 20241

1分ほどすると上記のようにログが出てきます。

  • unicorn プロセスが実行されていないことを検知
  • unicorn プロセスを起動させようと試みる
  • /etc/init.d/unicornを実行
  • PID: 20241でunicornが起動

といった具合で自動で起動されました。バッチリですね!

まとめ

今回は、monitを導入してunicornプロセスが落ちた場合に自動で再起動されるようにしました。
unicornだけではなく、nginxやpostfixといったミドルウェアが謎の変死を遂げるのであれば、monitの導入を考えても良いでしょう。

また、monitを導入すると、導入の背景にあった人間が24時間365日監視を行うということから解放されます。
心配が一つ減るのは良いですね。

ただ、monitで再起動がかかるのは良いのですが、根本の原因は解決しなければなりません。
運用の負担はどんどん減らしていきましょう。

特定の名前のEC2のPublicIPを取得する方法

概要

TV放映などで、一時的にスケールアップさせたい時に、オートスケールの機構がなければ職人による技が光ります。
今回は、スポットリクエストで増やしたEC2インスタンスのIPをAWS ESS access policyに登録したかったので、IPを取得する必要がありました。

コマンド

aws ec2 describe-instances --filter "Name=tag:Name,Values=hogehoge" | grep ASSOCIATION | sort | uniq | awk '{print ""$4""}'

MySQL Staging環境のDBをdumpしてローカル環境のMySQLにimportする方法

概要

Snapmart社のstagingのDB環境は、AWS RDSを使用しています。

毎日24時にproduction環境のスナップショットからstaging用DBを作成し、update文で個人情報やメールアドレスなどをマスクした状態にしています。
また、stagingが配置されているVPCは社内ネットワークからしかアクセスできないようしています。
そのため、自宅からリモートで作業を行いたい場合、stagingのDBに接続できず思うように開発ができません。 *1

今回は、手っ取り早くローカルのmysqlにデータをdump/importするための手順を記します。

準備するもの・こと

AWS RDS staging用DBのデータ

AWS RDS staging用DBのデータをダンプします。 ダンプするデータは、特定のデータベースのテーブル定義データと、テーブルの中身のデータの二つに分けて取得します。

mysqldump -uhoge -phoge -hhoge.com -d -n > OUTPUT_TABLE 
mysqldump -uhoge -phoge -hhoge.com -t table_name > OUTPUT_DATA

そして、ダンプしたファイルをscpでローカルにもってきます。

元々のDBの削除と作成

めんどくさいのでRails経由で行います。

bundle exec rake db:drop RAILS_ENV=development
bundle exec rake db:create RAILS_ENV=development

import

データ部をimportする際に-fはエラーを無視してimportするぜ!というオプションです。 duplicate entyエラーが出てしまっていたのですが、細かいことは気にしないので、スルーすることにしました。

sql_local < ~/Downloads/OUTPUT_TABLE
sql_local -f < ~/Downloads/OUTPUT_DATA

追記 201903201343 メモ

DDLとデータで分けたりせずにdump実行すれば、dumpファイルに

DROP TABLE IF EXISTS XXXTable

がつくので、ローカルDBの初期化処理は必要ない。

*1:VPN使えば良いって話はありますが

vimでsnake_caseをCamelCaseに置換する

RailsとJavaScriptを同時に扱っていると*1、snake_caseからCamelCaseに置換したい時がしばしば訪れます。 一つずつ修正した方が、人間の温かみを感じることができるコードになると思うのですが、置換の方法も知った方が良いと思ったので、メモとして残しておきます。

:s/_\(.\)/\u\1/g

*1:Style Guideで記法が異なる言語を同時に扱っている場合

よちよちReactNative #1 を開催しました!

概要

ReactNativeを使い始めたものの、基本的に参考になる記事が少なくて詰みかけてしまったり、英語の記事がメイン(これはまだ良いけど)であったり、参考書が少ないので、初心者にはだいぶハードルが高いなぁと感じていました。 写真者同士で助け合ったり、気軽に質問できる場があったら良いなぁ〜と思い、よちよちReactNativeを開催することにしました!

どんな感じだったか

参加予定者が6名だったので結構集まった感あったのですが、土曜日の10時スタートというストイックな時間設定にしてしまったので、自分含めて4名での開催となりました!もくもく会は遊びじゃねぇんだよ!

イベント開催(connpassで人集め ~ 会場作り ~ 当日のアレコレ)が初めてだったので、良い体験になりました。 また、もくもく会にしたので、運営の手間とか難しさがほぼなく、ワンオペでもなんとかなることがわかりました!(寝坊で爆死しない限りなんとかなる!)

参加者のみなさんは、プログラムを書きながら実況もする訓練されたエンジニアが多かったので、Twitterを追ったりするのも楽しかったです。

togetter.com

振り返り

継続的に改善していくために、KPT(K: 良かったこと/続けていきたいこと、P: 改善点、T: 次に取り組むこと)で振り返りをしていきます!

Keep

  • connpass使って初めてイベント開催したので、使い方とか流れが完全に理解できた
  • 会社のエアコンにタイマー機能があることを知り、開始2時間前に起動するように設定したから、朝のんびりできた
  • 最大の難関である起床に成功した。
  • 意外と準備に手間がかからなかった(ワンオペでもそれなりにいけることがわかった)
  • 社内メンバーの助け舟を借りて、会社までの到着マニュアルを公開することができた
  • いつもカッチリキッチリ、台本用意したりして頑張るんだけど、ゆるっっと司会したりするのも良かった。
  • 楽しかった!

Problem

  • 会社の入り方というか、裏口がわかりにくい。
  • 裏口は鍵がかかっているので、開けに行かなければならない。
  • 土曜日の10時は気合いを入れすぎた?
  • 個人的に進捗が良くなかった...。(ディスプレイが無いなかで戦うのはキツかった)
  • 若干寒かったような気がする?
  • 会場の机の大きさが一人ひとつが限界っぽい?
  • イベントの目的(初心者の互助の場)が達成されたかは謎
  • ご飯行きましょう!って言うの忘れた...。

Try

  • 会社到着までのマニュアルを事前に公開しておく
  • 空調の設定温度が暖房25℃だったので、26℃に設定する。
  • ご飯行きましょう!って言う。もしくは、ご飯行く枠といかない枠を用意する
  • 自分は比較的経験者寄りなので、極力サポートしていく

まとめ

小規模だし、運営者として何をしたってわけではないのですが、人を集めてイベント開催することの楽しさがわかりました! 発表するようになったり、イベントで飲食したりするようになると、もっと楽しくなるんだろうなぁ〜と思いました!

ycyc-react-native.connpass.com

Expo + React Native + TypeScript環境を作る手順まとめ

概要

React Nativeによるアプリ開発では、Expoを使うと中々捗るのですが、*1
2018年11月3日にExpo SDKがv31.0.0にバージョンアップしました。 blog.expo.io

このリリースの大きなポイントとしては、2018年9月にリリースされたReact Native 0.57に対応したことです。 React Nativeはバージョン0.57でTypeScriptをサポートしました。 そのため、Expoを使ってもTypeScriptでの開発ができるようになります。

今回はExpo + React Native環境をTypeScript化するための手順をまとめます。

手順

プロジェクトを作る

$ expo init typescript-project
$ cd typescript-project

TypeScriptのインストールと設定

TypeScriptを使用するのでbabelは削除します。

$ rm babel.config.js
$ yarn remove babel-preset-expo

インストール

続いて、TypeScript関連のモジュールをインストールします。

yarn add typescript tslint @types/expo @types/react @types/react-native
yarn add -D tslint

tsconfig.json の設定

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es2015", "esnext.asynciterable"],
    "jsx": "react-native",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "types": []
  }
  "include": [                                                                                                            
    "./App.tsx"                                                                                                            
  ],                                                                                                                       
  "exclude": [                                                                                                             
    "node_modules"                                                                                                         
  ]
}

tslint.json の設定

{
  "extends": [
    "tslint:recommended"
  ],
  "rules": {
    "no-any": true,
    "prefer-array-literal": [true, { "allow-type-parameters": true }],
    "variable-name": [true, "ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"]
  }
}

以上でインストールと設定は終わりです

App.jsをTypeScript化

単純に拡張子を.tsxにするだけでTypeScript化ができます。簡単ですね。
jsxを含まない場合は、.tsとします。

$ mv App.js App.tsx

動作確認

TypeScriptの恩恵を受ける

App.jsをApp.tsxにTypeScript化したので、わざとコンパイルエラーとなるような修正を入れて、tslintやtscで怒られてみましょう。
App.tsxを開き、下記のコードを追加します。

...省略
const a: number = 1;                                                                                                       
const b: string = a  
...省略

定数bへの代入している行ではセミコロンが抜けているため、tslintで怒られるはずです。 文字列型の定数bに数値型の定数aを代入しようとしているため、コンパイルエラーになるはずです。

$ yarn tslint App.tsx

ERROR: App.tsx[5, 20]: Missing semicolon

予想通りの結果です!

$ yarn tsc -p ./ --noEmitOnError
App.tsx:5:7 - error TS2322: Type 'number' is not assignable to type 'string'.

5 const b: string = a
        ~

App.tsx:5:7 - error TS6133: 'b' is declared but its value is never read.

5 const b: string = a
        ~

Found 2 errors.

error Command failed with exit code 1.

こちらは、型が異なるも代入でfailしていることに加え、定数bを使用していないこともわかりました。 *2

シミュレーターの起動

最後にApp.tsxがこれまで通り動作するかを確認します。

$ yarn start

f:id:watasihasitujidesu:20181209150433p:plain

問題なく動作しましたね!

まとめ

今回はExpo + React Nativeの環境にTypeSCriptを導入する手順について書きました。 これまで通り(?)TypeScriptのinstallと設定を書き、.jsを.tsxに変更するだけでTypeScriptの恩恵を受けることができました。

TypeScriptの導入とは話が別ですが、Expo SDKがReact Nativeを追従する速さには驚きですね。 9月のリリースに対して11月に対応するのは素晴らしいです。 React Natibe のブログExpoのブログは要チェックですね。

*1:詳細は割愛

*2:--noEmitOnErrorオプションをつけることで、コンパイルエラーがあった場合に.jsファイルを生成しないようにしています。

Go言語でパッケージを作る方法

概要

Goでスクリプトを書いていると、巨大なmain関数ができてしまったり、1つのファイルに色々な関数がごちゃまぜになることはよくあります。 そこで、今回はGo言語のパッケージを作る方法について紹介します。

元々のスクリプト

下記のスクリプトをパッケージ化していくことを想定して説明します。

package main

import "fmt"

func foo(num int) int{
  return num * num
}

func main(){
  fmt.Println(foo(5))
}

main関数からfoo関数を呼んでいます。
foo関数は引数でint型を受け取り、引数を引数で除算した値を戻り値とします。

foo関数のパッケージ化

フォルダ構成の変更

まず、パッケージ化するために新たにフォルダを作成します。

mkdir sample

作成したフォルダにfoo.goを配置します。
この時点でのフォルダ構成は下記の通りです。

.
├── main.go
└── sample
    └── foo.go

foo.goの修正

配置したfoo.goの内容は下記の通りです。

package sample // 1

func Foo(num int) int{ // 2
  return num * num
}

パッケージ化するには、パッケージ化対象のファイルの先頭にpackage {パッケージ名}とします。
今回は、sampleフォルダを作っているので、パッケージ名をsampleにします。

また、関数名は大文字から始めます。
これはGo言語の仕様で、名前の先頭一文字が大文字になっていれば、パッケージの利用者側から参照可能となるためです。

main関数の修正

main関数は下記の通りに修正します。

package main

import (
  "fmt"
  "./sample" // 1
)

func main(){
  fmt.Println(sample.Foo(5)) // 2
}

sampleパッケージを作成したので、それを参照しなければなりません。
パッケージの参照はimport文の中で指定します。参照先は相対パスになります。

最後に、foo.goで定義した関数を呼び出します。
呼び出し方はパッケージ名.関数名となります。今回だとsample.Foo(5)です。

まとめ

今回はGo言語のパッケージについて紹介しました。

Go言語のパッケージ化は下記を行うだけです。

  • パッケージ化したい関数などを別フォルダの別ファイルとして定義し、関数名を大文字にする
  • パッケージ化したものをimportし、package名.関数名で呼び出す

パッケージ化することにより、あるまとまった機能単位に分割ができます。
何も考えずに書いてしまうと、ついmain関数が肥大化してしまうので注意が必要です。

TreasureDataのCLIで各テーブルのスキーマを取得するためのワンライナー

概要

TreasureDataを運用していて、存在するDBとその中のテーブルのスキーマを一覧で取得したいことがあったので調べて見ました。 GUI上からDBとテーブルを一つずつ確認するのはさすがにツラいのでサクッと見れる方法はこちらです。

td db:list -f tsv | awk '{ print ""$1""}' | grep -v Name | awk '{ print "echo \""$1"\" ;td db:show "$1" -f tsv; echo \"\\n\""; }'|sh

ワンライナーのコマンド解説

やっていることはとても簡単で、td db:listでDBのリストを出力し、そのDBリストを元にtd db:showでスキーマを確認
といったことをやっています。

LocalStackを使用してGoで書いたLambdaからDynamoDBを呼び出す方法

概要

以前の記事でLocalStackをインストールしました。 今回はLocalStackを使用してGoで書いたLambdaからDynamoDBを呼び出す方法を紹介します。

DynamoDBの操作

LocalStackはAWSのマネジメントコンソールと違い、各サービスに対しての操作はCLIから行います。 まずは、DynamoDBに対してテーブルとデータを登録します。

テーブル作成

LocalStackを立ち上げた直後はテーブルが存在しません。

$ aws --endpoint-url=http://localhost:4569 dynamodb list-tables
{
    "TableNames": []
}

テーブル生成コマンドは下記の通りです。 このコマンドでは、「Nameという文字列のカラムがキー」である「testテーブル」を作成しています。

aws --endpoint-url=http://localhost:4569 dynamodb create-table \
--table-name test \
--attribute-definitions AttributeName=Name,AttributeType=S \
--key-schema AttributeName=Name,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

次にデータを1つ登録します。

aws --endpoint-url=http://localhost:4569 dynamodb put-item \
--table-name test \
--item '{ "Name": { "S": "testtest" } }'

最後に、登録したデータが取得できるかを確認します。

aws --endpoint-url=http://localhost:4569 dynamodb scan --table-name test

GoでLambdaの処理を書く

続いて、DynamoDBに登録したデータを呼び出すLambdaの処理を書きます。 今回はGo言語で書きます。

Go言語の環境構築

環境構築はbrewで済ませることができます。

brew install go

続いて、Lambda関数で使用するパッケージをインストールしていきます。

go get -u github.com/aws/aws-sdk-go/aws \
  github.com/aws/aws-sdk-go/aws/session \
  github.com/aws/aws-sdk-go/aws/credentials \
  github.com/aws/aws-lambda-go/lambda \
  github.com/guregu/dynamo 

DynamoDBへの操作はSDKをそのまま使う操作がだいぶ辛いです。 そこで、DynamoDBへの操作を容易に行えるgithub.com/guregu/dynamoを使います。
具体的にどれだけ楽になるかを同じデータ更新処理で比較してみます。

SDKの場合

param := &dynamodb.UpdateItemInput{
    TableName: aws.String("TableName"),
    Key: map[string]*dynamodb.AttributeValue{
      "id": {
        N: aws.String("123"),
      },
    },
    ExpressionAttributeNames: map[string]*string{
      "#username": aws.String("username"),
    },
    ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
      ":username_value": {
        S: aws.String("hoge"),
      },
    },
    UpdateExpression: aws.String("set #username = :username_value"),
    ReturnConsumedCapacity: aws.String("NONE"),
    ReturnItemCollectionMetrics: aws.String("NONE"),
    ReturnValues: aws.String("NONE"),
  }

  resp, err := ddb.UpdateItem(param)

guregu/dynamoの場合

type User struct {
  id       int `dynamo:"id"`
  username string
}

db := dynamo.New(session.New(), &aws.Config{
  Region: aws.String("us-east-1"),
})

table := db.Table("TableName")
user  := User{id: 123, username: "hoge"}
err   := table.Put(user).Run()

guregu/dynamoを使用した方が簡潔に書けますね!

処理を書く

先述のguregu/dynamoを使ってDynamoDBに接続する処理を書きます。 ポイントは、エンドポイントをLocalStackのエンドポイントに変更することです。

package main

import (
  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/aws/credentials"
  "github.com/aws/aws-lambda-go/lambda"
  "github.com/guregu/dynamo"
)

type Event struct {
  Name string `json:"name"`
}

type Response struct {
  Result string `json:"Name:"`
}


type Test struct {
  Name string `dynamo:"Name"`
}

func handler(event Event) (Response, error) {
  conf := &aws.Config{
    Credentials: credentials.NewStaticCredentials("dumy", "dumy", ""),
    Region:      "us-east-1",
    Endpoint:    "http://localhost:4569",
  }
  sess, err := session.NewSession(conf)

  db    := dynamo.New(sess)
  table := db.Table("test")

  var result Test
  err = table.Get("Name", "testtest").One(&result)

  println(result.Name)

  return Response{Result: result.Name}, err
}
func main(){
  //handler(Event{Name: "testtest"})
  lambda.Start(handler)
}

デプロイパッケージの作成

スクリプトを書いたあとは、デプロイパッケージの作成をしてLambdaにデプロイします。

macOSを使用している場合は、ハンドラ関数がLambda実行コンテキストと互換性を持たせなければなりません。そのため、コンパイルするときにLinux用のGOOS*1環境変数を設定する必要があります。

GOOS=linux GOARCH=amd64 go build -o handler
zip handler.zip ./handler

Lambdaへのデプロイは下記コマンドです。

# 新規作成(コンテナ立ち上げ直後)
aws lambda create-function \
  --endpoint-url http://localhost:4574 \
  --region us-east-1 \
  --profile localstack \
  --function-name handler \
  --runtime go1.x \
  --role r1 \
  --handler handler \
  --zip-file fileb://handler.zip

# 既存ハンドラ関数の更新
aws lambda update-function-code \
  --endpoint-url http://localhost:4574 \
  --region us-east-1 \
  --profile localstack \
  --function-name handler \
  --zip-file fileb://handler.zip --publish

動作確認

デプロイまでしておきつつ、動作確認はLocalStackを経由せずに手元から直接Goを動かすのが速くて楽です。

実行は下記コマンドで行います。

go run handler.go

LocalStackに登録したLambda関数を呼び出す時の問題点

せっかくデプロイしたので、LocalStack上のLambdaを呼び出したいですよね。 実行自体は下記のコマンドで実行します。

aws lambda --endpoint-url=http://localhost:4574 invoke \
  --function-name handler \
  --payload '{ "name": "testtest"}'\
  result.log

しかし、LocalStack上のLambdaからDynamoDBに接続できない問題があります。 エンドポイントを変えずにAWSにデプロイし、実行すると問題なく接続できるので、LocalStack固有の問題なのではないかと思います。

参考までに、発生したエラーはこちらです。

HTTPConnectionPool(host='localhost', port=4574): Read timed out. (read timeout=60)
localstack_1  | START RequestId: b7879ca4-4d51-15b6-ea01-1145f4c29e0c Version: $LATEST
localstack_1  | RequestError: send request failed
localstack_1  | caused by: Post http://localhost:4569//: dial tcp 127.0.0.1:4569: connect: connection refused
localstack_1  | END RequestId: b7879ca4-4d51-15b6-ea01-1145f4c29e0c
localstack_1  | REPORT RequestId: b7879ca4-4d51-15b6-ea01-1145f4c29e0c  Duration: 51179.39 ms   Billed Duration: 51200 ms   Memory Size: 1536 MB    Max Memory Used: 9 MB
localstack_1  | {
localstack_1  |   "errorMessage": "RequestError: send request failed\ncaused by: Post http://localhost:4569//: dial tcp 127.0.0.1:4569: connect: connection refused",
localstack_1  |   "errorType": "baseError"
localstack_1  | }

まとめ

今回はGoで書いたLambdaからDynamoDBを呼び出す方法を紹介しました。 DynamoDBに接続するためのGoの処理はguregu/dynamoを使用することで、複雑な処理を書かなくて済みました。 LocalStackはCLIでしか操作できない点は難点であるものの、Lambdaを手元ですぐに実行できるのは大きなメリットだと思います。ただし、LocalStackに登録したLambda関数からDynamoDBへはうまく接続できないので、go runなどで直接goの関数を叩かなければならないという手間も発生します。*2

*1:Goオペレーティングシステム

*2:未開の地感がありますね

LocalStackのインストールからDocker Composeで動かすまでの手順

概要

以前ServerlessFrameworkでLambdaからDynamoDBを呼び出す処理を書いていました。しかしServerlessFramework単体だと修正, デプロイ, 動作確認のビルドライフサイクルが遅くなる問題があります。 今回は、その問題を解消すべく、LocalStackのインストールからDocker Composeで動かすまでの手順について紹介します。

LocalStackとは

LocalStackは、AWSの主要サービスのモックをローカル環境で動作させるツールです。

現在モックとして提供できるサービスはこちらです。

API Gateway, Kinesis, DynamoDB, DynamoDB Streams, Elasticsearch, S3, Firehose, Lambda, SNS, SQS, Redshift, ES (Elasticsearch Service), SES, Route53, CloudFormation, CloudWatch, SSM, SecretsManager

AWSのモックサービスをローカルに立てることで下記のようなメリットがあります。

  • ローカルでビルドライフサイクルを回すことができるため、コードの修正とテストの反復を高速に行える
  • AWS利用料を支払わなくて済む

インストール

Dockerのインストール

今回はLocalStackをDocker(Docker Compose)で動かします。

docs.docker.com

Docker for Macの場合、Dockerのインストールと共にDocker Composeが使用可能になります。

プロファイル設定

LocalStack用のクレデンシャル情報を作成します。アクセスキーとシークレットアクセスキーは"dummy"のような感じで問題ありません。

$ aws configure --profile localstack
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: us-east-1
Default output format [None]: text

$ cat ~/.aws/credentials
[localstack]
aws_access_key_id = dummy
aws_secret_access_key = dummy

$ cat ~/.aws/config
[profile localstack]
region = us-east-1
output = text

LocalStackのインストールと設定

LocalStackは単純にgit cloneを行うだけです。

git clone https://github.com/localstack/localstack
cd localstack

Macを使用している場合、起動時に共有パスの設定をする必要があります。具体的なエラーは下記の通りです。

ERROR: for localstack Cannot start service localstack: b'Mounts denied: The path /var/folders/dx/dnnz2ft55rn2j7s6m0mbzkl80000gn/Tis not shared from OS X and is not known to Docker.You can configure shared paths from Docker -> Preferences... -> File Sharing.See https://docs.docker.com/docker-for-mac/osxfs/#namespaces for more info..'

f:id:watasihasitujidesu:20181031084345p:plain

このエラーが出た場合はDockerマークをクリック => Preference => File Sharing => /var/foldersを設定し、Docker再起動で解決します。

また、LambdaがDocker環境で動作させるための環境変数を設定します。 .envファイルを作成し、下記の通り設定します。

#!/usr/bin/env bash
export LAMBDA_EXECUTOR=docker

編集が終わったらsource .envで環境変数を設定します。

動作確認

ブラウザ

最後Docker Composeで起動します。

docker-compose up

LocalStackの起動確認はブラウザからでも確認できます。 http://0.0.0.0:8080

aws コマンド

ブラウザからだけでなく、awsコマンドを実行した場合にLocalStackで起動したモックに対して操作ができるか確認します。

aws --endpoint-url=http://localhost:4569 dynamodb list-tables
{
    "TableNames": []
}

上記はDynamoDBのテーブル一覧を取得するコマンドですが、--endpoint-urlオプションを指定してローカルに向けています。 基本的には他のAWSサービスも--endpoint-urlを指定するだけです。

まとめ

git cloneして多少設定するだけで、AWSのモック環境がすぐに手に入りました。手軽にモック環境が手に入るのは、ローカルでスクリプトの修正と確認をするサイクルの高速化につながります。また、単純にAWS利用料金もかからないので、サクッと動作確認などしたい場合は有用なツールだと感じました。

ただ、デメリットとしてはAWS各サービスの操作は全てCLIとなります。 そのため、awsコマンドに慣れていなかったり知らない場合は、awsコマンド自体を覚えたり調べる時間がかかってしまいます。 AWSの公式ドキュメントを参照すればわかるのですが、結構な手間になると感じました。 継続的に使用する場合は、CloudFormationを使って自動化しておくと良いでしょう。

Re:VIEWで書いた文章の校正をCircleCIとtextlintでGitHubのPRに自動コメントする仕組み

概要

技術書典でRe:VIEWを使っていたものの、文章を書き、textlintを回すというCI環境を整えることができませんでした。また、このCI環境は、ブログ執筆においても有効であるため、このタイミングで構築することにしました。

今回はRe:VIEWで書いた文章の校正をCircleCIとtextlintでGitHubのPRに自動コメントする仕組みを作ったのでその紹介をします。

仕様🖋

仕様は下記の通りです。

  • Re:VIEWで書くファイルの拡張子である.reをこれをサポートすること
  • Markdownで書くファイルの拡張子である.mdをサポートすること
  • master以外のブランチにpushした場合にCircleCIでtextlintが実行されること
  • textlintで警告が出た場合は、GitHubのPRに自動でコメントすること

f:id:watasihasitujidesu:20181013193515p:plain

構築手順👨‍💻

textlintの導入

まず最初にローカルでtextlintが実行できるようにします。 textlintはnpmで入れていきます。

$ node -v
v8.11.3

$ npm -v
6.3.0

続いて、textlintのインストールと、textlintで使うルールのインストールを行います。

$ npm init -y
$ npm install --save-dev textlint textlint-rule-max-ten

上記で入れるtextlint-rule-max-tenは「一つの文で出現する読点の数をチェックする」ルールです。

次に、textlintのデフォルトでは、Re:VIEWの拡張子である.reに対応していないため、プラグインを入れる必要があります。

$ npm install --save-dev textlint-plugin-review

最後にtextlintの設定ファイルで、ルールとプラグインを有効にします。

{
  "rules": {
    "max-ten": true
  },
    "plugins": [
      "review"
    ]
}

この状態でtextlintを実行すると下記のような結果となります。

$ ./node_modules/.bin/textlint sample.re

/Users/naoshihoshi/repo/sample.re
  1:11  error  一つの文で""3つ以上使用しています  max-ten

✖ 1 problem (1 error, 0 warnings)

今回は便宜上、textlint-rule-max-tenだけの紹介ですが、そのほかにも様々なルールが存在するので、お好みで設定していくのが良いと思います。

CircleCIの設定

CircleCIは2017年夏あたりにバージョン2.0がリリースされましたので、今回はそれで作っていきます。

バージョン2.0からは.circleci/config.ymlの書き方が変わりましたが、公式のドキュメントがとてもわかりやすかったです。
個人的には、後述するconfig.ymlの紹介で全体感を見た後にドキュメントを読むとより理解が進みやすくなると思います。

circleci.com

.circleci/config.yml

バージョン2.0からはDockerが使えるようになります。また、CircleCIが用意しているイメージも各言語揃っています。 今回、GitHubへのコメントにRubyのgemを使うので、Rubyのイメージを使用します。

version: 2
jobs:
  build:
    docker:
      - image: circleci/ruby:2.4.1-node-browsers
    working_directory: ~/repo
    steps:
      - checkout
      - run:
          command: npm install
      - run:
          command: ./.circleci/review-textlint.sh

最終行で指定している./.circleci/review-textlint.shでtextlintの実行とGitHubのPRヘコメントを実行します。

textlintの実行とGitHubのPRヘコメント

まず、GitHubへの操作をするためのアクセストークンを取得し、CircleCIに設定をします。

GitHubのアクセストークンを取得

GitHubにアクセスし、Settings > Developer settings > Personal access tokensページに遷移し、Generate new tokenでアクセストークンを取得します。

このトークンは次で使うのでメモっておいてください。また、このトークンは、この画面を離れると二度と表示されなくなります。

GitHubのアクセストークンをCircleCIに設定

CircleCIにアクセスし、Settings > ${ユーザー名} > ${リポジトリ名} > Environment Variablesに遷移しAdd Variableボタンを押下します。
すると、モーダルが表示されるので、下記の通り入力します。

  • Name: GitHub_ACCESS_TOKEN
  • Value: GitHubで作ったアクセストークン

f:id:watasihasitujidesu:20181013190945p:plain

これで準備が整いました。最後にスクリプトを書いていきます。

.circleci/review-textlint.sh

#!/bin/bash

# Test if pull request
if [ "$CI_PULL_REQUEST" = "false" ] || [ -z "$CI_PULL_REQUEST" ]; then
  echo 'not pull request.'
  exit 0
fi

REMOTE_MASTER_BRANCH="remotes/origin/master"

gem install --no-document findbugs_translate_checkstyle_format checkstyle_filter-git saddler saddler-reporter-GitHub

# filter files and lint
echo "Put textlint review comments to GitHub"

declare diffFiles=$(git diff ${REMOTE_MASTER_BRANCH} --diff-filter=ACMR --name-only | grep -a '\.[re$|md$]')
echo ${diffFiles}

if [ -n "$diffFiles" ]; then
  echo ${diffFiles} | xargs ./node_modules/.bin/textlint -f checkstyle \
  | sed -e 's/\&quot;/\&apos;/g' \
  | checkstyle_filter-git diff ${REMOTE_MASTER_BRANCH} \
  | saddler report \
      --require saddler/reporter/github \
      --reporter Saddler::Reporter::Github::PullRequestReviewComment
  exit 1
fi

それでは処理をざっくり説明していきます。

GitHubのPRの存在確認
まず、GitHub上にPRがない場合はコメント先がないため、処理をスキップする条件分岐を書いています。

if [ "$CI_PULL_REQUEST" = "false" ] || [ -z "$CI_PULL_REQUEST" ]; then
  echo 'not pull request.'
  exit 0
fi

gem install
次にgem installを実行します。 今回は、GitHubへのコメントはSaddlerを使って行います。

gem install --no-document findbugs_translate_checkstyle_format checkstyle_filter-git saddler saddler-reporter-GitHub

textlint対象ファイル抽出
この処理は、textlintの対象ファイルを抽出するコマンドです。 リモートのmasterと、CircleCIで回しているリポジトリのdiffを取得し、拡張子が.reと.mdのファイルだけを抜き出しています。

declare diffFiles=$(git diff ${REMOTE_MASTER_BRANCH} --diff-filter=ACMR --name-only | grep -a '\.[re$|md$]')
echo ${diffFiles}

GitHubへコメント送信
最後に、textlintを実行した結果をXML化し、GitHubのPRへコメント送信処理を行います。
エラーがある場合は、XMLのエスケープ処理を行なった上で、GitHubへ送信します。
具体的な処理はSanddlerにお任せしています。

if [ -n "$lintResult" ]; then
  echo ${lintResult} | sed -e 's/\&quot;/\&apos;/g' \
  | checkstyle_filter-git diff ${REMOTE_MASTER_BRANCH} \
  | saddler report \
      --require saddler/reporter/GitHub \
      --reporter Saddler::Reporter::GitHub::PullRequestReviewComment
  exit 1
fi

処理は以上です。

動作確認🚨

まず、適当なリポジトリにブランチを切ります。
次に、.reか.mdファイルを作成します。ファイルの中身は読点を3つ以上含んだ文を書いておきます。

例)

読点が、3つ以上の、文にしておくと、GitHubに、コメントが、つくはず。

最後に、そのファイルをpushします。

CircleCIの確認

f:id:watasihasitujidesu:20181013192617p:plain

良い感じに落ちてますね!

GitHubのPRコメントの確認

f:id:watasihasitujidesu:20181013192716p:plain

ばっちりですね👏

まとめ✨

今回は、CircleCIとGitHubのPRを用いて、textlintを実行、コメントしてくれる環境を作りました。
執筆していたときは、都度textlintを実行していたのが本当にめんどくさかったです...。
今回構築してみて、比較的簡単に構築することができたので、これから執筆活動をする方は、まずこのCI環境を作ってから執筆することをお勧めします!