- はじめに
- CloudFormationとは?
- Terraformとは?
- CloudFormationとTerraformの違い
- CloudFormationとTerreformでの実装
- CloudFormationとTerraformでの実行
- 結局どちらのツールを使用したらよいのか
- まとめ
- 参考リソース
はじめに
こんにちは。株式会社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インスタンスを作成するように定義されています。
Resources: AppServer: Type: AWS::EC2::Instance Properties: ImageId: "ami-830c94e3" InstanceType: "t2.micro" Tags: [ { "Key": "Name", "Value": "ExampleAppServerInstance" } ]
Terraformは、HashiCorpが開発したHCL(HashiCorp Configuration Language)を使用します。HCLは、JSONベースの構文とHCL独自の構文から構成されています。{}で囲まれた中に作成するリソースの設定を記述していきます。resourceブロックを使用することで、リソースを作成することができます。下記コードでは、app_serverという名前でタイプがaws_instanceのリソースを作成するという意味になります。つまり、EC2インスタンスを作成しています。
provider "aws" { profile = "default" region = "us-west-2" } resource "aws_instance" "app_server" { ami = "ami-830c94e3" instance_type = "t2.micro" tags = { Name = "ExampleAppServerInstance" } }
CloudFormationとTerreformでの実装
これまでの内容で、CloudFormationとTerraformの違いについて理解できました。それでは、CloudFormationとTerraformを使って具体的な実装を行います。
今回は以下のような状況を想定します。
■想定された状況
- コスト面を考えてCloudWatch LogsのログデータをS3に定期的にエクスポートしたい
- ログデータのエクスポートに関して、リアルタイム性は求めていない
- 複数のログデータをエクスポートしたい
今回のポイントの一つとして、複数のログデータをエクスポートする点があります。アプリケーションを運用していると、Auroraデータベースの監査ログやアプリケーションログなど、様々なログデータを管理する必要があります。様々なAWSサービスを利用する複雑なアプリケーションになれば、ログデータの数も多くなる可能性があります。
CloudWatch Logsでもログデータを保管することはできます。しかし、CloudWwatch Logsの東京リージョンのアーカイブ料金は0.033USD/GBであるのに対して、S3の方がアーカイブ料金が0.025USD/GBと料金が安いです。アプリケーションの規模が大きくなるにつれてログデータの量も多くなるため、できるだけ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に移動して処理が完了します。
■ステートマシン定義
{ "StartAt": "Initialize", "TimeoutSeconds": 600, "States": { "Initialize": { "Type": "Task", "Resource": "LambdaのARN", "ResultPath": "$.log_groups_info", "Next": "CreateExportTask" }, "CreateExportTask": { "Type": "Task", "Resource": "LambdaのARN", "ResultPath": "$.log_groups_info", "Next": "WaitFiveSeconds" }, "DescribeExportTasks": { "Type": "Task", "Resource": "LambdaのARN", "ResultPath": "$.describe_export_task", "Next": "CheckStatusCode" }, "CheckStatusCode": { "Type": "Choice", "Choices": [ { "Variable": "$.describe_export_task.status_code", "StringEquals": "COMPLETED", "Next": "IsAllLogsExported?" }, { "Or": [ { "Variable": "$.describe_export_task.status_code", "StringEquals": "PENDING" }, { "Variable": "$.describe_export_task.status_code", "StringEquals": "RUNNING" } ], "Next": "WaitFiveSeconds" } ], "Default": "ExportFailed" }, "WaitFiveSeconds": { "Type": "Wait", "Seconds": 5, "Next": "DescribeExportTasks" }, "IsAllLogsExported?": { "Type": "Choice", "Choices": [ { "Variable": "$.log_groups_info.completed_flag", "BooleanEquals": true, "Next": "Done" } ], "Default": "CreateExportTask" }, "Done": { "Type": "Succeed" }, "ExportFailed": { "Type": "Fail" } } }
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のロググループを取得します。
import boto3 logs_client = boto3.client('logs') TARGET_LOG_GROUPS = [ # 下記ロググループは自分の環境のロググループに置き換えてください {"log_group_name": "/aws/lambda/test1", "prefix": "lambda/test1"}, {"log_group_name": "/aws/lambda/test2", "prefix": "lambda/test2"}, # 必要に応じて追加する ] def lambda_handler(event, context): log_groups_count = len(TARGET_LOG_GROUPS) print( { "event": event, "target_log_groups": TARGET_LOG_GROUPS, "log_groups_count": log_groups_count } ) return { "index": 0, "target_log_groups": TARGET_LOG_GROUPS, "log_group_count": log_groups_count }
create_export_task.py
エクスポート処理をするLambda関数の内容は以下の通りです。
import boto3 import datetime import os from typing import Tuple logs_client = boto3.client('logs') S3_BUCKET_NAME = os.environ["BUCKET_NAME"] def specify_time_range_for_export(today: datetime.date) -> Tuple[int, int]: today_datetime = datetime.datetime( year=today.year, month=today.month, day=today.day, hour=0, minute=0, second=0) from_time = today_datetime - datetime.timedelta(days=1, hours=9) to_time = today_datetime - datetime.timedelta(hours=9) milliseconds_from_time = int(from_time.timestamp() * 1000) milliseconds_to_time = int(to_time.timestamp() * 1000) return milliseconds_from_time, milliseconds_to_time def lambda_handler(event, context): index = event['log_groups_info']['index'] log_groups_count = event["log_groups_info"]["log_group_count"] today = datetime.date.today() yesterday = today - datetime.timedelta(days=1) TARGET_LOG_GROUPS = event["log_groups_info"]["target_log_groups"] from_time, to_time = specify_time_range_for_export(today) response = logs_client.create_export_task( logGroupName=TARGET_LOG_GROUPS[index]['log_group_name'], fromTime=from_time, to=to_time, destination=S3_BUCKET_NAME, destinationPrefix=yesterday.strftime( '{}/%Y/%m/%d'.format(TARGET_LOG_GROUPS[index]['prefix'])) ) index += 1 print( { "event": event, "response": response } ) return { "index": index, "completed_flag": index == log_groups_count, "task_id": response['taskId'], "log_group_count": log_groups_count, "target_log_groups": TARGET_LOG_GROUPS }
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で作成されたエクスポートタスクのステータスを確認します。
import boto3 logs_client = boto3.client('logs') def lambda_handler(event, context): task_id = event['log_groups_info']['task_id'] response = logs_client.describe_export_tasks( taskId=task_id ) status_code = response['exportTasks'][0]['status']['code'] print( { "event": event, "Task ID": task_id, 'Status Code': status_code } ) return { 'status_code': status_code }
describe-export-tasksは指定したタスクIDのステータスを取得できます。describe-export-tasksを使うことで、指定したエクスポートIDのエクスポートタスクが完了しているか否かのステータスを取得します。
CloudFormationでの実装
ネストされたスタック
今回の実装では様々なAWSリソースを作成する必要があるため、複数のymlファイルを用意してスタックを分割して運用をします。そのためにネストされたスタックを用いて実装を行います。
ネストされたスタックにおいては、親と子のスタックが存在します。例えば、下記画像においては、AはBの親スタックであり、BはAの子スタックとなります。
引用: ネストされたスタックの操作
ネストされたスタックでは、親スタックを作成することで、子スタックも合わせて作成できます。
結果的に、CloudFormationにおいて一度の実行で関連する全てのリソースを作成できます。
ネストされたスタックを作成するには、ResourceのTypeをAWS::CloudFormation::Stackにして、あらかじめS3にテンプレートをアップロードしておく必要があります。
そのため、以下画像のようにバケット内において各サービス毎にフォルダーを作成します。そして、作成したフォルダー内にymlテンプレートを配置します。
各ファイル説明は長くなるため、折りたたんでおきます。
CloudFormationテンプレート
親スタック
ExportCloudWatchLogsToS3.yml
AWSTemplateFormatVersion: "2010-09-09" Description: Creates the resources needed to export CloudWatch Logs to S3 Parameters: BucketLambdaTemplateURL: Type: String DescribeLogGroupsLambdaTemplateURL: Type: String CreateExportTaskLambdaTemplateURL: Type: String DescribeExportTaskLambdaTemplateURL: Type: String StepFunctionsStateMachineTemplateURL: Type: String CronExportLogsEventRuleURL: Type: String FailOrTimeOutNotificationEventRuleURL: Type: String SendEmailNotificationURL: Type: String EmailAddress: Type: String LambdaBucket: Type: String CreateExportTaskLambdaCode: Type: String DescribeExportTaskLambdaCode: Type: String DescribeLogGroupsLambdaCode: Type: String Resources: CloudWatchLogsBucket: Type: "AWS::CloudFormation::Stack" Properties: TemplateURL: !Ref BucketLambdaTemplateURL DescribeLogGroupsLambda: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Ref DescribeLogGroupsLambdaTemplateURL Parameters: S3BucketParam: !Ref LambdaBucket S3KeyParam: !Ref DescribeLogGroupsLambdaCode CreateExportTaskLambda: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Ref CreateExportTaskLambdaTemplateURL Parameters: BucketName: !GetAtt CloudWatchLogsBucket.Outputs.BucketName S3BucketParam: !Ref LambdaBucket S3KeyParam: !Ref CreateExportTaskLambdaCode DependsOn: CloudWatchLogsBucket DescribeExportTaskLambda: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Ref DescribeExportTaskLambdaTemplateURL Parameters: S3BucketParam: !Ref LambdaBucket S3KeyParam: !Ref DescribeExportTaskLambdaCode StepFunctionsStateMachine: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Ref StepFunctionsStateMachineTemplateURL Parameters: DescribeLogGroupsLambdaArn: !GetAtt DescribeLogGroupsLambda.Outputs.Arn CreateExportTaskLambdaArn: !GetAtt CreateExportTaskLambda.Outputs.Arn DescribeExportTaskLambdaArn: !GetAtt DescribeExportTaskLambda.Outputs.Arn DependsOn: - DescribeLogGroupsLambda - CreateExportTaskLambda - DescribeExportTaskLambda SendEmailNotification: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Ref SendEmailNotificationURL Parameters: EmailAddress: !Ref EmailAddress CronExportLogsEventRule: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Ref CronExportLogsEventRuleURL Parameters: StepFunctionsStateMachineArn: !GetAtt StepFunctionsStateMachine.Outputs.Arn DependsOn: StepFunctionsStateMachine FailOrTimeOutNotificationEventRule: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Ref FailOrTimeOutNotificationEventRuleURL Parameters: StepFunctionsStateMachineArn: !GetAtt StepFunctionsStateMachine.Outputs.Arn SendEmailNotificationTopicArn: !GetAtt SendEmailNotification.Outputs.Arn DependsOn: - StepFunctionsStateMachine - SendEmailNotification
ExportCloudWatchLogsToS3.ymlは、rootスタックとなるテンプレートファイルです。このファイルに定義されているResourcesから、S3に保存されているテンプレートを呼び出して各リソースを作成します。
リソースのタイプをAWS::CloudFormation::Stackにすることで、ネストされたスタックを作成しています。TemplateURLのプロパティではテンプレートが保存されているS3オブジェクトのKeyを指定します。TemplateURLの値に関しては、動的に取得できるようにParametersを設定しています。
S3関連の実装
S3.yml
AWSTemplateFormatVersion: "2010-09-09" Description: This stack creates S3 bucket for exporting logs Resources: CloudWatchLogsBucket: Type: "AWS::S3::Bucket" DeletionPolicy: Retain Properties: BucketName: !Join - "-" - - "cloudwatchlogs" - !Select - 0 - !Split - "-" - !Select - 2 - !Split - "/" - !Ref "AWS::StackId" BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 VersioningConfiguration: Status: Enabled S3BucketPolicy: Type: 'AWS::S3::BucketPolicy' Properties: Bucket: !Ref CloudWatchLogsBucket PolicyDocument: Version: "2012-10-17" Statement: - Action: "s3:GetBucketAcl" Effect: Allow Resource: !GetAtt CloudWatchLogsBucket.Arn Principal: Service: - logs.ap-northeast-1.amazonaws.com - Action: "s3:PutObject" Effect: Allow Resource: !Sub 'arn:aws:s3:::${CloudWatchLogsBucket}/*' Principal: Service: - logs.ap-northeast-1.amazonaws.com Outputs: BucketName: Value: !Ref CloudWatchLogsBucket Description: "Name for the logs bucket."
S3.ymlは、CloudWatch Logsのログデータ転送先のバケットを作成します。今回はログデータをバケットに保存するため、DeletionPolicyはRetainを設定しています。Retainに設定することで、スタックを削除したとしてもバケットは残ります。
Lambda関連の実装
CloudFormationでLambdaの実装をするにあたって、今回はS3バケットにLambdaコードのzipファイルをあらかじめアップロードしておきます。そして、そのS3バケットからLambdaコードを取得するように実装します。
あらかじめS3バケットを作成してテンプレートをアップロードしておきます。
aws s3 mb s3://任意のバケット名
今回は以下のように、codeというprefix配下にLambdaコードのzipファイルをアップロードします。
Lambdaに関しては、以下3つテンプレートファイルを作成します。
- describe_log_groups.yml
- create_export_task.yml
- describe_export_task.yml
describe_log_groups.yml
AWSTemplateFormatVersion: '2010-09-09' Description: This stack create DescribeLogGroupsLambda function. Parameters: S3BucketParam: Type: String Description: Name of bucket which the lambda code is stored S3KeyParam: Type: String Description: The object key prefix Resources: DescribeLogGroupsLambda: Type: AWS::Lambda::Function Properties: FunctionName: DescribeLogGroups Runtime: python3.9 Timeout: 900 Role: Fn::GetAtt: - "LambdaExecutionRole" - "Arn" Handler: describe_log_groups.lambda_handler Code: S3Bucket: !Ref S3BucketParam S3Key: !Ref S3KeyParam Description: Invoke a function during stack creation. TracingConfig: Mode: Active LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CloudWatchLogsDescribeExportTasks PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "logs:CreateLogStream" - "logs:PutLogEvents" - "logs:DescribeLogGroups" Resource: "*" Outputs: Arn: Description: The ARN of the Lambda function that describes log groups. Value: !GetAtt DescribeLogGroupsLambda.Arn Export: Name: DescribeLogGroupsLambda
今回はS3バケットからPythonソースコードを取得するため、Codeプロパティにおいて、S3Bucket、S3Keyを指定しています。
Outputsでは作成されるLambdaリソースのARNを出力するようにします。この値はStepFunctionsのステートマシンで使用します。
create_export_task.yml
AWSTemplateFormatVersion: '2010-09-09' Description: This stack create CreateExportTaskLambda function. Parameters: BucketName: Type: String Description: Name for the logs bucket. S3BucketParam: Type: String Description: Name of bucket which the lambda code is stored S3KeyParam: Type: String Description: The object key prefix Resources: CreateExportTaskLambda: Type: AWS::Lambda::Function Properties: FunctionName: CreateExportTask Runtime: python3.9 Timeout: 900 Role: Fn::GetAtt: - "LambdaExecutionRole" - "Arn" Environment: Variables: BUCKET_NAME: !Ref BucketName Handler: create_export_task.lambda_handler Code: S3Bucket: !Ref S3BucketParam S3Key: !Ref S3KeyParam Description: Invoke a function during stack creation. TracingConfig: Mode: Active LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CloudWatchLogsDescribeExportTasks PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "logs:CreateExportTask" Resource: "arn:aws:logs:*:*:*" Outputs: Arn: Description: The ARN of the Lambda function that create export task. Value: !GetAtt CreateExportTaskLambda.Arn Export: Name: CreateExportTaskLambda
create_export_task.ymlは、CloudWatch LogsのログデータをエクスポートするためのLambdaを作成します。 ParametersのBucketNameでエクスポート先のバケット名の値を受け取ります。
describe_export_task.yml
AWSTemplateFormatVersion: '2010-09-09' Description: This stack create DescribeExportTaskLambda function. Parameters: S3BucketParam: Type: String Description: Name of bucket which the lambda code is stored S3KeyParam: Type: String Description: The object key prefix Resources: DescribeExportTaskLambda: Type: AWS::Lambda::Function Properties: FunctionName: DescribeExportTask Runtime: python3.9 Timeout: 900 Role: Fn::GetAtt: - "LambdaExecutionRole" - "Arn" Handler: describe_export_task.lambda_handler Code: S3Bucket: !Ref S3BucketParam S3Key: !Ref S3KeyParam Description: Invoke a function during stack creation. TracingConfig: Mode: Active LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CloudWatchLogsDescribeExportTasks PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: "logs:DescribeExportTasks" Resource: "*" Outputs: Arn: Description: The ARN of the Lambda function that describes export task. Value: !GetAtt DescribeExportTaskLambda.Arn Export: Name: DescribeExportTaskLambda
describe_export_task.ymlは、CloudWatch Logsのエクスポートタスクが実行中か否かのステータスを取得するLambdaを作成します。上記2つのLambdaと同じ用に、作成されるLambdaのARNをOutputsで出力します。
StepFunctions関連の実装
ExportLogsToS3StateMachine.yml
AWSTemplateFormatVersion: '2010-09-09' Description: "This stack creates state machine" Parameters: DescribeLogGroupsLambdaArn: Type: String Description: 'The ARN of the Lambda function that describes log groups.' CreateExportTaskLambdaArn: Type: String Description: 'The ARN of the Lambda function that create export task.' DescribeExportTaskLambdaArn: Type: String Description: 'The ARN of the Lambda function that describes export task.' Resources: ExportLogsToS3StateMachine: Type: AWS::StepFunctions::StateMachine Properties: StateMachineName: ExportLogsToS3 DefinitionString: !Sub | { "StartAt": "Initialize", "TimeoutSeconds": 600, "States": { "Initialize": { "Type": "Task", "Resource": "${DescribeLogGroupsLambdaArn}", "ResultPath": "$.log_groups_info", "Next": "CreateExportTask" }, "CreateExportTask": { "Type": "Task", "Resource": "${CreateExportTaskLambdaArn}", "ResultPath": "$.log_groups_info", "Next": "WaitFiveSeconds" }, "DescribeExportTasks": { "Type": "Task", "Resource": "${DescribeExportTaskLambdaArn}", "ResultPath": "$.describe_export_task", "Next": "CheckStatusCode" }, "CheckStatusCode": { "Type": "Choice", "Choices": [ { "Variable": "$.describe_export_task.status_code", "StringEquals": "COMPLETED", "Next": "IsAllLogsExported?" }, { "Or": [ { "Variable": "$.describe_export_task.status_code", "StringEquals": "PENDING" }, { "Variable": "$.describe_export_task.status_code", "StringEquals": "RUNNING" } ], "Next": "WaitFiveSeconds" } ], "Default": "ExportFailed" }, "WaitFiveSeconds": { "Type": "Wait", "Seconds": 5, "Next": "DescribeExportTasks" }, "IsAllLogsExported?": { "Type": "Choice", "Choices": [ { "Variable": "$.log_groups_info.completed_flag", "BooleanEquals": true, "Next": "Done" } ], "Default": "CreateExportTask" }, "Done": { "Type": "Succeed" }, "ExportFailed": { "Type": "Fail" } } } RoleArn: Fn::GetAtt: - "StepFunctionsIAMRole" - "Arn" StepFunctionsIAMRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - states.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CloudWatchLogsDeliveryFullAccessPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "logs:CreateLogDelivery" - "logs:GetLogDelivery" - "logs:UpdateLogDelivery" - "logs:DeleteLogDelivery" - "logs:ListLogDeliveries" - "logs:PutResourcePolicy" - "logs:DescribeResourcePolicies" - "logs:DescribeLogGroups" Resource: "*" - PolicyName: LambdaInvokeScopedAccessPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "lambda:InvokeFunction" Resource: - !Ref DescribeLogGroupsLambdaArn - !Ref CreateExportTaskLambdaArn - !Ref DescribeExportTaskLambdaArn Outputs: Arn: Value: !Ref ExportLogsToS3StateMachine Description: "The Arn of ExportLogsToS3StateMachine"
ExportLogsToS3StateMachine.ymlはStepFunctionsのステートマシンを作成するためのテンプレートです。Parametersで作成されるLambdaのARNを受け取ります。そして、受け取ったARNの値をステートマシンの定義部分で使用します。
Outputsでは、作成されるステートマシンのARNを出力します。この値は、EventBridgeで定期実行させるRuleを作成する際に使用されます。
EventBridge関連の実装
CronExportLogsEventRule.yml
AWSTemplateFormatVersion: "2010-09-09" Description: This stack create event rule to execute state machine daily Parameters: StepFunctionsStateMachineArn: Type: String Description: The StepFunctions StateMachine Arn to set target Resources: ScheduledStateMachineExecutionRule: Type: 'AWS::Events::Rule' Properties: Description: "This event rule execute state machine daily" EventBusName: default ScheduleExpression: "cron(0 14 * * ? *)" State: ENABLED Targets: - Arn: !Ref StepFunctionsStateMachineArn Id: 'StepFunctionsStateMachine' RoleArn: !GetAtt - EventBridgeIAMrole - Arn EventBridgeIAMrole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: !Sub events.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: InvokeStepFunctions PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - 'states:StartExecution' Resource: !Ref StepFunctionsStateMachineArn
CronExportLogsEventRule.ymlは、StepFunctionsのステートマシンを毎日実行するためのEventBridgeルールを作成するためのテンプレートです。Parametersでは、ステートマシンのARNを受け取ります。
ScheduleExpressionプロパティでcron式を定義することで、どのタイミングでステートマシンを実行させるかを設定できます。
FailOrTimeOutNotificationEventRule.yml
AWSTemplateFormatVersion: "2010-09-09" Description: This stack create event rule to publish the message when the state machine fails or time out Parameters: StepFunctionsStateMachineArn: Type: String Description: The StepFunctions StateMachine Arn to set target SendEmailNotificationTopicArn: Type: String Description: An arn of sns topic for the target Resources: FailOrTimeOutNotificationEventRule: Type: 'AWS::Events::Rule' Properties: Description: "This event rule is triggered when the state machine fails or time out" EventBusName: default EventPattern: source: - aws.states detail-type: - Step Functions Execution Status Change detail: status: - "TIMED_OUT" - "FAILED" stateMachineArn: - !Ref StepFunctionsStateMachineArn State: ENABLED Targets: - Arn: !Ref SendEmailNotificationTopicArn Id: FailOrTimeOutNotificationEventRule
FailOrTimeOutNotificationEventRule.ymlは、ステートマシンの処理失敗を検知するためのEventBridgeルールを作成します。
今回はEventPatternプロパティで、ステートマシンがTIMED_OUT、FAILEDのステータスとなった際にSNSをターゲットにイベント通知を行うように設定します。
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という子モジュールが存在しています。
root-module ├── main.tf ├── modules │ ├── child_module_1 │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ └── child_module_2 │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── outputs.tf └── variables.tf
複数のモジュールが存在しますが、ルートモジュールでmoduleブロックを使用することで一度に呼び出すことができます。
module "child_module_1" { source = "./modules/child_module_1" } module "child_module_2" { source = "./modules/child_module_2" servers = 5 }
moduleブロックにはsource引数が必要です。source引数にはモジュールが定義されているローカルディレクトリのパスを指定します。
モジュールが他のモジュール内で使用される場合、使用される側のモジュールで引数を指定する必要があります。上記のコードの場合、child_module_2モジュールではserversという変数が定義されています。
default値が設定されていない変数は必須の引数となります。デフォルト値を持つ変数も、モジュールの引数として指定することで、デフォルト値を上書きできます。
ディレクトリ構成
moduleブロックを使用するため、今回は以下のようなディレクトリ構成で実装を行います。トップディレクトリにmain.tfを起きつつ、各リソースを作成するファイルをmodulesディレクトリに配置します。そして、トップディレクトリのmain.tfから各moduleを呼び出します。
├── main.tf ├── modules │ ├── eventbridge │ │ ├── cron_export_logs_event_rule │ │ │ ├── main.tf │ │ │ └── variables.tf │ │ └── fail_or_time_out_notification_event_rule │ │ ├── main.tf │ │ └── variables.tf │ ├── lambda │ │ ├── create_export_task │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ ├── describe_export_task │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ └── describe_log_groups │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── s3 │ │ ├── exported-bucket │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ └── lambda-bucket │ │ ├── files │ │ │ ├── create_export_task.zip │ │ │ ├── describe_export_task.zip │ │ │ └── describe_log_groups.zip │ │ ├── lambda │ │ │ ├── create_export_task.py │ │ │ ├── describe_export_task.py │ │ │ └── describe_log_groups.py │ │ ├── maint.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── sns │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ └── step-functions │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── outputs.tf ├── terraform.tfvars └── variables.tf
各ファイル説明は長くなるため、折りたたんでおきます。
Terraformファイル
ルートモジュール
main.tf
provider "aws" { profile = var.profile region = var.aws_region } module "s3" { source = "./modules/s3/exported-bucket" } module "lambda-bucket" { source = "./modules/s3/lambda-bucket" } module "describe_log_groups_lambda" { source = "./modules/lambda/describe_log_groups" lambda_bucket_name = module.lambda-bucket.lambda_bucket_name object_key = module.lambda-bucket.describe_log_groups } module "create_export_task_lambda" { source = "./modules/lambda/create_export_task" bucket_name = module.s3.bucket_name lambda_bucket_name = module.lambda-bucket.lambda_bucket_name object_key = module.lambda-bucket.create_export_task } module "describe_export_task_lambda" { source = "./modules/lambda/describe_export_task" lambda_bucket_name = module.lambda-bucket.lambda_bucket_name object_key = module.lambda-bucket.describe_export_task } module "step_functions" { source = "./modules/step-functions" describe_log_groups_lambda_arn = module.describe_log_groups_lambda.arn create_export_task_lambda_arn = module.create_export_task_lambda.arn describe_export_task_lambda_arn = module.describe_export_task_lambda.arn } module "send_email_notification_topic" { source = "./modules/sns" email_address = var.email_address } module "cron_export_logs_event_rule" { source = "./modules/eventbridge/cron_export_logs_event_rule" state_machine_arn = module.step_functions.state_machine_arn } module "fail_or_time_out_notification_event_rule" { source = "./modules/eventbridge/fail_or_time_out_notification_event_rule" state_machine_arn = module.step_functions.state_machine_arn sns_topic_arn = module.send_email_notification_topic.send_email_notification_topic_arn }
providerブロックは、特定のプロバイダー(上記例ではaws)の設定を行います。profile属性は、AWSの設定ファイルに保存されているクレデンシャル情報を指定します。クレデンシャル情報は機密情報のため、ハードコーディングするべきではありません。
複数存在するmoduleブロックで、今回作成する各リソースのモジュールを呼び出しています。
s3リソース関連以外のモジュールにおいては、引数を指定することでinput valueを渡しています。
variables.tf
variable "profile" { type = string default = "default" } variable "aws_region" { type = string default = "ap-northeast-1" } variable "email_address" { type = string default = "sample@gmail.com" }
variable.tfでは、main.tfのproviderブロックで必要な情報を記述しています。クレデンシャルの情報はdefault、リージョンは東京リージョンを指定しています。
terraform.tfvars
email_address = "メールアドレス"
terraform.tfvarsには、メールアドレスとエクスポートしたいログのprefixを記述しておきます。terraform.tfvarsの内容がvariables.tfに上書きされます。メールアドレスはリモートリポジトリに上げるべき情報ではないため、Gitの管理下には置かないようにします。
S3関連のモジュール
s3のディレクトリには、exported-bucketとlambda-bucketの2つのモジュールを配置しています。
両モジュールの用途の違いは以下のとおりです。
- exported-bucketモジュール: ログデータのエクスポート先であるバケットを作成するためのモジュール
- lambda-bucketモジュール: Lambdaで必要なPythonのソースコードを保管するためのバケットを作成するモジュール
exported-bucketモジュール
main.tf
resource "aws_s3_bucket" "this" { bucket = var.bucket } resource "aws_s3_bucket_server_side_encryption_configuration" "this" { bucket = aws_s3_bucket.this.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } resource "aws_s3_bucket_policy" "this" { bucket = aws_s3_bucket.this.id policy = data.aws_iam_policy_document.s3_get_bucket_acl.json } data "aws_iam_policy_document" "s3_get_bucket_acl" { statement { sid = "AWSS3GetBucketAcl" principals { type = "Service" identifiers = ["logs.ap-northeast-1.amazonaws.com"] } effect = "Allow" actions = [ "s3:GetBucketAcl", ] resources = [ "${aws_s3_bucket.this.arn}", ] } statement { sid = "AWSS3PutObject" principals { type = "Service" identifiers = ["logs.ap-northeast-1.amazonaws.com"] } effect = "Allow" actions = [ "s3:PutObject", ] resources = [ "${aws_s3_bucket.this.arn}/*", ] condition { test = "StringEquals" variable = "s3:x-amz-acl" values = ["bucket-owner-full-control"] } } }
S3バケットは、aws_s3_bucketリソースを指定することで作成できます。また、aws_s3_bucket_server_side_encryption_configurationリソースで暗号化の設定、aws_s3_bucket_policyリソースでバケットポリシーの設定を行っています。
outputs.tf
output "bucket_name" { description = "The name of the bucket." value = aws_s3_bucket_policy.this.id } output "s3_bucket_arn" { description = "The ARN of the bucket. Will be of format arn:aws:s3:::bucketname." value = aws_s3_bucket.this.arn }
outputs.tfでは、バケット名とバケットのARNを出力するように設定します。それぞれ、ログデータをエクスポートするLambda関数において必要な情報です。
variables.tf
variable "bucket" { description = "(Optional, Forces new resource) The name of the bucket. If omitted, Terraform will assign a random, unique name." type = string default = null }
variables.tfでは、バケット名のための変数を定義します。
lambda-bucketモジュール
lambda-bucketモジュールは、Lambda関数で使用するPythonソースコードを管理するためのS3バケットを作成します。先程のexported-bucketモジュールとは違い、バケットを作成するだけでなく、オブジェクトをアップロードする必要があります。そのため、今回は次の2ステップが必要となります。
◎ステップ
- バケットを作成する
- 作成したバケットにPythonソースコードをオブジェクトとしてアップロードする
main.tf
resource "aws_s3_bucket" "lambda_bucket" { bucket = var.bucket force_destroy = true } resource "aws_s3_bucket_server_side_encryption_configuration" "this" { bucket = aws_s3_bucket.lambda_bucket.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } resource "aws_s3_object" "describe_log_groups" { bucket = aws_s3_bucket.lambda_bucket.id key = "describe_log_groups.zip" source = data.archive_file.describe_log_groups.output_path etag = filemd5(data.archive_file.describe_log_groups.output_path) } resource "aws_s3_object" "create_export_task" { bucket = aws_s3_bucket.lambda_bucket.id key = "create_export_task.zip" source = data.archive_file.create_export_task.output_path etag = filemd5(data.archive_file.create_export_task.output_path) } resource "aws_s3_object" "describe_export_task" { bucket = aws_s3_bucket.lambda_bucket.id key = "describe_export_task.zip" source = data.archive_file.describe_export_task.output_path etag = filemd5(data.archive_file.describe_export_task.output_path) } data "archive_file" "describe_log_groups" { type = "zip" source_file = "${path.module}/lambda/describe_log_groups.py" output_path = "${path.module}/files/describe_log_groups.zip" } data "archive_file" "create_export_task" { type = "zip" source_file = "${path.module}/lambda/create_export_task.py" output_path = "${path.module}/files/create_export_task.zip" } data "archive_file" "describe_export_task" { type = "zip" source_file = "${path.module}/lambda/describe_export_task.py" output_path = "${path.module}/files/describe_export_task.zip" }
archive_fileデータリソースは、指定したファイルをアーカイブできます。今回は、Lambdaで使用するPythonソースコードをzip形式でアーカイブします。
aws_s3_objectリソースは、S3のオブジェクトリソースを作成します。bucke
引数に作成するバケットを、source
にアーカイブす
結果的に、作成したバケットにアーカイブしたPythonソースコードをS3オブジェクトとしてアップロードできます。
outputs.tf
output "lambda_bucket_name" { description = "Name of the S3 bucket used to store function code." value = aws_s3_bucket.lambda_bucket.id } output "describe_log_groups" { value = aws_s3_object.describe_log_groups.id } output "create_export_task" { value = aws_s3_object.create_export_task.id } output "describe_export_task" { value = aws_s3_object.describe_export_task.id }
outputs.tfでは、バケット名とバケットにアップロードする各オブジェクトのオブジェクトキーを出力します。出力されるオブジェクトキーは、Lambdaリソースを作成する際に使用します。
variables.tf
variable "bucket" { description = "(Optional, Forces new resource) The name of the bucket. If omitted, Terraform will assign a random, unique name." type = string default = null }
variables.tfでは、バケット名のための変数を定義します。
Lambda関連のモジュール
moduleディレクトリ下のlambdaディレクトリには、今回の実装で必要な各Lambda関数のためのモジュールを配置します。
今回は以下3つのLambda関数が必要なため、それぞれのモジュールを作成します。
- create_export_taskモジュール
- describe_export_taskディレクトリ
- describe_log_groupsディレクトリ
create_export_taskモジュール
main.tf
resource "aws_lambda_function" "this" { function_name = var.function_name description = var.description handler = var.handler runtime = var.runtime role = aws_iam_role.lambda.arn s3_bucket = var.lambda_bucket_name s3_key = var.object_key environment { variables = { BUCKET_NAME = var.bucket_name } } depends_on = [aws_cloudwatch_log_group.lambda] } resource "aws_cloudwatch_log_group" "lambda" { name = "/aws/lambda/${var.function_name}" } resource "aws_iam_role" "lambda" { name = var.role_name description = var.role_description assume_role_policy = data.aws_iam_policy_document.assume_role.json inline_policy { name = "create_export_task" policy = data.aws_iam_policy_document.inline_policy.json } } data "aws_iam_policy_document" "assume_role" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } } data "aws_iam_policy_document" "inline_policy" { statement { actions = [ "logs:CreateExportTask", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] resources = ["arn:aws:logs:*:*:*"] } }
aws_lambda_functionリソースは、Lambda関数を作成します。今回はS3からPythonソースコードを取得するため、s3_bucketとs3_key引数を指定しています。s3_keyには作成するLambda関数で使用するPythonソースコードのオブジェクトキーを指定します。
outputs.tf
output "arn" { description = "The ARN of the Lambda Function" value = aws_lambda_function.this.arn }
outputs.tfには、Lambda関数のARNが出力されるように記述します。このARNの値は、StepFunctionsのステートマシンで使用します。
このoutputs.tfファイルは、残り3つの全てのLambda関数のモジュールで必要となります。
variables.tf
########### # Function ########### variable "function_name" { type = string default = "CreateExportTask" } variable "handler" { type = string default = "create_export_task.lambda_handler" } variable "runtime" { type = string default = "python3.9" } variable "description" { type = string default = "Lamba for creating export task" } variable "timeout" { description = "The amount of time your Lambda Function has to run in seconds." type = number default = 3 } variable "role_name" { type = string default = "CreateExportTaskRoleForLambdaFunction" } variable "role_description" { description = "Description of IAM role to use for Lambda Function" type = string default = "Role for CreateExportTaskRoleForLambdaFunction" } variable "bucket_name" {} variable "object_key" {} variable "lambda_bucket_name" {}
variables.tfで定義されているbucket_name、object_key、lambda_bucket_nameに関しては、ルートモジュールから値を受け取ります。そのため、空の{}で定義しています。これらの値は、Terraformによって作成されるバケット名、Lambdaのオブジェクトキー、Lambdaコードが格納されているバケット名を表します。
describe_export_taskモジュール
main.tf
resource "aws_lambda_function" "this" { function_name = var.function_name description = var.description handler = var.handler runtime = var.runtime role = aws_iam_role.lambda.arn s3_bucket = var.lambda_bucket_name s3_key = var.object_key depends_on = [aws_cloudwatch_log_group.lambda] } resource "aws_cloudwatch_log_group" "lambda" { name = "/aws/lambda/${var.function_name}" } resource "aws_iam_role" "lambda" { name = var.role_name description = var.role_description assume_role_policy = data.aws_iam_policy_document.assume_role.json inline_policy { name = "create_export_task" policy = data.aws_iam_policy_document.inline_policy.json } } data "aws_iam_policy_document" "assume_role" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } } data "aws_iam_policy_document" "inline_policy" { statement { actions = [ "logs:DescribeExportTasks" ] resources = ["*"] } }
main.tfでは、CloudWatch Logsのエクスポート処理のステータスを取得するためのLambda関数を作成します。aws_lambda_functionリソースのs3_bucket、s3_keyで該当Lambdaコードを指定します。
variables.tf
########### # Function ########### variable "function_name" { type = string default = "DescribeExportTask" } variable "handler" { type = string default = "describe_export_task.lambda_handler" } variable "runtime" { type = string default = "python3.9" } variable "description" { type = string default = "Lamba for describe export task" } variable "timeout" { description = "The amount of time your Lambda Function has to run in seconds." type = number default = 3 } variable "role_name" { type = string default = "DescribeExportTaskRoleForLambdaFunction" } variable "role_description" { description = "Description of IAM role to use for Lambda Function" type = string default = "Role for DescribeExportTaskRoleForLambdaFunction" } variable "object_key" {} variable "lambda_bucket_name" {}
create_export_taskモジュールと同じ用に、object_key、lambda_bucket_nameの値をルートモジュールから受け取ります。この値を用いてLambdaコードをS3バケットから取得します。
outputs.tf
output "arn" { description = "The ARN of the Lambda Function" value = aws_lambda_function.this.arn }
outputs.tfでは、ステートマシンで必要なLambdaのARNの値を出力します。
describe_log_groupsモジュール
main.tf
resource "aws_lambda_function" "this" { function_name = var.function_name description = var.description handler = var.handler runtime = var.runtime role = aws_iam_role.lambda.arn s3_bucket = var.lambda_bucket_name s3_key = var.object_key depends_on = [aws_cloudwatch_log_group.lambda] } resource "aws_cloudwatch_log_group" "lambda" { name = "/aws/lambda/${var.function_name}" } resource "aws_iam_role" "lambda" { name = var.role_name description = var.role_description assume_role_policy = data.aws_iam_policy_document.assume_role.json inline_policy { name = "create_export_task" policy = data.aws_iam_policy_document.inline_policy.json } } data "aws_iam_policy_document" "assume_role" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } } data "aws_iam_policy_document" "inline_policy" { statement { actions = [ "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups" ] resources = ["*"] } }
基本的な理解は上記2つのLambdaモジュールと同じです。aws_lambda_functionのs3_bucketとs3_keyの値に応じてLambdaコードをS3バケットから取得します。
variables.tf
########### # Function ########### variable "function_name" { type = string default = "DescribeLogGroups" } variable "handler" { type = string default = "describe_log_groups.lambda_handler" } variable "runtime" { type = string default = "python3.9" } variable "description" { type = string default = "Lamba for describe log groups" } variable "timeout" { description = "The amount of time your Lambda Function has to run in seconds." type = number default = 3 } variable "role_name" { type = string default = "DescribeLoggroupsForLambdaFunction" } variable "role_description" { description = "Description of IAM role to use for Lambda Function" type = string default = "Role for DescribeLogGroupsForLambdaFunction" } variable "object_key" {} variable "lambda_bucket_name" {}
上記2つのLambdaモジュールと同じ用に、object_key、lambda_bucket_nameの値をルートモジュールから受け取ります。
outputs.tf
output "arn" { description = "The ARN of the Lambda Function" value = aws_lambda_function.this.arn }
outputs.tfでは、ステートマシンで使用するためのARNを出力します。
StepFunctions関連のモジュール
main.tf
locals { log_group_arn = aws_cloudwatch_log_group.sfn.arn role_name = "StepFunctionsIAMRole" definition = <<EOF { "StartAt": "Initialize", "TimeoutSeconds": 600, "States": { "Initialize": { "Type": "Task", "Resource": "${var.describe_log_groups_lambda_arn}", "ResultPath": "$.log_groups_info", "Next": "CreateExportTask" }, "CreateExportTask": { "Type": "Task", "Resource": "${var.create_export_task_lambda_arn}", "ResultPath": "$.log_groups_info", "Next": "WaitFiveSeconds" }, "DescribeExportTasks": { "Type": "Task", "Resource": "${var.describe_export_task_lambda_arn}", "ResultPath": "$.describe_export_task", "Next": "CheckStatusCode" }, "CheckStatusCode": { "Type": "Choice", "Choices": [ { "Variable": "$.describe_export_task.status_code", "StringEquals": "COMPLETED", "Next": "IsAllLogsExported?" }, { "Or": [ { "Variable": "$.describe_export_task.status_code", "StringEquals": "PENDING" }, { "Variable": "$.describe_export_task.status_code", "StringEquals": "RUNNING" } ], "Next": "WaitFiveSeconds" } ], "Default": "ExportFailed" }, "WaitFiveSeconds": { "Type": "Wait", "Seconds": 5, "Next": "DescribeExportTasks" }, "IsAllLogsExported?": { "Type": "Choice", "Choices": [ { "Variable": "$.log_groups_info.completed_flag", "BooleanEquals": true, "Next": "Done" } ], "Default": "CreateExportTask" }, "Done": { "Type": "Succeed" }, "ExportFailed": { "Type": "Fail" } } } EOF } resource "aws_sfn_state_machine" "this" { name = var.name role_arn = aws_iam_role.this.arn definition = local.definition logging_configuration { log_destination = lookup(var.logging_configuration, "log_destination", "${local.log_group_arn}:*") include_execution_data = true level = "ALL" } type = upper(var.type) depends_on = [aws_cloudwatch_log_group.sfn] } ########### # IAM Role ########### resource "aws_iam_role" "this" { name = local.role_name description = var.role_description assume_role_policy = data.aws_iam_policy_document.assume_role.json inline_policy { name = "excute_step_functions" policy = data.aws_iam_policy_document.inline_policy.json } } data "aws_iam_policy_document" "assume_role" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["states.amazonaws.com"] } } } data "aws_iam_policy_document" "inline_policy" { statement { effect = "Allow" actions = [ "logs:CreateLogDelivery", "logs:GetLogDelivery", "logs:UpdateLogDelivery", "logs:DeleteLogDelivery", "logs:ListLogDeliveries", "logs:PutResourcePolicy", "logs:DescribeResourcePolicies", "logs:DescribeLogGroups" ] resources = ["*"] } statement { effect = "Allow" actions = ["lambda:InvokeFunction"] resources = [ "${var.describe_log_groups_lambda_arn}", "${var.create_export_task_lambda_arn}", "${var.describe_export_task_lambda_arn}" ] } } ################## # CloudWatch Logs ################## resource "aws_cloudwatch_log_group" "sfn" { name = "/aws/sfn/${var.name}" }
aws_sfn_state_machineリソースでStepFunctionsのステートマシンを作成します。
ローカル変数として、definitionにステートマシンの定義を記述します。
variables.tf
################ # Step Function ################ variable "name" { description = "The name of the Step Function" type = string default = "ExportLogsToS3" } variable "type" { description = "Determines whether a Standard or Express state machine is created. The default is STANDARD. Valid Values: STANDARD | EXPRESS" type = string default = "STANDARD" validation { condition = contains(["STANDARD", "EXPRESS"], upper(var.type)) error_message = "Step Function type must be one of the following (STANDARD | EXPRESS)." } } ################# # CloudWatch Logs ################# variable "logging_configuration" { description = "Defines what execution history events are logged and where they are logged" type = map(string) default = {} } ########### # IAM Role ########### variable "role_description" { description = "Description of IAM role to use for Step Function" type = string default = null } variable "describe_log_groups_lambda_arn" {} variable "create_export_task_lambda_arn" {} variable "describe_export_task_lambda_arn" {}
variable.tfでは、Lambdaモジュールで作成したLambdaのARNの値を取得します。この値はステートマシンのステート定義で使用されます。
outputs.tf
output "state_machine_arn" { value = aws_sfn_state_machine.this.arn }
outputs.tfでは、ステートマシンのarnを出力します。この値は、EventBridgeでステートマシンの処理を定期実行する際に使用します。
EventBridge関連のモジュール
EventBridgeに関しては、以下2つのモジュールを作成します。
- cron_export_logs_event_ruleモジュール
- fail_or_time_out_notification_event_ruleモジュール
cron_export_logs_event_ruleモジュール
main.tf
resource "aws_cloudwatch_event_rule" "this" { name = "${var.event_rule_name}" schedule_expression = "cron(0 14 * * ? *)" } resource "aws_cloudwatch_event_target" "this" { rule = aws_cloudwatch_event_rule.this.name arn = var.state_machine_arn role_arn = aws_iam_role.event_bridge.arn } resource "aws_iam_role" "event_bridge" { name = var.role_name assume_role_policy = data.aws_iam_policy_document.assume_role.json inline_policy { name = "create_export_task" policy = data.aws_iam_policy_document.inline_policy.json } } data "aws_iam_policy_document" "assume_role" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["events.amazonaws.com"] } } } data "aws_iam_policy_document" "inline_policy" { statement { actions = [ "states:StartExecution" ] resources = [var.state_machine_arn] } }
aws_cloudwatch_event_ruleリソースでEventBridgeルールを作成します。
aws_cloudwatch_event_targetリソースは、EventBridgeターゲットリソースを作成します。rule引数に作成したEventBridge Ruleリソースを指定することで、特定のターゲットに対してEventBridgeのルールを適用させることができます。
variables.tf
variable "event_rule_name" { type = string default = "ExcuteStepFunctions" } variable "role_name" { type = string default = "ExcuteStepFunctions" } variable "state_machine_arn" {}
variables.tfのstate_machine_arnでStepFunctionsの値を受け取ります。
fail_or_time_out_notification_event_ruleモジュール
main.tf
resource "aws_cloudwatch_event_rule" "fail_or_time_out_notification_event_rule" { name = "${var.event_rule_name}" event_pattern = <<EOF { "source": ["aws.states"], "detail-type": ["Step Functions Execution Status Change"], "detail": { "status": ["FAILED", "TIMED_OUT"], "stateMachineArn": ["${var.state_machine_arn}"] } } EOF } resource "aws_cloudwatch_event_target" "this" { rule = aws_cloudwatch_event_rule.fail_or_time_out_notification_event_rule.name arn = var.sns_topic_arn }
このモジュールでは、ステートマシンが処理に失敗したか否かを検知するためのEventBridgeルールを作成します。aws_cloudwatch_event_ruleリソースのevent_patternでFAILEDとTIMED_OUTのステータスでトリガーされるイベントパターンを定義します。
variables.tf
variable "event_rule_name" { type = string default = "FailOrTimeOutNotification" } variable "role_name" { type = string default = "ExcuteStepFunctions" } variable "state_machine_arn" {} variable "sns_topic_arn" {}
variables.tfでは、他モジュールで作成されるステートマシンのARNとSNSトピックのARNを取得します。
CloudFormationとTerraformでの実行
それでは、CloudFormationとTerraformで行ったそれぞれの実装を実際に実行します。
CloudFormationの実行
今回はコンソールではなく、コマンドラインでCloudFormationのテンプレートをデプロイします。コマンドラインでデプロイをするには、aws cloudformation deployコマンドを実行する必要があります。
注意点として、capabilities
オプションにおいて、CAPABILITY_IAM
を指定する必要があります。CAPABILITY_IAMを指定することで、IAMリソースを作成できます。
aws cloudformation deploy --template-file ./ExportCloudWatchLogsToS3.yml \ --stack-name ExportCloudWatchLogsToS3 \ --capabilities CAPABILITY_IAM \ --parameter-overrides BucketLambdaTemplateURL=https://s3.ap-northeast-1.amazonaws.com/バケット名/S3オブジェクトのprefix \ DescribeLogGroupsLambdaTemplateURL=https://s3.ap-northeast-1.amazonaws.com/バケット名/S3オブジェクトのprefix \ CreateExportTaskLambdaTemplateURL=https://s3.ap-northeast-1.amazonaws.com/バケット名/S3オブジェクトのprefix \ DescribeExportTaskLambdaTemplateURL=https://s3.ap-northeast-1.amazonaws.com/バケット名/S3オブジェクトのprefix \ StepFunctionsStateMachineTemplateURL=https://s3.ap-northeast-1.amazonaws.com/バケット名/S3オブジェクトのprefix \ CronExportLogsEventRuleURL=https://s3.ap-northeast-1.amazonaws.com/バケット名/S3オブジェクトのprefix \ FailOrTimeOutNotificationEventRuleURL=https://s3.ap-northeast-1.amazonaws.com/バケット名/S3オブジェクトのprefix \ SendEmailNotificationURL=https://s3.ap-northeast-1.amazonaws.com/バケット名/S3オブジェクトのprefix \ LambdaBucket="CloudFormationのテンプレートが保存されているバケット名" \ CreateExportTaskLambdaCode=Lambdaのzipファイルのprefix \ DescribeExportTaskLambdaCode=Lambdaのzipファイルのprefix \ DescribeLogGroupsLambdaCode=Lambdaのzipファイルのprefix \ EmailAddress=通知先のメールアドレス \
リソースの作成が完了すると、以下のようにSuccessfully created/updated stackと表示がされます。
aws cloudformation deploy --template-file ./ExportCloudWatchLogsToS3.yml \ --stack-name ExportCloudWatchLogsToS3 \ --capabilities CAPABILITY_IAM \ ...省略... Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - ExportCloudWatchLogsToS3
Terraformの実行
基本的には以下のステップで実行を行います。
- terraform init
- terraform plan
- terraform apply
terraform init
❯ terraform init Initializing modules... Initializing the backend... Initializing provider plugins... - Reusing previous version of hashicorp/archive from the dependency lock file - Reusing previous version of hashicorp/aws from the dependency lock file - Using previously-installed hashicorp/archive v2.2.0 - Using previously-installed hashicorp/aws v4.8.0 Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.
terraform initコマンドは、Terraformの設定ファイルを含む作業ディレクトリを初期化します。
コマンドを実行すると、以下のディレクトリ、ファイルが作成されます。
- .terraformディレクトリ
- .terraform.lock.hcl
.terraformディレクトリは、キャッシュされたプロバイダーのプラグインやモジュールを管理します。
.terraform.lock.hcl
は、Terraformの依存関係を固定するファイルです。terraform initコマンドが実行される度に作成または更新されます。.terraform.lock.hcl
は、将来的な依存関係の変化を議論するために、gitでのバージョン管理はするべきです。
terraform plan
❯ terraform plan Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create <= read (data resources) Terraform will perform the following actions: # module.create_export_task_lambda.aws_cloudwatch_log_group.lambda will be created + resource "aws_cloudwatch_log_group" "lambda" { + arn = (known after apply) + id = (known after apply) + name = "/aws/lambda/CreateExportTask" + retention_in_days = 0 + tags_all = (known after apply) } ...省略... # module.step_functions.aws_sfn_state_machine.this will be created + resource "aws_sfn_state_machine" "this" { + arn = (known after apply) + creation_date = (known after apply) + definition = (known after apply) + id = (known after apply) + name = "ExportLogsToS3" + role_arn = (known after apply) + status = (known after apply) + tags_all = (known after apply) + type = "STANDARD" + logging_configuration { + include_execution_data = true + level = "ALL" + log_destination = (known after apply) } + tracing_configuration { + enabled = (known after apply) } } Plan: 28 to add, 0 to change, 0 to destroy. Changes to Outputs: + bucket_name = (known after apply) ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
terraform planコマンドを実行することで、作成、更新、削除されるリソースを事前に確認できます。terraform planコマンドでは実際にリソースは作成されません。
terraform apply
❯ terraform apply Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create <= read (data resources) Terraform will perform the following actions: # module.create_export_task_lambda.aws_cloudwatch_log_group.lambda will be created + resource "aws_cloudwatch_log_group" "lambda" { + arn = (known after apply) + id = (known after apply) + name = "/aws/lambda/CreateExportTask" + retention_in_days = 0 + tags_all = (known after apply) } ...省略... # module.step_functions.aws_sfn_state_machine.this will be created + resource "aws_sfn_state_machine" "this" { + arn = (known after apply) + creation_date = (known after apply) + definition = (known after apply) + id = (known after apply) + name = "ExportLogsToS3" + role_arn = (known after apply) + status = (known after apply) + tags_all = (known after apply) + type = "STANDARD" + logging_configuration { + include_execution_data = true + level = "ALL" + log_destination = (known after apply) } + tracing_configuration { + enabled = (known after apply) } } Plan: 28 to add, 0 to change, 0 to destroy. Changes to Outputs: + bucket_name = (known after apply) Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value:
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では一緒に働ける仲間を募集しています。 興味があるかたはぜひ採用ページを御覧ください。 divx.co.jp