TerraformのStateをS3からTerraform Cloudに移行する

TerraformのStateをチームで管理するときに、複数の環境からplanやapplyを実行できるようにするためにS3などのクラウドのストレージをバックエンドにすることが多いと思います。

通常のストレージをバックエンドにしていると、複数の環境から同時にapplyが実行されたときに壊れてしまう可能性があります。CIで実行するようにしていても、設定が適切ではなく平行に動いてしまうような場合などに壊れる可能性があります。

Terraform CloudにはStateのロック機能がありこのような事故から開放されるため、Stateの管理をS3からTerraform Cloudに移行したいと思います。

環境

  • Terraform v0.12.24
  • 元のBackendはS3
  • Terraform Cloudのアカウントは作成済み

手順

Terraform Cloudへのアクセス設定

まずはterraformコマンドがTerraform Cloudにアクセスできるようにします。

terraformのバージョンが0.12.21以降の場合は terraform login コマンドを実行して、ブラウザに表示されたトークンを入力すれば完了です。

www.terraform.io

loginコマンドが使えない場合は $HOME.terraformrc というファイルを作成し、以下のようにトークンを設定します。

credentials "app.terraform.io" {
  token = "xxxxxx.atlasv1.zzzzzzzzzzzzz"
}

また、TF_CLI_CONFIG_FILE という環境変数を設定すれば、任意の場所に任意の名前で設定ファイルを作成できます。

www.terraform.io

www.terraform.io

backendの書き換え

S3の設定が書かれていたbackendをTerraform Cloudのものに書き換えます。

  backend "s3" {
    ...
  }

これを削除して以下のように書き換えます。

  backend "remote" {
    hostname     = "app.terraform.io"
    organization = "your_organization_name"

    workspaces {
      name = "your_workspaces_name"
    }
  }

移行

準備が整ったので移行させていきます。

terraform init を実行するだけで移行ができます。
実行すると以下のように聞かれるので yes を入力します。

$ terraform init

Initializing the backend...
Backend configuration changed!

Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the backends.


Terraform detected that the backend type changed from "s3" to "remote".
Acquiring state lock. This may take a few moments...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "s3" backend to the
  newly configured "remote" backend. No existing state was found in the newly
  configured "remote" backend. Do you want to copy this state to the new "remote"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value:

yes を入力し、問題なく成功すれば以下のように表示されます。

Successfully configured the backend "remote"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

これで移行が完了です。

参考

Kubernetes上で動くGoサーバーでプロファイラを動かしWeb UIで表示する

Goにはプロファイラとして標準パッケージにpprofが搭載されています。
pprofの使い方としてはすでに多数の優良記事が存在するため、ここでは扱いません。

今回はpprofをk8s上で動くサーバーに対して実行し、結果をWebUIで表示する必要があったのでそのやり方を紹介します。

環境

インフラ構成

  • GoサーバーのHTTP API用のPortが80
  • pprof用のPortが6060
  • プロファイラ結果のWeb UI用のPortが8080
    • このPortをServiceのNodePortでMacに公開

やり方

pprofの設定

まずは通常通りpprofの設定をしてGoをビルドします。

import (
    ...
    _ "net/http/pprof"
)

func main() {
    ...

    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    ...
}

このバイナリがk8sのPodで動いているとします。

Serviceの定義

通常のAPIへのアクセスに使うServiceが以下です。

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-service
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80

これに追加してpprofのWebページにアクセスするためのServiceを定義します。
このServiceのTypeをNodePortにすることでお手軽にアクセスできるようになります。

apiVersion: v1
kind: Service
metadata:
  name: my-service-pprof
spec:
  selector:
    app: my-service-api
  type: NodePort
  ports:
    - name: http-pprof
      protocol: TCP
      port: 8080
      targetPort: 8080
      nodePort: 30080

pprofの実行

Goのバイナリが動くPodに入り以下のコマンドでpprofを実行します。

$ pprof -http=0.0.0.0:8080 /usr/local/bin/my-api http://localhost:6060/debug/pprof/profile

ここで大事なのはpprofのWebページのアドレスを 0.0.0.0 にすることです。ここを 127.0.0.1 にしていると外部からアクセスできなくなってしまいます。

あとは計測したいエンドポイントにアクセスし以下のようなメッセージが表示された後、ホストからNodePortの 30080 にアクセスすればプロファイル結果がWebUIで表示されます。

Saved profile in /root/pprof/pprof.my-api.samples.cpu.001.pb.gz
Serving web UI on http://0.0.0.0:8080
Couldn't find a suitable web browser!
Set the BROWSER environment variable to your desired browser.

Magiskのroot化を維持したままOTAアップデートする

Magiskでroot化するとOTAアップデートが無効化されるため、セキュリティパッチなどのOSのアップデートを行うことができなくなります。
しかし、適切な手順を踏むことでroot化を維持したままOTAアップデートを行うことができます。

検証環境

  • Pixel3
  • Android 10
  • TWRPを使用しないroot化(boot.imgのパッチ)

手順

注意事項

以下の手順の中で再起動を促すボタンが数回表示されますが、再起動してはいけない場面が多数あります。
手順の中で再起動をすると記載したとき以外は再起動のボタンが表示されてもタップしないように注意してください。
root化を維持できなくなります。

大まかな流れ

  • 一時的にMagiskをアンインストール
  • アップデートをインストール
  • アップデート後のイメージにMagiskをインストールするように設定する

Magiskのアンインストール

まずはOTAアップデートをインストールするために一時的にMagiskをアンインストールします。

Magisk Managerを起動し、アンインストールをタップします。
その後に表示されるダイアログでイメージの復元を選択します。
復元が完了しました!と表示されればOKです。

