じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

Terraformの条件分岐にうってつけの日

Infrastructure as Codeの概念とTerraformの役割

Infrastructure as Code (IaC) は、現代のインフラ管理の根幹を成すものです。IaCがどんなものか様々な言論があると思いますが、ここではソフトウェア開発のプラクティスに基づくインフラストラクチャ自動化のアプローチとIaC 本に準拠しておきます。IaCによる自動化、バージョン管理、テスト、そして継続的インテグレーションなどのプラクティスは、システム管理の世界に革命をもたらしました。ちなみに個人的には各々のプラクティスを一つずつ実践しない度にIaCの価値は一つずつ確実に下がっていくものだと確信してます。ですが、各々にコストがかかるものなので各プラクティスをどこまで実践するかは非常に難しい問題だとも同時に思います。その中で周知の事実だとは思いますがTerraformは、これらのプラクティスを宣言的なインフラストラクチャの管理、定義、および構成に応用することで、効率性と柔軟性の高いインフラストラクチャ管理を可能にします。このツールは、設計から実装までの過程を劇的に変える可能性を秘めています。

Infrastructure as Codeの概念とTerraformの役割に関する参考リンク

Infrastructure as Codeの原則とTerraform

Infrastructure as Codeの原則には、以下のような要素が含まれます​​:

  • 簡単に再現できるシステム: Terraformを使用することで、インフラストラクチャをコードとして定義し、簡単に再現可能なシステムを構築できます。
  • 使い捨てにできるシステム: サーバーなどのリソースを一時的なものとして扱い、必要に応じて簡単に生成・破棄できます。
  • 統一的なシステム: 全てのインフラストラクチャのコンポーネントを統一的な方法で管理します。
  • 反復できるプロセス: 同じ設定を繰り返し適用することで、一貫性と信頼性を保ちます。

これらの原則に基づいて、Terraformは以下のような機能を提供します:

  • リソースの自動生成と管理: Terraformを使用すると、インフラストラクチャのリソースを自動的に生成・管理できます。
  • 宣言的なインフラの構築: Terraformを通じて、インフラストラクチャの状態を宣言的に定義し、計画的かつ一貫性のある方法でインフラを構築・更新します。
  • バージョン管理のサポート: Terraformの設定ファイルはバージョン管理システムで管理でき、変更履歴を追跡できます。
  • モジュールと再利用可能なコンポーネント: Terraformではモジュールを使って、コードの再利用性を高めます。

なのでそれ以外のプラクティスに関しては別のソリューションで実現してあげる必要があります。

Terraformの条件分岐のテクニックと利用場面

もう少し能書きを垂れるかなって思ったんですけどもう飽きたので普通にテクニックや使い方の話をしていきます。Terraformは基本的に宣言的なインフラ定義ツールですが、宣言的だけでは現実の複雑な要求を満たすのが難しい場合があります。そのため、Terraformは手続き型プログラミングに近い柔軟性も提供します。条件分岐やループなど、より具体的な制御が必要な場面で役立つ機能を組み込んで、効率的かつ柔軟なインフラ管理を実現しています。それでは、これらのテクニックや利用場面について、具体的な例を交えて詳しく見ていきましょう。

ループ (countとfor_each)

ループは、同じタイプのリソースを複数回作成する際に便利です。countfor_eachを使用して、コードの重複を避けながら、効率的にリソースを管理できます。

利用場面A: 異なる環境に同一種類のリソースを複数作成

resource "aws_instance" "dev_servers" {
  count         = 5
  instance_type = "t2.micro"
  # その他の設定
}

利用場面B: 複数のユーザーにIAMロールを割り当て

resource "aws_iam_user" "users" {
  for_each = toset(["alice", "bob", "charlie"])
  name     = each.value
  # その他の設定
}

条件分岐 (countを使用)

条件分岐を使用すると、環境やパラメータに基づいてリソースの作成を制御できます。これにより、開発環境と本番環境などで異なるリソース設定を実現できます。

利用場面A: 本番環境でのみデータベースのインスタンスを作成

