CloudFormationとTerraformをCloudWatch Logsのログデータエクスポート処理を実装して比較する
はじめに
こんにちは。株式会社divxのエンジニア倉橋です。
インフラをコード化する技術、いわゆるInfrastructure as Code(IaC)に関しては、現在様々な選択肢が存在します。ITエンジニアの方なら、CloudFormationやTerraformといった言葉を聞いたことがある人は少なくないかもしれません。
一方で、様々な選択肢が存在すると、どのツールを選択すべきかを判断するのが難しくなります。 そこで、今回はIaCツールで多く採用されるCloudFormationとTerraformに着目して、実際に実装を行いながら、どのような場合にどちらのツールを使用した方がいいのかを考えます。
本記事の流れとしては、まずはCloudFormationとTerraformがどういったものなのかを確認して両者の違いを比較します。次に、両者のツールを実際に使用して実装を行います。実装内容は、CloudWatch LogsのログデータをS3に定期的にエクスポートさせる内容です。
最後に、実際に実装を行った感想を踏まえて、どのような場合にどちらのツールを使用したほうがいいのかを考えます。
CloudFormationとは?
CloudFormationとは、AWSのリソースをテキストファイル、またはテンプレートで管理できるAWSが提供するサービスです。
CloudFormationを使用することで、リソースの管理に費やす時間を減らすことができ、アプリケーション開発に集中する時間を増やすことができます。
CloudFormationには、大きく3つの特徴があります。
シンプルにインフラを管理することができる
スケーラブルなWebアプリケーションを運用する場合、Auto Scalingグループやロードバランサー、Amazon Relational Database Serviceなどを利用する必要があるかもしれません。このような様々なサービスを利用する場合、それぞれのサービスが連携して動作するように複雑な設定をする必要があります。
このような場面において、CloudFormationを使うことで簡単にインフラを管理できます。CloudFormationでは、テンプレートからスタックと呼ばれるものを作成できます。このスタックを作成することで、関連するAWSのリソースをすべて作成して動作させることができます。
また、リソースを削除する場合、スタックを削除することで、関連するすべてのリソースを削除できます。
インフラを素早く複製することができる
AWSでインフラを構築する際、以下のような要望があるかもしれません。
- 別のリージョンに同じ構成のインフラを構築したい
- マルチアカウント運用をするにあたって、アカウントごとにインフラを構成したい
上記の要望に関しては、CloudFormationを使用することで簡単に実現できます。
CloudFormationでは、スタックを作成することでAWSのリソースを作成させることができます。そのため、スタックを作成するためのテンプレートさえ準備しておけば、他のリージョンやアカウントにおいて、そのテンプレートからスタックを作成することで簡単にインフラを構築できます。つまり、インフラを素早く複製できます。
インフラの変更を簡単に追跡することができる
CloudFormationではインフラをテキストファイルで管理するため、どのような変更を行うかをより視覚的に理解できます。
テンプレートのバージョン管理システムを利用することで、いつ、誰が、どのような変更を加えたかを正確に把握できます。また、インフラの変更を取り消す必要がある場合、以前のバージョンのテンプレートを使用することで簡単に実現できます。
Terraformとは?
Terraformとは、HashiCorpによって開発されたIaCを実現するオープンソースのソフトウェアツールです。基本的な考え方はCloudFormationと重複する部分があり、インフラをコードで管理できます。
ひとつの大きなポイントとして、CloudFormationがAWSのリソースを管理するのに対して、TerraformはAWSやGCP、Azureなどのマルチベンダーに対応しています。つまり、複数のクラウドに渡ってインフラを構築できます。
Terraformは大きく3つのワークフローで構成されます。
Write
まずはインフラを構築するにあたって、複数のリソースをコードで定義します。
Plan
Planの段階においては、Writeで定義したインフラのコードに基づいて、インフラを新規作成、更新、削除する実行計画を作成します。
Apply
Applyの段階においては、Planで作成した実行計画を正しい順序で実際に実行します。つまり、この段階で実際にインフラが構築されます。
CloudFormationとTerraformの違い
CloudFormationとTerreformの概要がわかったところで、両者の違いに着目します。
対応可能プロバイダー
CloudFormationは、AWSのリソースを作成できます。しかし、AzureやGCPで構成されたサービスには対応していません。 Terraformは、AWSだけでなくAzureやGCPなどの様々なプロバイダーに対応できます。その他にも様々なプロバイダーに対応しています。
言語
CloudFormationは、JSONまたはYAMLのどちらかを使用できます。 CloudFormationでは、Resourcesというブロックに作成するリソースを定義します。下記コードでは、インスタンスタイプがt2.microのEC2インスタンスを作成するように定義されています。
Terraformは、HashiCorpが開発したHCL(HashiCorp Configuration Language)を使用します。HCLは、JSONベースの構文とHCL独自の構文から構成されています。{}で囲まれた中に作成するリソースの設定を記述していきます。resourceブロックを使用することで、リソースを作成することができます。下記コードでは、app_serverという名前でタイプがaws_instanceのリソースを作成するという意味になります。つまり、EC2インスタンスを作成しています。
参考: Build Infrastructure - Terraform AWS Example
CloudFormationとTerreformでの実装
これまでの内容で、CloudFormationとTerraformの違いについて理解できました。それでは、CloudFormationとTerraformを使って具体的な実装を行います。
今回は以下のような状況を想定します。
■想定された状況
- コスト面を考えてCloudWatch LogsのログデータをS3に定期的にエクスポートしたい
- ログデータのエクスポートに関して、リアルタイム性は求めていない
- 複数のログデータをエクスポートしたい
今回のポイントの一つとして、複数のログデータをエクスポートする点があります。アプリケーションを運用していると、Auroraデータベースの監査ログやアプリケーションログなど、様々なログデータを管理する必要があります。様々なAWSサービスを利用する複雑なアプリケーションになれば、ログデータの数も多くなる可能性があります。
CloudWatch Logsでもログデータを保管することはできます。しかし、CloudWwatch Logsの東京リージョンのアーカイブ料金は0.033USD/GBであるのに対して、S3の方がアーカイブ料金が0.025USD/GBと料金が安いです。アプリケーションの規模が大きくなるにつれてログデータの量も多くなるため、できるだけS3などのストレージサービスにログデータは移行しておきたいです。
参考:
Amazon CloudWatch の料金
Amazon S3 の料金
アーキテクチャ解説
今回はCloudWatch LogsのログデータをS3に定期的にエクスポートするにあたって、以下の構成で実装を行います。
EventBridge RuleでStepFunctionsのステートマシンを実行します。StepFunctionsでは、Lambdaを呼び出してCloudWatch LogsのログデータをS3にエクスポートします。
2つ目のEventBridge Ruleを作成して、もしもStepFunctionsの処理に失敗した場合にSNSを通してEmailに通知を行います。
Kinesis Data FirehoseではなくLambdaを使ってエクスポートする理由
Kinesis Data FirehoseはほぼリアルタイムにデータをS3などに送信することができるサービスです。
Kinesis Data Firehoseを使うことで簡単にS3にエクスポート処理を行うことができます。しかし、今回はリアルタイムなエクスポート処理は求められておらず、Kinesis Data Firehoseには別途取り込み料金がかかります。そのため、Lmabdaを使ってエクスポートするように実装を行います。
Step Functionsを使用する理由
CloudWatch Logsのエクスポート処理を行うCreateExportTaskは、各AWSアカウントにおいて一度に一つのエクスポートタスクしか持つことができません。
つまり、同時に複数のロググループをエクスポートすることはできず、順次ロググループごとにエクスポート処理を行う必要があります。
そのため、今回はStep Functionsを使用してLambdaによるエクスポート処理を順次行わせます。Step FunctionsのWait Stateを使用することでエクスポート処理が完了するまで待ち、処理が完了次第次のエクスポート処理が実行されるように自動化をさせます。
Step Functionsのステートマシンの解説
ワークフロー図
各ステートの解説
CreateExportTask
CloudWatch LogsのログデータをS3にエクスポートする処理を行います。エクスポート処理はLambdaでboto3を用いて実装します。
WaitFiveSeconds
5秒間待つ処理を行います。5秒経過したらDescribeExportTasksステートに移動してエクスポートステータスを取得します。
DescribeExportTasks
CreateExportTaskのステータスを取得します。ステータスに関しては、以下のパターンが考えられます。
■ステータス
- COMPLETED
- FAILED
- PENDING
- RUNNING
CheckStatusCode
DescribeExportTasksで取得したCreateExportTaskのステータスに応じて、次にどのステートに進むかを決めます。
CreateExportTaskのステータスに応じたステートの進み方は以下のようになります。
- COMPLETED(エクスポート完了) → isAllLogsExported
- RUNNING・PENDING(エクスポート処理中) → WaitFiveSeconds
- FAILED(エクスポート失敗) → ExportFailed
IsAllLogsExported
すべてのロググループがエクスポートされたかを確認します。
もしもまだエクスポートするべきロググループが残っている場合、CreateExportTaskステートに移動して再度エクスポート処理を行います。
すべてのロググループのエクスポート処理が完了している場合、Succeedに移動して処理が完了します。
■ステートマシン定義
TimeoutSeconds
タイムアウト値を設定することができます。Step Functionsはステートの移動に応じて料金が課金されます。そのため、意図せずStep Functionsの処理が長時間継続してしまうと、料金が想定よりも高くなってしまう可能性があります。タイムアウト値を設定することはAWSが提唱するベストプラクティスの一つでもあるため設定しておきます。
Lambda関数の解説
Lambda関数は以下の3ファイルを作成します。
- describe_log_groups.py
- create_export_task.py
- describe_export_task.py
describe_log_groups.py
describe_log_groups.pyは、エクスポート対象のCloudWatch Logsのロググループを取得します。
create_export_task.py
エクスポート処理をするLambda関数の内容は以下の通りです。
create_export_taskはロググループからS3バケットにログデータをエクスポートするためのエクスポートタスクを作成します。
specify_time_range_for_export関数は、エクスポートするロググループの日時範囲を取得するための関数です。エクスポートする開始と終わりの時間を返します。この返り値は、create_export_task関数の引数に渡します。今回は前日分のロググループを取得するように実装しています。
注意点として、時間指定はミリ秒指定する必要があるため、specify_time_range_for_export関数内でミリ秒にキャストしています。
describe_export_task.py
describe_export_task.pyは、create_export_task.pyで作成されたエクスポートタスクのステータスを確認します。
describe-export-tasksは指定したタスクIDのステータスを取得できます。describe-export-tasksを使うことで、指定したエクスポートIDのエクスポートタスクが完了しているか否かのステータスを取得します。
CloudFormationでの実装
ネストされたスタック
今回の実装では様々なAWSリソースを作成する必要があるため、複数のymlファイルを用意してスタックを分割して運用をします。そのためにネストされたスタックを用いて実装を行います。
ネストされたスタックにおいては、親と子のスタックが存在します。例えば、下記画像においては、AはBの親スタックであり、BはAの子スタックとなります。
引用: ネストされたスタックの操作
ネストされたスタックでは、親スタックを作成することで、子スタックも合わせて作成できます。
結果的に、CloudFormationにおいて一度の実行で関連する全てのリソースを作成できます。
ネストされたスタックを作成するには、ResourceのTypeをAWS::CloudFormation::Stackにして、あらかじめS3にテンプレートをアップロードしておく必要があります。
そのため、以下画像のようにバケット内において各サービス毎にフォルダーを作成します。そして、作成したフォルダー内にymlテンプレートを配置します。
Terraformでの実装
モジュール構成
次に、Terraformでの実装方法を確認します。
Terraformは全ての設定をモジュールとして扱います。terraformコマンドを実行したディレクトリは、ルートモジュールとして扱われます。
モジュールは、主に以下のファイルで構成されます。
- main.tf : モジュールの中心的な設定を記述するファイルです。作成するリソースの設定を中心に記述します。
- variables.tf : モジュール内で使用する変数を定義するファイルです。variables.tfで定義した値は、var.<NAME>の形で取得できます。
- output.tf : モジュールの出力内容を定義するファイルです。モジュールの出力内容は、他のモジュールにインフラの情報を渡すために使用できます。例えば、モジュール内で作成したリソースのARNの値を出力することで、他のモジュール内でそのARNの値を利用できます。
参考: Module structure
moduleブロック
今回は複数のリソースを作成するにあたって、moduleブロックを用いて実装を行います。moduleブロックを使用することで、複数のリソースを作成する際に、ファイルを分割して実装できます。
下記の例では、root-moduleという大本のルートモジュールが存在しています。そして、modulesディレクトリの中にchild_module_1とchild_module_2という子モジュールが存在しています。
複数のモジュールが存在しますが、ルートモジュールでmoduleブロックを使用することで一度に呼び出すことができます。
moduleブロックにはsource引数が必要です。source引数にはモジュールが定義されているローカルディレクトリのパスを指定します。
モジュールが他のモジュール内で使用される場合、使用される側のモジュールで引数を指定する必要があります。上記のコードの場合、child_module_2モジュールではserversという変数が定義されています。
default値が設定されていない変数は必須の引数となります。デフォルト値を持つ変数も、モジュールの引数として指定することで、デフォルト値を上書きできます。
ディレクトリ構成
moduleブロックを使用するため、今回は以下のようなディレクトリ構成で実装を行います。トップディレクトリにmain.tfを起きつつ、各リソースを作成するファイルをmodulesディレクトリに配置します。そして、トップディレクトリのmain.tfから各moduleを呼び出します。
CloudFormationとTerraformでの実行
それでは、CloudFormationとTerraformで行ったそれぞれの実装を実際に実行します。
CloudFormationの実行
今回はコンソールではなく、コマンドラインでCloudFormationのテンプレートをデプロイします。コマンドラインでデプロイをするには、aws cloudformation deployコマンドを実行する必要があります。
注意点として、capabilitiesオプションにおいて、CAPABILITY_IAMを指定する必要があります。CAPABILITY_IAMを指定することで、IAMリソースを作成できます。
リソースの作成が完了すると、以下のようにSuccessfully created/updated stackと表示がされます。
Terraformの実行
基本的には以下のステップで実行を行います。
- terraform init
- terraform plan
- terraform apply
terraform init
terraform initコマンドは、Terraformの設定ファイルを含む作業ディレクトリを初期化します。
コマンドを実行すると、以下のディレクトリ、ファイルが作成されます。
- .terraformディレクトリ
- .terraform.lock.hcl
.terraformディレクトリは、キャッシュされたプロバイダーのプラグインやモジュールを管理します。
.terraform.lock.hclは、Terraformの依存関係を固定するファイルです。terraform initコマンドが実行される度に作成または更新されます。.terraform.lock.hclは、将来的な依存関係の変化を議論するために、gitでのバージョン管理はするべきです。
terraform apply
Plan: 28 to addという表示から、28のリソースが新しく作成されることを確認できます。
Only 'yes' will be accepted to approve.という表示にもあるように、ターミナル上で「yes」と入力をしてエンターキーを押すことで実際にリソースが作成されます。
リソース作成に成功した場合、ターミナル上に以下のように表示されます。28のリソースが新しく作成されたことを確認できます。
Apply complete! Resources: 28 added, 0 changed, 0 destroyed.
結局どちらのツールを使用したらよいのか
CloudFormationとTerraformの両者で実装を行いました。実際に実装を行った感想を踏まえ、以下の観点で両者を比較します。
- マルチベンダーに対応しているか否か
- コードの書き方
- インフラ適用までのフロー
マルチベンダーに対応しているか否か
Terraformの強みのひとつとして、マルチベンダーに対応していることが挙げられると思います。CloudFormationではAzureやGCPには対応できないため、複数のインフラサービスを使用している場合、Terraformを選択肢として選ぶのは間違っていないと思います。
コードの書き方
CloudFormationは、YAMLで記述することができるため、コードの書き方に関しては特に困ることはありませんでした。公式のTemplate referenceにも丁寧に記述方法が記載されているため、ドキュメントを読みながら実装を行えば、スムーズに実装ができると思います。
Terraformのコードの書き方に関して、最初はTerraform独自の記述方法やmoduleブロックの使い方に少し慣れが必要かもしれません。しかし、一度慣れてしまうと簡単に記述することができ、インターネット上にベストプラクティスとなるテンプレートがたくさんあるため、特段困ることはないと思います。Terraform AWS modulesにアクセスすると、すでに多くのモジュールを確認できます。また、Terraformで複数のリソースを作成する際、module機能を使うことでシンプルな構成が実現できたと思います。個人的には、Terraformを使って一番便利だと感じました。
インフラ適用までのフロー
CloudFormationでは、change setsを使用することで事前にリソースにどのような影響があるかを確認することができます。実際にリソースを作成する際は、コンソール、またはCLIのdeployコマンドでデプロイを行うことができます。
Terraformは、リソースを作成するまでの確認ステップがわかりやすかったです。terraform planとterraform applyによってリソース作成の「計画」と「実行」が分離されているためです。事前にterraform planコマンドを実行するだけで、どのような変更がされるかを簡単に確認できるため、自信をもって変更を適用させることができました。
個人的な意見
サービスが複数のインフラサービスを使用している場合、Terraformのマルチベンダー対応が強みとなるため、Terraformを選ぶのは間違いではないと思います。Terraformのコードに関しては、最初は少し慣れが必要かもしれませんが、ベストプラクティスなどのコードがすでにたくさんあるため、学習コストが懸念点になることはあまりないと思います。moduleブロックやterraform planコマンドでのリソース変更確認など、使いやすいと感じたのはTerraformでした。
一方で、サードパーティのリソースをあまり使用しておらず、AWSメインで使用している場合はCloudFormationを選択するのがよいかもしれません。TerraformはAWSの公式サービスではないため、新しいAWSサービスにTerraformが対応するのに時間がかかる可能性があるためです。
まとめ
今回は、CloudFormationとTerreformの概要を把握して、実際に実装を行いながら両者の違いについて着目しました。
個人的な意見としては、コードの書き方や構成のしやすさに関しては、Terraformの方が書きやすいと感じることが多かったです。特に、今回は複数のリソースを作成する必要があったため、Terraformのmoduleブロックの有り難みを感じました。この感覚は、実際に両者で実装を行い、比較することではじめてわかったことです。改めて、実際に技術を使ってみることは大切だと思いました。
両者ともそれぞれの特徴があるため、IaCでどちらを採用するかは組織事情によって変わってくると思います。大きなポイントは、「マルチベンダーへの対応が必要なのか」という点です。将来的に、AWS以外のクラウドサービスなどを使用する可能性がある場合は、Terraformの利用を視野に入れておくと良いと思います。
divxでは一緒に働ける仲間を募集しています。
興味があるかたはぜひ採用ページを御覧ください。