Enable logging in Amazon ElasticSearch - Cloudformation

Enable logging in Amazon ElasticSearch - Cloudformation

If you use cloudformation and is facing difficulties while setting up logging in for ElasticSearch, this blog will help you

ElasticSearch cloudformation

Following is a sample cloudformation template for creating ElasticSearch. It is a part of a nested stack and hence some parameter input validation and things like that are missing but for the scope of this blog, this will suffice.

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "Elastic Search",
  "Parameters": {
    "Environment": {
      "Type": "String"
    },
    "ProjectName": {
      "Type": "String"
    },
    "CFVPCSubnetES1": {
      "Type": "String"
    },
    "CFVPCSubnetES2": {
      "Type": "String"
    },
    "CFVPCSGES": {
      "Type": "String"
    },
    "ESUserpassword": {
      "NoEcho": true,
      "Type": "String"
    },
    "ESStorage": {
      "Type": "String"
    }
  },
  "Conditions": {
    "ProdCondition": {
      "Fn::Equals": [
        {
          "Ref": "Environment"
        },
        "prod"
      ]
    }
  },
  "Resources": {
    "CFES": {
      "Type": "AWS::Elasticsearch::Domain",
      "Properties": {
        "AccessPolicies": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "AWS": "*"
              },
              "Action": "es:*",
              "Resource": {
                "Fn::Sub": "arn:aws:es:ap-northeast-1:337609275973:domain/${ProjectName}-${Environment}-es/*"
              }
            }
          ]
        },
        "AdvancedSecurityOptions": {
          "Enabled": true,
          "InternalUserDatabaseEnabled": true,
          "MasterUserOptions": {
            "MasterUserName": {
              "Fn::Sub": "${ProjectName}-${Environment}esadmin"
            },
            "MasterUserPassword": {
              "Ref": "ESUserpassword"
            }
          }
        },
        "DomainEndpointOptions": {
          "EnforceHTTPS": "true",
          "TLSSecurityPolicy": "Policy-Min-TLS-1-2-2019-07"
        },
        "DomainName": {
          "Fn::Sub": "${ProjectName}-${Environment}-es"
        },
        "EBSOptions": {
          "EBSEnabled": "true",
          "VolumeSize": {
            "Ref": "ESStorage"
          },
          "VolumeType": "gp2"
        },
        "ElasticsearchClusterConfig": {
          "Fn::If": [
            "ProdCondition",
            {
              "DedicatedMasterCount": 3,
              "DedicatedMasterEnabled": "true",
              "DedicatedMasterType": "t3.medium.elasticsearch",
              "InstanceCount": "2",
              "InstanceType": "t3.medium.elasticsearch",
              "ZoneAwarenessConfig": {
                "AvailabilityZoneCount": "2"
              },
              "ZoneAwarenessEnabled": "true"
            },
            {
              "DedicatedMasterEnabled": "false",
              "InstanceCount": "1",
              "InstanceType": "t3.small.elasticsearch",
              "ZoneAwarenessEnabled": "false"
            }
          ]
        },
        "ElasticsearchVersion": "7.8",
        "SnapshotOptions": {
          "AutomatedSnapshotStartHour": "15"
        },
        "EncryptionAtRestOptions": {
          "Enabled": true
        },
        "LogPublishingOptions": {
          "ES_APPLICATION_LOGS": {
            "CloudWatchLogsLogGroupArn": {
              "Fn::GetAtt": [
                "CFCWLogGroupApp",
                "Arn"
              ]
            },
            "Enabled": true
          },
          "AUDIT_LOGS": {
            "CloudWatchLogsLogGroupArn": {
              "Fn::GetAtt": [
                "CFCWLogGroupAudit",
                "Arn"
              ]
            },
            "Enabled": true
          }
        },
        "NodeToNodeEncryptionOptions": {
          "Enabled": true
        },
        "VPCOptions": {
          "SecurityGroupIds": [
            {
              "Ref": "CFVPCSGES"
            }
          ],
          "SubnetIds": {
            "Fn::If": [
              "ProdCondition",
              [
                {
                  "Ref": "CFVPCSubnetES1"
                },
                {
                  "Ref": "CFVPCSubnetES2"
                }
              ],
              [
                {
                  "Ref": "CFVPCSubnetES1"
                }
              ]
            ]
          }
        }
      }
    },
    "CFCWLogGroupAudit": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "LogGroupName": {
          "Fn::Sub": "/aws/aes/domains/${ProjectName}-${Environment}-es/audit-logs"
        }
      }
    },
    "CFCWLogGroupApp": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "LogGroupName": {
          "Fn::Sub": "/aws/aes/domains/${ProjectName}-${Environment}-es/application-logs"
        }
      }
    }
  },
  "Outputs": {
    "CFES": {
      "Value": {
        "Ref": "CFES"
      }
    },
    "CFESEndpoint": {
      "Value": {
        "Fn::GetAtt": [
          "CFES",
          "DomainEndpoint"
        ]
      }
    }
  }
}

If you run the above cloudformation for the first time in a region in an account, you will face the following error

The Resource Access Policy specified for the CloudWatch Logs log group /es/es-domain/AppLogs does not grant sufficient permissions for Amazon Elasticsearch Service to create a log stream. Please check the Resource Access Policy. (Service: AWSElasticsearch; Status Code: 400; Error Code: ValidationException; Request ID: 915d5df2-6b0c-432b-ad6f-0257ab32a85f; Proxy: null)

Solution

To fix the above issue, run the following command using AWS CLI and the cloudformation template for elastic search logging will work in the next run.

 aws --region ap-northeast-1 logs put-resource-policy --policy-name elasticsearch-logging --policy-document '{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": [ "es.amazonaws.com" ] }, "Action": [ "logs:CreateLogStream", "logs:PutLogEvents", "logs:PutLogEventsBatch" ], "Resource": "arn:aws:logs:*:*:log-group:/aws/aes/domains/*:*" } ] }'

What it does is, it creates a resource policy for cloudwatch logs in the specified region which allows elastic search to write logs to the appropriate log group

{
	"Version": "2012-10-17",
	"Statement": [{
		"Effect": "Allow",
		"Principal": {
			"Service": ["es.amazonaws.com"]
		},
		"Action": ["logs:CreateLogStream", "logs:PutLogEvents", "logs:PutLogEventsBatch"],
		"Resource": "arn:aws:logs:*:*:log-group:/aws/aes/domains/*:*"
	}]
}