Bläddra i källkod

Merge pull request #70 from mdr-engineering/feature/ftd_MSOCI-879_VPCFlowLogsviaFirehose

VPC Flowlogs to the HEC
Frederick Damstra 4 år sedan
förälder
incheckning
12b382b6c1

+ 16 - 0
base/account_standards/flowlogs.tf

@@ -51,3 +51,19 @@ resource "aws_iam_role_policy" "flowlogs" {
 }
 EOF
 }
+
+# Spit vpc flow logs to splunk
+module "kinesis_firehose" {
+  source = "../../thirdparty/terraform-aws-kinesis-firehose-splunk"
+  region = var.aws_region
+  arn_cloudwatch_logs_to_ship = "arn:${var.aws_partition}:logs:${var.aws_region}::log-group:/vpc_flow_logs/*"
+  name_cloudwatch_logs_to_ship = "vpc_flow_logs"
+  hec_token = var.aws_flowlogs_hec_token
+  hec_url = "https://${var.hec_pub}:8088"
+  firehose_name = "vpc_flow_logs_to_splunk"
+  tags = merge(var.standard_tags, var.tags)
+  cloudwatch_log_retention = 30 # keep kinesis logs this long
+  log_stream_name = "SplunkDelivery_VPCFlowLogs"
+  s3_bucket_name = "kinesis-flowlogs-${var.aws_account_id}-${var.aws_region}"
+}
+

+ 2 - 0
base/account_standards/vars.tf

@@ -46,6 +46,8 @@ variable "aws_region" { type = string }
 variable "environment" { type = string }
 variable "key_pairs" { type = map }
 variable "c2_accounts" { type = map }
