こんにちは。 nobushi です。
今回は Terraform で CI/CD 運用を行なってみたいと思います。
Terraform は Infrastructure as Code (IaC) のツールの一つです。IaC といえば AWS ならCloudFormation もありますが、 Terraform の場合、 AWS はもちろん、Azure 、GCP 等、いろんなプラットフォームに対応できるのが魅力の一つです。
本ブログでは GCP をターゲットとして CI/CD の運用例を紹介したいと思います。
例として、サンプルのコンテナイメージをCloudRun 上に構築してみます。
導入
まずはローカルで Terraform の環境を構築してみましょう。 Tarraform 自体は公式のDockerイメージを使用します。 また、環境変数等の設定を簡易化するため docker-compose を使用します。
docker-compose が使える環境で任意のディレクトリに以下の3ファイルを配置してください。
/
credentials.json
docker-compose.yml
main.tf
また、事前に GCP 上での準備が必要です。
GCP上の事前準備
事前に GCP上で以下のことを行なってください。
- 対象のプロジェクトの作成
- 作成したプロジェクトでサービスアカウントの作成。「編集者」「IAM/セキュリティ管理者」権限
- サービスアカウントのキーのダウンロード。json形式
- Terraform 状態保存用の CloudStorage バケットの作成
credentials.json
{ "type": "service_account", ...}
ダウンロードした GCP の対象プロジェクトのサービスアカウントのキーファイルです。 credentials.json
というファイル名にしてください。 今回のサンプルではとりあえず「編集者」と「IAM/セキュリティ管理者」権限があれば良いです。
docker-compose.yml
version: '3.5'services: terraform: image: hashicorp/terraform:1.2.0 working_dir: /terraform volumes: - .:/terraform environment: GOOGLE_APPLICATION_CREDENTIALS: /terraform/credentials.json
コマンドの簡易化のために使用しているだけなので Docker コマンド単独でも同じことができます。
コンテナ内に独立した作業ディレクトリ( /terraform
)を設定し、カレントディレクトリと同期しています。 また、前述の GCP のキーファイルを環境変数に設定しています。 この環境変数を設定することで GCP と Terraform の権限設定は完了です。
main.tf
terraform {
backend "gcs" {
bucket = "<bucket-name>"
}
}
provider "google" {
project = "<project-name>"
region = "us-central1"
zone = "us-central1-c"
}
variable "container_image" {}
resource "google_cloud_run_service" "hello" {
name = "hello"
location = "us-central1"
template {
spec {
containers {
image = var.container_image
}
}
}
}
resource "google_cloud_run_service_iam_member" "hello" {
location = google_cloud_run_service.hello.location
service = google_cloud_run_service.hello.name
role = "roles/run.invoker"
member = "allUsers"
}
まず以下の部分は Terraform の設定です。
terraform {
backend "gcs" {
bucket = "<bucket-name>"
}
}
backend
ブロックで Terraform で作成した構成情報をどこに保存するかを指定しています。 Terraform は実行時にこの保存情報を参照してどのリソースが管理下にあるかを把握し、変更差分を抽出したりしますのでこの設定は重要です。 例えば local を指定した場合、その PC からしかリソース更新ができなくなりますので CI/CD 運用ができないことはもちろん、運用上の大きなリスクになり得ます。 CI/CD 運用を前提とするのであれば、デプロイ対象のリソース群と同じ場所のストレージ( GCP なら GCS )に保存するのが良いと思います。
このサンプルではGoogle Cloud Storage に保存することを指示しています。 <bucket-name>
の部分を GCP 上の事前準備 で作成したバケット名に変更してください。
次は Provider の設定です。
provider "google" {
project = "<project-name>"
region = "us-central1"
zone = "us-central1-c"
}
ここで GCP を使用すること、また、デフォルトで使用するプロジェクトとロケーションを指定しています。<project-name>
の部分を GCP 上の事前準備 で作成したプロジェクト名に変更してください。
以降は CloudRun の設定です。
variable "container_image" {}
resource "google_cloud_run_service" "hello" {
name = "hello"
location = "us-central1"
template {
spec {
containers {
image = var.container_image
}
}
}
}
resource "google_cloud_run_service_iam_member" "hello" {
location = google_cloud_run_service.hello.location
service = google_cloud_run_service.hello.name
role = "roles/run.invoker"
member = "allUsers"
}
variable
の部分は外部から与えられる変数の定義を意味します。
ここでは CloudRun で実行するコンテナイメージのパスを外部から指定可能にしています ( google_cloud_run_service
の中の var.container_image
)。 その理由は後述します。google_cloud_run_service
が CloudRun の定義です。 ここでは最低限必要な、名前、場所、イメージのみ指定しています。google_cloud_run_service_iam_member
は CloudRun をパブリックアクセス可能にするための最低限の定義です。
実行
実行してみましょう。
> docker-compose run --rm terraform init
> docker-compose run --rm -e TF_VAR_container_image="us-docker.pkg.dev/cloudrun/container/hello:latest" terraform apply
* apply
の際に確認を求められるので yes
と入力します。apply
で -e TF_VAR_container_image="us-docker.pkg.dev/cloudrun/container/hello:latest"
と環境変数を指定していますが、 TF_VAR_
で始まる環境変数は Terraform の変数と解釈され、 TF_VAR_
以降の文字列が .tf ファイル内の variable
に設定されます。 ですので、 main.tf の var.container_image
をここで指定している形になります。
コマンドが正常終了したら、 GCP のコンソールから CloudRun を見てみましょう。
生成にしばらく時間がかかると思いますが、生成が完了したら URL が表示されますので、クリックしてみてください。 「It’s running!」と書いてある画面が出てきたら問題なく動いています。
CI/CD への適用
では、 CI/CD でどう運用するか、ですが、デプロイが必要な状況は主に2通りあると思います。
- 環境自体(構成等)の更新が必要なケース
- 構成するアプリケーションの更新が必要なケース
それぞれのケースを説明します。
環境の更新
例えばサーバーがもう1台必要になる等、環境の更新が必要になるケースもあると思います。 そういった場合でも環境を全て .tf ファイルで記述するようにしておけば、
「環境の更新」 = 「.tf ファイルの更新」
ということになりますので話は単純です。
.tf を Git で管理し、その更新時の CI/CD で上記のコマンドを実行すれば良いです。
ただし、上記の内容は説明を単純にするために docker-compose を使う等しているので、 CI/CD に合わせた方法で実行するのが良いと思います。 例えばGitHub Actions を使用する場合は、Terraform の公式アクション を使うのが最も簡単でしょう。
アプリケーションの更新
頻度としてはこちらの方が圧倒的に多いと思います。 機能追加、バグフィックス等サーバーアプリケーションが更新されたときにそれを自動的にデプロイするケースです。
* この記事ではアプリケーションは Docker コンテナで構築される前提です。
前述のサンプル ではコンテナイメージのパスを variable
変数に設定し、コマンドから指定できるように構成しました。 その理由はコンテナ更新時の対応のためです。
ここで、 latest
タグを指定しておけば常に最新のイメージをデプロイしてくれるのでは?と思われるかもしれません。 しかし、 Terraform はイメージの文字列のみをみて判断するらしく、 パスの文字列に変化がないので全ての latest
は同じもの、と判断され結果が No changes
となってしまいます。
このため、コンテナイメージのパス文字列はタグ形式ではなく、ダイジェスト形式で指定する必要があります。
前述のコマンド では長くなるのでタグ形式( us-docker.pkg.dev/cloudrun/container/hello:latest
)で指定しましたが、 実際の運用ではダイジェスト形式で指定するので、こういう形です。
us-docker.pkg.dev/cloudrun/container/hello@sha256:717e538e1ef8f955a54834e213d080bde6a8b3513fcc406df0d5d5ed3ed2853b
手順としては以下の通りです。
- アプリケーションの更新
- CI/CD によりアプリケーションのコンテナイメージをビルド、レジストリにpush
- レジストリよりダイジェスト形式のイメージパスを取得
- 取得したイメージパスを引数として .tf の
apply
を起動
これでアプリケーションが更新された場合、 ダイジェスト形式のイメージパスが変化するので Terraform により変更が検出され、 自動的にデプロイされることになります。
所感
IaC はとても便利だと思います。 ここでは触れていませんが、きちんと作っておけばdev環境やちょっとした自分専用のテスト環境等を作る際に、 コマンドだけで環境作成、破棄ができるのでとても重宝します。
デプロイも手作業で行う場合はマニュアルの更新や問題があった場合に何が間違っていたのかよくわからないケースもあると思います (手作業であれば何を行なったかは曖昧になったりするので)。
CI/CD と IaC で環境更新を行う場合は問題があった場合も基本的にコードの不具合、 あるいは実行環境の不具合として残っているので以降の問題解決に繋げやすいです。