全力で怠けたい

怠けるために全力を尽くしたいブログ。

Slack メッセージの書式設定に再入門したメモ。

f:id:ebc_2in2crc:20200720232123p:plain

Slack のメッセージの書式設定は Markdown ぽいやつで雰囲気で書いてたけど雰囲気で再入門したのでメモしておく。

Slack メッセージの書式設定

Slack のメッセージの書式は Markdown ぽいやつで設定したりショートカットで設定したり GUI で設定したりできるけど、Markdown ぽいやつだといい感じに箇条書きが書けなかったりするみたいなのでそのへんの書式を設定するやり方を調べたのを書いていく。

Slack は Mac のデスクトップ版でバージョン 4.7.0 を使ってる。

最初にやっておくこと

環境設定 > 詳細設定 を表示して マークアップでメッセージを書式設定する のチェックを外しておく。

このあとの書式設定のやり方は マークアップでメッセージを書式設定する のチェックが入ってないのが前提にしてる。 マークアップでメッセージを書式設定する にチェックを入れないほうがいい感じに書式設定ができるっぽいので (たぶん)

太字

テキストを太字にするには対象のテキストを * (アスタリスク) で囲む。

斜体

テキストを太字にするには対象のテキストを _ (アンダースコア) で囲む。

取り消し線

取り消し線をテキストに付けるには対象のテキストを ~ (チルダ) で囲む。

インラインコード

インラインコードは ``` (バッククォート x3) でインラインコードを始めて ``` (バッククォート x3) でインラインコードを終わる。 Shift + Enter でインラインコードを次の行に継続する。

引用タグ

引用タグはテキストの先頭を > にする。 Shift + Enter で引用タグを次の行に継続する。

順序リスト

テキストの先頭に 1. と半角スペースを入れる。 Shift + Enter で順序リストを次の行に継続する。

箇条書き

テキストの先頭に * と半角スペースを入れる。 Shift + Enter で箇条書きを次の行に継続する。

参考サイト

特定のポートを使ってるプロセスを lsof コマンドで確認する方法。

特定のポートを使ってるプロセスを lsof コマンドで確認する方法をメモしておく。

特定のポートを使ってるプロセスを確認する方法

lsof コマンドを使って特定のポートを使ってるプロセスを確認する方法を書いていく。

lsof コマンド

lsof コマンドは man lsof すると lsof - list open files と表示するとおりオープンしているファイルを表示するコマンド。

Linux はいわゆるファイル以外にもネットワークソケット、デバイスドライバーとかプロセス情報などもファイルとして扱うので、オープンしているファイルとそのファイルをオープンしているプロセスを調べることで特定のポートを使ってるプロセスを調べることができる。

lsof コマンドは -i オプションを指定するとネットワークソケットに関するプロセスだけを表示するので主に -i オプションの指定方法を書いていく。

lsof コマンドのバージョン。

$ lsof -v
lsof version information:
    revision: 4.91
    latest revision: ftp://lsof.itap.purdue.edu/pub/tools/unix/lsof/
    latest FAQ: ftp://lsof.itap.purdue.edu/pub/tools/unix/lsof/FAQ
    latest man page: ftp://lsof.itap.purdue.edu/pub/tools/unix/lsof/lsof_man
    configuration info: libproc-based
    Anyone can list all files.
    /dev warnings are disabled.
    Kernel ID check is disabled.

lsof コマンドの表示項目

lsof コマンドの表示項目について軽くメモしておく。

$ lsof | head -1
COMMAND    PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME

それぞれの項目の意味を書いていく。

項目 意味
COMMAND ファイルを開いてるプロセスのコマンド名。e.g. Google, com.docke
PID プロセス ID
USER ユーザー名
FD ファイルディスクリプタ
TYPE 種類。e.g. IPv4, IPv6, unix
DEVICE バイス
SIZE/OFF
NODE プロトコル。e.g. TCP, UDP
NAME ファイル名あるいはポート

-i:<ポート番号> で 特定のポートを使ってるプロセスを確認

-i オプション で -i:<ポート番号> みたいにすると特定のポート番号を使ってるプロセスだけを表示する。

たとえば 8080 番ポートを使ってるプロセスを確認するなら lsof -i:8080 みたいに実行する。

$ lsof -i:8080
COMMAND    PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Google     496  who   21u  IPv6 0xbf915afa7c00c5e1      0t0  TCP localhost:59317->localhost:http-alt (ESTABLISHED)
Google     496  who   22u  IPv6 0xbf915afa7c00d221      0t0  TCP localhost:59318->localhost:http-alt (ESTABLISHED)
com.docke 1511  who   51u  IPv6 0xbf915afa9f554121      0t0  TCP *:http-alt (LISTEN)
com.docke 1511  who   52u  IPv6 0xbf915afa7c00cc01      0t0  TCP localhost:http-alt->localhost:59317 (ESTABLISHED)
com.docke 1511  who   53u  IPv6 0xbf915afa7c00b9a1      0t0  TCP localhost:http-alt->localhost:59318 (ESTABLISHED)

-i:<サービス名> で 特定のポートを使ってるプロセスを確認

ウェルノウンポートが割り当てられてるサービスは -i オプションで -i:<サービス名> みたいにサービス名を指定することもできる。

たとえば ssh (22 番ポート) を使ってるプロセスを確認するなら lsof -i:ssh みたいに実行する。 ウェルノウンポートはポート番号とサービス名のマッピングはわりと自明なのと自分が使ってるプログラムはウェルノウンポートとは違うポートを使ったりするので -i オプションをこの使い方で使うことはあまりない。

