😎

WSL(Ubuntu 20.04)でZenn CLIを使いTerraform(Google Cloud)の記事が快適に書けるようになるまで

2022/09/28に公開

こんにちは。
環境整備や見た目に時間をかけすぎて本題に入りそこねがちなクラウドエースの吉崎です。

本稿もそうですが、Zenn CLIを使って記事の作成やプレビューを行っています。
しかし、WSL(Ubuntu)環境で Zenn CLI を使って Terraform(Google Cloud)の記事を書くまでの道のりは、難しくもありませんが簡単でもありません。
次回、別の端末で同じ環境を構築するための備忘として、誰か同じ環境を構築して Zenn で記事を書こうと思っている人のため、本稿ではその道のりを記します。
なお、WSL(Ubuntu)のインストールは割愛しますので、WSL(Ubuntu)はご自分で準備してください。

環境

  • Windows 11 21H2
  • Ubuntu 20.04.4 LTS on WSL2

道のり

リポジトリを更新

インストールされるパッケージが古いものにならないようにリポジトリを更新します。

$ apt update

Node.js と npm をインストールする

# インストール
$ apt install -y nodejs

# バージョン確認
$ nodejs -v
v10.19.0
# インストール
$ apt install -y npm

# バージョン確認
$ npm -v
6.14.4

本稿の執筆時点では node.js v10.19.0, npm v6.14.4 がインストールされました。
しかし、これらのバージョンは Zenn ClI にサポートされていないので、新しいバージョンをインストールする必要があります。
下記はそのまま Zenn CLI をインストールしたときの警告例です。

$ npm install zenn-cli
npm WARN notsup Unsupported engine for zenn-cli@0.1.125: wanted: {"node":">=14.0.0"} (current: {"node":"10.19.0","npm":"6.14.4"})

Node.js と npm の新しいバージョンをインストールする

  1. n をインストールする

n は対話的に Node.js のバージョンを管理するツールです。
Python でいう Pyenv です。いや、厳密には Nodenv が Pyenv にあたりますね。
n を使うことで、Node.js のバージョンを簡単に管理出来るようになります。

$ npm install -g n

-g オプションでグローバルにインストールします。
このオプションを付けない場合、作業ディレクトリでのみ有効となります。

  1. 安定版確認
$ n --stable
16.17.0

--latestとすると最新バージョンを参照できます。

  1. 安定バージョンインストール
$ n stable
16.17.0

筆者の環境では上記コマンドがタイムアウトとなったため、以下のように問い合わせ先 DNS サーバを変更したところ進めました。

$ vim /etc/resolv.conf
nameserver 8.8.8.8 # Google Public DNS
nameserver 8.8.4.4 # Google Public DNS

ちなみに、こちらのファイルは WSL を再起動すると消えます。
どうやって残すかは こちらの記事 を参照ください。

改めて Node.js のバージョンを確認します。

$ node --version
v16.17.0

安定版になっていますね。
旧バージョンは不要なのでアンインストールしても構いません。

ちなみに npm は Node.js に同梱されているため、npm のバージョンも上がっています。

$ npm -v
8.15.0

Zenn CLI を使う

  1. インストールする
$ npm install -g zenn-cli
  1. 初期化する

記事を管理するディレクトリで init コマンドを実行します。

$ npx zenn init

  🎉  Done!
  早速コンテンツを作成しましょう

  👇  新しい記事を作成する
  $ zenn new:article

  👇  新しい本を作成する
  $ zenn new:book

  👇  投稿をプレビューする
  $ zenn preview

絵文字が使われていて見やすいですね。
npx コマンドはローカルにインストールされたモジュールもインストールされていないモジュールも実行出来る便利なコマンドです。

init を実行したディレクトリは以下のようになります。

$ ls
README.md  articles  books

Terraform を準備する

公式ページに Ubuntu のインストール手順が記載されていますのでその通りに実行します。

$ wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
$ sudo apt update && sudo apt install terraform

というわけで Terraform がインストールされました。

$ terraform version
Terraform v1.3.0
on linux_amd6

gcloud CLI を準備する

こちらも公式ドキュメントの通りコマンドを実行します。

# gcloud CLI のインストール
$ sudo apt-get install apt-transport-https ca-certificates gnupg
$ echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
$ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
$ sudo apt-get update && sudo apt-get install google-cloud-sdk

# gcloud CLI の初期化
$ gcloud init

gcloud init 実行後は、

  • ブラウザでリンクを開き、認証コードを取得する
  • 認証コードをターミナルに入力する
  • プロジェクト ID を設定する
  • デフォルトリージョン・ゾーンを選択する

という作業を実施しますが、これらは割愛します。

Application Default Credentials(ADC) を設定する

Terraform で Google プロバイダを使う際、サービスアカウントキーを credentials に指定するのではなく、ADC の認証情報を使うようにします。
これは、セキュリティ上、サービスアカウントキーを出来るだけ作成しないことが望ましいためです。
詳しくはサービスアカウントキーのベストプラクティスを参照ください。

$ gcloud auth application-default login

こちらもリンクをブラウザで開き、認証コードをターミナルに入力する必要があります。

ちなみに実行時の出力を見ると分かりますが、ADC の情報は $HOME/.config/gcloud/application_default_credentials.json に格納されます。
以下のような内容になっていると思います。(重要そうな部分は x で伏せています)