ここで再起動をしてはいけません。

アップデートのインストール

1つ前の作業で正規のイメージになったため、アップデートのインストールができるようになりました。正規の手順でOTAアップデートを行います。

バイスの[設定]→[システム]→[システムの更新]に移動し、 [ダウンロードとインストール]ボタンを押してインストールします。

インストールが完了すると再起動のボタンが表示されますが、タップしてはいけません。

Magiskの再インストール

再度Magisk Managerを開き、インストールをタップします。
タップ後に表示されるダイアログで非アクティブスロットにインストール(OTA後)を選択します。
警告が表示されますが、はいを選択してインストールを行います。
インストールが完了すると、再起動のボタンが表示されるのでタップして再起動します。

これでOTAアップデートにMagiskがインストールされます。

参考

Magiskでroot化したPixel3のテザリング制限を解除する

民泊系と呼ばれるSIMを使っている場合、制限がかかっていてテザリングをすることができません。

しかしMagiskでroot化していればこの制限を解除でき、民泊系の数百GB/月という容量を使ってテザリングをすることができます。

前提

必要なモジュールのインストール

テザリングの制限を解除するためにMagiskHide Props Configを使います。
このツールはbusyboxというツールに依存しているためこちらもインストールします。

インストールはMagisk Managerのタブにあるダウンロードを表示し、そこにあるモジュールの一覧から見つけ出して画面の案内にそってインストールします。

テザリング制限を解除

適当なターミナルアプリを用意して以下のコマンドを順番に入力していけば解除できます。
ターミナルアプリがインストールされていない場合は以下のアプリを入れましょう。

play.google.com

ターミナルから以下のコマンドを順に入力します。

  • su
  • props
  • 4
  • n
  • net.tethering.noprovisioning
  • true
  • y

yを入力後に端末が再起動します。

再起動後、設定からテザリングを有効にできます。

MacのCLI環境の構築を自動化する

前回、IDEをやめてvim-lspと独自コマンドに移行したことを紹介しましたが、あれだけだと少し問題があります。

akaimo.hatenablog.jp

PCを買い替えたときやクリーンインストールをしたときなどに同じ環境を簡単に構築することができないともう一度構築することが面倒になってしまい、インストールするだけで使えるIDEでいいやとなってしまいます。

そこで今回はAnsibleを使ってCLI環境の構築を自動化したときに簡単に使えるように意識したポイントを紹介します。
Ansible自体の使い方は紹介しませんので別の記事を参照してください。

自動化したコードは以下のリポジトリで公開しています。

github.com

ポイント

以下のことを意識して自動化しました。

  • 詳細な手順が書かれたドキュメントを用意する
  • できるだけ実行するコマンドを少なくする
  • アップデートしやすくする
  • 別の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で公開しているため、適宜参照してください。

github.com

プロジェクト内の検索

これは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>

f:id:akaimo3:20191219113722p:plain

これで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
}

f:id:akaimo3:20191219120409p:plain

このスクリプトだとlessで開いていますが、vimで開くようにするなどカスタマイズ次第では無数のつなげ方ができると思います。

おおよその場所しかわからないファイルを見つける

すべてを把握できていないプロジェクトでは、「名前はわからないけどあそこらへんにあったファイルを見たい」ということが稀にあります。

このようなときのためにツリー表示をすることができるプラグインを入れています。基本的にはfzf経由でファイルを開くためツリーはオフにしておき、困ったときだけトグルでオンにしています。

github.com

特定のファイルを対象にしたコマンドの実行が面倒

IDEでは開いているファイルやツリー上のファイルをクリックして何かしら便利なコマンドが実行できたりします。
当然vimにはそのような機能はなく、新たにプラグインを入れるか別のターミナルから同等のコマンドを実行する必要があります。

機能ごとにプラグインを入れるのも複雑になるため、ターミナルで同等のコマンドを実行することが多くなります。
この場合、目的のファイルが存在するディレクトリが深い場合はそこそこ時間がかかります。

そこでよく使うコマンドはfzfを使って曖昧検索で対象を見つけて実行できるようにします。

今回は個人的によく使うgitの変更ログをみるコマンドを作成しました。

git_history() {
  file=$(rg --files --hidden . | fzf --preview 'cat {}')
  if [ -n "$file" ]; then
    git log -p ${file}
  fi
}

このコマンドを実行するとカレントディレクトリ以下に存在するファイルがfzfにプレビュー付きで表示され、選択するとgitの変更履歴が表示されます。

f:id:akaimo3:20191219142832p:plain f:id:akaimo3:20191219142902p:plain

おわりに

LSPが登場したことでIDE以外でもIDEの主要機能を使って開発することができるようになりました。IDEには主要機能以外にも様々な便利機能が存在しますが、それらはLSPの責務外なため存在しません。しかしコマンドを組み合わせることで同等の機能を作ることはできます。この記事が快適なCLI環境の構築の手助けになれば幸いです。

HelmでKubernetesのマニフェストファイルを管理する

Helmを使って公開されているChartをインストール方法はたくさん紹介されていますが、独自のChartを作成する方法が簡潔に紹介されている記事が無かったので紹介します。

Helm自体の紹介やインストール方法などは省略します。

環境

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.tpldefine "変数名"と宣言されているものが読み込まれます
  • .Chart
    • Chart.yaml`に書かれた値を読み込んでいます
  • .Release
    • 生成時に引数として渡されたものが読み込まれます

その他の変数や関数はなんとなく雰囲気で理解できそうですが、以下の公式ドキュメントに書かれています。

helm.sh

helm.sh

helm.sh

インストール

生成したマニフェストファイルをパイプで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