Terraform Tips 3選

この記事は、2024年6月24日(月)に行われた社内勉強会における発表を、さくナレ編集部にて記事化したものです。

はじめに

さくらインターネット インターネットサービス部の稲垣孝洋と申します。2021年度に新卒で入社した者で、今はインターネットサービス部のサービスデベロップメントというところで開発しています。趣味はGo/PHP/Reactを使ったウェブ開発とか、最近は社内のPostgreSQLサーバの運用を手伝いたいと思ってPgpool-IIの勉強をしてたりします。ゲームはApexとかVALORANTとかクレーンゲームにはまっています。

この記事で伝えたいこと

本日は「Terraform Tips 3選」というテーマで発表します。お話ししたいことを並べてみます。ただ「Terraform Tips 3選」と言いつつ、3つ目はcloud-initの話になっています。

tfstateをさくらのオブジェクトストレージに保存する

1つ目がtfstateをさくらのオブジェクトストレージに保存するという話です。以下のようにbackendの設定を書けば、さくらのオブジェクトストレージでtfstateを管理することができます。

backend "s3" {
  bucket = "tfstate-bucket"
  key    = "staging.tfstate"
  endpoints = {
    s3 = "https://s3.isk01.sakurastorage.jp"
  }
  region                      = "jp-north-1"
  profile                     = "terraform-s3"
  shared_credentials_files    = ["../../_secrets/s3.credentials"]
  skip_requesting_account_id  = true
  skip_credentials_validation = true
  skip_region_validation      = true
  skip_metadata_api_check     = true
  skip_s3_checksum            = true
}

注意点としては、endpointsをさくらのオブジェクトストレージにしてあげることと、リージョンとしてjp-north-1を指定しなければならないことです。それから、いくつかのskip_*をtrueに設定する必要があります。社内では何個いるのかみたいな議論がありますが、私が最新版で試したときは上記の5つを設定したら動きました。

それから、backendのs3の設定において、オブジェクトストレージのアクセスキーの指定のしかたがいくつかありますが、個人的には上記に示すようにprofileとshared_credentials_filesを指定する方法が気に入っています。これはどういうものかというと、shared_credentials_filesに指定したファイルにTOML形式でアクセスキーを記述しておき、それを使うというものです。

上記の例では、_secretsというディレクトリの中にs3.credentialsというファイルを置いています。さらに.gitignoreファイルにこのディレクトリを書くことで、GitHubのリポジトリに含めないようにして管理しています。よって、backendの設定もそのままハードコードしておいて、使うときはここにファイルを置くだけでOKというふうにしています。

Ansible Providerを利用して機密情報を暗号化して管理する

次のTipsは、Ansible Providerというものを利用して、機密情報を暗号化して管理するというところです。

Ansible Providerとは何かというと、TerraformからAnsibleを実行することができるプロバイダです。Terraformのセットアップと同時にAnsibleも流してセットアップをするというのが主な使い方ですが、ansible-vaultにも対応しているので、暗号化したファイルを復号することができます。

# 暗号化したファイルを復号
resource "ansible_vault" "db_secret" {
  vault_file          = "stg_db_secret.encrypted.yml"
  vault_password_file = "../../_secrets/ansible-vault.password"
}

# 復号したデータをでコードしてローカル変数に代入
locals {
  db_secret = yamldecode(ansible_vault.db_secret.yaml)
}

resource "sakuracloud_enhanced_db" "db" {
  ...
  # 復号したデータを利用
  password = local.db_secret.password
  ...
}

上記のような感じでansible_vaultリソースを使って暗号化したYAMLファイルを復号し、デコードした変数を、この例ではさくらのクラウドのエンハンスドデータベースのパスワードに利用しています。

こうすることでパスワードをハードコードしなくてよくなるっていうのと、tfvarsを使わなくてもよいのがちょっといいところかなと思ってます。tfvarsをチームで共有するのは面倒くさいと思っているのですが、この方法なら暗号化のパスワードだけ共有していれば、あとは全部リポジトリに入っていますし、暗号化されてるのである程度は安全ですよというふうになっています。

cloud-initを利用する

最後のTipsがcloud-initを利用するという話です。さくらのクラウドではcloud-init対応イメージを使うことができて、起動時にcloud-initを使ったセットアップを行います。今回はその利用方法と、ネットワークの設定例を紹介したいと思います。

cloud-initとは

cloud-initとはまずなんぞやっていうのがありまして、システムの起動時にサーバのセットアップを自動化するツールになってます。さくらのクラウドでいうと、従来から存在するディスク修正機能の代わりになるのかなと思います。ディスク修正機能との違いは、ディスク修正はさくらのクラウドで用意しているセットアップサーバで行われていますが、cloud-initの場合は利用者のサーバ上で行われます。

cloud-initの仕組み

cloud-initの仕組み

cloud-initの仕組みですが、cloud-initのサービスが起動すると、cidataというラベルが付けられたファイルシステムを読み込んで設定を行います。さくらのクラウドでは、コントロールパネルやTerraformで指定した設定ファイルをディスクとしてマウントすることでcloud-initの機能を実装しています。

そのファイルシステムには、meta-data、user-data、network-configというファイルが存在しています。meta-dataは、クラウドプロバイダ(ここではさくらのクラウド)が自動的に生成しています。user-dataがコントロールパネルやTerraformで指定するセットアップ用の記述ファイルです。network-configというのはネットワークの設定を記述したファイルですが、現在のさくらのクラウドではまだ対応していないので存在しません。