$ lsof -i:ssh
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ssh     17350  who    3u  IPv4 0xbf915afa83a0c291      0t0  TCP xxx.xxx.xxx.xxx:59395->ec2-yyy-yyy-yyy-yyy.ap-northeast-1.compute.amazonaws.com:ssh (ESTABLISHED)

-n オプション: 名前解決をしない

lsof コマンドはデフォルトで接続先ホストの名前解決をするけど -n オプションを指定すると名前解決しなくなる。 lsof コマンドが名前解決しなくなる分、lsof コマンドの実行速度が速くなる (かもしれない)

-n オプションを指定しないと接続先ホストが ec2-yyy-yyy-yyy-yyy.ap-northeast-1.compute.amazonaws.com みたいに名前解決して表示する。

$ lsof -i:22
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ssh     17350  who    3u  IPv4 0xbf915afa83a0c291      0t0  TCP xxx.xxx.xxx.xxx:59395->ec2-yyy-yyy-yyy-yyy.ap-northeast-1.compute.amazonaws.com:ssh (ESTABLISHED)

-n オプションを指定すると接続先ホストが yyy-yyy-yyy-yyy みたいに IP アドレスが表示する。

$ lsof -n -i:22
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ssh     17585  who    3u  IPv4 0xbf915afa960b1291      0t0  TCP xxx.xx.xxx.xxx:59623->yyy.yyy.yyy.yyy:ssh (ESTABLISHED)

-P オプション: ポート番号をサービス名に変換しない

lsof コマンドはデフォルトでポート番号をサービス名に変換して表示するけど -P オプションを指定するとポート番号をサービス名に変換しなくなる。 lsof コマンドがポート番号をサービス名に変換しなくなる分、lsof コマンドの実行速度がチョット速くなる (かもしれない)

-P オプションを指定しないと22番ポートが ssh みたいにサービス名に変換して表示する。

$ lsof -n -i:22
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ssh     17585  who    3u  IPv4 0xbf915afa960b1291      0t0  TCP xxx.xx.xxx.xxx:59623->yyy.yyy.yyy.yyy:ssh (ESTABLISHED)

-P オプションを指定すると22番ポートがそのまま 22 と表示する。 個人的にはポート番号はサービス名に変換しないでそのまま表示するほうが分かりやすいので -P オプションはデフォで指定してる。

$ lsof -P -i:22
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ssh     17585  who    3u  IPv4 0xbf915afa960b1291      0t0  TCP xxx.xxx.xxx.xxx:59623->ec2-yyy.yyy.yyy.yyy.ap-northeast-1.compute.amazonaws.com:22 (ESTABLISHED)

-i オプションの他の使い方

-i オプションは -iUDP みたいにして UDP のポートを使ってるプロセスだけ表示するとか -i@ec2-yyy-yyy-yyy-yyy.ap-northeast-1.compute.amazonaws.com みたいにして接続先ホストが ec2-yyy-yyy-yyy-yyy.ap-northeast-1.compute.amazonaws.com のプロセスだけ表示するとか -i6 みたいにして IPv6 を使ってるプロセスだけ表示するとかできてかなり便利。

参考サイト

Pixela の CLI ツール pa を作りました。

PixelaCLI ツール pa を作りました。

github.com

pa とは?

paPixelaCLI ツールです。

pa はシェルの補完機能を使ってコマンドとかフラグをサクサクと軽快に入力できて PIxela サービスをとても簡単に利用できます。あと、タイプが苦手でタイプミスが多いという人は結構いると思いますが (自分はタイプがかなり苦手です)、シェルの補完が効くのでタイプ量が必要最小限に抑えることができてタイプミスのイライラから解放されます!

シェルの補完は pa completion zsh みたいなコマンドでシェルの補完スクリプトを生成したのをシェルに読み込ませて利用することができます。 シェルの補完スクリプトの生成は Zsh, Bash, Fish と PowerShell の補完スクリプトの生成に対応しています。

シェルの補完のデモを GIF にしてみました。なんとなくですが雰囲気が伝わると思います。

demo

Pixela とは?

任意の数値を登録してアレのあれっぽくグラフを作れるクールな API サービスです。

詳しくは↓

pixe.la blog.a-know.me

使い方

PIxela の API

こんな感じで使います。

フラグが結構多いですがシェルの補完が効くのでサクサク入力できます。

$ pa graph create \
    --id=your-graph-id \
    --name=your-graph-name \
    --type=int \
    --unit=count \
    --color=ichou

$ pa graph get | jq
{
  "graphs": [
    {
      "id": "your-graph-id",
      "name": "your-graph-name",
      "unit": "count",
      "type": "int",
      "color": "ichou",
      "timezone": "",
      "purgeCacheURLs": null,
      "selfSufficient": "none",
      "isSecret": false,
      "publishOptionalData": false
    }
  ]
}

現時点の Pixela の最新バージョンの v1.18.0 のすべての API をサポートしています。

Pixela のユーザー名とトーク

Pixela のユーザー名は --username フラグで指定して Pixela のトークンは --token フラグで指定します。

$ pa graph get --username=yourname --token=thisissecret

Pixela のユーザー名とトークンは環境変数で指定することもできます。

Pixela のユーザー名は PA_USERNAME 環境変数で指定して Pixela のトークンは PA_TOKEN フラグで指定します。

$ export PA_USERNAME=yourname
$ export PA_TOKEN=thisissecret
$ pa graph get

Pixela のユーザー名とトークンは設定ファイルで指定することもできます。 v1.1.0 以降が設定ファイルに対応しています。

$ cat ~/.pa
username = "yourname"
token = "thisissecret"
$ pa graph get

