Terraformで行うAmazon CloudWatch LogsからS3へのログ転送

2ヶ月ぶりでお久しぶりでもないです インフラの戸田です。

最近はモンスターハンターワールド:アイスボーンをずっとやっております。

いろんなモンスターが増えて楽しいですね~~ モンハンしかしていない生活になってしまいました。

では、本題にいきます。

CloudWatch Logsに保存されているログをS3に移動させて、料金を削減したいという声がありました。

移動させるためには、いくつか手段がありますが、今回はKinesis Data Firehose を使い、ログ転送をする方法をTerraformでコード化しつつ、実現することになりました。

経緯

  • CloudWatch Logsにログが溜まりがち
  • ログはS3に全部保存したい
  • CloudWatch LogsはS3に比べて料金が高い
  • そのため、CloudWatch LogsからS3にログを移動したい

という天の声があり、調査をしました。

料金について

調査をしてみると、CloudWatch Logsの料金がS3に比べて高いことがわかりました。(そりゃそうだ)

サービス 料金
CloudWatch Logs 0.033 USD/GB
S3 0.025 USD/GB
GLACIER 0.005USD/GB
S3 Intelligent-Tiering Storage(高頻度アクセス) 0.025USD/GB
S3 Intelligent-Tiering Storage(低頻度アクセス) 0.019USD/GB

サービスによって料金は変わります。

そのため、CloudWatch Logsにずっとログを保存するよりもS3に保存したほうが安上がりになりますね。

ちなみに、Intelligent-Tiering Storageとは自動的にコスト最適化をしてくれるストレージクラスになります。

今回はIntelligent-Tiering Storageを使います。設定方法は簡単ですので、S3を使っている方はおすすめです。

CloudWatch LogsからS3にログを置く方法

CloudWatch LogsからS3にログを置く方法も複数選択肢がありました。

  • CloudWatch LogsからKinesis Data Firehoseを使ってS3に置く方法
  • Lambdaを使って、S3に置く方法

「CloudWatch Logs s3」で検索するとこの二つの方法がよく引っ掛かります。

Lambdaの場合、Terraform以外のコードの管理もしなくてはいけません。

マネージドサービスに任せれる事は任せてしまおうという精神のもと、Terraformだけで完結する「CloudWatch LogsからKinesis Data Firehoseを使ってS3に置く方法」になりました。

Terraform

今回Terraformでやること

今回作成することは、下記イメージの通りです。

f:id:AdwaysEngineerBlog:20190918174020p:plain

おおむねこの図の通りですが、Kinesis Data Firehoseの仕様上、1つのロググループに対して1つのS3のパスを指定する必要があります。

そのため、正確なイメージ図では、下記のイメージ図になります。

f:id:AdwaysEngineerBlog:20190918174032p:plain

S3に置きたいログの数だけKinesis Data Firehoseが増えます。

Terraformで作るもの

  • S3 × 2
  • Kinesis Data Firehose × ロググループの数
  • IAM role (Kinesis Data FirehoseからS3用)
  • サブスクリプション (Cloudwatch LogsからKinesis Data Firehose)

ファイル構成

対象のロググループは下記

  • アプリケーション関係のロググループ
  • Lambdaのロググループ
  • RDS関係のロググループ

ファイルの構成は下記の通りです。

.
├── app.tfvars
├── backend.tf
├── default_vars.tf
├── kinesis_firehose.tf
├── lambda.tfvars
├── main.tf
├── rds.tfvars
├── s3
│   ├── app.tfvars -> ../app.tfvars
│   ├── backend.tf
│   ├── default_vars.tf -> ../default_vars.tf
│   ├── lambda.tfvars -> ../lambda.tfvars
│   ├── main.tf
│   ├── rds.tfvars -> ../rds.tfvars
│   └── s3.tf
└── subscription_fileter.tf

これらを環境ごとに作成するため、Terraformのworkspace機能を使います。

workspaceの機能について

Terraform には、Workspaces という機能があります。