terraformでuser-dataを指定する方法

ではuser-dataをTerraformで指定するにはどうするかですが、さくらのクラウドのサーバリソースのuser_dataフィールドに文字列で指定します。設定例を以下に示します。

resource "sakuracloud_server" "server" {
  name = "server"
  ...

  user_data = <<-EOF
    #cloud-config
    fqdn: server.example.com
    timezone: Asia/Tokyo
  EOF

  lifecycle {
    ignore_changes = [
      user_data,
    ]
  }
}

ヒアドキュメントを使えば上記の例のように複数行書くことができます。こちらの例ではFQDNとタイムゾーンの設定をしています。

ただ、この書き方は、user_dataが多くなるとtfファイルが巨大になってしまったり、FQDNはサーバによって異なるので、部分的にちょっと値が違うだけの記述が多くなってしまいます。

user-dataをテンプレートファイルから生成する

そこで、この方法の代わりに、user-dataをテンプレートファイルから生成するようにできます。

Terraformにはtemplatefile関数というのがあります。

templatefile(path,vars)

pathで指定したテンプレートファイルにvarsで指定した変数を展開し、生成された文字列を返すという関数があるので、これを使うと便利です。先ほどの例をtemplatefileを使って書き直すと以下のようになります。

resource "sakuracloud_server" "server" {
  name = "server"
  ...

  user_data = templatefile("userdata.yaml", {
    fqdn = "server.example.com"
  })

  ...
}

こんな感じで、user_dataのところにテンプレートファイル(上位の例ではuserdata.yaml)と変数を指定します。userdata.yamlの方は以下のように記述します。

#cloud-config
fqdn: ${fqdn}
timezone: Asia/Tokyo

こちらは展開するところに変数を書きます。こうすることで複数のリソースから再利用できるような形で記述することができます。

user-dataを用いたネットワークの設定

次に、user-dataを用いたネットワーク設定の紹介です。

先ほども申し上げた通り、さくらのクラウドはnetwork-configに対応していないので、network-configを使った設定ができません。そこで、user-dataを使ってnetplanの設定ファイルを生成し、それを適用するということをして設定を行います。それが以下のような感じのuser-dataになります。

write_files:
  - path: /etc/netplan/60-netcfg.yaml
    owner: root:root
    permissions: 0o644
    content: |
      network:
        ethernets:
          ens3:
            addresses:
              - 192.168.0.254/24
            nameservers:
              addresses:
              - 210.188.224.10
              - 210.188.224.11
            routes:
              - to: default
                via: 192.168.0.1
        renderer: networkd
        version: 2

runcmd:
  - sudo netplan apply

こちらでは、ファイルを設置するwrite_filesというモジュールを使って、netplanの設定(上記の例ではcontent以下)を、netplanの設定ファイルの置き場(上記の例では/etc/netplan/60-netcfg.yaml)に保存します。そして最後にruncmdという、コマンドを実行するモジュールを使って適用させます。こうすることで、このインターフェースに固定IPアドレスを設定するようなことができたり、複数のインターフェースがある場合も、それぞれにネットワークの設定を行うような書き方ができます。

ただ、注意点としては、runcmdの実行順番が最後のため、ネットワーク接続が必要なモジュールが失敗してしまうという問題があります。どういうことかというと、例えばGitHubにある公開鍵を登録できるssh_import_idというモジュールがありますが、実行順としてはssh_import_idが先に実行されてしまうので、これを実行する時点ではネットワーク設定ができていないためcloud-initがエラーで終了してしまいます。

もし、このようにネットワーク接続が必要なモジュールをここで使いたいという場合は、runcmdで設定するのではなく、bootcmdという別のモジュールで設定する必要があります。bootcmdは実行順番的にはほぼ最初に近いところで実行してくれるモジュールで、ここで実行して設定しておけば、後から出てくるssh_import_idなども全部成功してくれます。

しかし、やり方としては先ほどと同じような感じでやりたいのですが、上図ではwrite_filesがbootcmdよりも後なので、bootcmdで起動するときに設定ファイルが存在しなければスクリプトで生成したりとか、あとこの方法ではnetlan applyが失敗してしまうのでrebootで反映させるなどの工夫が必要になります。具体的には下記のような感じで記述します。

bootcmd:
  - |
    if [ ! -e "/etc/netplan/60-netcfg.yaml" ]; then
    echo "network:
      ethernets:
        ens3:
          addresses:
            - 192.168.0.254/24
          nameservers:
            addresses:
            - 210.188.224.10
            - 210.188.224.11
          routes:
            - to: default
              via: 192.168.0.1
      renderer: networkd
      version: 2" | tee /etc/netplan/60-netcfg.yaml
    reboot
    fi

設定ファイルが存在しなければ、echo文で設定ファイルを生成し、rebootで再起動します。再起動後もここは再度実行されますが、そのときにはファイルが存在するので何もしないという処理になっています。

まとめると、セットアップにネットワーク接続が必要なのであればwrite_fileとruncmdを使うのがシンプルで、セットアップにネットワークが必要だったらbootcmdで工夫するのがよいかと思います。

まとめ

今回はTerraformに関する3つのTipsを紹介しました。

  • さくらのオブジェクトストレージにtfstateを保存する
  • Ansible Providerを利用して機密情報を暗号化して管理する
  • cloud-initを利用する

ご清聴ありがとうございました。