application_default_credentials.json
$ cat $HOME/.config/gcloud/application_default_credentials.json
{
  "client_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
  "client_secret": "xxxxxxxxxxxxxxxxxxxxxxxx",
  "quota_project_id": "xxxxxxxxxxxxxxxxxxxxxxxx",
  "refresh_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "type": "authorized_user"
}

Terraform の状態を管理するファイル(tfstate)を格納する GCS バケットを作成する

まずは google プロバイダを使って初期化します。

main.tf を以下の通り作成してください。

main.tf
provider "google" {
  project = "xxxxxxxxxxxxxxxxxxxxxxxx" # ご自身のプロジェクトIDを指定してください
  region  = "asia-northeast1" # 大阪リージョン
}

terraform init します。

$ terraform init

以下のような出力となると思います。
これで初期化は OK です。

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/google...
- Installing hashicorp/google v4.37.0...
- Installed hashicorp/google v4.37.0 (signed by HashiCorp)

次に tfstate を管理するための GCS バケットを作成します。
構成は 公式ドキュメント を参考にしています。

では、main.tf に以下の通り追加してください。

main.tf
provider "google" {
  project = "ca-yoshizaki-test-352704"
  region  = "asia-northeast2"
}

+ resource "random_id" "instance_id" {
+   byte_length = 8
+ }
+ 
+ resource "google_storage_bucket" "tfstate" {
+   name          = "bucket-tfstate-${random_id.instance_id.hex}"
+   force_destroy = false
+   location      = "asia-northeast2"
+   storage_class = "STANDARD"
+ }

そして terraform plan を実行します。

$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_storage_bucket.tfstate will be created
  + resource "google_storage_bucket" "tfstate" {
      + force_destroy               = false
      + id                          = (known after apply)
      + location                    = "ASIA-NORTHEAST2"
      + name                        = (known after apply)
      + project                     = (known after apply)
      + self_link                   = (known after apply)
      + storage_class               = "STANDARD"
      + uniform_bucket_level_access = (known after apply)
      + url                         = (known after apply)

      + versioning {
          + enabled = (known after apply)
        }
    }

  # random_id.instance_id will be created
  + resource "random_id" "instance_id" {
      + b64_std     = (known after apply)
      + b64_url     = (known after apply)
      + byte_length = 8
      + dec         = (known after apply)
      + hex         = (known after apply)
      + id          = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

terraform apply でバケットを作成します。

$ terraform apply
~
random_id.instance_id: Creating...
random_id.instance_id: Creation complete after 0s [id=L1tLaaUlhZ4]
google_storage_bucket.tfstate: Creating...
google_storage_bucket.tfstate: Creation complete after 1s [id=bucket-tfstate-2f5b4b69a525859e]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
~

作成されたバケット名はクリップボードに控えておくと良いです。

次にバケットが作成されたことを確認します。

$ gsutil ls | grep bucket-tfstate-2f5b4b69a525859e
gs://bucket-tfstate-2f5b4b69a525859e/

gcloud storage buckets list でも確認出来ますが、上記の方がシンプルです。

現時点では terraform.tfstate はローカルにありますので、これを GCS に移行します。

main.tfbackend を追加します。

main.tf
provider "google" {
  project = "ca-yoshizaki-test-352704"
  region  = "asia-northeast2"
}

resource "random_id" "instance_id" {
  byte_length = 8
}

resource "google_storage_bucket" "tfstate" {
  name          = "bucket-tfstate-${random_id.instance_id.hex}"
  force_destroy = false
  location      = "asia-northeast2"
  storage_class = "STANDARD"
}

+ terraform {
+   backend "gcs" {
+     bucket  = "bucket-tfstate-2f5b4b69a525859e"
+   }
+ }

この状態で terraform init を実行します。
このコマンドは、ローカルの tfstate を GCS バケットにアップロードしてくれます。

$ terraform init

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "gcs" backend. No existing state was found in the newly
  configured "gcs" backend. Do you want to copy this state to the new "gcs"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value:

yes とすると GCS バケットに tfstate がアップロードされます。

$ gsutil ls gs://bucket-tfstate-2f5b4b69a525859e/
gs://bucket-tfstate-2f5b4b69a525859e/default.tfstate

やっと GCS バケットに tfstate を配置出来ました。

新しい記事を書く

後は Zenn 公式 を見て記事を作成してください。

npx zenn new:article

筆者のディレクトリ構成は以下の通りとなっています。
記事作成時にランダムに作成された slug のディレクトリを terraform 配下に作成し、その中に Terraform コードを書いていっています。
VSCode を使っており、エディター画面で記事を編集し、ターミナルで .tf ファイルを編集しています。

$ tree -a
.
├── articles
│   ├── 923a505b8a3c59.md
│   └── d0906b43de4a74.md
├── books
└── terraform
    ├── 923a505b8a3c59
    │   └── main.tf
    └── d0906b43de4a74
         └── main.tf

※一部内容を割愛

特に工夫を凝らしたディレクトリ構成ではありませんので、今後記事が増えると改善されていくでしょう。

まとめ

引っ掛かりポイントは、

  • Ubuntu のデフォルトのリポジトリでインストールされる Node.js のバージョンが古い
  • Hashicorp の gpg キーを取得するコマンドがタイムアウトする(ことがある?)
  • Terraform で Google プロバイダを使用する際に ADC の設定が必要

でした。

この後 GitHub 連携により、push をトリガーに記事が自動で公開されることでしょう。

Discussion