DockerのBuildKitがヒアドキュメントをサポートしました

f:id:ingnis:20201120144028j:plain

Dockerでヒアドキュメントが使えるようになるみたいです。

DockerfileでのRUNコマンドの記述が楽になりそうです。

RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y ...

参考サイト www.docker.com

ヒアドキュメントの機能を利用するには、環境変数 DOCKER_BUILDKIT=1にする必要があります。

buildコマンドで利用するイメージを変更するために、Dockerfileの冒頭に# syntax=docker/dockerfile:1.3-labsを追加します。

ヒアドキュメントが使用できるコマンドはRUNCOPYになります。

RUNコマンドでヒアドキュメントを使う

上のRUNコマンドをヒアドキュメント化するには以下のように記述します。

# syntax=docker/dockerfile:1.3-labs

FROM ubuntu:20.04

RUN <<EOF
apt-get update
apt-get upgrade -y
apt-get install -y ...
EOF

ヒアドキュメントを使うことで、RUNコマンドでの&& \の数珠つなぎから開放されDockerfileの可読性が上がりそうですね。

COPYコマンドでヒアドキュメントを使う

# syntax = docker/dockerfile:1.3-labs

FROM alpine

COPY <<-"EOF" /app/script.sh
    echo hello ${FOO}
EOF

RUN FOO=abc ash /app/script.sh >> /hello
  1. ヒアドキュメントの内容で/app/script.shを作成します。(EOFをダブルクオート囲っているので環境変数は展開されない)

  2. 次のRUNコマンドで/app/script.shを実行します。(/hellohello abcが記述される)

私には利用用途があまり思い浮かびませんが、マルチステージビルドで複雑なbuildをするときに使うときが来るかもしれません。

短いですが以上です。

インゲージではエンジニアを募集しいています。 ご応募お待ちしてます。

↓↓↓↓↓↓↓↓↓↓↓↓↓

ingage.co.jp

過去コミットを分割してたら、知らないファイル変更が現れた

f:id:shutooike:20210818144401p:plain

はじめに

topic-a ブランチで新たに作成した ComponentX を topic-b ブランチでも使いたくなったので、

topic-a からコンポーネントXを作成したコミットだけ cherry-pick して別PRにしたいが、

コンポーネントXを作成したコミットには他の変更(コンポーネントXの適用)も入っているため単純に cherry-pick するだけでは無理なので

コンポーネントXを作成したコミットを分割しようとしてハマった話をします。

ハマりどころ

「git 過去コミット 分割」でググるとこんな記事を見つけました。

cha-shu00.hatenablog.com

先人に感謝しつつ、なるほど git rebase -i で edit して git reset HEAD^ して、別々に commit したらいいのか・・

やってみます。

$ git log
* ccccccc (HEAD -> topic-a) fix typo
* bbbbbbb add ComponentX
* aaaaaaa fix style

git rebase -i して

$ git rebase -i aaaaaaa

pick bbbbbbb add ComponentX
pick ccccccc fix typo

# Rebase aaaaaaa..ccccccc onto aaaaaaa (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
....(omitted)

分割したいコミットを pick → edit に変更

- pick bbbbbbb add ComponentX
+ edit bbbbbbb add ComponentX

git status

$ git status
interactive rebase in progress; onto aaaaaaa
Last command done (1 command done):
   edit bbbbbbb add ComponentX
