ぺい

渋谷系アドテクエンジニアの落書き

出来るエンジニアはサボる

ここ最近、実装するが人並みに出来るようになってきた。そして、いまはミニマムを作るを考えることに非常に悩まされることが多い。

どんな開発現場での文脈か

株式会社Zucksのエンジニアは、ビジネスサイドからやりたい雰囲気、またはやりたいお気持ちを感じ取って、それについてヒアリングして、どうやって作るか、そもそも作るか?というところから考え、最終的にはリリースからその後の保守まで面倒を見るというのが、普段の業務内容になります。とりあえず、実装っていうのは、この工程全体を指しています。

油断していると人間は作りすぎる

入社当初は、考慮すべきことが考慮されていないが、主な課題で、いまは、色々考慮しすぎて、設計がファットになりがち。本当に油断していると、考慮すべきでないことまで考えてしまう。
ただ、自分の場合、設計->相談->コーディングという手順を必ず踏んでいるので、ファット仕様は設計段階で指摘されるので、出来上がったものが、大ゴケするということは起きていないのが、不幸中の幸いという感じ。

出来るエンジニアはサボるのがうまい

悪いように聞こえるかもしれないけど、僕が思う出来るエンジニアは「サボる」のが非常にうまい。どうやったらそんな発想が出来るんだってくらいうまくサボる。ここで重要なのは、ただサボるのではなく、色々考慮した上で、サボるという点。最小限で作って、価値を作る。もし、問題が起きたら?は想定済みといった感じ。

やるべきことと、あったらいいよねを切り分ける。

早くサボれるようになりたい。

そして、するべきとかって問題じゃなかった問題。 tikasan.hatenablog.com

エッセンシャル思考 最少の時間で成果を最大にする を読んだけど、めちゃくちゃ良かった

とりあえず、読んでほしい

エッセンシャル思考 最少の時間で成果を最大にする

エッセンシャル思考 最少の時間で成果を最大にする

勤務先の同チームの先輩が読んでいたのをきっかけに、ポチってみたのですが、冗談抜きでめちゃくちゃ良かった。
この本の内容は、自分の中に留めておきたいことも非常に多かったので、今回わざわざ記事にしてみました。

この本を読むことで得られること

  • 本当に重要なことは何か?(家族や仕事、自分自身について)
  • 最も注力するべきことに注力するためには、どうすれば良い?

などが、色々な著名人の話を入れつつ、詳しく解説されています。自分の人生をどう過ごすか?について、考えさせられる内容となっています。

立ち止まって多くの選択肢を考えるが、本質的なものだけをやる

私達は、仕事をする上で、様々な課題に直面します。しかし、自分という人間は一人、やれる仕事の量には限界があります。全部やろうというのはやめる。何をいまやるべきなのか?それは本当に必要なのか?必要であれば、どうやってやるべき?どこまで?をしっかり考え抜く。必要そう、というものは切り捨て、本当にいま必要なことに注力し、最大の成果を出す。

全部をやると全てが中途半端になるし、思考を巡らせる暇がないので気持ちが忙しくなる

色々な仕事が飛んできて、それらを全て真っ向から受け止め続けると、「忙しい」が仕事を埋め尽くし、工夫をする余地がなくなる。これは本当に必要なことか?を考え、必要でなければ切り捨てる。

何事にもトレードオフが存在する。全部はやれない。何をやるべきかを考えて何かを捨てる

二兎追う者は一兎も得ず。仕事でも同じ。

遊びを作ることは不可欠。ストレス軽減にもなり、仕事も創造的になる

休日は徹底的に遊ぶ。休む。そして、業務時間中を最高に集中する。少し遊ぶくらいが、仕事に対するモチベーションを上げてくれる。

仕事ができる自信があるなら、目の前のチャンスを断って昼寝する勇気を持つ

これ以上、仕事を請け負うことは出来ないけど、大きなチャンスが目の前にあったとしましょう。もし、自分に本当に仕事が出来る自信があるなら、思い切って断って、休む勇気を持つ。そして、しっかり休息を取る。そして、本当に掴むべきチャンスを掴む。

睡眠は仕事の効率を高めてくれる

私達人間は、大半が1日8時間程度の睡眠取る必要があります(一定数ショートスリーパーという人達も存在しますが)。睡眠を取らずに仕事をすれば、成果を出せるというのはほとんど間違いで、望ましいのは、眠たい時にしっかり寝ることで、常に高い生産性を保つことができ、最終的には長く眠っている人の方が成果を出す。(ちなみに、寝不足は酔っ払いと同じくらい酷い状態らしい)

これだ!というもの以外はノーとする。90点ルール

様々な選択肢がある中で、「なんか必要そう」などで、採用するのはやめよう。そんなことでは、いつまでも本質に注力が出来ない。「これだ!」だけを選択することが、高い成果を出し続けることが出来る。(採用でも同じという話があった)

目的は完全に明確でなければ、人を動かすことは出来ない

目的が明確であれば、チームが向いている力の先も同じになり、大きな力を生む。逆に不明確な目的は勘違いやトラブルしか産まない。

達成が明確でないと、評価が出来ない