resource "aws_db_instance" "prod_db" {
  count = var.is_production ? 1 : 0
  # データベースの設定
}

利用場面B: 開発環境ではリソースを作成せず、本番環境でのみ特定のリソース(例: S3バケット)を作成したい場合。

resource "aws_s3_bucket" "prod_bucket" {
  count  = var.env == "prod" ? 1 : 0
  bucket = "my-production-bucket"
  acl    = "private"
}

ここではvar.env変数がprod(本番環境)の場合にのみS3バケットを作成します

利用場面C: 特定の機能フラグ(例: 監視機能の有効化)がオンの場合にのみ、関連リソース(例: CloudWatchアラーム)をデプロイしたい。

resource "aws_cloudwatch_metric_alarm" "example_alarm" {
  count               = var.enable_monitoring ? 1 : 0
  alarm_name          = "High-CPU-Utilization"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  threshold           = "80"
  # その他の設定
}

この例では、var.enable_monitoringtrueの場合にのみCloudWatchアラームを作成します。

ゼロダウンタイムデプロイメント (create_before_destroyを使用)

ゼロダウンタイムデプロイメントは、システムやアプリケーションの更新時にサービスを停止することなく、新しいバージョンへの移行を行う手法です。Terraformにおけるゼロダウンタイムデプロイメントでは、create_before_destroyライフサイクル設定を使用して、新しいリソースを古いリソースを削除する前に作成します。これにより、サービスが継続的に稼働しつつ、背後で安全にリソースの更新や交換が行われます。

利用場面A: アプリケーションの更新時に新旧インスタンスの平滑な切り替え

resource "aws_instance" "app_server" {
  ami           = "ami-newversion"
  instance_type = "t2.micro"
  lifecycle {
    create_before_destroy = true
  }
  # その他の設定
}

このコードは、新しいAMIでEC2インスタンスを作成します。create_before_destroytrueに設定されているため、新しいインスタンスが完全に起動し、運用準備が整うまで旧インスタンスは削除されません。これにより、アプリケーションの更新中もサービスが継続して提供されます。

利用場面B: インフラのリファクタリング時に既存リソースの無停止更新

resource "aws_s3_bucket" "storage" {
  bucket = "my-new-bucket-name"
  lifecycle {
    create_before_destroy = true
  }
  # その他の設定
}

この設定では、新しいS3バケットが作成される際、既存のバケットは新しいバケットの設定が完了し、利用可能になるまで保持されます。これにより、データの移行やバケットの設定変更が行われる際にも、サービスの中断を回避できます。

ゼロダウンタイムデプロイメントの限界

ゼロダウンタイムデプロイメントは最高だと思った皆様、悲報です。ゼロダウンタイムデプロイメントを行う際にcreate_before_destroyを使用すると、いくつかの問題点があります。特に、オートスケーリングポリシーを使うと、デプロイメントごとに自動スケーリンググループ(ASG)のサイズが最小サイズに戻ることが問題です。これは、デプロイメント時にサーバー数が本来の数より少なくなる可能性があるためです。解決策として、カスタムスクリプトを使用してAWS APIでデプロイメント前のインスタンス数を取得する方法があります。

しかし、より重要なのは、複雑なタスクにはネイティブな解決策を使用することが望ましいということです。たとえば、AWSではinstance refreshというオートスケーリンググループ用のネイティブソリューションが提供されており、これはAWSによって完全に管理され、エラー処理も適切です。ただし、このプロセスは時に遅いことが欠点です。一般的には、instance refreshのようなネイティブなデプロイメントオプションを使うことが推奨されています。なので、Providerの実装次第という部分もあると思います。

Lifecycle をちゃんとやっていると、柔軟性と安全性が格段に向上する