+variable "aws_flowlogs_hec_token" { type = string }
+variable "hec_pub" { type = string }
 
 # Calculate some local variables
 locals {

+ 13 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/CHANGELOG.md

@@ -0,0 +1,13 @@
+# Change Log for Terraform AWS Kinesis Firehose Splunk
+
+## 2.0.0 - Potentially Breaking Change
+  * Upgrade lambda to `node12.x` runtime (thanks [kevinkuszyk](https://github.com/kevinkuszyk))
+  * Add latest javascript from the lambda blueprint (thanks [kevinkuszyk](https://github.com/kevinkuszyk)
+  * Update README
+
+## 1.0.0 - Breaking Change
+  * Upgraded for Terraform 12 compatibility (thanks [kevinkuszyk](https://github.com/kevinkuszyk))
+  * Added git ignore file
+
+## 0.1.0
+  * Initial release

+ 25 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/LICENSE

@@ -0,0 +1,25 @@
+Amending Apache license language & file headers. 
+
+New text:
+
+     Copyright 2019, The Walt Disney Company
+
+     Licensed under the Apache License, Version 2.0 (the "Apache License")
+     with the following modification; you may not use this file except in
+     compliance with the Apache License and the following modification to it:
+     Section 6. Trademarks. is deleted and replaced with:
+
+     6. Trademarks. This License does not grant permission to use the trade
+        names, trademarks, service marks, or product names of the Licensor
+        and its affiliates, except as required to comply with Section 4(c) of
+        the License and to reproduce the content of the NOTICE file.
+
+     You may obtain a copy of the Apache License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the Apache License with the above modification is
+     distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+     KIND, either express or implied. See the Apache License for the specific
+     language governing permissions and limitations under the Apache License.

+ 2 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/README.XDR.md

@@ -0,0 +1,2 @@
+Changes:
+

+ 68 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/README.md

@@ -0,0 +1,68 @@
+# Send CloudWatch Logs to Splunk via Kinesis Firehose
+
+This module configures a Kinesis Firehose, sets up a subscription for a desired CloudWatch Log Group to the Firehose, and sends the log data to Splunk.  A Lambda function is required to transform the CloudWatch Log data from "CloudWatch compressed format" to a format compatible with Splunk.  This module takes care of configuring this Lambda function.
+
+## Usage Instructions
+
+In order to send this data to Splunk you will need to first obtain an HEC Token from your Splunk administrator.
+
+Once you have received the token, you can proceed forward in creating a `module` resource, such as the one in the Example below.
+
+You will use a KMS key of your choice to encrypt the token, as it is sensitive.  See `hec_token` input variable below for more information.
+
+##### Example
+```
+module "kinesis_firehose" {
+  source = "disney/kinesis-firehose-splunk/aws"
+  aws_region = "us-east-1"
+  arn_cloudwatch_logs_to_ship = "arn:aws:logs:us-east-1:<aws_account_number>:log-group:/test/test01:*"  
+  name_cloudwatch_logs_to_ship = "/test/test01"
+  hec_token = "<KMS_encrypted_token>"
+  kms_key_arn = "arn:aws:kms:us-east-1:<aws_account_number:key/<kms_key_id>"
+  hec_url = "<Splunk_Kinesis_ingest_URL>"
+}
+
+```
+
+### Inputs
+
+| Variable Name | Description | Type  | Default | Required |
+|---------------|-------------|-------|---------|----------|
+| region | The region of AWS you want to work in, such as us-west-2 or us-east-1 | string | - | yes |
+| arn_cloudwatch_logs_to_ship | arn of the CloudWatch Log Group that you want to ship to Splunk. | string | - | yes |
+| name_cloudwatch_logs_to_ship | name of the CloudWatch Log Group that you want to ship to Splunk. | string | - | yes |
+| hec_token | Splunk security token needed to submit data to Splunk vai HEC URL. Encyrpted with [this](https://www.terraform.io/docs/providers/aws/d/kms_secrets.html#example-usage) procedure using a KMS key of your choice. | string | - | yes |
+| kms_key_arn | arn of the KMS key you used to encrypt the hec_token | string | - | yes |
+| encryption_context | aws_kms_secrets encryption context | map | `{}` | no |
+| hec_url | Splunk Kinesis URL for submitting CloudWatch logs to splunk | string | - | yes |
+| hec_endpoint_type | The Splunk HEC endpoint type. | string | `Raw` | no |
+| nodejs_runtime | Runtime version of nodejs for Lambda function | string | `nodejs12.x` | no |
+| firehose_name  | Name of the Kinesis Firehose | string | `kinesis-firehose-to-splunk` | no |
+| kinesis_firehose_buffer | Best to read it [here](https://www.terraform.io/docs/providers/aws/r/kinesis_firehose_delivery_stream.html#buffer_size) | integer | `5` | no |
+| kinesis_firehose_buffer_interval | Buffer incoming data for the specified period of time, in seconds, before delivering it to the destination | integer | `300` | no |
+| s3_prefix | Optional prefix (a slash after the prefix will show up as a folder in the s3 bucket).  The "YYYY/MM/DD/HH" time format prefix is automatically used for delivered S3 files. | string | `kinesis-firehose/` | no |
+| hec_acknowledgment_timeout | The amount of time, in seconds between 180 and 600, that Kinesis Firehose waits to receive an acknowledgment from Splunk after it sends it data. | integer | `300` | no |
+| s3_backup_mode | Defines how documents should be delivered to Amazon S3. Valid values are `FailedEventsOnly` and `AllEvents`. | string | `FailedEventsOnly` | no |
+| enable_fh_cloudwatch_logging | Enable kinesis firehose CloudWatch logging. (It only logs errors). | boolean | `true` | no |
+| tags | Map of tags to put on the resource | map | `null` | no |
+| cloudwatch_log_retention | Length in days to keep CloudWatch logs of Kinesis Firehose | integer | `30` | no |
+| log_stream_name | Name of the CloudWatch log stream for Kinesis Firehose CloudWatch log group | string | `SplunkDelivery` | no |
+| s3_bucket_name  | Name of the s3 bucket Kinesis Firehose uses for backups | string | `kinesis-firehose-to-splunk` | no |
+| s3_compression_format | The compression format for what the Kinesis Firehose puts in the s3 bucket | string | `GZIP` | no |
+| kinesis_firehose_lambda_role_name | Name of IAM Role for Lambda function that transforms CloudWatch data for Kinesis Firehose into Splunk compatible format | string | `KinesisFirehoseToLambaRole` | no |
+| lambda_iam_policy_name | Name of the IAM policy that is attached to the IAM Role for the lambda transform function | string | `Kinesis-Firehose-to-Splunk-Policy` | no |
+| lambda_function_timeout | The function execution time at which Lambda should terminate the function. | integer | `180` | no |
+| kinesis_firehose_iam_policy_name | Name of the IAM Policy attached to IAM Role for the Kinesis Firehose | string | `KinesisFirehose-Policy` | no |
+| cloudwatch_to_firehose_trust_iam_role_name | IAM Role name for CloudWatch to Kinesis Firehose subscription | string | `CloudWatchToSplunkFirehoseTrust` | no |
+| cloudwatch_to_fh_access_policy_name | Name of IAM policy attached to the IAM role for CloudWatch to Kinesis Firehose subscription | string | `KinesisCloudWatchToFirehosePolicy` | no |
+| cloudwatch_log_filter_name | Name of Log Filter for CloudWatch Log subscription to Kinesis Firehose | string | `KinesisSubscriptionFilter` | no |
+| subscription_filter_pattern | Filter pattern for the CloudWatch Log Group subscription to the Kinesis Firehose. See [this](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html) for filter pattern info. | string | `""` (no filter) | no |
+
+#### Acknowledgements
+
+_Author_
+- Mitchell L. Cooper - Maintainer
+
+_Reviewers_
+- Ian Ward
+- Justice London

+ 2 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/SOURCE

@@ -0,0 +1,2 @@
+https://github.com/disney/terraform-aws-kinesis-firehose-splunk.git
+

+ 273 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/files/kinesis-firehose-cloudwatch-logs-processor.js

@@ -0,0 +1,273 @@
+/*
+For processing data sent to Firehose by Cloudwatch Logs subscription filters.
+
+Cloudwatch Logs sends to Firehose records that look like this:
+
+{
+  "messageType": "DATA_MESSAGE",
+  "owner": "123456789012",
+  "logGroup": "log_group_name",
+  "logStream": "log_stream_name",
+  "subscriptionFilters": [
+    "subscription_filter_name"
+  ],
+  "logEvents": [
+    {
+      "id": "01234567890123456789012345678901234567890123456789012345",
+      "timestamp": 1510109208016,
+      "message": "log message 1"
+    },
+    {
+      "id": "01234567890123456789012345678901234567890123456789012345",
+      "timestamp": 1510109208017,
+      "message": "log message 2"
+    }
+    ...
+  ]
+}
+
+The data is additionally compressed with GZIP.
+
+The code below will:
+
+1) Gunzip the data
+2) Parse the json
+3) Set the result to ProcessingFailed for any record whose messageType is not DATA_MESSAGE, thus redirecting them to the
+   processing error output. Such records do not contain any log events. You can modify the code to set the result to
+   Dropped instead to get rid of these records completely.
+4) For records whose messageType is DATA_MESSAGE, extract the individual log events from the logEvents field, and pass
+   each one to the transformLogEvent method. You can modify the transformLogEvent method to perform custom
+   transformations on the log events.
+5) Concatenate the result from (4) together and set the result as the data of the record returned to Firehose. Note that
+   this step will not add any delimiters. Delimiters should be appended by the logic within the transformLogEvent
+   method.
+6) Any additional records which exceed 6MB will be re-ingested back into Firehose.
+*/
+const zlib = require('zlib');
+const AWS = require('aws-sdk');
+
+/**
+ * logEvent has this format:
+ *
+ * {
+ *   "id": "01234567890123456789012345678901234567890123456789012345",
+ *   "timestamp": 1510109208016,
+ *   "message": "log message 1"
+ * }
+ *
+ * The default implementation below just extracts the message and appends a newline to it.
+ *
+ * The result must be returned in a Promise.
+ */
+function transformLogEvent(logEvent) {
+    return Promise.resolve(`${logEvent.message}\n`);
+}
+
+function putRecordsToFirehoseStream(streamName, records, client, resolve, reject, attemptsMade, maxAttempts) {
+    client.putRecordBatch({
+        DeliveryStreamName: streamName,
+        Records: records,
+    }, (err, data) => {
+        const codes = [];
+        let failed = [];
+        let errMsg = err;
+
+        if (err) {
+            failed = records;
+        } else {
+            for (let i = 0; i < data.RequestResponses.length; i++) {
+                const code = data.RequestResponses[i].ErrorCode;
+                if (code) {
+                    codes.push(code);
+                    failed.push(records[i]);
+                }
+            }
+            errMsg = `Individual error codes: ${codes}`;
+        }
+
+        if (failed.length > 0) {
+            if (attemptsMade + 1 < maxAttempts) {
+                console.log('Some records failed while calling PutRecordBatch, retrying. %s', errMsg);
+                putRecordsToFirehoseStream(streamName, failed, client, resolve, reject, attemptsMade + 1, maxAttempts);
+            } else {
+                reject(`Could not put records after ${maxAttempts} attempts. ${errMsg}`);
+            }
+        } else {
+            resolve('');
+        }
+    });
+}
+
+function putRecordsToKinesisStream(streamName, records, client, resolve, reject, attemptsMade, maxAttempts) {
+    client.putRecords({
+        StreamName: streamName,
+        Records: records,
+    }, (err, data) => {
+        const codes = [];
+        let failed = [];
+        let errMsg = err;
+
+        if (err) {
+            failed = records;
+        } else {
+            for (let i = 0; i < data.Records.length; i++) {
+                const code = data.Records[i].ErrorCode;
+                if (code) {
+                    codes.push(code);
+                    failed.push(records[i]);
+                }
+            }
+            errMsg = `Individual error codes: ${codes}`;
+        }
+
+        if (failed.length > 0) {
+            if (attemptsMade + 1 < maxAttempts) {
+                console.log('Some records failed while calling PutRecords, retrying. %s', errMsg);
+                putRecordsToKinesisStream(streamName, failed, client, resolve, reject, attemptsMade + 1, maxAttempts);
+            } else {
+                reject(`Could not put records after ${maxAttempts} attempts. ${errMsg}`);
+            }
+        } else {
+            resolve('');
+        }
+    });
+}
+
+function createReingestionRecord(isSas, originalRecord) {
+    if (isSas) {
+        return {
+            Data: Buffer.from(originalRecord.data, 'base64'),
+            PartitionKey: originalRecord.kinesisRecordMetadata.partitionKey,
+        };
+    } else {
+        return {
+            Data: Buffer.from(originalRecord.data, 'base64'),
+        };
+    }
+}
+
+
+function getReingestionRecord(isSas, reIngestionRecord) {
+    if (isSas) {
+        return {
+            Data: reIngestionRecord.Data,
+            PartitionKey: reIngestionRecord.PartitionKey,
+        };
+    } else {
+        return {
+            Data: reIngestionRecord.Data,
+        };
+    }
+}
+
+exports.handler = (event, context, callback) => {
+    Promise.all(event.records.map(r => {
+        const buffer = Buffer.from(r.data, 'base64');
+
+        let decompressed;
+        try {
+            decompressed = zlib.gunzipSync(buffer);
+        } catch (e) {
+            return Promise.resolve({
+                recordId: r.recordId,
+                result: 'ProcessingFailed',
+            });
+        }
+
+        const data = JSON.parse(decompressed);
+        // CONTROL_MESSAGE are sent by CWL to check if the subscription is reachable.
+        // They do not contain actual data.
+        if (data.messageType === 'CONTROL_MESSAGE') {
+            return Promise.resolve({
+                recordId: r.recordId,
+                result: 'Dropped',
+            });
+        } else if (data.messageType === 'DATA_MESSAGE') {
+            const promises = data.logEvents.map(transformLogEvent);
+            return Promise.all(promises)
+                .then(transformed => {
+                    const payload = transformed.reduce((a, v) => a + v, '');
+                    const encoded = Buffer.from(payload).toString('base64');
+                    return {
+                        recordId: r.recordId,
+                        result: 'Ok',
+                        data: encoded,
+                    };
+                });
+        } else {
+            return Promise.resolve({
+                recordId: r.recordId,
+                result: 'ProcessingFailed',
+            });
+        }
+    })).then(recs => {
+        const isSas = Object.prototype.hasOwnProperty.call(event, 'sourceKinesisStreamArn');
+        const streamARN = isSas ? event.sourceKinesisStreamArn : event.deliveryStreamArn;
+        const region = streamARN.split(':')[3];
+        const streamName = streamARN.split('/')[1];
+        const result = { records: recs };
+        let recordsToReingest = [];
+        const putRecordBatches = [];
+        let totalRecordsToBeReingested = 0;
+        const inputDataByRecId = {};
+        event.records.forEach(r => inputDataByRecId[r.recordId] = createReingestionRecord(isSas, r));
+
+        let projectedSize = recs.filter(rec => rec.result === 'Ok')
+                              .map(r => r.recordId.length + r.data.length)
+                              .reduce((a, b) => a + b, 0);
+        // 6000000 instead of 6291456 to leave ample headroom for the stuff we didn't account for
+        for (let idx = 0; idx < event.records.length && projectedSize > 6000000; idx++) {
+            const rec = result.records[idx];
+            if (rec.result === 'Ok') {
+                totalRecordsToBeReingested++;
+                recordsToReingest.push(getReingestionRecord(isSas, inputDataByRecId[rec.recordId]));
+                projectedSize -= rec.data.length;
+                delete rec.data;
+                result.records[idx].result = 'Dropped';
+
+                // split out the record batches into multiple groups, 500 records at max per group
+                if (recordsToReingest.length === 500) {
+                    putRecordBatches.push(recordsToReingest);
+                    recordsToReingest = [];
+                }
+            }
+        }
+
+        if (recordsToReingest.length > 0) {
+            // add the last batch
+            putRecordBatches.push(recordsToReingest);
+        }
+
+        if (putRecordBatches.length > 0) {
+            new Promise((resolve, reject) => {
+                let recordsReingestedSoFar = 0;
+                for (let idx = 0; idx < putRecordBatches.length; idx++) {
+                    const recordBatch = putRecordBatches[idx];
+                    if (isSas) {
+                        const client = new AWS.Kinesis({ region: region });
+                        putRecordsToKinesisStream(streamName, recordBatch, client, resolve, reject, 0, 20);
+                    } else {
+                        const client = new AWS.Firehose({ region: region });
+                        putRecordsToFirehoseStream(streamName, recordBatch, client, resolve, reject, 0, 20);
+                    }
+                    recordsReingestedSoFar += recordBatch.length;
+                    console.log('Reingested %s/%s records out of %s in to %s stream', recordsReingestedSoFar, totalRecordsToBeReingested, event.records.length, streamName);
+                }
+            }).then(
+              () => {
+                  console.log('Reingested all %s records out of %s in to %s stream', totalRecordsToBeReingested, event.records.length, streamName);
+                  callback(null, result);
+              },
+              failed => {
+                  console.log('Failed to reingest records. %s', failed);
+                  callback(failed, null);
+              });
+        } else {
+            console.log('No records needed to be reingested.');
+            callback(null, result);
+        }
+    }).catch(ex => {
+        console.log('Error: ', ex);
+        callback(ex, null);
+    });
+};

+ 359 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/main.tf

@@ -0,0 +1,359 @@
+# Kenisis firehose stream
+# Record Transformation Required, called "processing_configuration" in Terraform
+resource "aws_kinesis_firehose_delivery_stream" "kinesis_firehose" {
+  name        = var.firehose_name
+  destination = "splunk"
+
+  s3_configuration {
+    role_arn           = aws_iam_role.kinesis_firehose.arn
+    prefix             = var.s3_prefix
+    bucket_arn         = aws_s3_bucket.kinesis_firehose_s3_bucket.arn
+    buffer_size        = var.kinesis_firehose_buffer
+    buffer_interval    = var.kinesis_firehose_buffer_interval
+    compression_format = var.s3_compression_format
+  }
+
+  splunk_configuration {
+    hec_endpoint               = var.hec_url
+    #hec_token                  = data.aws_kms_secrets.splunk_hec_token.plaintext["hec_token"]
+    hec_token                  = var.hec_token
+    hec_acknowledgment_timeout = var.hec_acknowledgment_timeout
+    hec_endpoint_type          = var.hec_endpoint_type
+    s3_backup_mode             = var.s3_backup_mode
+
+    processing_configuration {
+      enabled = "true"
+
+      processors {
+        type = "Lambda"
+
+        parameters {
+          parameter_name  = "LambdaArn"
+          parameter_value = "${aws_lambda_function.firehose_lambda_transform.arn}:$LATEST"
+        }
+        parameters {
+          parameter_name  = "RoleArn"
+          parameter_value = aws_iam_role.kinesis_firehose.arn
+        }
+      }
+    }
+
+    cloudwatch_logging_options {
+      enabled         = var.enable_fh_cloudwatch_logging
+      log_group_name  = aws_cloudwatch_log_group.kinesis_logs.name
+      log_stream_name = aws_cloudwatch_log_stream.kinesis_logs.name
+    }
+  }
+
+  tags = var.tags
+}
+
+# S3 Bucket for Kinesis Firehose s3_backup_mode
+resource "aws_s3_bucket" "kinesis_firehose_s3_bucket" {
+  bucket = var.s3_bucket_name
+  # new version of aws doesn't let you specify the region
+  #region = var.region
+  acl    = "private"
+
+  server_side_encryption_configuration {
+    rule {
+      apply_server_side_encryption_by_default {
+        sse_algorithm = "AES256"
+      }
+    }
+  }
+
+  tags = var.tags
+}
+
+# Cloudwatch logging group for Kinesis Firehose
+resource "aws_cloudwatch_log_group" "kinesis_logs" {
+  name              = "/aws/kinesisfirehose/${var.firehose_name}"
+  retention_in_days = var.cloudwatch_log_retention
+
+  tags = var.tags
+}
+
+# Create the stream
+resource "aws_cloudwatch_log_stream" "kinesis_logs" {
+  name           = var.log_stream_name
+  log_group_name = aws_cloudwatch_log_group.kinesis_logs.name
+}
+
+## handle the sensitivity of the hec_token variable
+#data "aws_kms_secrets" "splunk_hec_token" {
+#  secret {
+#    name    = "hec_token"
+#    payload = var.hec_token
+#
+#    context = var.encryption_context
+#  }
+#}
+
+# Role for the transformation Lambda function attached to the kinesis stream
+resource "aws_iam_role" "kinesis_firehose_lambda" {
+  name        = var.kinesis_firehose_lambda_role_name
+  path        = "/lambda/"
+  description = "Role for Lambda function to transformation CloudWatch logs into Splunk compatible format"
+  force_detach_policies = true
+
+  assume_role_policy = <<POLICY
+{
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": "sts:AssumeRole",
+      "Principal": {
+        "Service": "lambda.amazonaws.com"
+      }
+    }
+  ],
+  "Version": "2012-10-17"
+}
+POLICY
+
+
+  tags = var.tags
+}
+
+data "aws_iam_policy_document" "lambda_policy_doc" {
+  statement {
+    actions = [
+      "logs:GetLogEvents",
+    ]
+
+    resources = [
+      var.arn_cloudwatch_logs_to_ship,
+    ]
+
+    effect = "Allow"
+  }
+
+  statement {
+    actions = [
+      "firehose:PutRecordBatch",
+    ]
+
+    resources = [
+      aws_kinesis_firehose_delivery_stream.kinesis_firehose.arn,
+    ]
+  }
+
+  statement {
+    actions = [
+      "logs:PutLogEvents",
+    ]
+
+    resources = [
+      "*",
+    ]
+
+    effect = "Allow"
+  }
+
+  statement {
+    actions = [
+      "logs:CreateLogGroup",
+    ]
+
+    resources = [
+      "*",
+    ]
+
+    effect = "Allow"
+  }
+
+  statement {
+    actions = [
+      "logs:CreateLogStream",
+    ]
+
+    resources = [
+      "*",
+    ]
+
+    effect = "Allow"
+  }
+}
+
+resource "aws_iam_policy" "lambda_transform_policy" {
+  name   = var.lambda_iam_policy_name
+  policy = data.aws_iam_policy_document.lambda_policy_doc.json
+}
+
+resource "aws_iam_role_policy_attachment" "lambda_policy_role_attachment" {
+  role       = aws_iam_role.kinesis_firehose_lambda.name
+  policy_arn = aws_iam_policy.lambda_transform_policy.arn
+}
+
+# Create the lambda function
+# The lambda function to transform data from compressed format in Cloudwatch to something Splunk can handle (uncompressed)
+resource "aws_lambda_function" "firehose_lambda_transform" {
+  function_name    = var.lambda_function_name
+  description      = "Transform data from CloudWatch format to Splunk compatible format"
+  filename         = data.archive_file.lambda_function.output_path
+  role             = aws_iam_role.kinesis_firehose_lambda.arn
+  handler          = "kinesis-firehose-cloudwatch-logs-processor.handler"
+  source_code_hash = data.archive_file.lambda_function.output_base64sha256
+  runtime          = var.nodejs_runtime
+  timeout          = var.lambda_function_timeout
+
+  tags = var.tags
+}
+
+# kinesis-firehose-cloudwatch-logs-processor.js was taken by copy/paste from the AWS UI.  It is predefined blueprint
+# code supplied to AWS by Splunk.
+data "archive_file" "lambda_function" {
+  type        = "zip"
+  source_file = "${path.module}/files/kinesis-firehose-cloudwatch-logs-processor.js"
+  output_path = "${path.module}/files/kinesis-firehose-cloudwatch-logs-processor.zip"
+}
+
+# Role for Kenisis Firehose
+resource "aws_iam_role" "kinesis_firehose" {
+  name        = var.kinesis_firehose_role_name
+  path        = "/aws_services/"
+  description = "IAM Role for Kenisis Firehose"
+  force_detach_policies = true
+
+  assume_role_policy = <<POLICY
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Principal": {
+        "Service": "firehose.amazonaws.com"
+      },
+      "Action": "sts:AssumeRole",
+      "Effect": "Allow"
+    }
+  ]
+}
+POLICY
+
+
+  tags = var.tags
+}
+
+data "aws_iam_policy_document" "kinesis_firehose_policy_document" {
+  statement {
+    actions = [
+      "s3:AbortMultipartUpload",
+      "s3:GetBucketLocation",
+      "s3:GetObject",
+      "s3:ListBucket",
+      "s3:ListBucketMultipartUploads",
+      "s3:PutObject",
+    ]
+
+    resources = [
+      aws_s3_bucket.kinesis_firehose_s3_bucket.arn,
+      "${aws_s3_bucket.kinesis_firehose_s3_bucket.arn}/*",
+    ]
+
+    effect = "Allow"
+  }
+
+  statement {
+    actions = [
+      "lambda:InvokeFunction",
+      "lambda:GetFunctionConfiguration",
+    ]
+
+    resources = [
+      "${aws_lambda_function.firehose_lambda_transform.arn}:$LATEST",
+    ]
+  }
+
+  statement {
+    actions = [
+      "logs:PutLogEvents",
+    ]
+
+    resources = [
+      aws_cloudwatch_log_group.kinesis_logs.arn,
+      aws_cloudwatch_log_stream.kinesis_logs.arn,
+    ]
+
+    effect = "Allow"
+  }
+}
+
+resource "aws_iam_policy" "kinesis_firehose_iam_policy" {
+  name   = var.kinesis_firehose_iam_policy_name
+  policy = data.aws_iam_policy_document.kinesis_firehose_policy_document.json
+}
+
+resource "aws_iam_role_policy_attachment" "kenisis_fh_role_attachment" {
+  role       = aws_iam_role.kinesis_firehose.name
+  policy_arn = aws_iam_policy.kinesis_firehose_iam_policy.arn
+}
+
+resource "aws_iam_role" "cloudwatch_to_firehose_trust" {
+  name        = var.cloudwatch_to_firehose_trust_iam_role_name
+  description = "Role for CloudWatch Log Group subscription"
+  path        = "/aws_services/"
+  force_detach_policies = true
+
+  assume_role_policy = <<ROLE
+{
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": "sts:AssumeRole",
+      "Principal": {
+        "Service": "logs.${var.region}.amazonaws.com"
+      }
+    }
+  ],
+  "Version": "2012-10-17"
+}
+ROLE
+
+}
+
+data "aws_iam_policy_document" "cloudwatch_to_fh_access_policy" {
+  statement {
+    actions = [
+      "firehose:*",
+    ]
+
+    effect = "Allow"
+
+    resources = [
+      aws_kinesis_firehose_delivery_stream.kinesis_firehose.arn,
+    ]
+  }
+
+  statement {
+    actions = [
+      "iam:PassRole",
+    ]
+
+    effect = "Allow"
+
+    resources = [
+      aws_iam_role.cloudwatch_to_firehose_trust.arn,
+    ]
+  }
+}
+
+resource "aws_iam_policy" "cloudwatch_to_fh_access_policy" {
+  name        = var.cloudwatch_to_fh_access_policy_name
+  description = "Cloudwatch to Firehose Subscription Policy"
+  policy      = data.aws_iam_policy_document.cloudwatch_to_fh_access_policy.json
+}
+
+resource "aws_iam_role_policy_attachment" "cloudwatch_to_fh" {
+  role       = aws_iam_role.cloudwatch_to_firehose_trust.name
+  policy_arn = aws_iam_policy.cloudwatch_to_fh_access_policy.arn
+}
+
+resource "aws_cloudwatch_log_subscription_filter" "cloudwatch_log_filter" {
+  name            = var.cloudwatch_log_filter_name
+  role_arn        = aws_iam_role.cloudwatch_to_firehose_trust.arn
+  destination_arn = aws_kinesis_firehose_delivery_stream.kinesis_firehose.arn
+  log_group_name  = var.name_cloudwatch_logs_to_ship
+  filter_pattern  = var.subscription_filter_pattern
+}
+

+ 10 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/terraform.tf

@@ -0,0 +1,10 @@
+#terraform {
+#  required_version = "~> 0.12.0"
+#}
+#
+#provider "aws" {
+#  version = "~> 2.7"
+#
+#  region = var.region
+#}
+

+ 151 - 0
thirdparty/terraform-aws-kinesis-firehose-splunk/variables.tf

@@ -0,0 +1,151 @@
+variable "region" {
+  description = "The region of AWS you want to work in, such as us-west-2 or us-east-1"
+}
+
+variable "hec_url" {
+  description = "Splunk Kinesis URL for submitting CloudWatch logs to splunk"
+}
+
+variable "hec_token" {
+  description = "Splunk security token needed to submit data to Splunk"
+}
+
+variable "nodejs_runtime" {
+  description = "Runtime version of nodejs for Lambda function"
+  default     = "nodejs12.x"
+}
+
+variable "firehose_name" {
+  description = "Name of the Kinesis Firehose"
+  default     = "kinesis-firehose-to-splunk"
+}
+
+variable "kinesis_firehose_buffer" {
+  description = "https://www.terraform.io/docs/providers/aws/r/kinesis_firehose_delivery_stream.html#buffer_size"
+  default     = 5 # Megabytes
+}
+
+variable "kinesis_firehose_buffer_interval" {
+  description = "Buffer incoming data for the specified period of time, in seconds, before delivering it to the destination"
+  default     = 300 # Seconds
+}
+
+variable "s3_prefix" {
+  description = "Optional prefix (a slash after the prefix will show up as a folder in the s3 bucket).  The YYYY/MM/DD/HH time format prefix is automatically used for delivered S3 files."
+  default     = "kinesis-firehose/"
+}
+
+variable "hec_acknowledgment_timeout" {
+  description = "The amount of time, in seconds between 180 and 600, that Kinesis Firehose waits to receive an acknowledgment from Splunk after it sends it data."
+  default     = 300
+}
+
+variable "hec_endpoint_type" {
+  description = "Splunk HEC endpoint type; `Raw` or `Event`"
+  default     = "Raw"
+}
+
+variable "s3_backup_mode" {
+  description = "Defines how documents should be delivered to Amazon S3. Valid values are FailedEventsOnly and AllEvents."
+  default     = "FailedEventsOnly"
+}
+
+variable "s3_compression_format" {
+  description = "The compression format for what the Kinesis Firehose puts in the s3 bucket"
+  default     = "GZIP"
+}
+
+variable "enable_fh_cloudwatch_logging" {
+  description = "Enable kinesis firehose CloudWatch logging. (It only logs errors)"
+  default     = true
+}
+
+variable "tags" {
+  type        = map(string)
+  description = "Map of tags to put on the resource"
+  default     = {}
+}
+
+variable "cloudwatch_log_retention" {
+  description = "Length in days to keep CloudWatch logs of Kinesis Firehose"
+  default     = 30
+}
+
+variable "log_stream_name" {
+  description = "Name of the CloudWatch log stream for Kinesis Firehose CloudWatch log group"
+  default     = "SplunkDelivery"
+}
+
+variable "s3_bucket_name" {
+  description = "Name of the s3 bucket Kinesis Firehose uses for backups"
+  default     = "kinesis-firehose-to-splunk"
+}
+
+#variable "encryption_context" {
+#  description = "aws_kms_secrets encryption context"
+#  type        = map(string)
+#  default     = {}
+#}
+
+variable "kinesis_firehose_lambda_role_name" {
+  description = "Name of IAM Role for Lambda function that transforms CloudWatch data for Kinesis Firehose into Splunk compatible format"
+  default     = "KinesisFirehoseToLambaRole"
+}
+
+variable "kinesis_firehose_role_name" {
+  description = "Name of IAM Role for the Kinesis Firehose"
+  default     = "KinesisFirehoseRole"
+}
+
+variable "arn_cloudwatch_logs_to_ship" {
+  description = "arn of the CloudWatch Log Group that you want to ship to Splunk."
+}
+
+variable "name_cloudwatch_logs_to_ship" {
+  description = "name of the CloudWatch Log Group that you want to ship to Splunk."
+}
+
+variable "lambda_function_name" {
+  description = "Name of the Lambda function that transforms CloudWatch data for Kinesis Firehose into Splunk compatible format"
+  default     = "kinesis-firehose-transform"
+}
+
+variable "lambda_function_timeout" {
+  description = "The function execution time at which Lambda should terminate the function."
+  default     = 180
+}
+
+variable "lambda_iam_policy_name" {
+  description = "Name of the IAM policy that is attached to the IAM Role for the lambda transform function"
+  default     = "Kinesis-Firehose-to-Splunk-Policy"
+}
+
+#variable "kms_key_arn" {
+#  description = "arn of the KMS key you used to encrypt the hec_token"
+#}
+
+variable "kinesis_firehose_iam_policy_name" {
+  description = "Name of the IAM Policy attached to IAM Role for the Kinesis Firehose"
+  default     = "KinesisFirehose-Policy"
+}
+
+variable "cloudwatch_to_firehose_trust_iam_role_name" {
+  description = "IAM Role name for CloudWatch to Kinesis Firehose subscription"
+  default     = "CloudWatchToSplunkFirehoseTrust"
+}
+
+variable "cloudwatch_to_fh_access_policy_name" {
+  description = "Name of IAM policy attached to the IAM role for CloudWatch to Kinesis Firehose subscription"
+  default     = "KinesisCloudWatchToFirehosePolicy"
+}
+
+variable "cloudwatch_log_filter_name" {
+  description = "Name of Log Filter for CloudWatch Log subscription to Kinesis Firehose"
+  default     = "KinesisSubscriptionFilter"
+}
+
+variable "subscription_filter_pattern" {
+  description = "Filter pattern for the CloudWatch Log Group subscription to the Kinesis Firehose. See [this](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html) for filter pattern info."
+  default     = "" # nothing is being filtered
+}
+