何を持って達成とするか?が分からないと、結果が評価出来ない。いま目標に近づいているのか、遠のいているのかが明確に見えることは、次の施策を考える上でも重要。

何をしたいの?が確定していないと意思決定が出来ない

結局、いま何がしたいのか?を明確にしなければ、強い自信を持って意思決定が出来ない。

ノーは上手に、きっぱり断る

やりたくないこと、出来ないことは、きっぱりと断る。中途半端に断ると相手に変な期待をもたせてしまうし、断るべき時に頼みを受けてしまうと、成果を出せずに相手の期待も裏切ってしまったり、自分の本来やるべきことに集中出来なかったりとお互い良いことがありません。断ったら嫌な気持ちになるかもしれないと思うなら、上手に断ろう。(別の提案投げてみるとか、適した人材紹介するとか色々ある)

やめるかやめないかを考える時は、1円も払ってない時でも、投資をするだろうか?で考える

何かに投資をしたり、何かを買った後に、失敗だったかもしれないと気づく時があります。しかし、人間は一度所有したものを手放すのが苦手な生物です。また、投資などはいつか成果が出るかもしれないと、どれだけ望みが薄いことでも、夢見がちです。そんな時は、一度、0ベースで考える。もし、同じものをもう一度買うタイミングがあったら、本当に買うだろうか?と、その時に買わないものは、恐らくさっさと手放すべきものです。

失敗は認めることで過去のものになる

あの時、失敗だったかもしれないと考えている間は、永遠に頭にそれが残り、集中を邪魔します。失敗はさっさと認めて、次に進もう。

やめてみたいことは、試しにやめてみる逆プロトタイプをすればハッキリする

何でこれやってんだろう?という疑問のある仕事や手続き。試しにやめてみる。本当に必要なら誰かが困り、催促が来るはず。

業務は複雑性は上がり続けるので、減らすことも大事

あることがきっかけで、ルールが増えたり、業務の工程が増えることがあります。それとは逆に必要でなくなったものは、減らすことも継続して行い。本当に必要なものだけを残す。

仕組み化することで、健全な状態を無意識に維持する

健康に過ごすためには、日々の努力が欠かせません。そういった努力は場当たり的に行うのではなく、毎日行うことが重要です。最終的には習慣化することで、健康を習慣にすることが出来る。

常に最悪のケースを想定する。そうしておけば、不測な事態も難なく越えられる。そのためには準備と計画には全力を注ぐ

なんとかなる。ではなく、どういう最悪がありえるのか?を想定することは大事。何かが起きたからでは遅いし、その対応にたくさんの労力を割くくらいなら、最初から計画を練って備えよう。

いま、この瞬間で最も重要なことに注力する。過去と未来は想像の中でしかない

私達の人生において、重要なことは、「いま」であり、過去や未来は想像の中にしか無い。いまこの瞬間に最も重要なことは何か?を考え行動することを続ける。

まとめ

上の箇条書きでは紹介はしませんでしたが、この本では、仕事のことも勿論ですが、家族とどう接するべきか?というトピックを要所々々で取り上げられていて、その度に、家族以上に重要なものはないという話が出てきます。娘の誕生日と重要な商談の日が被ったら〜とか、嫁さんとの時間の過ごし方とか、今後の家族との接し方についても、考えさせられる内容で、そこらへんも非常に良い話ばかりなので、ぜひ読んでみてください。

O/Rマッパーを使う理由

f:id:tikasan0804:20180805205507p:plain
※サムネがO\Rとなってますが、表記ブレです

記事にした経緯

私の所属するVOYAGE GROUPでは、技術力評価会という、エンジニアがエンジニアを評価する会があります。その発表の素振り、つまり、事前練習会みたいのが行われたりします。 そこで、O/Rマッパーを使っている部分の話になった時に、「O/Rマッパーって何が便利なんですか?なんで使うんですか?SQLを書いたらだめなんですか?」という質問が飛び出しました。

これは私に対して来た質問ではなかったのですが、確かに何で使おうんだろうねっていうのが言語化出来ていなかったので、まとめてみようと思いました。

O/Rマッパーとは

オブジェクト関係マッピング - Wikipedia
Wikipediaには、オブジェクト関係マッピングという名前で紹介されていました。そこに書いてある内容を要約すると・・・

データベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。

つまり、オブジェクト指向とデータベースの考えの差分を吸収して、どっちでもいい感じに使えるようにする技法。

では、何故この技法が生まれたのかを理解していく。

登場した背景

オブジェクト指向言語で、リレーショナルデータベース(以下データベースとする)を使う時に、取得してきたレコードをオブジェクトにマッピングする作業が必要になります。わからない人向けに手動マッピングのコード例を貼っておきます。

※以下のコードはかなりいい加減なのと、適当にコピってきたなので、流し読みしてください

Connection conn = null;

try {
  conn = DriverManager.getConnection(url, user, password);

  Statement stmt = conn.createStatement();
  String sql = "SELECT id, first_name FROM users";
  ResultSet rs = stmt.executeQuery(sql);
  List<User> users = new ArrayList<User>();
  while(rs.next()) {
      User user = new User();      
      // usersテーブルのid を UserクラスのIdフィールドに入れる(マッピング)
      user.setId(rs.getString("id"));
      user.setFirstName(rs.getString("first_name"));
     users.add(user);
  } 
}catch (SQLException e){
  out.println("SQLException:" + e.getMessage());
}