Next commands to do (105 remaining commands):
   pick ccccccc fix typo
  (use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch 'topic-a' on 'aaaaaaa'.
  (use "git commit --amend" to amend the current commit)
  (use "git rebase --continue" once you are satisfied with your changes)

コミットを一つ戻して、git status すると

$ git reset --soft HEAD^
$ git status
interactive rebase in progress; onto aaaaaaa
(省略)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue 
        modified:   foo/views/settings/pages/UseComponentXPage.vue 
        modified:   foo/models/A.ts
        modified:   foo/models/B.ts
        modified:   foo/models/C.ts
        .
        .
        .

ん?!??

コミット bbbbbb での変更以外のファイルまで modified になっている。

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue # <- コミット bbbbbb での変更
        modified:   foo/views/settings/pages/UseComponentXPage.vue # <- コミット bbbbbb での変更
        # 以下、知らない子たち
        modified:   foo/models/A.ts
        modified:   foo/models/B.ts
        modified:   foo/models/C.ts
        .
        .
        .        

なぜこんなことに・・・・

解決

ESLint 君の autofix の仕業でした。

webpack コンテナを落として再度同じことをやると

$ git status
interactive rebase in progress; onto aaaaaaa
(省略)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue 
        modified:   foo/views/settings/pages/UseComponentXPage.vue 

今度はコミット bbbbbb での変更だけが表示されました!

あとは個別にファイルをコミットして rebase --continue をしたら過去コミットの分割ができました!

さいごに

そういえば前にもこんなことあったなーと思ったので備忘録として残しておきます。

というか、適切な粒度でコミットしましょう。

弊社では適切な粒度でコミットできるエンジニアを絶賛大募集中です!

カジュアル面談も有りますのでご興味あればぜひお気軽に下記リンクからお願いします!

ingage.co.jp

後日談

コンポーネントXを作成したコミットには他の変更(コンポーネントXの適用)も入っているため単純に cherry-pick するだけでは無理なので

勝手に無理だと思い込んでいたんですが、実際やってみると

$ git log
* ccccccc (HEAD -> topic-a) fix typo
* bbbbbbb add ComponentX
* aaaaaaa fix style

$ git switch main

$ git switch -c cherry-pick-component-x
Switched to a new branch 'cherry-pick-component-x'

$ git cherry-pick bbbbbbb
error: The following untracked working tree files would be overwritten by merge:
        foo/common/elements/CoponentX.vue
Please move or remove them before you merge.
Aborting
fatal: cherry-pick failed

$ git status
On branch cherry-pick-component-x
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        foo/common/elements/CoponentX.vue

nothing added to commit but untracked files present (use "git add" to track)

と cherry-pick 自体は失敗するんですが、untracked な ComponentX.vue ファイルは生成されました。

綺麗な歴史にこだわりがないなら、これで十分だなーと思いました。

ではまた!

ChatOps: SlackからLambdaに連携してみる(5)

id:kizashi1122 です

f:id:ingage:20210620201734p:plain

いよいよ大詰めです。

Lambda の中で「Slack に投稿する」ということをします。 今回は Ruby を使っています。Ruby だと以下の gem を使いたくなります。

github.com

Lambda で gem を使うってどうするんでしょうか。 今回はこちらのやり方でいきましょう。ローカルで bundle install して zip で固めてアップロード、ですね。

docs.aws.amazon.com

他のやり方としては、SAM cli を使うだとか、CDK を使うというやり方もあると思いますがそれはまた別の機会に。

zip ファイルの作成

手元で、適当にディレクトリを作成し、その中で以下のファイルを作りましょう。

  • lambda_function.rb
  • Gemfile

lambda_function.rb は今の内容そのままでいいです。確か、今は

require 'json'

def lambda_handler(event:, context:)
    # p event
    
    req = JSON.parse(event['body'])
    if ENV['SLACK_VERIFICATION_TOKEN'] != req['token']
        return { statusCode: 401, body: "VERIFICATION TOKEN UNMATCHED" }
    end
    
    { statusCode: 200, body: 'done' }
end

こうなっていたでしょうか。 Gemfile は

source "https://rubygems.org"
gem 'slack-ruby-client'

で、よいでしょう。

次に、同じ階層で、

$ bundle install --path vendor/bundle

と実行します。Lambda の実行環境が Ruby 2.7 なので、ローカルも Ruby 2.7 系にしておいてください。 これ大事です。 実行すると、DEPRECATED とでますが気にしない。次に、ZIP で固めます。

$ zip -r function.zip lambda_function.rb vendor Gemfile*

出来上がった function.zip をアップロードします。

f:id:ingage:20210628230840p:plain

左側のファイルツリーに vendor ディレクトリがみえたでしょうか?

エコーする部分のコーディング

前回のエントリでエコーする部分にはあたりをつけました。 となるとこんなかんじでしょうか。

require 'json'
require 'slack-ruby-client'

def lambda_handler(event:, context:)
    req = JSON.parse(event['body'])
  
    if ENV['SLACK_VERIFICATION_TOKEN'] != req['token']
        return { statusCode: 401, body: "VERIFICATION TOKEN UNMATCHED" }
    end

    text = req['event']['blocks'].first['elements'].first['elements'].last['text']&.strip
    post_message(text) # 同じ内容を投稿
    
    { statusCode: 200, body: 'done' }
end

def post_message(message)
  Slack.configure do |config|
    config.token = ENV["SLACK_TOKEN"]
  end
  client = Slack::Web::Client.new
  client.chat_postMessage(channel: ENV["SLACK_CHANNEL"], text: message)
end

メッセージ送信用のトークンと投稿先のチャンネルは環境変数で指定しています。

パーミッションの追加とトークンの取得

SLACK_TOKEN は何を設定すればよいのでしょうか。

Slack の echoapp の設定画面に戻りましょう。 左メニューの「OAuth & Permissions」に移動します。

f:id:ingage:20210629091433p:plain

するとすでに上部にトークンが表示されています。これを SLACK_TOKEN として環境変数に設定しておきましょう。
SLACK_CHANNEL は投稿先のチャンネルを定義しておいてください。

ここで大事なのはこの時点ではこのトークンにはチャンネルへのメッセージ投稿権限がないことです。
chat:write の権限をつけておきます。

パーミッションを変更したことによりアプリの再インストールが必要になりますので、最初にアプリのインストールをしたのと同じ手順でインストールしましょう。

f:id:ingage:20210629091716p:plain

これで完了です。

テスト

テストしてみます。

f:id:ingage:20210629091822p:plain

やったね!

弊社では、上述の「エコーする」部分を AWS の Security Group に IP アドレスを追加する処理として運用しています。 AWSのAPIでもいいし、外部のAPIでもいいし、いろいろな使い方ができそうですね。

インゲージではエンジニアを募集しいています。 ご応募お待ちしてます。

↓↓↓↓↓↓↓↓↓↓↓↓↓

ingage.co.jp

PostgreSQLでパフォーマンスを比較してみた (EXISTSとINNER JOIN)

はじめまして。5月に入社したryohei515です。

前職ではOracleを使ってSQLを書く機会がよくあり、パフォーマンスチューニング等も行ってきました。

インゲージに入社してからSQLを書く機会があったのですが、DBがPostgreSQLであるため、これまで使っていた細かなSQLのパフォーマンスの小技がPostgreSQLでも通用するのか気になり、検証してみました。

環境準備

Dockerを使って、PostgreSQLのコンテナを作成します。最新の13.3を使用しています。

ちょっと検証したい時に、ローカルを汚さずに簡単に環境が作れるので便利ですね!

docker run --rm -d -p 15432:5432 -v postgres-tmp:/var/lib/postgresql/data -e POSTGRES_HOST_AUTH_METHOD=trust postgres:13.3

※ パフォーマンスを見たいだけなので、パスワード不要となるよう、POSTGRES_HOST_AUTH_METHODtrueに設定しました。

今回の検証用のデータは、postgrteSQLのTutorialで提供されている、dvdrentalを使用しようと思います。

ER図も用意されており、SQLの動作確認や学習なんかにも使えそうですね。

PostgreSQL Sample Database Diagram

データ登録の前に、事前にDATABASEを作成しておきます。

psql -h localhost -p 15432 -U postgres -c "CREATE DATABASE dvdrental;"

以下のサイトよりzipファイルをダウンロードし、ダウンロードしたディレクトリに移動します。

PostgreSQL Sample Database

ダウンロードファイルを、unzipコマンドで展開し、.tarのファイルを作成します。

$ unzip ./dvdrental.zip
Archive:  /Users/makabe/Library/Mobile Documents/com~apple~CloudDocs/Documents/memo/091_blog/20210719_SQL_performance/dvdrental.zip
  inflating: dvdrental.tar

事前に作成しておいたDBに、以下のコマンドでデータ作成します。

(特にメッセージ等表示されず、完了します。)

pg_restore -U postgres -d dvdrental -h localhost -p 15432 ./dvdrental.tar

これで以下のコマンドを打ち込めば準備完了です。

psql -h localhost -p 15432 -U postgres -d dvdrental

パフォーマンスチェック

環境は準備できたので、パフォーマンスを測ってみたいと思います。

EXISTSではなくINNER JOINを使う」というのを、これまでよく使っていたので、実行時間やコストにどんな差が出るか見てみます。

(ざっと検索してみたところ、PostgreSQLでも同じそう…)

SQLの内容は、Rentalテーブルのデータで、length > 100Filmがどれだけあるかという内容で見ていきます。

EXISTSで書いてみるとこんな感じ。

select
  r.*
from
  rental r
where exists(
  select
    1
  from
    inventory i
  inner join
    film f
  on i.film_id = f.film_id
  where 1=1
  and r.inventory_id = i.inventory_id
  and f.length > 100
)

INNER JOINで書いてみるとこんな感じかと思います。

select
  r.*
from
  rental r
inner join
  inventory i
on r.inventory_id = i.inventory_id
inner join
  film f
on i.film_id = f.film_id
where 1=1
and f.length > 100
;

PostgreSQLでは、SQLの頭にEXPLAIN ANALYZEをつけることで、実行計画を見ることができるので、それぞれ出力してみるとこうなりました。

EXISTS

                                                          QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
 Hash Semi Join  (cost=191.88..653.14 rows=9771 width=36) (actual time=4.001..12.163 rows=9723 loops=1)
   Hash Cond: (r.inventory_id = i.inventory_id)
   ->  Seq Scan on rental r  (cost=0.00..310.44 rows=16044 width=36) (actual time=0.014..2.067 rows=16044 loops=1)
   ->  Hash  (cost=157.00..157.00 rows=2790 width=4) (actual time=3.915..3.918 rows=2785 loops=1)
         Buckets: 4096  Batches: 1  Memory Usage: 130kB
         ->  Hash Join  (cost=74.11..157.00 rows=2790 width=4) (actual time=0.564..2.816 rows=2785 loops=1)
               Hash Cond: (i.film_id = f.film_id)
               ->  Seq Scan on inventory i  (cost=0.00..70.81 rows=4581 width=6) (actual time=0.016..0.662 rows=4581 loops=1)
               ->  Hash  (cost=66.50..66.50 rows=609 width=4) (actual time=0.512..0.514 rows=610 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 30kB
                     ->  Seq Scan on film f  (cost=0.00..66.50 rows=609 width=4) (actual time=0.007..0.354 rows=610 loops=1)
                           Filter: (length > 100)
                           Rows Removed by Filter: 390
 Planning Time: 2.396 ms
 Execution Time: 12.817 ms
(15 rows)

INNER JOIN

------------------------------------------------------------------------------------------------------------------------------
 Hash Join  (cost=172.62..596.63 rows=5343 width=36) (actual time=3.954..10.271 rows=9723 loops=1)
   Hash Cond: (r.inventory_id = i.inventory_id)
   ->  Seq Scan on rental r  (cost=0.00..310.44 rows=16044 width=36) (actual time=0.011..1.635 rows=16044 loops=1)
   ->  Hash  (cost=153.55..153.55 rows=1525 width=4) (actual time=3.892..3.895 rows=2785 loops=1)
         Buckets: 4096 (originally 2048)  Batches: 1 (originally 1)  Memory Usage: 130kB
         ->  Hash Join  (cost=70.66..153.55 rows=1525 width=4) (actual time=0.609..3.106 rows=2785 loops=1)
               Hash Cond: (i.film_id = f.film_id)
               ->  Seq Scan on inventory i  (cost=0.00..70.81 rows=4581 width=6) (actual time=0.013..0.632 rows=4581 loops=1)
               ->  Hash  (cost=66.50..66.50 rows=333 width=4) (actual time=0.572..0.574 rows=610 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 30kB
                     ->  Seq Scan on film f  (cost=0.00..66.50 rows=333 width=4) (actual time=0.012..0.409 rows=610 loops=1)
                           Filter: (length > 100)
                           Rows Removed by Filter: 390
 Planning Time: 1.070 ms
 Execution Time: 10.788 ms
(15 rows)

Rentalの件数が16000件しかないので、大した違いはありませんが、Planning TimeExecution TimeともにINNER JOINのSQLの方が速いことがわかります。

各SQLの実行計画で主に違う点は、一番上の計画のようです。

Hash Semi Join  (cost=191.88..653.14 rows=9771 width=36) # EXISTS
Hash Join  (cost=172.62..596.63 rows=5343 width=36)      # INNER JOIN
  • cost
    • 左側の数字: 初期処理の推定コスト。 出力段階が開始できるようになる前に消費される時間。
    • 右側の数字: 全体推定コスト。
  • rows: 行の推定数。
  • width: 行のバイト単位の長さ。

ここでもコストが初期処理の推定コスト・全体推定コスト共にINNER JOINの方が小さく、これが処理時間の違いにつながったのかと思います。

rowsが異なるのは、あくまで推定値だからということでしょうか。

まとめ

環境構築して実行したSQLの実行計画より、INNER JOINの方がEXISTSより早いことを確認することができました。

ただINNER JOINだと、SQLを読む側にとって抽出条件と分かりにくいので、件数が少ないようなテーブルであれば、EXISTSの方がよいでしょう。ケースバイケースですね。

また、PostgreSQLのTutorialのデータ量だと、パフォーマンスも問題にならず、時間がかかると実感できなかったので、同様のことをする機会があれば、今度はデータ量をもっと多くして試してみたいと思います。

参考サイト

QUIC を試してみた

こんにちは masm11 です。 QUIC の RFC が出ましたね。

https://datatracker.ietf.org/doc/html/rfc9000

5月なので2ヶ月も前ですが… というわけで、今回は遅ればせながら QUIC を試してみたいと思います。

nginx を設定

まず、nginx を QUIC 対応版にします。Arch Linux を使っているので AUR からインストールします。

wget https://aur.archlinux.org/cgit/aur.git/snapshot/nginx-quic.tar.gz
tar zxf nginx-quic.tar.gz
cd nginx-quic 
makepkg -s
sudo pacman -U nginx-quic-1.21.0-3-x86_64.pkg.tar.zst

そして /etc/nginx/nginx.conf を設定します。

server ブロックに以下のように quic と http3 を指定します。

   server {
        listen       443 ssl http2;
        listen       [::]:443 ssl http2;
        listen       443 quic http3;
        listen       [::]:443 quic http3;

server ブロックに以下の一行を追加します。

       add_header Alt-Svc 'h3=":443"; ma=86400, h3-29=":443"; ma=86400';

これは「QUIC も使えるよ〜」という宣言です。

nginx を restart します。

sudo systemctl restart nginx

軽く確認してみます。

shiro:nginx % sudo ss -lnp | grep :443
udp   UNCONN 0      0             0.0.0.0:443              0.0.0.0:*     users:(("nginx",pid=21153,fd=9),("nginx",pid=21152,fd=9))
udp   UNCONN 0      0                [::]:443                 [::]:*     users:(("nginx",pid=21153,fd=10),("nginx",pid=21152,fd=10))
tcp   LISTEN 0      511           0.0.0.0:443              0.0.0.0:*     users:(("nginx",pid=21153,fd=7),("nginx",pid=21152,fd=7))
tcp   LISTEN 0      511              [::]:443                 [::]:*     users:(("nginx",pid=21153,fd=8),("nginx",pid=21152,fd=8))
shiro:nginx % 

TCP に加えて UDP も bind してますね。

ファイアウォールに穴を開ける

使っているツールによりますが、iptables ならこんな感じでできます。

sudo iptables -I INPUT -p udp --destination-port 443 -j ACCEPT

確認

ではブラウザからアクセスしてみましょう。

サーバで以下のように実行しておき、

sudo tcpdump -i ens3 -n -s 0 -w cap.dat port 443

Firefox からアクセスしてみます。

Firefox のデベロッパーツールで見ると、alt-svc ヘッダと HTTP/3 であることが確認できます。

f:id:masm11:20210716215941p:plain

また、tcpdump が生成した cap.dat を手元にコピーして wireshark で見ると、 QUIC で通信できていることが確認できます。

f:id:masm11:20210716215953p:plain

なお QUIC は、まずは TCP でアクセスし、UDP で接続できそうなら (alt-svc で指定されていたら) 次から UDP を使います。なので、最初のうちは TCP になります。 確認する際は多少まとまった通信をした方が良いです。

まとめ

今回は QUIC をテストしてみました。 最新技術っていいですよね 。

インゲージでは最新技術もわくわくなエンジニアを募集しています。以下のページからお願いします。

https://ingage.co.jp/recruit/

ChatOps: SlackからLambdaに連携してみる(4)

id:kizashi1122 です。

f:id:ingage:20210620201734p:plain

ひとまず Slack から Lambda を呼び出すところまでは終わりました。 (といってもURL確認用のリクエストを受け取っただけです)

ワークスペースにアプリをインストールする

まだコードは何も書いてないですが、Slack から使ってみたいのでインストールします。

f:id:ingage:20210628122716p:plain

クリックします。次の画面では「Accept」すればよいです。 チェックが入りましたね。

f:id:ingage:20210628122833p:plain

ではこの「echoapp」をSlackのチャンネルに追加してみましょう。最初はテスト用に作ったチャンネルで試すのがいいかもしれません。ここでは #app-test としています。

f:id:ingage:20210628123503p:plain

上記の「Add an App」をクリックして「echoapp」を選択して追加します。

f:id:ingage:20210628123608p:plain

追加されました。ではメンションしてみましょう。

f:id:ingage:20210628124724p:plain

何も起きません・・・。
そもそも Lambda まで届いているのかすらわかりません。ログを確認しましょう。

ここから確認できます。

f:id:ingage:20210628124903p:plain

タイムスタンプを見るにちゃんと届いてそうです。ログを出してみます。Ruby なので p で引数の event を標準出力に出力してみるだけです。
ログ出力コードを入れてデプロイし、更にメンションしてみます。

同じようにログを確認し、さらにリンクを飛んで CloudWatch Logs までいきます。

f:id:ingage:20210628130034p:plain

へー、イベントの中身ってこうなってるのねーというのがよくわかります。

中で気になるものを赤枠で囲ってます。1つ目が token です。簡単なリクエストチェック(Slack からのリクエストであるということの検証)に使えます。
2つ目はエコーする内容です。ここをそのまま返せばエコーできそうだなとわかります(このときは「@echoapp hello2」と投稿してました)。

1つ目について、トークンの一致性を見るだけなら簡単なので実装しておきましょう。本当は署名チェックをするのがよいようです。

Verifying requests from Slack | Slack

Slack から送られるトークンについてはここにあります。

f:id:ingage:20210628131244p:plain

これを Lambda 側の環境変数にいれておき(画面省略)、コード上ではこう比較すればよいでしょう。

require 'json'

def lambda_handler(event:, context:)
    # p event
    req = JSON.parse(event['body'])
    if ENV['SLACK_VERIFICATION_TOKEN'] != req['token']
        return { statusCode: 401, body: "VERIFICATION TOKEN UNMATCHED" }
    end
    
    { statusCode: 200, body: 'done' }
end

さて、次はいよいよLambda側からエコーする部分を作りましょう。

続きはこちら。

ChatOps: SlackからLambdaに連携してみる(5) - インゲージ開発者ブログ