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インスタンスを作成するように定義されています。

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"
  }
}

参考: 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に移動して処理が完了します。

■ステートマシン定義

{
    "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_1child_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ステップが必要となります。

◎ステップ

  1. バケットを作成する
  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の実行

基本的には以下のステップで実行を行います。

  1. terraform init
  2. terraform plan
  3. 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 planterraform 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

参考リソース

Exporting log data to Amazon S3

What is AWS CloudFormation?

Boto3 documentation

terraform.io

What is Terraform?

Terraform vs. CloudFormation, Heat, etc.