こういったマッピングを毎クエリ書く必要が出てきます。これはオブジェクト指向言語が悪いとか、データベースが悪いという話ではなく、設計のズレ。これをインピーダンスミスマッチと言います。

  • リレーショナルデータベース設計: 検索や登録更新処理に最適なモデル定義
  • オブジェクト指向設計:データモデルを現実世界のモデルに即したものとして定義

そもそも、思想が違うので、どちらかの思想が合わせるといったことすると、不都合が起きることは当然ですよねっていうことはこの時点でわかる。それらの不都合を解決するために出てきたのがO/Rマッパーです。

O/Rマッパーがない世界感

SQLにはオブジェクト指向を考慮した設計はなされていないため、SQLを扱おうとすると、オブジェクト指向にある柔軟性のメリットが無に帰すわけです。どういうことか?

何かのSQL構築したいと考えた場合、オブジェクトに入れたデータをデータベースに保存するために、一旦オブジェクトからデータを抜き出し、SQLを構築する専用メソッドが必要があります。これだけでもだるい作業なのですが、もし、データベース側でカラム名、型の変更があった時に、SQL作成部分のコードは毎度実装修正する必要が出てきます。(再利用性とは・・・状態)

また、マッピングを書く処理は非常に単調で退屈なコードです。しかし、これを間違えると普通に事故ります。しかも、型が同じであればそれっぽく動いたりもするので、これを人間が手作業で全部やるのは辛い。(コードジェネレーターで解決するという手もありますが、今回は割愛)

使うことで得られるメリット

よくあるO/Rマッパーは、オブジェクトを作成して、データベースのデータを更新するメソッドに渡すだけで、あとはそのメソッド内で、クエリ作成に必要なマッピングを中でやってくれます。
もし、フィールドを増やしたい減らしたいといったことが起きても、updateメソッドに渡すオブジェクトに変更加えるだけで済みます。このメリットはレコードを取得する時にもです。

ちなみに、マッピング情報をxml形式で書いたり、アノテーションで定義をすることが多いです。面倒のように思えますが、そこさえ書けていれば、コードを書く時はオブジェクトを扱うように操作が出来るようになります。

User user = new User();  
user.setId(1);
user.setFirstName("hoge");

// いい感じに変換してくれる(型解釈や構文の構築など)
// UPDATE users SET first_name = 'hoge' WHERE id = 1
db.update(user);

SQLを考えなくてよくなる?

オブジェクト指向言語からオブジェクトをそのまま扱うような感覚でデータベースを扱うことが出来るようにしたのが、O/Rマッパーですが、だからといってSQLを知らなくていいというわけではありません。これはどういったクエリが発行されているのか?は理解しておかないと、「スロークエリ」や「N + 1問題」といった問題に対して何も出来なくなってしまうので、精通しているまではやらなくても、読めるようにしておくことは必須です。

そもそも、O/Rマッパーが登場した背景には、SQL理解しなくても使えるようにではなく、分かっていることを何度も書かないで済むようにしたいや、オブジェクト指向とリレーショナルデータベースのメリットを最大限発揮するために考えられたものなので、知らなくていいやという話ではないのです。

まとめ

世に出ているO/Rマッパーはマッピングだけではなく、付随して便利な機能を持ったものがたくさんあるので、ずばり何がしたいんだっけ?がブレブレだったので、学びがあった。

IntelliJ IDEA プロジェクト内移動を使いこなす

f:id:tikasan0804:20180729133436p:plain

案外ちゃんと使いこなせていないソースの行き来機能

IntelliJ IDEAと言いつつ、スクリーンショットはGolandでやっています。

わりと規模大きめの開発が多くなってきて、様々なファイルを行き来することが増えてきて、結構ファイルを探すという無駄な時間が発生していることに気づいたので、今回は費用対効果高そうだったので、プロジェクト内移動のNavigation機能についてまとめたいと思う。

この記事に書いていることは以下の本に大体書いています。

ショートカットも基本的にわかる範囲で紹介します。(ideaVimを普段使っているのでわかる範囲で書く) また、私は相変わらずショートカットの記号が覚えられないので、ここではAltやCtrlと書きます。

macOS Sierra: メニューに表示される記号

コードジャンプ系

シンボル定義箇所へジャンプ

定義されている変数や関数の定義元にジャンプする。

  • ideaVim Ctrl + ]
  • Mac Command + B
  • Win Ctrl + B

一箇所しかない場合は、一発でジャンプしますが、複数定義箇所がある場合は、ポップアップが出てきます。

f:id:tikasan0804:20180729121021p:plain

シンボルの利用箇所の一覧

  • Mac Option + F7
  • Win Alt + F7

何かのシンボルにカーソルを合わせて、使うと、その定義箇所から利用箇所が一覧で見れる。

f:id:tikasan0804:20180729120802p:plain

ジャンプを戻る

ジャンプ先から戻る

  • ideaVim Ctrl + t
  • Mac Command + [
  • Win Ctrl + Alt

ジャンプ先に戻る

  • Mac Command + ]
  • Win Ctrl + Alt +