典型的な使い方としては、development, staging, production といった Workspace を用意して、切り替えて使うものになりますが、今回は app, lambda,rdsの三種類の環境を用意します。

workspaceを使うことで state ファイルなどを workspace 毎のディレクトリに格納することになります。

Teffaromファイル群

実際に使う場合は自分の環境のプロファイルやAWSアカウントに置換して使ってください。

main.tf

provider "aws" {
  version = "~> 2.17"
  profile = "${var.profile}"
  region  = "${var.region}"
}

backend.tf

terraform {
  backend "s3" {
    bucket    = "toda-test-terraform-state"
    key       = "terraform.tfstate"
    region    = "ap-northeast-1"
    profile   = "<profile_name>"
  }
}

app.tfvars と lambda.tfvars と rds.tfvars はそれぞれ変数を設定している

region = "ap-northeast-1"

backet_name = "toda-test-bucket"

#指定したいCloudwatch Logsの
log_group_name = [
    "toda_log_group_1",
    "toda_log_group_2",
    "toda_log_group_3",
    "toda_log_group_4"
]

#上のロググループに対応したKinesis Data Firehoseの名前
kinesis_name = [
  "toda_kinesis_log_group_1",
  "toda_kinesis_log_group_2",
  "toda_kinesis_log_group_3",
  "toda_kinesis_log_group_4"
]

kinesis_firehose.tf

resource "aws_kinesis_firehose_delivery_stream" "firehose" {
  name        = "${element(var.kinesis_name, count.index)}"
  destination = "s3"
  count       = "${length(var.log_group_name)}"

  s3_configuration {
    role_arn        = "${aws_iam_role.firehose_role.arn}"
    bucket_arn      = "arn:aws:s3:::${var.backet_name}"
    buffer_size     = 5
    buffer_interval = 60
    prefix          = "${element(var.kinesis_name, count.index)}"
    cloudwatch_logging_options {
      enabled = true
      log_group_name = "${element(var.log_group_name, count.index)}"
      log_stream_name = "*"
    }
  }
}

resource "aws_iam_role" "firehose_role" {
  name               = "${terraform.workspace}_firehose_role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "firehose.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "firehose_role_policy" {
  name   = "${terraform.workspace}_firehose_role_policy"
  role   = "${aws_iam_role.firehose_role.id}"

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "glue:GetTableVersions"
            ],
            "Resource": "*"
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::${var.backet_name}",
                "arn:aws:s3:::${var.backet_name}/*",
                "arn:aws:s3:::%FIREHOSE_BUCKET_NAME%",
                "arn:aws:s3:::%FIREHOSE_BUCKET_NAME%/*"
            ]
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "lambda:InvokeFunction",
                "lambda:GetFunctionConfiguration"
            ],
            "Resource": "arn:aws:lambda:ap-northeast-1:<account id>:function:%FIREHOSE_DEFAULT_FUNCTION%:%FIREHOSE_DEFAULT_VERSION%"
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-1:<account id>:log-group:*"
            ]
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "kinesis:DescribeStream",
                "kinesis:GetShardIterator",
                "kinesis:GetRecords"
            ],
            "Resource": "arn:aws:kinesis:ap-northeast-1:<account id>:stream/%FIREHOSE_STREAM_NAME%"
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:ap-northeast-1:<account id>:key/%SSE_KEY_ID%"
            ],
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "kinesis.%REGION_NAME%.amazonaws.com"
                },
                "StringLike": {
                    "kms:EncryptionContext:aws:kinesis:arn": "arn:aws:kinesis:%REGION_NAME%:<account id>:stream/%FIREHOSE_STREAM_NAME%"
                }
            }
        }
    ]
}
EOF
}

subscription_fileter.tf

resource "aws_cloudwatch_log_subscription_filter" "kinesis_function_logfilter" {
  name            = "kinesis_function_logfilter"
  count           = "${length(var.log_group_name)}"
  log_group_name  = "${element(var.log_group_name, count.index)}"
  filter_pattern  = ""
  destination_arn = "${element(aws_kinesis_firehose_delivery_stream.firehose.*.arn, count.index)}"
  role_arn        = "${aws_iam_role.cwl_to_kinesisfirehose_role.arn}"
  distribution    = "ByLogStream"
}

