MacのCLI環境の構築を自動化する
前回、IDEをやめてvim-lspと独自コマンドに移行したことを紹介しましたが、あれだけだと少し問題があります。
PCを買い替えたときやクリーンインストールをしたときなどに同じ環境を簡単に構築することができないともう一度構築することが面倒になってしまい、インストールするだけで使えるIDEでいいやとなってしまいます。
そこで今回はAnsibleを使ってCLI環境の構築を自動化したときに簡単に使えるように意識したポイントを紹介します。
Ansible自体の使い方は紹介しませんので別の記事を参照してください。
自動化したコードは以下のリポジトリで公開しています。
ポイント
以下のことを意識して自動化しました。
- 詳細な手順が書かれたドキュメントを用意する
- できるだけ実行するコマンドを少なくする
- アップデートしやすくする
- 別のOSでも動くようにする
1つずつ紹介していきます。
詳細な手順が書かれたドキュメントを用意する
いくら自動化していても、どのように実行すればいいか忘れてしまうと構築することができません。自動化した直後は覚えていても時間が経てばほぼ確実に忘れます。そのため自動化のキック方法をREADMEなどに書いておくべきです。
また、自動化に使っている依存ツール(今回であればAnsible)があればそれのインストール方法も書いておくと良いでしょう。その他にも前提条件があれば書いておきます。
READMEだけで構築が完了する粒度を意識します。
できるだけ実行するコマンドを少なくする
完全な自動化は難しいところもありますが、できる限り自動化して少ないコマンドで構築が終わるようにします。
このときにインストール対象に合わせていくつかのコマンドに分けることをオススメします。
これをやっておくことで構築するときに、そのとき必要なものだけインストールすることができます。
さらに、OSのアップデートなどで自動化が壊れたときにデバッグと修復が行いやすくなります。
アップデートしやすくする
構築後に新たな拡張をした場合、次に構築するときに追加した部分も反映されて欲しいです。そのためには自動化した部分を適宜アップデートする必要があります。
アップデートするときに手間がかかるような状態だと面倒になり放置してしまい、最終的にはアップデートする必要があることを忘れます。
これはもったいないのでアップデートしやすくしておきます。
具体的にやることとしては、
with_items
で重複の排除と項目の列挙- 複数のタスクで読み込まれる項目は
include_vars
で別ファイルから読み込むregister
で変数にする
を行いました。
これを行うと定義が見やすくなるだけでなく、実行ログに項目が表示されるようになるためエラーとなった項目を素早く見つけることができます。
また、各タスクにきちんとname
を定義しておくこともメンテナンス性の向上につながります。
別のOSでも動くようにする
インストールコマンドで各OSのパッケージマネージャを使うなど、OSごとに処理が分かれる部分がでてくると思います。
1つの環境でしか動かさないのであれば問題ありませんが、CLI環境はOSによる差を吸収しやすい部分ですし、サーバーにインストールする可能性も考慮するとどのOSでも構築できるようにしておくと便利だと思います。
OSによる分岐を行うには、タスクにwhen
を追加しOSの判定をすれば良いです。
Macの場合はansible_os_family == 'Darwin'
と書けば判定できます。
OSによる違いがたくさんある場合はOSごとにファイルを用意してinclude
に対してwhen
を使ってまとめて切り替えたりもできます。
おわりに
今回はCLI環境の構築を自動化するテクニックを紹介しました。
CLI環境は自分に一番合った設定にカスタマイズできる反面、インストーラーやインポート機能のようなものはなく、再度構築するのに時間がかかったり最悪の場合は再現できなくなってしまうリスクもあります。このようなことにならないためにも自動化をしておくことがオススメです。
自動化しておくことでサーバーで作業するときにも気軽に同じ環境を構築できパフォーマンスもあがります。
IDEをやめてvim-lspに移行したのでCLI環境を整える
先日メインで使っているマシンを修理にださなければいけなくなり、1週間ほど古いスペックの低いマシンを使うことになりました。
その環境では複数個のIDEを起動しておくのが難しく困っていたところLanguage Server Protocolの存在を思い出し使ってみることにしました。
私はIDE以外で使っているエディタはvimのため、vim-lspを入れました。
LSP
LSPの説明や導入方法は他の記事に譲りますが、IDEが持っている言語に関する機能である
- 関数や変数の補完、リネーム
- 定義元や宣言元へのジャンプ
- エラーやワーニングの表示
- フォーマット
- ドキュメントの表示
などがあり、基本的な開発なら問題なく行えます。
実際に開発していると物足りなくなってくる
いざ開発をしてみるとIDEでの開発と比較して不便なところがあります。
- プロジェクト内を対象としたファイル名とファイル内のテキストに対する検索が簡単にできない
- おおよその場所しかわからない目当てのファイルを見つけるのが遅い
- プロジェクト内の特定のファイルを対象にした任意のコマンドの実行が面倒
これらを解決するためにCLI環境を整えたので紹介します。
なお、これから紹介する設定はGitHubで公開しているため、適宜参照してください。
プロジェクト内の検索
これはIDEで最もよく使うコマンドと言っても過言でないぐらい頻繁に使うものなので、IDEと同じレベルのスムーズさが欲しいところです。
簡単なコマンドで検索を開始してプレビューを見ながら選択し、選んだファイルをエディタで開きたいです。
そこで高速な検索を行えるripgrepとCLIでインクリメンタルに曖昧検索ができるfzfを導入することにしました。CLIで曖昧検索をするツールは他にも存在しますが、fzfはvimから使うことができるのとプレビュー機能があることからfzfにしました。
以下のような設定をするとIDEのような検索が行えます。
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } Plug 'junegunn/fzf.vim' command! -bang -nargs=* Rg \ call fzf#vim#grep( \ 'rg --column --line-number --no-heading --color=always --smart-case '.shellescape(<q-args>), 1, \ <bang>0 ? fzf#vim#with_preview('up:60%') \ : fzf#vim#with_preview('right:50%:hidden', '?'), \ <bang>0) command! -bang -nargs=? -complete=dir Files \ call fzf#vim#files(<q-args>, fzf#vim#with_preview(), <bang>0) nnoremap <Leader>f :Files<CR> nnoremap <Leader>s :Rg<CR> nnoremap <Leader>b :Buffer<CR>
これでIDEのような検索が行えるようになりました。基本的にはIDEと同等の速度でスムーズに使えますが、プロジェクトが巨大になると検索に時間がかかるようになりCPUの負荷が高まります。これはインデックスされたものから検索しているIDEには敵いません。
vim以外からも使えるようにする
この検索はただのコマンドの組み合わせなのでvimを経由しなくても使うことができます。
以下のようにzshrcに書くと、検索ワードに引っかかったファイルをプレビューで見ながらインクリメンタルに絞り込みできます。
fif() { if [ ! "$#" -gt 0 ]; then echo "Need a string to search for!"; return 1; fi file=$(rg --files-with-matches --no-messages "$1" | fzf --preview "highlight -O ansi -l {} 2> /dev/null | rg --colors 'match:bg:yellow' --ignore-case --pretty --context 10 '$1' || rg --ignore-case --pretty --context 10 '$1' {}") if [ -n "$file" ]; then less ${file} fi }
このスクリプトだとlessで開いていますが、vimで開くようにするなどカスタマイズ次第では無数のつなげ方ができると思います。
おおよその場所しかわからないファイルを見つける
すべてを把握できていないプロジェクトでは、「名前はわからないけどあそこらへんにあったファイルを見たい」ということが稀にあります。
このようなときのためにツリー表示をすることができるプラグインを入れています。基本的にはfzf経由でファイルを開くためツリーはオフにしておき、困ったときだけトグルでオンにしています。
特定のファイルを対象にしたコマンドの実行が面倒
IDEでは開いているファイルやツリー上のファイルをクリックして何かしら便利なコマンドが実行できたりします。
当然vimにはそのような機能はなく、新たにプラグインを入れるか別のターミナルから同等のコマンドを実行する必要があります。
機能ごとにプラグインを入れるのも複雑になるため、ターミナルで同等のコマンドを実行することが多くなります。
この場合、目的のファイルが存在するディレクトリが深い場合はそこそこ時間がかかります。
そこでよく使うコマンドはfzfを使って曖昧検索で対象を見つけて実行できるようにします。
今回は個人的によく使うgitの変更ログをみるコマンドを作成しました。
git_history() { file=$(rg --files --hidden . | fzf --preview 'cat {}') if [ -n "$file" ]; then git log -p ${file} fi }
このコマンドを実行するとカレントディレクトリ以下に存在するファイルがfzfにプレビュー付きで表示され、選択するとgitの変更履歴が表示されます。
おわりに
LSPが登場したことでIDE以外でもIDEの主要機能を使って開発することができるようになりました。IDEには主要機能以外にも様々な便利機能が存在しますが、それらはLSPの責務外なため存在しません。しかしコマンドを組み合わせることで同等の機能を作ることはできます。この記事が快適なCLI環境の構築の手助けになれば幸いです。
HelmでKubernetesのマニフェストファイルを管理する
Helmを使って公開されているChartをインストール方法はたくさん紹介されていますが、独自のChartを作成する方法が簡潔に紹介されている記事が無かったので紹介します。
Helm自体の紹介やインストール方法などは省略します。
環境
- Helm 3.0.0
- Kubernetes 1.15.5
Chartの初期化
$ helm create example
で必要となるファイル一式が作成されます。
中身は以下のようになっています。
$ tree example example ├── Chart.yaml ├── charts ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ └── test-connection.yaml └── values.yaml 3 directories, 9 files
このようなファイルが作成されますが、使わないファイルが多いので以下のファイル以外を削除します。
- example/Chart.yaml
- example/templates/deployment.yaml
- example/templates/_helpers.tpl
- example/values.yaml
テンプレートの内容
example/templates
配下がテンプレートが配置されるディレクトリになります。
deployment.yaml
の中を見てみます。
apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "example.fullname" . }} labels: {{- include "example.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "example.selectorLabels" . | nindent 6 }} template: metadata: labels: {{- include "example.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "example.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: 80 protocol: TCP livenessProbe: httpGet: path: / port: http readinessProbe: httpGet: path: / port: http resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }}
これに変数が代入されてマニフェストファイルが生成されます。
マニフェストファイルの生成
helm template
コマンドでマニフェストファイルを生成します。
$ helm template ./example --name-template example --namespace default --- # Source: example/templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: example_template labels: helm.sh/chart: example-0.1.0 app.kubernetes.io/name: example app.kubernetes.io/instance: example_template app.kubernetes.io/version: "1.16.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: example app.kubernetes.io/instance: example_template template: metadata: labels: app.kubernetes.io/name: example app.kubernetes.io/instance: example_template spec: serviceAccountName: example_template securityContext: {} containers: - name: example securityContext: {} image: "nginx:1.16.0" imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 protocol: TCP livenessProbe: httpGet: path: / port: http readinessProbe: httpGet: path: / port: http resources: {}
テンプレートの変数
テンプレートに代入された変数がどこに定義されたものか紹介します。
.Values
values.yaml
で宣言されているものを.
つなぎで指定し読み込みます
include
_helpers.tpl
でdefine "変数名"
と宣言されているものが読み込まれます
.Chart
- Chart.yaml`に書かれた値を読み込んでいます
.Release
- 生成時に引数として渡されたものが読み込まれます
その他の変数や関数はなんとなく雰囲気で理解できそうですが、以下の公式ドキュメントに書かれています。
インストール
生成したマニフェストファイルをパイプでkubectlに渡せばOKです。
$ helm template ./example --name-template example --namespace default | kubectl apply -f -
また、helmが無い環境でもインストールするためにyamlファイルとして保存してしまうのも1つのやり方だと思います。
$ helm template ./example --name-template example --namespace default > bundle.yaml
fastlaneのpilotでThis request is forbidden for security reasons - The API key in use does not allow this requestとエラーになったときの対処法
環境
fastlane 2.131.0
エラー内容
Spaceship::UnexpectedResponse: [!] This request is forbidden for security reasons - The API key in use does not allow this request /Users/travis/build/xxx/xxx/vendor/bundle/ruby/2.4.0/gems/fastlane-2.131.0/spaceship/lib/spaceship/connect_api/client.rb:134:in `handle_response' /Users/travis/build/xxx/xxx/vendor/bundle/ruby/2.4.0/gems/fastlane-2.131.0/spaceship/lib/spaceship/connect_api/client.rb:105:in `patch' /Users/travis/build/xxx/xxx/vendor/bundle/ruby/2.4.0/gems/fastlane-2.131.0/spaceship/lib/spaceship/connect_api/testflight/testflight.rb:78:in `patch_beta_app_review_detail'
対応策
TestFlightへアップロードするアカウントの権限をapp manager以上の権限にすることで解決します。
developerだとこのエラーになります。
備考
2FAの対応でアップロード用のアカウントを作ったときにこの問題に遭遇することが多いようです。
以下のissueでdeveloper権限でこの問題を回避する方法が望まれていますが、まだ回避策は無いようです。
gRPCサーバーのデバッグとServer Reflection
gRPCはRPCの1つでProtocol Buffersでシリアライズ化することで高速な通信を実現させています。そのため、通常のREST-likeなWEB APIのデバッグでつかうcurlやPostmanのようなGUIツールでデバッグすることができません。
そこで今回はgRPCで開発するときに役に立つデバッグの方法を紹介します。
CLIクライアント
一番基本となるデバッグ方法は自由にリクエストを送ることだと思います。
以下のようにいくつかCLIからgRPCのリクエストを送ることができるツールがあります。
公式が提供しているツールを使うのもいいですが、curl-likeなほうが使い慣れた人が多いと思うのでgrpcurlがオススメです。
このようにしてリクエストを送ることができます。
$ grpcurl -plaintext -import-path ./api -proto route_guide.proto -d '{"latitude": 10, "longitude": 20}' localhost:10000 RouteGuide.GetFeature
Server Reflection
gRPCのはデバッグ用の機能としてServer Reflection Protocolというものがあります。
通常は上記の例のように、リクエストを送ってレスポンスを表示するには関連する.proto
ファイルを指定する必要があります。
しかし、Server Reflection Protocolを使えばサーバーから必要な情報を取得することができ、.proto
ファイルが無くても実行することができます。
この機能を使うにはサーバーにReflectionの設定をする必要がありますが、数行に追加で済みます。
import ( "google.golang.org/grpc" + "google.golang.org/grpc/reflection" ) grpcServer := grpc.NewServer() + reflection.Register(grpcServer) ... _ = grpcServer.Serve(lis)
これでServer Reflectionの対応は完了です。
また、Server Reflectionに対応することで以下のようにサーバーに実装されているサービスの一覧や詳細も.proto
ファイルなしで確認できるようになります。
とくにサービスの一覧の表示は途中から開発に参加したプロジェクトなどではとても役に立つと思います。
$ grpcurl -plaintext localhost:10000 list RouteGuide grpc.reflection.v1alpha.ServerReflection
$ grpcurl -plaintext localhost:10000 list RouteGuide RouteGuide.GetFeature
$ grpcurl -plaintext localhost:10000 describe RouteGuide.GetFeature RouteGuide.GetFeature is a method: rpc GetFeature ( .Point ) returns ( .Feature );
環境変数
サーバー側のデバッグ情報として環境変数を設定することで追加のデバッグ情報を出力することができます。
これは各言語の実装により指定する項目が変わるので注意する必要があります。
Go
Goでは以下のように環境変数を設定すれば追加のログが出力されます。
$ export GRPC_GO_LOG_VERBOSITY_LEVEL=99 $ export GRPC_GO_LOG_SEVERITY_LEVEL=info
ドキュメントにも少しだけ記述があります。
また、Goの場合はGo自体に実装されたhttp2のデバッグ機能を使うのもオススメです。
こちらはフレームの内容なども出力されるため、とても役に立つと思います。
$ export GODEBUG=http2debug=2
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: wrote SETTINGS len=0 2019/09/15 21:41:14 http2: Framer 0xc0003001c0: read SETTINGS len=0 2019/09/15 21:41:14 http2: Framer 0xc0003001c0: wrote SETTINGS flags=ACK len=0 2019/09/15 21:41:14 http2: Framer 0xc0003001c0: read SETTINGS flags=ACK len=0 2019/09/15 21:41:14 http2: Framer 0xc0003001c0: read HEADERS flags=END_HEADERS stream=1 len=68 2019/09/15 21:41:14 http2: decoded hpack field header field ":method" = "POST" 2019/09/15 21:41:14 http2: decoded hpack field header field ":scheme" = "http" 2019/09/15 21:41:14 http2: decoded hpack field header field ":path" = "/RouteGuide/GetFeature" 2019/09/15 21:41:14 http2: decoded hpack field header field ":authority" = "localhost:10000" 2019/09/15 21:41:14 http2: decoded hpack field header field "content-type" = "application/grpc" 2019/09/15 21:41:14 http2: decoded hpack field header field "user-agent" = "grpc-go/1.21.0" 2019/09/15 21:41:14 http2: decoded hpack field header field "te" = "trailers" 2019/09/15 21:41:14 http2: Framer 0xc0003001c0: read DATA flags=END_STREAM stream=1 len=9 data="\x00\x00\x00\x00\x04\b\n\x10\x14" 2019/09/15 21:41:14 http2: Framer 0xc0003001c0: wrote WINDOW_UPDATE len=4 (conn) incr=9 2019/09/15 21:41:14 http2: Framer 0xc0003001c0: wrote PING len=8 ping="\x02\x04\x10\x10\t\x0e\a\a"
Cベースの実装
Cをベースとした言語(C++, Python, Ruby, Objective-C, PHP, C#)の実装では以下のような環境変数を設定することで追加のログが出力できます。
$ export GRPC_VERBOSITY=DEBUG $ export GRPC_TRACE=all
Cベースの場合は他にも設定できる項目があり、以下のドキュメントに書かれています。
また、これらの環境変数はgRPCを内部で使用しているSDKなどでも有効です。
特にGoogleのSDKではgRPCがたくさん使われているため、それらのデバッグでも有効活用できると思います。
Digdagのbackfillを無効化する
Digdagでスケジュール実行の設定をしているワークフローを一時停止したあとに停止を解除した場合は、停止していた期間に実行されるはずだったセッションを実行するBackfillという機能があります。
GUIでPAUSEした後にRESUMEしたときや、CLIでのenable/disableを実行したときが該当します。
この機能はデフォルトで有効になっているため、冪等性が保たれていないジョブなどを一時停止した場合はBackfillを無効にする必要があります。
Backfillを無効にする方法として、digdag reschedule
を実行する方法とマニフェストファイルでskip_delayed_by
を設定する方法があります。
rescheduleコマンドを使う方法
rescheduleコマンドでスケジュールを更新して現時刻よりも前にスケジューリングされたセッションをスキップするように設定します。
まずはBackfillを無効にしたいワークフローのIDを取得します。
CLIからは以下のコマンドで取得できます。
$ digdag schedules
取得したIDで以下のコマンドを実行すればBackfillを無効にできます。
$ digdag reschedule <schedule id> --skip-to "$(date '+%Y-%m-%d %H:%M:%S %z')"
このやり方で注意しなければいけないポイントとして、Backfillが無効になるのは今回の一時停止で実行されなかったセッションだけであるところです。
後日また一時停止したときにもBackfillを無効にする場合は再度rescheduleコマンドを実行する必要があります。
冪等性が無い場合など、毎回Backfillを無効にしたい場合は2つめのやり方で設定できます。
skip_delayed_byを設定する方法
マニフェストファイルのschedule
にskip_delayed_by
を設定することで、指定された期間よりも以前のセッションをスキップするように設定できます。
例としては以下のようになります。
schedule: skip_delayed_by: 3m +setup: ...
この例の場合は一時停止を解除した時刻の3mよりも前のセッションを無効にします。
つまり、この設定を1s
とかにしておけばBackfillを完全に無効にできます。
ワークフローの内容によりBackfillが実行されると不都合がある場合は、この設定を入れておくことをオススメします。
Go ModulesなプロジェクトのDockerビルドを高速化する
Go Modulesに対応させたプロジェクトをDocker内でビルドして実行するとします。
単純にDockerfileを書いてしまうとソースコードに変更が入るたびにModulesのダウンロードが走ってしまい、とても時間がかかってしまいます。
そこでDockerのイメージレイヤのキャッシュ機能を使って高速化する方法を紹介します。
Go Modulesのキャッシュ
キャッシュが効かずに毎回Modulesのダウンロードが行われる原因としては、Docker内でGoのビルドをするためにソースコードをコピーしている COPY
コマンド部分が、前回の状態と異なるためキャッシュを使うことができないところにあります。
そのため、ソースコードをコピーする前にModulesのダウンロードを行えば、ソースコードの変更を行っても前回のキャッシュが使え、大幅な短縮になります。
以下が一例です。
FROM golang:1.12.4 as build ENV GO111MODULE=on WORKDIR /go/src/path/to/proj COPY go.mod . COPY go.sum . RUN set -x \ && go mod download COPY . . RUN set -x \ && go build -o /go/bin/app FROM ubuntu:18.04 COPY --from=build /go/bin/app /go/bin/app CMD ["/go/bin/app"]
ソースコード全体をコピーする前にModules関連だけをコピーして、 go mod download
を実行してダウンロードしています。
これでModulesに変更が入らない限り COPY . .
から実行されるため、大幅な短縮になります。