クラス間の移動

子クラスから親クラスへ移動。(Javaとかの継承)

  • ideaVim クラス名に合わせて、シンボルへのジャンプで同じことができる
  • Mac Command + U
  • Win Ctrl + U

親クラスから、子クラスや実装クラスを見ることもできる

  • ideaVim クラス名に合わせて、シンボルへのジャンプで同じことができる
  • Mac Option + Command + B
  • Win Ctrl + Alt + B

ファイル系

直近のファイルを開く

Ctrlを押したまま、Tabを押した回数だけ前のファイルに戻れます。

  • Mac,Win Ctrl + Tab

f:id:tikasan0804:20180729123422p:plain

直近のファイル一覧

上のTab を何度も押す形式は正直だるいので、開きっぱなしにできるこっちのがおすすめ。

  • Win Command + E
  • Mac Ctrl + E

f:id:tikasan0804:20180729123752p:plain

ディレクトリ系

ナビゲーションバーを使って移動

ナビゲーションバーを開いて、矢印キーEnterとかをつかうと、ソースを開く事もできます。

  • Mac Command + 1
  • Win Alt + 1

f:id:tikasan0804:20180729125325p:plain

ナビゲーションバーの操作型

新規ファイル作成

  • Mac Command + N
  • Win Alt + Insert

ファイル名変更

  • Mac,Win Shift + F6

現在開いているファイルにフォーカスを当てる。

  • Mac Command +
  • Win Alt + Home

現在開いているファイルをナビゲーションバーと同期させる

いま開いているファイルってソースどこ?っていうのが案外わかりにくかったりするので、自分は同期させたりしています。
※上に出てる階層表示でわかるっちゃわかりますが

f:id:tikasan0804:20180729130537p:plain
f:id:tikasan0804:20180729130541p:plain

検索

Search Everywhere

色んな検索をサクッとできる。

  • Mac,Win Shift 2回押す

f:id:tikasan0804:20180729131305p:plain

種別付き検索

ファイル名検索

  • Mac Command + Shift + O
  • Win Ctrl + Shift + N

クラス名検索

  • Mac Command + O
  • Win Ctrl + N

シンボル名検索

  • Mac Option + Command + O
  • Win Ctrl + Shift + Alt + N

シンボル・検索除外

Excludedに指定すると検索対象として出なくなる。
よくあるケースとしては、jsのdistフォルダ出てくるなあああああ!とか。

f:id:tikasan0804:20180729131715p:plain

TODO一覧

FIXME TODO をコード上で書くと、一覧して見ることが出来る。

  • Mac Command + 6
  • Win わからなかった

f:id:tikasan0804:20180729132637p:plain

チャットと対話の使い分け

コミュニケーションは難しい

社会人になって、早いもので数ヶ月が経ちました。
いま、自分は広告配信システムの開発(株式会社Zucks)をしているのですが、学生から社会人になってから、最も難しいと感じ、日々課題を感じているのがコミュニケーションです。苦労しながらも、最近わかってきたことの一つであるチャットと対話の使い分けどうすれば良いんだっけってところをまとめてみる。
※弊社では営業も開発も、Slackをチャットツールとして使っています

チャットと対話の質的な違い

チャット

  • 証拠能力が高い
  • 時間のある時に返せる
  • 他の人が見える
  • 書いたこと以上のことは伝わらない(前後関係とかで分かることもある)
  • ひとまず雑に投げれる

対話

  • 温度感が伝わる
  • 深い議論が出来る
  • 時間が取られる
  • 体力使う

上に書いたこと以外にも色々あるとは思いますが、雑に思いついたものを書きました。

それぞれの立ち位置

どちらもした方が良いことには変わりはないのですが、あえて言うなら、チャットは マスト 対話はベター という感じで、この差を生んでいるのは、証拠能力です。
対話は話をしている人以外には議論が見えません。じゃあ、対話は駄目なのか!?っていう話ではなく、対話をしたことをざっくりで良いのでチャットとかに残す。または結構な分量になるなら、Googleドキュメントとかに議事録残す。

「という話をした」はチャットで共有する

弊グループ全体がどうかは知りませんが、少なくともZucksでは、「という話をした」というワードがSlackにかなり出てくる。これは、誰とどういう話をしたとか、対話で決まったこととかをSlackに流す習慣がある。これはかなり良いことで、議論に参加していない人でも、ひとまずチームがどういう動きをしているかを知ることが出来る。知りたい情報なら、チャットとかで聞けば良いということが出来る。これがどこにも残っていないと、それすら出来ないので、この習慣は非常にありがたい。(これは別に誰にも指示されていない)

slack上で「という話をしたで2000件も出てきた」 f:id:tikasan0804:20180717213108p:plain

中にはこんな雑な「という話をした」 f:id:tikasan0804:20180717215011p:plain

チャットを投げる時はどういう時

自分の中で、2つの条件が決まっていて、以下の2つです。

  • 何を質問したら良いか分からない
  • 質問を完全に言語化出来る自信がある

何を質問したら良いか分からないのにチャット!???