lifecycle引数は、リソースの作成と破棄に関するカスタムルールを作成することで、Terraform操作の流れを制御します。これにより、特定のリソースの変更やインフラへの影響を防ぎつつ、リソースニーズに基づいて潜在的なダウンタイムを最小限に抑えることができます​​。

  • prevent_destroy: このオプションは、特定のリソースの削除を防ぐために使用されます。例えば、ある属性の変更によりリソースの置換が必要になりダウンタイムが発生する可能性がある場合、prevent_destroyを使ってリソースの削除を防ぐことができます​​。
  • create_before_destroy: この属性を使用すると、古いリソースを破棄する前に新しいリソースを作成できます。これにより、リソースの置換によるダウンタイムを避けることが可能です。create_before_destroyがない場合、Terraformはまずインスタンスを破棄し、その後再作成しますが、これによりダウンタイムが発生する可能性があります​​。
  • ignore_changes: Terraformのワークフロー外で行われた変更を無視するために使用されます。例えば、AWS CLIで行われた変更をignore_changesを使ってTerraformの操作に影響しないようにすることができます

lifecycleの学びの意義は、インフラ管理の柔軟性と安全性を高めることにあります。異なるlifecycleオプションを使用することで、意図しないリソースの削除を防いだり、インフラの再作成時のダウンタイムを最小限に抑えたり、外部からの変更をTerraformのプランに影響させないようにすることができます。これにより、Terraformを使ったインフラの管理がより安全かつ効率的になります。

結論とか

これらの使い方はもちろんのこと原則を理解しながら活用することで、インフラストラクチャの管理において幸せな世界観を目指していきましょう。

『Terraform: Up & Running』の日本語版第3版のリリースを心から祝福してます。この本は、Terraformの基本から応用までを幅広くカバーし、多くの開発者やシステム管理者にとってよても良い本となることでしょう。手元においておいて本当に損がない書籍かと思います。

参考資料

余談

Ansible やDockerではどのようにループや条件分岐を実現しているかAnsibleでは組み込まれている機能で実現できますがDockerでは、ループや条件分岐は通常、Dockerfile内では直接実現できません。しかし、Docker Composeやスクリプトを使用して間接的にこれらを処理することができます。Kubernetesでも、ループや条件分岐はマニフェストファイル(YAML)内で直接的にはサポートされていませんが、Helmチャートのようなテンプレートエンジンを使用することで、これらの動作を実現できます。Helmは条件分岐や変数の代入などを可能にするテンプレート機能を提供しているのでそれぞれ紹介します。

ループ

loopキーワードを使用して繰り返しタスクを実行します。

- name: パッケージのインストール
  yum:
    name: "{{ item }}"
    state: present
  loop:
    - httpd
    - memcached

この例では、.Values.services内の各サービスに対してループを行い、それぞれのnameportを出力しています。

{{- range .Values.services }}
- name: {{ .name }}
  port: {{ .port }}
{{- end }}

Docker Composeでのループと条件分岐

Docker Composeでは直接的なループや条件分岐のサポートはありませんが、環境変数を利用して擬似的にこれらを実現できます。

services:
  web:
    image: "webapp:${WEBAPP_TAG}"
    environment:
      - DEBUG=${DEBUG_MODE}

この例では、WEBAPP_TAGDEBUG_MODE環境変数を使用しています。

条件分岐

ステートメントを使用して、特定の条件に基づいてタスクを実行します。

- name: 開発環境でのみ実行するタスク
  command: echo "これは開発環境用のタスクです"
  when: env == 'development'

- name: 本番環境でのみ実行するタスク
  command: echo "これは本番環境用のタスクです"
  when: env == 'production'
{{- if .Values.debug }}
environment: "development"
{{- else }}
environment: "production"
{{- end }}

ここでは、.Values.debugの値に基づいて環境を設定しています。debugtrueならdevelopment、そうでなければproductionが選択されます。

この節では、Ansible、Docker、そしてKubernetesにおけるループと条件分岐の実装方法を比較しました。これらのツールはそれぞれに独自のアプローチを持っており、その違いを理解することで、適切なツール選択や実装戦略を行う上での参考になります。また、異なるツールでどのように同じ問題を解決しているかを知ることは、より深い技術的理解や柔軟な対応能力を身につけるために重要です。