resource "aws_iam_role" "cwl_to_kinesisfirehose_role" {
  name               = "${terraform.workspace}_cwl_kinesisfirehose_role"
  assume_role_policy = <<EOF
{
  "Version": "2008-10-17",
  "Statement": {
    "Effect": "Allow",
    "Principal": { "Service": "logs.ap-northeast-1.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }
}
EOF
}

resource "aws_iam_role_policy" "iam_for_kinesis" {
  name   = "${terraform.workspace}_cwl_kinesisfirehose_role_policy"
  role   = "${aws_iam_role.cwl_to_kinesisfirehose_role.id}"
  
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement":[
      {
        "Effect":"Allow",
        "Action":["firehose:*"],
        "Resource":["arn:aws:firehose:ap-northeast-1:<account-id>:*"]
      },
      {
        "Effect":"Allow",
        "Action":"iam:PassRole",
        "Resource":"${aws_iam_role.cwl_to_kinesisfirehose_role.arn}"
      }
    ]
}
EOF
}

新しくログファイルを設定したい場合は<対象の環境>.tfvarsに新しくロググループの名前とKinesis Data Firehoseの名前を設定する。

s3/s3.tf

resource "aws_s3_bucket" "bucket" {
  bucket        = "${var.backet_name}"
  acl           = "private"

lifecycle_rule {
    enabled = true
    transition {
      days = 0
      storage_class = "INTELLIGENT_TIERING"
    }
 
    transition {
      days = 200
      storage_class = "GLACIER"
    }
  }
}

lifecycle設定を入れることで、最初はINTELLIGENT_TIERINGで自動でコスト最適化をしてくれます。 200日経過したらGLACIERになり、さらに格安になるという形になります。

s3/backend.tf

terraform {
  backend "s3" {
    bucket    = "toda-backend-bucket"
    key       = "s3.tfstate"
    region    = "ap-northeast-1"
    profile   = "<profile_name>"
  }
}

このバケットは手動で作成をします。

作成する理由は、Terraformでs3作成と同時にtfstateの管理を実行することができないためです。

鶏が先か卵が先か問題と思ってくれたら大丈夫です。

実行手順

まず、手動でS3のバケットを一つ作成します。

s3配下で下記コマンドを実行

terraform workspace select <対象の環境>

terraform plan -var-file <対象の環境>.tfvars

terraform apply -var-file <対象の環境>.tfvars

環境ごとのバケットが作成されます。

次に下記コマンドをディレクトリルートで実行すると、サブスクリプション、IAM Role、Kinesis Data Firehoseを作成し、ログの転送が始まります。

terraform workspace select <対象の環境>

terraform plan -var-file <対象の環境>.tfvars

terraform apply -var-file <対象の環境>.tfvars

今回terraformでやらなかったこと

Cloudwatch Logs の保存期間の有効化は行っておりません。

これに関しては、今後Cloudwatch Logsを使う際に、毎回Terraformで設定を行うよりも手動で設定したほうが早いと考えたためになります。

コード化を無理にする必要もないと話し合いました。

感想

初めてTerraformのWorkspacesを使いました。

Workspacesの使い方は難しいという評判をよく耳にしていましたが、このような活用方法なら難しいことをせずに見やすく運用もしやすいのではないかと思いました。

まとめ

今回はTerraformでCloudwatch Logsに置かれたログをKinesis Data Firehoseを用いてS3に置いてみました。

Cloudwatch Logsはログを集約して、確認する分にはいいですが、ログが貯まって料金が増えてしまうと問題です。定期的に削除しましょう。

S3のIntelligent-Tieringに置くことでコストが安く、管理もできるのでおすすめです。

以上となります。 最後までご覧いただきありがとうございます。