チャットなのに、そんな雑で良いのか??!!!って感じかもしれませんが、まず前提として、このフェーズの時は、誰かにメンションをつけて投げるのではなく、雑にチャットに書く。作業をしている中でのモヤモヤとかはissue経由で全部チャットに流れるので、GitHub経由で流すこともかなり多い。
これは、いまここらへんで悩んでいるけど、誰に質問したらいいかも分からないし、質問するべき項目も分からない。そんな時に、チーム内の人に相談をしても、時間ばっかり食ってしまうので、こういうフェーズの時はチャットに書くようにしている。分かる人がいたら、返してくれることもあれば、自己解決することもよくある。

訳が分かっていないことしか分かっていないことをボヤく
f:id:tikasan0804:20180717215023p:plain

強いおじさんが助けてくれることがよくある
f:id:tikasan0804:20180717215841p:plain
f:id:tikasan0804:20180717215851p:plain

質問を完全に言語化出来る自信がある

さっきとは違って、完全に理解しているフェーズの時は、チャットで投げることが多い。というのも、言語化が出来ているレベルになっている時は、ばちっと!っとチャットで投げる。このフェーズに入っている時は、短いチャットのやり取りで終わる。しかし、思ったよりちゃんと話した方がいいかも?ってなった時は、対話をして解決するということもある。そして、話が終わったら、「という話をした」で質問内容の答えをチャットで共有する。

チャットで無理な時は丸テーブルで話そう「マルテ」が発動する f:id:tikasan0804:20180717221557p:plain

対話をする時

対話をする時もチャットと同様で条件が2つあって

  • チャットだけだと長くなりそう
  • ペアプロ、ペア作業、ペアhoge

チャットだけだと長くなりそう

これは、質問を完全に言語化出来る自信がある に書いたように、話した方が良さそうだ!って思った時に対話する。

ペアプロ、ペア作業、ペアhoge

ペアで一緒に進めた方が早そう。ということもあって、弊社では結構ペアプロをすることがあります。その理由としては、プロダクトコードの構成とかを理解するのに時間がかかりそうとか、言語ごとにあるパラダイムとかをサクッと理解しようみたいな時、あとは削除作業のような怖い作業はペアで確認しながらやったり、あとは何で落ちているか本当に分からないコードとかを一緒に読むこともあったりします。

etc...

  • issueに自分の意思決定は全て書く
  • GUI操作はスクリーンショットを貼る
  • 質問は短くわかりやすくすることを頑張る
  • 事実と事実からの推測かを明確にする
  • 数字が出せるなら、多いとか少ないではなく数字を書く

なんか、他にも色々ある気がする。

今回書いたこと

チームとして「こうしなさい!」ということは一切言われていません。うちのチーム的にこうやって動いた方がいい感じになりそうと思って、日々やってることをまとめたものになります。書いてて思ったのが、こういう文化ってどうやって作ってるんだろうなと思いました。

シェル、シェルスクリプトの使い方まとめ

f:id:tikasan0804:20180710081015p:plain

自分向けメモ。たまに更新していく。

Shellとは

ユーザーが入力したコマンドを解釈し、該当コマンドを実行する。シェルにはモードが2つある。

上記がどういったものかは割愛する。

単純コマンド

$ echo HelloWorld
HelloWorld

コマンド名と任意の引数で構成されるもの。

リダイレクト

上記をリダイレクトさせることが出来るのが、リダイレクトです。(良い言い方がわからない)

標準入力にファイルを入力する例

$ cat stdin
HelloWorld

$ < stdin
HelloWorld

標準出力をファイルに書き込む例

$ cat stdin
HelloWorld
$ cat stdin > out

$ cat out
HelloWorld

標準出力をファイルに追記する例

$ cat stdin
HelloWorld
$ cat stdin >> out2
$ cat stdin >> out2
$ cat out2
HelloWorld
HelloWorld

標準出力・標準エラーを別ファイルに書き込む

ファイル記述子

div:
        echo 1
        2
$ make div 1> log_1 2> log_2
$ cat log_1
echo 1
1
2
$ cat log_2
make: 2: No such file or directory
make: *** [div] Error 1

標準出力・エラーまとめてファイルに書き込む例

err:
        echo HelloWorld
        a
$ make err > log 2>&1
$ cat log
echo HelloWorld
HelloWorld
a
make: a: No such file or directory
make: *** [err] Error 1

パイプ

構文の組み合わせ

treeの出力をlessに渡す

$ tree / | less

標準エラー出力もパイプに渡す

hoge 2>&1 | less

環境変数の一時変更

LANG=C man ls

&&(AND) ||(OR)

パイプライン1 && パイプライン2はパイプライン1の終了ステータスが成功(0)なら、パイプライン2が実行される。
パイプライン1 || パイプライン2はパイプライン1か2のどちらかが成功(0)すればおk.

err:
        echo HelloWorld
        a
$ make err && echo OK
echo HelloWorld
HelloWorld
a
make: a: No such file or directory
make: *** [err] Error 1

$ make err || echo OK
echo HelloWorld
HelloWorld
a
make: a: No such file or directory
make: *** [err] Error 1
OK

シェルスクリプト

行頭便利フォーマット

#!/usr/bin/env bash

# Fail on unset variables and command errors
set -ue -o pipefail