pa は次の優先順位でユーザー名とトークンを使用します。 それぞれの項目はその下の項目よりも優先されます。

シェルの補完スクリプトの生成

Zsh, Bash, Fish, PowerShell の補完スクリプトを生成して利用できます。

$ pa completion <SHELL> > /path/to/completion

Zsh を使っているならこんな感じのコマンドでシェルの補完が効くようになります。

$ pa completion zsh > ~/.zsh/completions/_pa
$ . ~/.zsh/completions/_pa

インストール方法

Homebrew

$ brew tap ebc-2in2crc/tap
$ brew install pa

Developer

$ go get -u github.com/ebc-2in2crc/pa/...

手動でのインストール

https://github.com/ebc-2in2crc/pa/releases からダウンロードした zip ファイルを展開した中にあるファイルをパスの通ったディレクトリに入れる。

動機

Pixela はシンプルでいろんなことに応用できるとても便利な API サービスです。

普段は pixela4go という Pixela の Go クライアントを使って Pixela のサービスを利用しているのですが「ちょっとしたことを試したり確認するときは CLI があると楽だよなー」と以前から結構感じていました。

実は Pixela は pi という公式 CLI ツールがあります。pi は Pixela を薄くラップした CLI ツールでコマンド体系が分かりやすくて自分もとても便利に使っているのですが、自分はかなりタイプが苦手なこと & pi はシェルの補完が効かないことが少し使いにくさを感じていました。 そんなときにふと、シェルの補完が効かないならシェルの補完が効くようにすればいいじゃん => Pixela との通信部分は pixela4go を使えば CLI 部分を実装するだけでよさそう => 作ってみよう! みたいな勢いで作りました。

pa という名前

コマンドの名前が長いとタイプするのが面倒くさいので短い名前にしようと最初から決めていて、Pixela の先頭1文字と末尾1文字をそれぞれもらって pa という名前にしました。

まとめ

バグとかあると思いますし「こーしたら使いやすくなる」とかあったら issue とか PR 作ったり声かけてもらえると嬉しいです。

ということで Pixela の CLI ツール pa の紹介でした。

AWS CLI v2 のぺージャーを無効にする方法。

AWS CLI v2 のぺージャーを無効にする方法をメモしておく。

AWS CLI のバージョン。

$ aws --version
aws-cli/2.0.30 Python/3.7.4 Darwin/19.5.0 botocore/2.0.0dev34

$ docker container run --volume ~/.aws:/root/.aws --rm --env AWS_PAGER= amazon/aws-cli --version
aws-cli/2.0.30 Python/3.7.3 Linux/4.19.76-linuxkit botocore/2.0.0dev34

AWS CLI v2 のぺージャーを無効にする方法

AWS CLI v2 はデフォルトでページャーを使うようになった

AWS CLI バージョン 2 では、クライアント側のページャープログラムを出力に使用できます。デフォルトでは、この機能はオペレーティングシステムのデフォルトのページャープログラムを介してすべての出力を返します。

AWS CLI v1 はデフォルトではページャーは使わなかったけど AWS CLI v2 はデフォルトで OS のデフォルトのページャーを使うようになった。

個人的にはページャーが必要なときは自分で使うから AWS CLI v2 の仕様変更はあまり嬉しみがないというかむしろ余計なお世話のように感じるけど、変わってしまったものは仕方ないので AWS CLI v2 がページャーを使わなくしていく。

config ファイルでページャーを無効にする

AWS CLI v2 はページャーを config ファイルの cli_pager オプションに設定しておくとそのページャーを使って出力するけど cli_pager オプションを空の文字列にしておくとページャーを使わなくなる。

[default]
cli_pager=

config ファイルを直接いじるかこんな感じのコマンドを実行すればいい。 自分が普段使ってる環境は config を設定しておくのがよさそう。

$ aws configure set cli_pager ''

$ cat ~/.aws/config
[default]
cli_pager=
# 省略

環境変数ページャーを無効にする

自分が普段使ってる環境は config を設定しておくのが手軽だけどシェルスクリプトを作って他の人に使ってもらったり CI で動かすときはページャーの使用は config の設定に依存したくないと思うので環境変数ページャーを無効にしていく。

AWS CLI v2 はページャーAWS_PAGER 環境変数に設定しておくとそのページャーを使って出力するけど AWS_PAGER 環境変数を空の文字列にしておくとページャーを使わなくなる。

export AWS_PAGER= みたいな感じで AWS_PAGER 環境変数をエクスポートしておくとよい。

$ export AWS_PAGER=
$ aws ec2 describe-regions

環境変数を一時的に設定してページャーを無効にする

AWS_PAGER をエクスポートして環境を汚したくないときは環境変数を一時的に設定してページャーを無効にしていく。

$ env AWS_PAGER= aws ec2 describe-regions

AWS CLI v2 を Docker で使ってるなら docker container run するときに AWS_PAGER 環境変数--env AWS_PAGER= みたいに指定していく。

$ docker container run --volume ~/.aws:/root/.aws --rm --env AWS_PAGER= amazon/aws-cli ec2 describe-regions

以上。

参考サイト

標準入力を Docker コンテナに接続する方法。

標準入力を Docker コンテナに接続する方法のメモ。

標準入力を Docker コンテナに繋ぐ方法

結論

一言で書いてしまうと docker container run とか docker container exec するときに -i フラグを指定すると標準入力が Docker コンテナに接続する。

実際にやってみる

こんな感じの Dockerfile を用意しておく。

FROM alpine:3.12

CMD ["cat"]

ざっくりと Dockerfile をビルドする。

$ docker image build -t docker-cat .

docker container run-i フラグを指定しないで実行すると標準入力は Docker コンテナに接続しない。

$ echo 'Hello World!' | docker container run --rm docker-cat

# 何も出力しない

docker container run-i フラグを指定して実行すると標準入力は Docker コンテナに接続する。

$ echo 'Hello World!' | docker container run --rm -i docker-cat
Hello World!

以上。


-it みたいな感じで -i フラグと -t をセットで指定しちゃうことが多くて -i フラグの意味がよく分かってなかったけど今は分かってると思う。help を見るの大事。

$ docker container run --help

Go のテンプレートの使い方のメモ。

Go のテンプレートを軽く調べたのでメモ。

Go のテンプレート

とりあえず使ってみる

Go のテンプレートはこんな感じで使う。

package main

import (
    "os"
    "text/template"
)

type Fruit struct {
    Name  string
    Count int
}

func main() {
    text := `持っている果物: {{.}}`
    tmpl, err := template.New("test").Parse(text)
    if err != nil {
        panic(err)
    }

    fruit := Fruit{
        Name:  "リンゴ",
        Count: 1,
    }
    err = tmpl.Execute(os.Stdout, fruit)
    if err != nil {
        panic(err)
    }
}

// 出力
// 持っている果物: {リンゴ 1}

テンプレートはこんな感じでデータに適用することで実行する。 テンプレートはデータに適用しなくてもいいけど普通はテンプレートを使うときはなにかデータに適用すると思う。

さっきはテンプレートを Fruit 構造体に適用したけどテンプレートは構造体だけじゃなくて配列、スライス、マップ、チャネルとかにも適用できる。 たとえばスライスにはこんな感じで適用する。

package main

import (
    "os"
    "text/template"
)

func main() {
    text := `持っている果物: {{.}}`
    tmpl, err := template.New("test").Parse(text)
    if err != nil {
        panic(err)
    }

    fruits := []Fruit{
        {Name: "リンゴ", Count: 1},
        {Name: "マンゴー", Count: 2},
    }
    err = tmpl.Execute(os.Stdout, fruits)
    if err != nil {
        panic(err)
    }
}

// 出力
// 持っている果物: [{リンゴ 1} {マンゴー 2}]

さっきのコードの {{.}} はテンプレート内で使えるアクションの1つ。 さっきのコードはスライスをそのまま参照して出力したけど、アクションを使うと適用したデータの要素たとえば構造体のフィールドとかマップのキーとかを参照したりテンプレートの実行を制御できる。

さっきのコードのテンプレート内でアクションをこんな感じに使うと構造体のスライスの要素それぞれを参照して出力とかができる。

テンプレートをデータに適用するとデータが走査されていってデータの現在位置は . (ドット) で参照できる。 テンプレート内のアクションを使って条件分岐したりデータを繰り返し処理して目的の出力を得ていくので、だいたいテンプレートの使い方を学ぶ = アクションの使い方を学ぶ感じ。

text := `{{range $index, $element := .}}
{{- .Name}} は {{.Count}} 個あります。
{{end}}`

// 出力
// リンゴ は 1 個あります。
// マンゴー は 2 個あります。

Actions: アクション

アクションは適用したデータの要素たとえば構造体のフィールドとかマップのキーとかを参照したりテンプレートの実行を制御する。

アクションは {{}} で囲んで記述する。 さきほどのコードは {{range $index, $element := .}}, {{.Count}} とか {{end}} がアクション。

テンプレート内のアクションじゃないところはそのまま出力するけど、アクションの左の区切り文字 {{ の直後にマイナス記号と半角スペース (-) を記述するとアクションの直前のテキストの末尾のホワイトスペースをトリムする。同じようにアクションの右の区切り文字 }} の直前に半角スペースとマイナス記号 (-) を記述するとアクションの直後のテキストの末尾のホワイトスペースをトリムする。

text := `{{23}} < {{45}}
{{23 -}} < {{- 45}}`

// 出力
// 23 < 45
// 23<45

アクションはたくさんあるのでそれぞれ書いていく。

comment (コメント)

{{/* a comment */}} と記述したところは出力しない。 コメントは複数行にすることもできる。

text := `{{/* コメントは出力しない */}}
Hello Golang!
{{- /* コメントは
複数行もできる */}}
Hello Template!
`

// 出力
// Hello Golang!
// Hello Template!

{{ pipeline}} はパイプライン (pipeline) をデフォルトで fmt.Print で出力する。 パイプラインは結構いろいろ書けるので後で少し詳しく書く。

text := `{{.}}
{{"リテラルを出力"}}
`

// 出力
// [{リンゴ 1} {マンゴー 2}]
// リテラルを出力

{{if pipeline}} T1 {{end}} は パイプラインがゼロ値のときは何も出力しない。パイプラインがゼロ値じゃなかったら T1 を出力する。

text := `{{if ""}}出力しない{{end}}
{{if "a"}}出力する{{end}}
`

// 出力
// 出力する

{{if pipeline}} T1 {{else}} T0 {{end}} はパイプラインがゼロ値のときは T0 を出力してパイプラインがゼロ値じゃなかったら T0 を出力する。

text := `{{if ""}}出力しない{{else}}"" はゼロ値{{end}}
{{if "a"}}"a" はゼロ値じゃない{{else}}{{end}}
`