# Prevent commands misbehaving due to locale differences
export LC_ALL=C
#!/bin/sh -e
# オプションつきシェルスクリプト

行頭の意味

シェルスクリプトの行頭に記述する #! で始まる行shebangという

bash絶対パス指定

#!/bin/bash

bashをenvを使って指定

#!/usr/bin/env bash

基本構文

if

引数によっての分岐

#!/bin/bash

if [ $#  -lt 2 ];then # 引数が2個未満
  echo "Usage: $0 file1 file2" 1>&2 # 標準エラー出力に出力
  exit 1
else
  echo "eeyan"
fi
$ sh if.sh
Usage: if.sh file1 file2

$ sh if.sh ls file1
eeyan

case

unameを使ったOS判定の例

$ uname -sr
Darwin 15.6.0
#!/bin/bash

case `uname -sr` in
  Linux*)
    ls -l --full-time "$@";;
  FreeBSD* | NetBSD* | OpenBSD* | Darwin*) # Darwinから始まる任意の文字列
    ls -lT "$@";; # ここが実行される
  SunOS' '5.*)
    ls -E "$@";;
  *) # 何もひっとしない場合
    echo unknown OS 1&>2;;
esac
$ sh case.sh
total 88
-rw-r--r--  1 jumpei  staff   13  7  5 08:23:24 2018 2
-rw-r--r--  1 jumpei  staff  202  7  5 08:24:23 2018 case.sh
-rw-r--r--  1 jumpei  staff  158  7  5 08:18:11 2018 if.sh
-rw-r--r--  1 jumpei  staff   48  7  4 08:44:16 2018 sample.sh
-rw-r--r--  1 jumpei  staff   56  7  4 08:44:35 2018 sample2.sh
-rw-r--r--  1 jumpei  staff   87  7  4 08:52:39 2018 sample3.sh
-rw-r--r--  1 jumpei  staff   47  7  4 20:14:09 2018 sample4.sh
-rw-r--r--  1 jumpei  staff   55  7  4 20:13:57 2018 sample5.sh
-rw-r--r--  1 jumpei  staff  198  7  5 08:03:53 2018 sample6.sh
-rw-r--r--  1 jumpei  staff   69  7  5 07:52:58 2018 sample7.sh
-rw-r--r--  1 jumpei  staff   81  7  5 07:55:30 2018 sample8.sh

for

* はカレントディレクトリのファイル名

#!/bin/bash

for file in *; do
  echo $file
done
$ sh for.sh
LICENSE
README.md
for.sh
for_arg.sh
for_bash.sh
for_jot.sh
for_seq.sh
sample
sh

引数 $@ を使ったループ

#!/bin/bash

for arg in "$@";do
  echo $arg
done

for arg;do # 上と同じ意味
  echo $arg
done
$ sh for_arg.sh a b c
a
b
c
a
b
c

bashzshのプレース展開を使ったループ

#!/bin/bash

for i in {1..10};do
  echo $i
done
$ sh for_bash.sh
1
2
3
4
5
6
7
8
9
10

jotを使ったループ

#!/bin/bash

for i in `jot 10`;do
  echo $i
done
$ sh for_jot.sh
1
2
3
4
5
6
7
8
9
10
for i in `seq 1 10`;do
  echo $i
done
1
2
3
4
5
6
7
8
9
10

サブシェル

リストを()で囲むとサブシェルになり、サブシェルはもとのシェルとは別扱いで実行されます。サブシェルの中での変数の変更、umask値を変えても、サブシェルから出ると戻ります。この挙動を利用して、何らか変更をさせて終わったら、元の状態で処理もしたいなどのシチュエーションでは便利です。シェルスクリプトをリダイレクト、パイプでつないでも同じことはできます。

シェル変数 IFS: に変更して、set コマンドを実行すると : が削除され、 PATH の中身が $1 $2 $3 のように位置パラメータに設定されます。これらの変更はサブシェルを抜けると解除されます。

#!/bin/sh

echo "IFS=$IFS"

(
  IFS=:
  echo "IFS=$IFS"
  set $PATH
  echo $3
)

echo "IFS=$IFS"
$ sh sub_shell.sh
IFS=

IFS=:
/Users/jumpei/.anyenv/envs/rbenv/shims
IFS=

グループコマンド

リストを{}でかこむとグループコマンドと呼ばれる複合コマンドになります。リダイレクトしたり、パイプに接続したり、次のシェル関数の本体として利用できる。グループコマンドはサブシェルと違って、終わった後も影響があります。

コマンドの結果をまとめてlogfileにリダイレクトしている例。

#!/bin/sh

{
  hostname
  date
  who
} > logfile
$ sh group.sh
$ cat logfile
jumpei-no-MacBook-Pro-3.local
2018年 7月 6日 金曜日 08時24分53秒 JST
jumpei   console  Jul  4 07:49
jumpei   ttys001  Jul  4 07:49

シェル関数

シェル関数内の変数は引数渡しのため、位置パラメータ以外はグローバル変数になりますそのため、関数内はローカルにしたい場合は、{} ではなく ()を使って、サブシェルにすれば可能です

#!/bin/bash

greet()
{
  echo "Hello"
}

greet

greet2()
{
  echo $1 "Hello"
}

greet2 A
$ sh func.sh
Hello
A Hello

エラー系TIPS

変数の設定漏れ防止

set -u
変数を宣言していないものを使った時に終了ステータスを失敗(0以外)にしてくれる。

#!/bin/bash

VAL=foo
echo $VAL_TYPO
echo FINISH
$ sh sample.sh; echo $?

FINISH
0

$ cat sample2.sh
#!/bin/bash

set -u

VAL=foo
echo $VAL_TYPO
echo FINISH

$ sh sample2.sh; echo $?
sample2.sh: line 6: VAL_TYPO: unbound variable
1

エラーとしたくないケースを回避

途中まで処理して落としたい。デフォルト値が使える(${parameter:-word})

#!/bin/bash

set -u
if [ -z "${1:-}" ]; then
    echo "HOW TO hoge" >&2
    exit 2
fi
$ sh sample3.sh
HOW TO hoge

エラーになったら中断

set -e

何もつけないとエラーがあっても、最後まで実行される。しかも、ステータスコードも成功になる。

#!/bin/bash

FOO=$(ls --l)
echo $FOO
echo "OK"
$ sh sample4.sh; echo $?
ls: illegal option -- -
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]

OK
0

エラー時に途中で終了し、ステータスコードも失敗が帰ってくるようになる

#!/bin/bash

set -e

FOO=$(ls --l)
echo $FOO
echo "OK"
$ sh sample5.sh; echo $?
ls: illegal option -- -
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
1

エラーを無視したいケース
grepで検索ヒットしない時にエラーステータスを返すので、それをいい感じにしたい。
以下のような感じにするか、コマンドでエラーとしないオプションとかを使うか
またはリストを使ったエラー無視などのパターンがあります。

#!/bin/bash

set -e

if find ./ | grep sample888.sh >/dev/null; then
  echo "Existing sample.sh"
else
  echo "Nothing sample.sh"
fi

rm a || true
rm a || : # trueの代わりに出来る

echo "OK"
$ sh sample6.sh; echo $?
Nothing sample.sh
rm: a: No such file or directory
rm: a: No such file or directory
OK
0

パイプライン内のエラーで中断する

set -eはパイプラインの一番右のコマンドのエラーは正しくエラーとしてくれるが、途中のコマンドのエラーは無視されます。

$ cat sample7.sh
#!/bin/bash

set -e

FOO=$(ls - l "$0" | wc -l )
echo $FOO
echo "OK"
$ sh sample7.sh; echo $?
ls: -: No such file or directory
ls: l: No such file or directory
1
OK
0

set -e -o pipefailをつけるとパイプライン中のエラーを検知してくれる。

#!/bin/bash

set -e -o pipefail

FOO=$(ls - l "$0" | wc -l )
echo $FOO
echo "OK"
$ sh sample8.sh; echo $?
ls: -: No such file or directory
ls: l: No such file or directory
1

エラーは標準エラー出力に出力しよう

#!/bin/bash

echo 'Expect stdout'
echo 'Expect stderr' 1>&2
$ sh ./output.sh >/dev/null
Expect stderr

$ sh ./output.sh 2>/dev/null
Expect stdout

bashに依存しているコマンドは#!/bin/bashと書かない

ubuntu上では以下のスクリプトは動きません。bashに依存しているなら、/bin/bashとしよう。

#!/bin/sh
list=( ($ls) )
echo $list

環境変数によって動かないを避ける

export LC_ALL=C

参考文献

AWS Serverless Application Model(SAM)を使ったサーバーレスアプリ構築

AWS Serverless Application Model(SAM)とは

f:id:tikasan0804:20180703082833p:plain

github.com

AWS Serverless Application Model (以降AWS SAMとする) は、AWS公式のサーバーレスアプリケーションを構築するためのフレームワークです。

以前の記事のServerlessでAWS Lambdaのデプロイをいい感じにする - ぺいで、Serverlessというつーるを紹介したのですが、SAMは、公式サポートなので、AWS以外のサーバーレス使う予定ない人は、基本的にSAMを使う方向で良さそうな気がします。

SAMとServerless Frameworkの違い

今回使ってみて、感じた違いについてまとめました。

Serverless Frameworkは、全部乗せの万能ツールという感じ。AWSに限らず様々なクラウドで統一の設定が使える。また、デプロイまでが本当サクッと出来る。SAMを使って思ったことですが、裏で暗黙的色々いい感じにやってるんだなと思った。
あと、コミュニティが盛んなこともあり、プラグインとかも色々出ているので、

SAMはAWSに特化していることもあり、AWSしか使わないなら、SAM使っとけばいいやんと使ってみて思った。

SAM使ってみる

どんなもんかをひとまず動かして体感してみる。インストールはpipで出来ます。

❯ python -V
Python 3.6.3
❯ pip install aws-sam-cli
...
❯ sam --version
SAM CLI, version 0.4.0

ちなみに、sam init を実行すると見本となるテンプレートが作られる。

SAM CLI のインストール - AWS Lambda

設定内容を追いかけてみる

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

上記の定義は、定型的に入れろとのことをそのまま鵜呑みにすると、バージョン指定という認識で良さそう。