// 出力
// "" はゼロ値
// "a" はゼロ値じゃない

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}{{if}} - {{else} に別の if を入れこんだやつ。

text := `
{{if ""}}出力しない
{{else if 0}}出力しない
{{else}}"" と 0 はゼロ値
{{end}}`

// 出力
// "" と 0 はゼロ値

{{range pipeline}} T1 {{end}} はパイプラインの要素を繰り返し処理する。

パイプラインは配列、スライス、マップ、あるいはチャネルのいずれかである必要がある。 パイプラインの長さゼロがゼロのときは何も出力しない。 パイプライン要素が繰り返し処理するときドット (.) は現在処理している要素を示す。

text := `{{range .}}{{.}}, {{end}}`
_ = tmpl.Execute(os.Stdout, []int{1, 2, 3})

// 出力
// 1, 2, 3, 

{{range pipeline}} T1 {{else}} T0 {{end}}{{range}} と同じだけどパイプラインの要素がないときは T0 を出力する。

text := `{{range .}}{{.}}, {{else}}パイプラインの要素がない{{end}}`
_ = tmpl.Execute(os.Stdout, nil)

// 出力
// パイプラインの要素がない

{{template "name"}} はテンプレートを名前で指定して nil データに適用と実行する。

text := `{{define "foo"}}*** 事前に定義したテンプレート ***{{end}}
{{template "foo"}}
{{template "foo"}}`

// 出力
// *** 事前に定義したテンプレート ***
// *** 事前に定義したテンプレート ***

{{template "name" pipeline}} はテンプレートを名前で指定してドット (.) をパイプラインにセットして適用と実行する。

text := `{{define "foo"}}{{range $index, $element := .}}{{$element}}, {{end}}{{end}}
{{template "foo" .}}
{{template "foo" .}}`
_ = tmpl.Execute(os.Stdout, []int{1, 2, 3})

// 出力
// 1, 2, 3, 
// 1, 2, 3, 

{{block "name" pipeline}} T1 {{end}}{{define "name"}} T1 {{end}}{{template "name" pipeline}} を実行する短縮記法。

text := `{{block "foo" .}}*** 事前に定義したテンプレート ***{{end}}`

// 出力
// *** 事前に定義したテンプレート ***

{{with pipeline}} T1 {{end}}{{if}} とだいたい同じだけどパイプラインがドット (.) にセットして実行する。

text := `{{range $index, $element := . -}}
{{with $element}}{{.}} はゼロ値じゃない{{end}}
{{end}}`
_ = tmpl.Execute(os.Stdout, []int{0, 1, 0, 2})

// 出力
// 1 はゼロ値じゃない
// 2 はゼロ値じゃない

{{with pipeline}} T1 {{else}} T0 {{end}}{{with pipeline}} T1 {{end}} と同じだけどパイプラインがゼロ値のときは T0 を出力する。

text := `{{range $index, $element := . -}}
{{with $element}}{{.}} はゼロ値じゃない{{else}}ゼロ値{{end}}
{{end}}`
_ = tmpl.Execute(os.Stdout, []int{0, 1, 0, 2})

// 出力
// ゼロ値
// 1 はゼロ値じゃない
// ゼロ値
// 2 はゼロ値じゃない

Arguments: 引数

Arguments は単なる値でいろいろ入れられる。

定数は untyped constant になって nil は untyped nil になる。

text := `
{{print true}}
{{print "文字列"}}
{{print 1}}
{{print 1.5}}
{{print .}}
{{print nil}}`

_ = tmpl.Execute(os.Stdout, []int{0, 1, 0, 2})

// 出力
// true
// 文字列
// 1
// 1.5
// [0 1 0 2]
// <nil>

ドット (.) はその時のドットの値。

text := `{{.}}`
_ = tmpl.Execute(os.Stdout, []int{0, 1, 0, 2})

// 出力
// [0 1 0 2]

$ で始まる変数名。 変数は $ という名前もつけられる。

text := `{{$name := "Alice"}}
{{$ := "Bob"}}
Hello {{$name}}!
Hello {{$}}!`

// 出力
// Hello Alice!
// Hello Bob!

構造体のフィールド。

.Field の形式で記述する。 .Field1.Field2 みたいにネストして参照できて $x.Field1.Field2 みたいに変数のフィールドも参照できる。

text := `{{.Name}} は {{.Count}} 個あります。`
fruit := Fruit{Name: "リンゴ", Count: 1}
_ = tmpl.Execute(os.Stdout, fruit)

// 出力
// リンゴ は 1 個あります。

マップのキー。

.Key の形式で記述する。 .Field1.Key1.Field2.Key2 みたいにネストして参照できて $x.key1.key2 みたいに変数のキーの値も参照できる。

text := `リンゴは {{.リンゴ}} 個あります。
マンゴーは {{.マンゴー}} 個あります。`

m := map[string]int{
    "リンゴ":  1,
    "マンゴー": 3,
}
_ = tmpl.Execute(os.Stdout, m)

// 出力
// リンゴは 1 個あります。
// マンゴーは 3 個あります。

引数のないメソッド。

メソッドは1つか2つの戻り値を返す。 .Field1.Key1.Method1.Field2.Key2.Method2 みたいにネストしてメソッドを呼び出せる。 2つの戻り値を返すときは2つ目の戻り値は error で、2つめの戻り値が nil 以外のときはテンプレートの実行が止まってその erorr が Execute() の戻り値になる。

type Fruit struct {
    Name  string
    Count int
}

func (p Fruit) String() (string, error) {
    s := fmt.Sprintf("%s は %d 個あります。", p.Name, p.Count)
    return s, nil
}

text := `{{.String}}`
fruit := Fruit{Name: "リンゴ", Count: 1}
_ = tmpl.Execute(os.Stdout, fruit)

// 出力
// リンゴ は 1 個あります。

引数のない関数。

引数のない関数の戻り値に関しての振る舞いは引数のないメソッドの戻り値と同じ。

text := `{{print "Hello Template!"}}`

// 出力
// Hello Template!

カッコ ((, )) でグループ化ができる。

{{print or "" "a" "b"}} テンプレートはエラーになるけど以下のように or をカッコで囲ってグループ化するとちゃんと実行する。

text := `{{print (or "" "a" "b")}}`

// 出力
// a

Pipeline: パイプライン

パイプラインはコマンドを パイプ (|) で繋げたもの。

コマンドは単なる値、メソッドあるいは関数で、コマンドは1つまたは2つの値を出力する。 コマンドが2つめの値を出力するとき2つめの出力は erorr で、コマンドの2つめ出力が nil 以外のときはテンプレートの実行が止まってその erorr が Execute() の戻り値になる。

text := `{{"a" | printf "%s"}}
{{"a" | printf "%s" | printf "%q"}}`

// 出力
// a
// "a"

Variables: 変数

変数は {{$variable := pipeline}} の記述で宣言と初期化をする。

宣言済みの変数は {{$variable = pipeline}} の記述で値を割り当てられる。 変数のスコープは変数が {{if}}, {{with}} あるいは {{range}} で宣言すると対応する {{end}} までが変数のスコープになる。

text := `{{$val := "a"}}{{$val}}
{{$val = "b"}}{{$val}}`

// 出力
// a
// b

{{range $index, $element := pipeline}} の記述で pipeline がスライスのときはスライスのインデックスが1つめの変数 $index にセットしてスライスの要素が2つめの変数 $element にセットする。 {{range $index, $element := pipeline}} の記述で pipeline がマッのときはマップのキーが1つめの変数 $index にセットしてマップの要素が2つめの変数 $element にセットする。 このへんの変数名は $index とか $element じゃなくてもいい。

{{range $element := pipeline}} みたいに変数が1つのときは pipeline がスライスのときはスライスの要素が1つめの変数 $element にセットして、pipeline がマップのときはマップの要素が1つめの変数 $element にセットする。このへんの振る舞いは通常の for 文の range の振る舞いと逆なので注意。

Functions: 関数

関数は定義済みの関数とユーザー定義の関数がある。 定義済みの関数をそれぞれ軽く書いて、あとユーザー定義の関数の定義と使い方を書いていく。

定義済みの関数

and は引数のうち最初に見つかったゼロ値を返す。

ゼロ値の引数がないときは最期の引数を返す。

text := `and "a" true 0 1 => {{and "a" true 0 1}}
and "a" true 1 => {{and "a" true 1}}`

// 出力
// and "a" true 0 1 => 0
// and "a" true 1 => 1

call は最初の引数に残りの引数を渡して実行した結果を返す。

最初の引数は1つまたは2つの戻り値を返す関数であること。 関数が返す2つめの戻り値は erorr で、関数の2つめの戻り値が nil 以外のときはテンプレートの実行が止まる。 あと関数の実引数が仮引数と一致しないときもテンプレートの実行が止まる。

text := `{{call .add 1 2}}`

data := map[string]interface{}{
    "add": func(a, b int) int { return a + b },
}
_ = tmpl.Execute(os.Stdout, data)

// 出力
// 3

html は引数を HTML としてエスケープした結果を返す。

text := `{{html "<html>"}}`

// 出力
// &lt;html&gt;

index は最初の引数に残りの引数をインデックスとして適用した結果を返す。

最初の引数は配列、スライスまたはマップである必要がある。

text := `{{index . 2}}`
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c"})

// 出力
// c

slice は最初の引数を残りの引数でスライスした結果を返す。

最初の引数は配列、スライスまたは文字列である必要がある。

text := `{{slice . 1 3}}`
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c", "d"})

js は引数を JavaScript としてエスケープした結果を返す。

text := `{{js "<script>alert('hello');</script>"}}`

// 出力
// \x3Cscript\x3Ealert(\'hello\');\x3C/script\x3E

len は引数の長さを返す。

text := `{{len .}}`
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c", "d"})

// 出力
// 4

not は引数のブール値の否定を返す。

text := `{{not true}}`

// 出力
// false

or は引数のうち最初に見つかった非ゼロ値を返す。

非ゼロ値の引数がないときは最期の引数を返す。

text := `or "" false 1 0 => {{or "" false 1 0}}
or "" false 0 => {{or "" false 0}}`

// 出力
// or "" false 1 0 => 1
// or "" false 0 => 0

print, printlnprintf はそれぞれ fmt.Sprint, fmt.Sprintlnfmt.Sprintfエイリアス

text := `{{print "Hello Template"}}!
{{println "Hello Template"}}!
{{printf "Hello %s!" "Template" }}`

// 出力
// Hello Template!
// Hello Template
// !
// Hello Template!

urlquery は引数をクエリストリングとしてエスケープした結果を返す。

text := `{{urlquery "https://golang.org/?lang=ja"}}`

// 出力
// https%3A%2F%2Fgolang.org%2F%3Flang%3Dja
text := `eq true true => {{eq true true}}
ne true true => {{ne true true}}
lt 1 1 => {{lt 1 1}}
lt 0 1 => {{lt 0 1}}
le 1 1 => {{le 1 1}}
le 0 1 => {{le 0 1}}
gt 1 1 => {{gt 1 1}}
gt 2 1 => {{gt 2 1}}
ge 1 1 => {{ge 1 1}}
ge 2 1 => {{ge 2 1}}`

// 出力
// eq true true => true
// ne true true => false
// lt 1 1 => false
// lt 0 1 => true
// le 1 1 => true
// le 0 1 => true
// gt 1 1 => false
// gt 2 1 => true
// ge 1 1 => true
// ge 2 1 => true

ユーザー定義の関数

ユーザー定義の関数は template.FuncMaptemplate.Funcs() に渡すと使えるようになる。 template.Funcs(funcMap FuncMap)template.Parse() よりも前に呼び出す必要がある。

funcs := map[string]interface{}{
    "sum": func(numbers []int) int {
        total := 0
        for _, v := range numbers {
            total += v
        }
        return total
    },
}
text := `{{sum .}}`
tmpl, err := template.New("test").Funcs(funcs).Parse(text)
_ = tmpl.Execute(os.Stdout, []int{1, 2, 3, 4, 5})

// 出力
// 15

ファイルに保存しているテンプレートを読む

ここまではテンプレートはプログラムの中にハードコードしてたけど実際にテンプレートを使うときはテンプレートはファイルに保存しておいてプログラムからテンプレートを読み込んで使うことが多そう。 ファイルに保存しているテンプレートは template.ParseFiles() とか ParseGlob() で読み込んで使っていく。

tmpl, err := template.ParseFiles("header.template", "main.template", "footer.template")
if err != nil {
    panic(err)
}
err = tmpl.ExecuteTemplate(os.Stdout, "main", nil)
if err != nil {
    panic(err)
}

// 出力
// *** ヘッダー ***
// *** メイン ***
// *** フッター ***

テンプレートを保存してるファイルはこんな感じ。

// header.template
{{define "header"}}*** ヘッダー ***{{end}}

// main.template
{{define "main"}}
{{template "header"}}
*** メイン ***
{{template "footer"}}
{{end}}

// footer.template
{{define "footer"}}*** フッター ***{{end}}

text/template と html/template

text/templatehtml/template は同じインタフェースだけど HTML を出力するときはセキュリティ上の問題から html/template を使うように書いてある。

To generate HTML output, see package html/template, which has the same interface as this package but automatically secures HTML output against certain attacks.

参考ページ

CloudFormation のスタックテンプレートから Secrets Manager のシークレットを動的に参照するメモ。

CloudFormation のスタックテンプレートから Secrets Manager のシークレットを動的に参照するメモ。

CloudFormation のスタックテンプレートから Secrets Manager のシークレットを動的に参照する

CloudFormation のスタックテンプレートからスタックを作っていると必ず一度は秘密情報の扱いをどうするか? みたいな事態に直面する。 ここでは秘密情報というのは DB のパスワードとか Web サービスのAPI キーみたいな「他人に漏れると金銭的損害を始めとしていろんな損害しかも大きな損害を被りそうなすごく秘匿性の高い情報」とかの意味で使うけど、普通に考えると DB のパスワードとか Web サービスの API キーとかを CloudFormation のスタックテンプレートの中に埋め込むのは明らかにまずいと思う。

そういう秘密情報を CloudFormation のスタックテンプレートに使うときは AWS Systems Manager の SecureString パラメータを使う方法と AWS Secrets Manager のシークレットを使う方法があるけどほとんどのケースは AWS Secrets Manager のシークレットを使う方法で事足りるので大概は AWS Secrets Manager のほうを使ってる。 => 一応、秘密情報はパラメータで渡すこともできるけどスタックを作るたびに指定するのは面倒くさい

そのへんのことを軽くメモしていく。

Secrets Manager のシークレットの登録

CloudFormation スタックテンプレートから参照するシークレットは事前に Secrets Manager に登録しておく。

$ aws secretsmanager create-secret --name=db-root-account --secret-string='{"username":"this-is-username","password":"this-is-password"}'
{
    "VersionId": "eaaac565-8481-4e22-8ae1-2e1ba125a233",
    "Name": "db-root-account",
    "ARN": "<ARN>"
}

このあと CloudFormation スタックテンプレートからバージョンを指定してシークレットを取得していくのでシークレットを更新しておく => VersionId のとこがシークレットを登録したときから変わる

$ aws secretsmanager update-secret --secret-id=db-root-account --secret-string='{"username":"this-is-new-username","password":"this-is-new-password"}'
{
    "VersionId": "5606eb1c-2892-4721-9936-324527c366b1",
    "Name": "db-root-account",
    "ARN": "<ARN>"
}

シークレットのバージョンを指定しないでシークレットを取得すると VersionStagesAWSCURRENT のシークレットを取得する。 VersionId のとこがシークレットを更新したときの出力が一致している。

$ aws secretsmanager get-secret-value --secret-id=db-root-account --version-id=5606eb1c-2892-4721-9936-324527c366b1
{
    "Name": "db-root-account",
    "VersionId": "5606eb1c-2892-4721-9936-324527c366b1",
    "SecretString": "{\"username\":\"this-is-new-username\",\"password\":\"this-is-new-password\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": 1593245998.012,
    "ARN": "<ARN>"
}

シークレットを更新したときの VersionId を指定してシークレットを取得するとさきほどと同じシークレットを取得する。

$ aws secretsmanager get-secret-value --secret-id=db-root-account --version-id=5606eb1c-2892-4721-9936-324527c366b1
{
    "Name": "db-root-account",
    "VersionId": "5606eb1c-2892-4721-9936-324527c366b1",
    "SecretString": "{\"username\":\"this-is-new-username\",\"password\":\"this-is-new-password\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": 1593245998.012,
    "ARN": "<ARN>"
}

シークレットを最初に登録したときの VersionId を指定してシークレットを取得すると最初に登録したシークレットを取得する。

$ aws secretsmanager get-secret-value --secret-id=db-root-account --version-id=eaaac565-8481-4e22-8ae1-2e1ba125a233
{
    "Name": "db-root-account",
    "VersionId": "eaaac565-8481-4e22-8ae1-2e1ba125a233",
    "SecretString": "{\"username\":\"this-is-username\",\"password\":\"this-is-password\"}",
    "VersionStages": [
        "AWSPREVIOUS"
    ],
    "CreatedDate": 1593245628.992,
    "ARN": "<ARN>"
}

CloudFormation のスタックテンプレートからシークレットを参照

CloudFormation スタックテンプレートから Secrets Manager に登録しているシークレットを動的に参照していく。作成するリソースはなんでもいいんだけどすぐに作れてお金がかからないセキュリティグループを作成していく。

まずバージョンは指定しないでシークレットを参照していく。 こんな感じのスタックテンプレートを書いて CloudFormation スタックを作成していく。

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  HogeSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: dynamic refernces
      GroupName: hoge-security-group
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          Description: '{{resolve:secretsmanager:db-root-account:SecretString:username::}}'
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          Description: '{{resolve:secretsmanager:db-root-account:SecretString:password::}}'
      VpcId: <VPC ID>

CLI でセキュリティグループの定義を見てみると更新したあとのシークレットが取得しているのが分かる。

$ aws ec2 describe-security-groups --group-names=hoge-security-group | jq '.SecurityGroups[].IpPermissions[].IpRanges[].Description'
"this-is-new-username"
"this-is-new-password"

次はシークレットのバージョンを指定してシークレットを参照していく。 シークレットを最初に登録したときの VersionIdeaaac565-8481-4e22-8ae1-2e1ba125a233 を Description のとこに追加して CloudFormation スタックを作成していく。

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  HogeSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: dynamic refernces
      GroupName: hoge-security-group
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          Description: '{{resolve:secretsmanager:db-root-account:SecretString:username::eaaac565-8481-4e22-8ae1-2e1ba125a233}}'
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          Description: '{{resolve:secretsmanager:db-root-account:SecretString:password::eaaac565-8481-4e22-8ae1-2e1ba125a233}}'
      VpcId: <VPC ID>

CLI でセキュリティグループの定義を見てみると最初に登録したシークレットが取得しているのが分かる。

$ aws ec2 describe-security-groups --group-names=hoge-security-group | jq '.SecurityGroups[].IpPermissions[].IpRanges[].Description'
"this-is-username"
"this-is-password"

CloudFormation スタックテンプレートからはこんな感じで Secrets Manager に登録しているシークレットを取得できる。

気にしておきたいこと

シークレットのリファレンスパターン

CloudFormation スタックテンプレートから Secrets Manager に登録しているシークレットを参照するときは {{resolve:secretsmanager:secret-id:secret-string:json-key:version-stage:version-id}} の形式で参照していく。 resolve:secretsmanager の部分は固定で、それ以外の部分はそれぞれの意味と使い方を書いていく。

secret-id

secret-id のとこは Secrets Manager に登録しているシークレットの ID を指定する。シークレットの完全な ARN を指定して別の AWS アカウントのシークレットを参照することもできる。

さきほどの例は db-root-account を指定した。

secret-string

SecretString 固定。

json-key

値を取得するシークレットのキー名を指定する。 CloudFormation は json-key を省略するとシークレットテキスト全体を取得する。

さきほどの例は usernamepassword を指定してシークレットのユーザー名とパスワードがそれぞれセキュリティグループの ingress ルールの Description に埋め込んだやつ。 もしさきほどの例で json-key を省略すると {"username":"this-is-new-username","password":"this-is-new-password"} がセキュリティグループの ingress ルールの Description に埋め込む感じ。

version-stage

version-stage を指定して取得するシークレットのバージョンを指定できる。 version-stage は AWSCURRENT とか AWSPREVIOUS とかがある。

version-stage と verson-id はどちらか1つしか指定できないので version-stage を指定するときは version-id は指定できず、version-stage も version-id も指定しないときは AWSCURRENT の version-stage を指定したのと同じになる。

さきほどの1つめの CloudFormation スタックテンプレートは version-stage も version-id も指定しないので AWSCURRENT の version-stage を指定したのと同じつまり更新後のシークレット this-is-new-usernamethis-is-new-password を取得している。

version-id

version-id を指定して取得するシークレットのバージョンを指定できる。 version-stage と verson-id はどちらか1つしか指定できないので version-stage を指定するときは version-id は指定できず、version-stage も version-id も指定しないときは AWSCURRENT の version-stage を指定したのと同じになる。

さきほどの2つめの CloudFormation スタックテンプレートは最初にシークレットを登録したときの version-id を指定しているので最初に登録したときのシークレット this-is-usernamethis-is-password を取得している。

必要なアクセス許可

Secrets Manager に格納されているシークレットを取得するには取得するシークレットに対して GetSecretValue を呼び出すためのアクセス権を持っている必要がある。

動的参照の制限

CloudFormation テンプレートスタックから Secrets Manager に登録しているシークレットを取得するとき固有の制限ではなくて動的参照に共通している制限だけどいくつか制限があるので書いておく。

  • スタックテンプレートは動的参照を最大で60個までしか含めない
  • AWS::Include とか AWS::Serverless などのトランスフォームの場合、AWS CloudFormation はトランスフォームを呼び出す前には動的な参照を解決せず、動的参照のリテラル文字列をトランスフォームに渡す
  • カスタムリソースは Secrets Manager に登録しているシークレットは動的参照できない
  • CloudFormation は最終値としてバックスラッシュを含む動的な参照は解決できない

参考ページ