Globals:
  Function:
    Timeout: 5

関数全体に対する設定。今回はタイムアウトまで5秒となっている。

Resources:
  HelloWorldFunction: # 関数名
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello-world/
      Handler: hello-world # Handler名 今回の場合、go build後のファイル名
      Runtime: go1.x
      Tracing: Active # X-Rayによるトラッキングの有効化
      Events: # どういうイベント(トリガー)を設定するか
        CatchAll:
          Type: Api
          Properties:
            Path: /hello
            Method: GET
      Environment: # 環境変数
        Variables:
          PARAM1: VALUE

関数の定義が書かれている。

Outputs:
  HelloWorldAPI:
    Description: "API Gateway endpoint URL for Prod environment for First Function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

  HelloWorldFunction:
    Description: "First Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn

  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

CloudFormationのOutputsセクションの定義。
出力 - AWS CloudFormation

main.go

package main

import (
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

var (
    // DefaultHTTPGetAddress Default Address
    DefaultHTTPGetAddress = "https://checkip.amazonaws.com"

    // ErrNoIP No IP found in response
    ErrNoIP = errors.New("No IP in HTTP response")

    // ErrNon200Response non 200 status code in response
    ErrNon200Response = errors.New("Non 200 Response found")
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    resp, err := http.Get(DefaultHTTPGetAddress)
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    if resp.StatusCode != 200 {
        return events.APIGatewayProxyResponse{}, ErrNon200Response
    }

    ip, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    if len(ip) == 0 {
        return events.APIGatewayProxyResponse{}, ErrNoIP
    }

    return events.APIGatewayProxyResponse{
        Body:       fmt.Sprintf("Hello, %v", string(ip)),
        StatusCode: 200,
    }, nil
}

func main() {
    lambda.Start(handler)
}

今回の関数。

main_test.go

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/aws/aws-lambda-go/events"
)

func TestHandler(t *testing.T) {
    t.Run("Unable to get IP", func(t *testing.T) {
        DefaultHTTPGetAddress = "http://127.0.0.1:12345"

        _, err := handler(events.APIGatewayProxyRequest{})
        if err == nil {
            t.Fatal("Error failed to trigger with an invalid request")
        }
    })

    t.Run("Non 200 Response", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(500)
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err != nil && err.Error() != ErrNon200Response.Error() {
            t.Fatalf("Error failed to trigger with an invalid HTTP response: %v", err)
        }
    })

    t.Run("Unable decode IP", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(500)
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err == nil {
            t.Fatal("Error failed to trigger with an invalid HTTP response")
        }
    })

    t.Run("Successful Request", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(200)
            fmt.Fprintf(w, "127.0.0.1")
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err != nil {
            t.Fatal("Everything should be ok")
        }
    })
}

関数に対するテスト。

Makefile

.PHONY: deps clean build

deps:
  go get -u ./...

clean: 
  rm -rf ./hello-world/hello-world
  
build:
  GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world

packageする

次に作成した関数をデプロイ用にパッケージ化します。

❯ aws s3 mb s3://hoge-fuga --region ap-northeast-1

パッケージの置き場所として、S3のバケットが必要なので、任意のバケットを用意するか、既にあるものを使います。作成するときは上のようなコマンドで作成する。

❯ sam package \                          
   --template-file template.yaml \        
   --output-template-file packaged.yaml \ 
   --s3-bucket hoge-fuga     

sam package コマンドで、作成したテンプレートから、パッケージングを実行する。デプロイに必要な情報は、今回の場合だと、outpu-template-fileで指定したpackaged.yamlで作成される。

デプロイ

❯ sam deploy \                   
   --template-file packaged.yaml \
   --stack-name sam-app \         
   --capabilities CAPABILITY_IAM  

sam deploy コマンドで実際にデプロイをします。--template-fileには先程出てきたpackaged.yamlを指定します。--stack-nameはCloudFormationのスタック名で、任意の名前を指定すれば良さそう。--capabilitiesはロールをいい感じにしておいて的なやつ。

f:id:tikasan0804:20180703081118p:plain デプロイ出来た!便利。

ローカル実行

SAMはDockerを使うことで、ローカル実行を可能にしている。

❯ sam local start-api
2018-07-03 08:19:00 Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
2018-07-03 08:19:00 You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2018-07-03 08:19:00  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
2018-07-03 08:19:02 Invoking hello-world (go1.x)
2018-07-03 08:19:02 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:go1.x Docker container image......
2018-07-03 08:19:05 Mounting /Users/jumpei/go/src/github.com/pei0804/serverless-sample/sam/sam-app/hello-world as /var/task:ro inside runtime container
START RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b Version: $LATEST
END RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b
REPORT RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b  Duration: 1180.19 ms    Billed Duration: 1200 ms        Memory Size: 128 MB     Max Memory Used: 10 MB
2018-07-03 08:19:08 No Content-Type given. Defaulting to 'application/json'.
2018-07-03 08:19:08 127.0.0.1 - - [03/Jul/2018 08:19:08] "GET /hello HTTP/1.1" 200 -
❯ curl 127.0.0.1:3000/hello
Hello, 103.2.249.5

ログはCloudWatchライクなものが出てくる。これは強い。