用Kibana将简单网站托管中的CloudFront实时日志可视化

首先

通过使用由亚马逊网络服务(AWS)提供的服务,如Amazon CloudFront和Amazon S3的组合,可以以低成本构建由HTML、JavaScript、图像和视频构成的静态网站的分发基础设施。本文将通过使用能够自动进行资源设置的AWS CloudFormation来说明如何迅速且无误地构建这些分发基础设施的步骤。请注意,本次使用的CloudFormation模板已在以下的GitHub存储库中公开。

    AWSCloudFormationTemplates/static-website-hosting-with-ssl

太长不读。

通过执行以下的CloudFormation模板,您可以快速且轻松地建立一个静态网站托管基础设施。只需点击下方按钮,即可在您的AWS账户(亚太地区东京 – ap-northeast-1)上运行这个CloudFormation模板。

cloudformation-launch-stack

请查看以前的文章以获取关于创建的AWS资源整体架构的图表。在本文中,我们将重点介绍以下资源。

architecture.png

使用Kibana来可视化CloudFront的实时日志。

与以往需要数分钟才能使用的标准日志相比,CloudFront的实时日志现在可以在生成后的几秒钟内访问。通过使用这个功能,我们可以进行更详细和更快速的监控,并及时根据发生的事件进行资源配置的快速更改。这个实时日志可以

    • 取得するログのサンプリング率

 

    • 取得するログのフィールド

 

    どのCloudFront Behavior に適用するか

可以指定任何日志,并将其发送到Kinesis Data Streams。同时,通过Kinesis Data Firehose,可以将到达Kinesis流的日志存储到Elasticsearch Service中,并通过使用Kibana轻松可视化日志内容。

Screenshot_2021-01-23 Default - Elastic.png

通过按照这里列出的步骤来创建资源,可以构建以下架构。这些步骤在亚马逊网络服务(Amazon Web Services)博客上以标题“使用Amazon CloudFront日志创建实时仪表板”进行了发布。

architecture

本文中,我們將使用CloudFormation模板以一鍵方式創建上述架構。同時,我們還將解釋為每個AWS服務分配多少資源來應對負載所需。

亚马逊金融数据流

首先,我们需要创建一个Kinesis流来接收来自CloudFront的实时日志。

Parameters:
  KinesisShardCount:
    Type: Number
    Default: 1
    MinValue: 1 
    Description: The shard count of Kinesis [required]

Resources:
  Kinesis:
    Type: 'AWS::Kinesis::Stream'
    Properties:
      Name: !Ref AWS::StackName
      RetentionPeriodHours: 24
      StreamEncryption:
        EncryptionType: KMS
        KeyId: alias/aws/kinesis
      ShardCount: !Ref KinesisShardCount

重要的是确定在这里使用多少个分片来构建Kinesis流。关于如何计算这个值,官方文档中提供了计算Kinesis数据流分片数的估算方法的描述,并且还包括了输出所有字段的实时日志的情况下,

1,000 Byte x 秒間リクエスト数 / 1,000,000 x 1.25 = シャード数

可以通过计算得出。

如果有可能发生最多每秒5000个请求的访问,请提供一个例子。

5,000 x 1,000 / 1,000,000 x 1.25 = 7

这样一来,我们可以看出,需要大约7个分片,包括缓存。

然而,对于出现突发流量的网站,由于每秒钟的日志输出会集中在特定的一秒钟,可能会超过文档中所述的值所能接收的数据量,从而导致出现 ProvisionedThroughputExceededException 异常。因此,为了保证安全,建议提前配置一个比文档中所述值更大的缓冲区,例如可以配置能够处理预期数据量的两倍的分片数量的分片。

根据流量调整分片数量

根据时间和事件的存在与否,CloudFront的流量会有所变动。由于Kinesis Data Streams的计费是按照分片数(分片时间)计费,并且无法接收超出其容量的实时日志,因此根据流向CloudFront的流量量,需要调整Kinesis流的分片数。

然而,在更改分片数量时存在一些限制。首先,调用UpdteShardCount API来更改分片数量只能执行将当前分片数量倍增或减半的操作。因此,无法将3个分片的Kinesis流更改为7个分片。因此,建议将分片数量设置为2的幂次方(1、2、4、8、16、32、64、128…)。此外,每个区域的初始分片数量为北弗吉尼亚(us-east-1)区域为500个分片,其他区域为200个分片。除此之外,对于执行UpdteShardCount API的次数还有限制。如果希望超过这些限制,请提交申请以放宽配额限制。

为了确认在Kinesis Data Streams中设置的容量是否适应负载,请监控以下CloudWatch指标。您可以从此链接中启用基于这些指标的CloudWatch警报。

ネームスペースメトリクス閾値AWS/KinesisGetRecords.IteratorAgeMillisecondsテンプレートで指定した値AWS/KinesisPutRecord.Successテンプレートで指定した値AWS/KinesisWriteProvisionedThroughputExceeded1分間に1回以上

亚马逊云端流量加速

接下来,进行CloudFront实时日志输出的配置。

首先,为了使CloudFront能够向Kinesis Data Streams输出日志,我们将使用IAM角色来授予权限。给予CloudFront的权限包括写入Kinesis的权限和创建用于数据加密的KMS密钥的权限。

Resources:
  IAMRoleForCloudFrontRealtimeLog:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: !Sub '${AWS::StackName}-KinesisPutPolicy-${AWS::Region}'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 'kinesis:DescribeStreamSummary'
                  - 'kinesis:DescribeStream'
                  - 'kinesis:PutRecord'
                  - 'kinesis:PutRecords'
                Resource:
                  - !GetAtt Kinesis.Arn
              - Effect: Allow
                Action:
                  - 'kms:GenerateDataKey'
                Resource:
                  - !GetAtt KMSKey.Arn
      RoleName: !Sub '${aws::StackName}-CloudFrontRealtimeLog-${AWS::Region}'

接下来,您需要指定先前创建的Kinesis Data Streams和IAM角色的ARN,以进行实时日志的配置。在字段(Fields)部分,您可以选择要输出至日志的字段。但在本模板中,我们将选择输出以下所有字段的配置。

フィールド名内容timestampエッジサーバーがリクエストへの応答を終了した日時c-ipリクエスト元のビューワーの IP アドレスtime-to-first-byteサーバー上で測定される、要求を受信してから応答の最初のバイトを書き込むまでの秒数sc-statusサーバーのレスポンスの HTTP ステータスコードsc-bytesサーバーがリクエストに応じてビューワーに送信したデータのバイトの合計数cs-methodビューワーから受信した HTTP リクエストメソッドcs-protocolビューワーリクエストのプロトコルcs-hostCloudFront ディストリビューションのドメイン名cs-uri-stemパスとオブジェクトを識別するリクエスト URL の部分cs-bytesビューワーがリクエストに含めたデータのバイトの合計数x-edge-locationリクエストを処理したエッジロケーションx-edge-request-idリクエストを一意に識別する不透明な文字列x-host-headerビューワーが、このリクエストの Host ヘッダーに追加した値time-takenサーバーが、ビューワーのリクエストを受信してからレスポンスの最後のバイトを出力キューに書き込むまでの秒数cs-protocol-versionビューワーがリクエストで指定した HTTP バージョンc-ip-versionリクエストの IP バージョンcs-user-agentリクエスト内の User-Agent ヘッダーの値cs-refererリクエスト内の Referer ヘッダーの値cs-cookieリクエスト内の Cookie ヘッダーcs-uri-queryリクエスト URL のクエリ文字列の部分x-edge-response-result-typeビューワーにレスポンスを返す直前にサーバーがレスポンスを分類した方法x-forwarded-forリクエスト元のビューワーの IP アドレスssl-protocolリクエストとレスポンスを送信するためにビューワーとサーバーがネゴシエートした SSL/TLS プロトコルssl-cipherリクエストとレスポンスを暗号化するためにビューワーとサーバーがネゴシエートした SSL/TLS 暗号x-edge-result-typeサーバーが、最後のバイトを渡した後で、レスポンスを分類した方法fle-encrypted-fieldsサーバーが暗号化してオリジンに転送したフィールドレベル暗号化フィールドの数fle-statusリクエストボディが正常に処理されたかどうかを示すコードsc-content-typeレスポンスの HTTP Content-Type ヘッダーの値sc-content-lenレスポンスの HTTP Content-Length ヘッダーの値sc-range-start範囲の開始値sc-range-end範囲の終了値c-port閲覧者からのリクエストのポート番号x-edge-detailed-result-typex-edge-result-type と同じ値c-countryビューワーの IP アドレスによって決定される、ビューワーの地理的位置を表す国コードcs-accept-encodingビューワーリクエスト内の Accept-Encoding ヘッダーの値cs-acceptビューワーリクエスト内の Accept ヘッダーの値cache-behavior-path-patternビューワーリクエストに一致したキャッシュ動作を識別するパスパターンcs-headersビューワーリクエスト内の HTTP ヘッダーcs-header-namesビューワーリクエスト内の HTTP ヘッダーの名前cs-headers-countビューワーリクエスト内の HTTP ヘッダーの数

此外,您可以通过更改SamplingRate值,在1%到100%之间指定CloudFront向Kinesis数据流发送日志的采样率。

Parameters:
  SamplingRate:
    Type: Number
    Default: 100
    MinValue: 1
    MaxValue: 100
    Description: The sampling rate of logs sent by CloudFront [required]

Resources:
  CloudFrontRealtimeLogConfig:
    Type: 'AWS::CloudFront::RealtimeLogConfig'
    Properties: 
      EndPoints: 
        - KinesisStreamConfig: 
            RoleArn: !GetAtt IAMRoleForCloudFrontRealtimeLog.Arn
            StreamArn: !GetAtt Kinesis.Arn
          StreamType: Kinesis
      Fields: 
        - timestamp
        - c-ip
        - time-to-first-byte
        - sc-status
        - sc-bytes
        - cs-method
        - cs-protocol
        - cs-host
        - cs-uri-stem
        - cs-bytes
        - x-edge-location
        - x-edge-request-id
        - x-host-header
        - time-taken
        - cs-protocol-version
        - c-ip-version
        - cs-user-agent
        - cs-referer
        - cs-cookie
        - cs-uri-query
        - x-edge-response-result-type
        - x-forwarded-for
        - ssl-protocol
        - ssl-cipher
        - x-edge-result-type
        - fle-encrypted-fields
        - fle-status
        - sc-content-type
        - sc-content-len
        - sc-range-start
        - sc-range-end
        - c-port
        - x-edge-detailed-result-type
        - c-country
        - cs-accept-encoding
        - cs-accept
        - cache-behavior-path-pattern
        - cs-headers
        - cs-header-names
        - cs-headers-count
      Name: RealtimeLogConfig
      SamplingRate: !Ref SamplingRate

如果超过了已经配置的Kinesis流的容量并且生成了实时日志,超出容量的部分实时日志将会丢失。然而,这不会导致CloudFront分发的行为异常。

此外,CloudFront是AWS提供的全球服务,但是所有边缘位置的访问日志都会输出到接收实时日志的Kinesis流中。可以在x-edge-location字段中查看请求是在哪个边缘位置处理的。

当将上述的实时日志设置附加到以前创建的CloudFront分发上时,刚刚设置的实时日志输出将在CloudFront上生效。

  CloudFront:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          RealtimeLogConfigArn: !GetAtt CloudFrontRealtimeLogConfig.Arn

亚马逊弹性搜索服务 (Amazon Elasticsearch Service)

Elasticsearch是一种分布式分析引擎,可以进行各种类型的搜索,如结构化、非结构化、地理信息和指标,并对大规模数据进行分析。Amazon Elasticsearch Service是一个简单且可扩展地部署、保护和运行Elasticsearch的托管服务。

通过使用这个Elasticsearch服务,您可以保存和分析应用程序和基础设施的日志,以便快速发现问题,并为应用程序添加搜索功能。因此,我们将使用Elasticsearch来分析CloudFront的实时日志,并使用被称为Kibana的Elasticsearch可视化工具将其可视化。

Parameters:
  ElasticSearchVolumeSize:
    Type: Number
    Default: 10
    MinValue: 10
    Description: The volume size (GB) of ElasticSearch Service [required]
  ElasticSearchDomainName:
    Type: String
    Default: cloudfront-realtime-logs
    AllowedPattern: .+
    Description: The domain name of ElasticSearch Service [required]
  ElasticSearchInstanceType:
    Type: String
    Default: r5.large.elasticsearch
    AllowedValues:
      - t3.small.elasticsearch
      - t3.medium.elasticsearch
      - t2.micro.elasticsearch
      - t2.small.elasticsearch
      - t2.medium.elasticsearch
      - m5.large.elasticsearch
      - m5.xlarge.elasticsearch
      - m5.2xlarge.elasticsearch
      - m5.4xlarge.elasticsearch
      - m5.12xlarge.elasticsearch
      - m4.large.elasticsearch
      - m4.xlarge.elasticsearch
      - m4.2xlarge.elasticsearch
      - m4.4xlarge.elasticsearch
      - m4.10xlarge.elasticsearch
      - c5.large.elasticsearch
      - c5.xlarge.elasticsearch
      - c5.2xlarge.elasticsearch
      - c5.4xlarge.elasticsearch
      - c5.9xlarge.elasticsearch
      - c5.18xlarge.elasticsearch
      - c4.large.elasticsearch
      - c4.xlarge.elasticsearch
      - c4.2xlarge.elasticsearch
      - c4.4xlarge.elasticsearch
      - c4.8xlarge.elasticsearch
      - r5.large.elasticsearch
      - r5.xlarge.elasticsearch
      - r5.2xlarge.elasticsearch
      - r5.4xlarge.elasticsearch
      - r5.12xlarge.elasticsearch
      - r4.large.elasticsearch
      - r4.xlarge.elasticsearch
      - r4.2xlarge.elasticsearch
      - r4.4xlarge.elasticsearch
      - r4.8xlarge.elasticsearch
      - r4.16xlarge.elasticsearch
      - r3.large.elasticsearch
      - r3.xlarge.elasticsearch
      - r3.2xlarge.elasticsearch
      - r3.4xlarge.elasticsearch
      - r3.8xlarge.elasticsearch
      - i3.large.elasticsearch
      - i3.xlarge.elasticsearch
      - i3.2xlarge.elasticsearch
      - i3.4xlarge.elasticsearch
      - i3.8xlarge.elasticsearch
      - i3.16xlarge.elasticsearch
    Description: The instance type of ElasticSearch Service [required]
  ElasticSearchMasterUserName:
    Type: String
    AllowedPattern: .+
    Description: The user name of ElasticSearch Service [required]
  ElasticSearchMasterUserPassword:
    Type: String
    AllowedPattern: .+
    NoEcho: true
    Description: The password of ElasticSearch Service [required]

Resources:
  KMSKey:
    Type: AWS::KMS::Key
    Properties: 
      Description: Encrypt CloudTrail Logs
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy: 
        Version: 2012-10-17
        Id: DefaultKeyPolicy
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
            Action: 'kms:*'
            Resource: '*'
          - Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action:
              - 'kms:GenerateDataKey*'
            Resource:
              - '*'
            Condition:
              StringLike:
                kms:EncryptionContext:aws:cloudtrail:arn:
                  - !Sub arn:aws:cloudtrail:*:${AWS::AccountId}:trail/*
          - Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action:
              - 'kms:DescribeKey'
            Resource:
              - '*'
      KeyUsage: ENCRYPT_DECRYPT
      PendingWindowInDays: 30
  ElasticSearchDomain:
    Type: 'AWS::Elasticsearch::Domain'
    Properties: 
      AccessPolicies:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - '*'
            Action:
              - es:*
            Resource: !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/*
      AdvancedSecurityOptions: 
        Enabled: true
        InternalUserDatabaseEnabled: true
        MasterUserOptions:
          MasterUserName: !Ref ElasticSearchMasterUserName
          MasterUserPassword: !Ref ElasticSearchMasterUserPassword
      DomainEndpointOptions: 
        EnforceHTTPS: true
      DomainName: !Ref ElasticSearchDomainName
      EBSOptions: 
        EBSEnabled: true
        VolumeSize: !Ref ElasticSearchVolumeSize
        VolumeType: gp2
      ElasticsearchClusterConfig: 
        DedicatedMasterCount: 3
        DedicatedMasterEnabled: true
        DedicatedMasterType: c5.large.elasticsearch
        InstanceCount: 3
        InstanceType: !Ref ElasticSearchInstanceType
        ZoneAwarenessConfig: 
          AvailabilityZoneCount: 3
        ZoneAwarenessEnabled: true
      ElasticsearchVersion: 7.8
      EncryptionAtRestOptions:
        Enabled: true
        KmsKeyId: !GetAtt KMSKey.Arn
      NodeToNodeEncryptionOptions: 
        Enabled: true
      SnapshotOptions: 
        AutomatedSnapshotStartHour: 0

以下是本模板的结构和注意事项。

DomainName には、「アカウントおよびリージョンに固有」「先頭が小文字」「3~28文字」「小文字のアルファベット、数字、ハイフンのみ使用可能」という制約が課せられています。

EBSOptions にて、アタッチするEBSボリュームのタイプとサイズを指定しています。

ElasticsearchClusterConfig にて、本番稼働用として奨励されている、マルチAZ + 専用データノード構成を規定しており、 データノード3台 + マスターノード3台 の構成としています。 AvailabilityZoneCount を3に設定しているため、 インスタンスは3つのAZに分散配置 されます。

EncryptionAtRestOptions にて、 保管時のデータ暗号化 を指定しています。 これと同時に 暗号化の際に使用するAWS KMSのカスタマーマスターキー(CMK)も作成 しています。

NodeToNodeEncryptionOptions にて、 ノード間の暗号化 を指定しています。

AutomatedSnapshotStartHour にて、 UTC時刻の午前0時にスナップショットが作成 されます。

访问控制

这个模板已经启用了细粒度访问控制(FGAC),下面是相关设置。

image.png
    1. 允许公共访问。

 

    1. 通过DomainEndpointOptions,强制使用HTTPS进行访问。

 

    1. 通过InternalUserDatabaseEnabled启用内部用户数据库,并通过MasterUserOptions指定主用户的用户名和密码。

 

    通过AccessPolicies,允许执行Elasticsearch服务的所有操作。

通过上述设置,可以使用事先设置的用户名和密码,从外部访问该域和Kibana。

域名的尺寸配置

使用 Elasticsearch Service 时应注意的是选择哪种实例类型,以及需要准备多大的 EBS 卷。关于这一点,在官方文档的 Amazon ES 域大小调整部分有详细说明。

假设我们谈论存储空间,

ソースデータ x (1+ レプリカの数) x 1.45 = 最小ストレージ要件

如果要保存24小时的实时日志,而这个日志源产生的流量是每秒5,000个请求的CloudFront分配,可以使用以下方程式。

5,000(件) x 3,600(秒)x 24(時間) x 1,000(byte) = 432(GB)

原始数据为432GB,应用上述公式后,

432(GB) x (1 + 1) x 1.45 = 1252 (GB)

需要1252GB的存储空间。

另外,在上述示例中,源数据每小时以18GB的比例增加,这对Elasticsearch服务来说会产生巨大的负担。官方文档中指出,如果存在高负载的聚合处理、频繁的文档更新或大量的查询处理,这些资源可能无法满足需求。如果集群被归类到这种类别中,建议为每100 GiB的存储需求配置vCPU x 2核心和接近8 GiB内存的配置。将此建议应用于上述示例,

1252(GB)/ 100(GB)x 2 = 25(コア)
1252(GB)/ 100(GB)x 8 = 100(GBメモリ)

可以考虑需要的情况下。如上所述,由于此模板中准备了3个数据节点,所以每个实例所需的核数和内存大小是。

25(コア)/ 3 (台) = 8.3(コア)
100(GBメモリ)/ 3 (台) = 33.3(GBメモリ) 

相邻的每个实例所需的EBS卷为:

1252(GB) / 3(台) = 417(GB)

以下のようになります。これに適合する インスタンス タイプ は、

m5.2xlarge.elasticsearch 以上のインスタンスタイプ

c5.4xlarge.elasticsearch 以上のインスタンスタイプ

r5.2xlarge.elasticsearch 以上のインスタンスタイプ

i3.2xlarge.elasticsearch 以上のインスタンスタイプ

在经验上,数据节点经常会出现堆空间不足的情况。因此,在上述示例中,选择作为内存优化实例的R5实例可能是一个不错的选择。关于主节点,可以参考独立主节点部分的内容。

Instance Count推奨される最小専用マスターインスタンスタイプ1–10c5.large.elasticsearch

根据描述,这个模板构建的配置似乎适用于c5.large.elasticsearch。但值得注意的是,这些值仅仅是计算上的数值,所以在决定数据节点和主节点的实例类型时,需要先施加预期的负载来验证其行为。

创建域后,如果要更改这些设置,则会执行Blue/Green部署过程并创建新的环境。由于这种设置更改需要时间且对主节点造成较大负载,因此需要足够的资源来处理与此部署过程相关的额外负荷。如果在资源不足的情况下进行设置更改,可能会导致设置更改(进行中)需要数小时才能完成。

请监控以下CloudWatch指标以确认Elasticsearch Service设置的容量是否适用于负载。您可以从此链接中基于这些指标批量启用CloudWatch警报。

ネームスペースメトリクス閾値AWS/ESClusterStatus.green0だった場合AWS/ESClusterIndexWritesBlocked1分間に1回以上AWS/ESMasterReachableFromNode0だった場合AWS/ESAutomatedSnapshotFailure1分間に1回以上AWS/ESKibanaHealthyNodes0だった場合AWS/ESFreeStorageSpaceテンプレートで指定した値AWS/ESMasterCPUUtilization50%を超えた場合AWS/ESMasterJVMMemoryPressure80%を超えた場合AWS/ESCPUUtilization50%を超えた場合AWS/ESJVMMemoryPressure80%を超えた場合AWS/ESSysMemoryUtilization80%を超えた場合

努力数据传递

最后进行 Kinesis Data Firehose 的设置。Firehose 是一个可以接收流式数据并进行转换,并将其发送到 Amazon S3、Amazon Redshift、Amazon Elasticsearch Service、通用的 HTTP 终端等服务的工具。本次的目标是将从 Kinesis Stream 传输的实时日志发送到 Elasticsearch Service 中。

此外,在Kinesis流中,存储记录时需要进行Base64编码,因此实时日志也以Base64编码的状态进行存储。因此,我们可以使用Firehose的数据转换功能来进行目标列的Base64解码。Firehose的数据转换功能是通过将AWS Lambda与转换过程相关联来实现的。

这个Lambda函数所附加的IAM角色如下所示。给予Lambda函数的权限是将日志写入CloudWatch Logs的权限。

Resources:
  IAMRoleForLambda:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      Description: A role required for Lambda to execute.
      Policies:
        - PolicyName: !Sub '${AWS::StackName}-AWSLambdaCloudWatchLogsPolicy-${AWS::Region}'
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'logs:CreateLogStream'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'logs:PutLogEvents'
                Resource: '*' 
      RoleName: !Sub '${AWS::StackName}-Lambda-${AWS::Region}'

而且,执行数据转换处理的Lambda函数如下所示。

Resources:
  Lambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import logging
          import base64
          import json

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          logger.info("Loading function")

          def lambda_handler(event, context):
              output = []

              # Based on the fields chosen during the creation of the Real-time log configuration.
              # The order is important and please adjust the function if you have removed certain default fields from the configuration.
              realtimelog_fields_dict = {
                  "timestamp": "float",
                  "c-ip": "str",
                  "time-to-first-byte": "float",
                  "sc-status": "int",
                  "sc-bytes": "int",
                  "cs-method": "str",
                  "cs-protocol": "str",
                  "cs-host": "str",
                  "cs-uri-stem": "str",
                  "cs-bytes": "int",
                  "x-edge-location": "str",
                  "x-edge-request-id": "str",
                  "x-host-header": "str",
                  "time-taken": "float",
                  "cs-protocol-version": "str",
                  "c-ip-version": "str",
                  "cs-user-agent": "str",
                  "cs-referer": "str",
                  "cs-cookie": "str",
                  "cs-uri-query": "str",
                  "x-edge-response-result-type": "str",
                  "x-forwarded-for": "str",
                  "ssl-protocol": "str",
                  "ssl-cipher": "str",
                  "x-edge-result-type": "str",
                  "fle-encrypted-fields": "str",
                  "fle-status": "str",
                  "sc-content-type": "str",
                  "sc-content-len": "int",
                  "sc-range-start": "int",
                  "sc-range-end": "int",
                  "c-port": "int",
                  "x-edge-detailed-result-type": "str",
                  "c-country": "str",
                  "cs-accept-encoding": "str",
                  "cs-accept": "str",
                  "cache-behavior-path-pattern": "str",
                  "cs-headers": "str",
                  "cs-header-names": "str",
                  "cs-headers-count": "int",
              }

              for record in event["records"]:

                  # Extracting the record data in bytes and base64 decoding it
                  payload_in_bytes = base64.b64decode(record["data"])

                  # Converting the bytes payload to string
                  payload = "".join(map(chr, payload_in_bytes))

                  # dictionary where all the field and record value pairing will end up
                  payload_dict = {}

                  # counter to iterate over the record fields
                  counter = 0

                  # generate list from the tab-delimited log entry
                  payload_list = payload.strip().split("\t")

                  # perform the field, value pairing and any necessary type casting.
                  # possible types are: int, float and str (default)
                  for field, field_type in realtimelog_fields_dict.items():
                      # overwrite field_type if absent or '-'
                      if payload_list[counter].strip() == "-":
                          field_type = "str"
                      if field_type == "int":
                          payload_dict[field] = int(payload_list[counter].strip())
                      elif field_type == "float":
                          payload_dict[field] = float(payload_list[counter].strip())
                      else:
                          payload_dict[field] = payload_list[counter].strip()
                      counter = counter + 1

                  # JSON version of the dictionary type
                  payload_json = json.dumps(payload_dict)

                  # Preparing JSON payload to push back to Firehose
                  payload_json_ascii = payload_json.encode("ascii")
                  output_record = {
                      "recordId": record["recordId"],
                      "result": "Ok",
                      "data": base64.b64encode(payload_json_ascii).decode("utf-8"),
                  }
                  output.append(output_record)

              logger.info("Successfully processed {} records.".format(len(event["records"])))

              return {"records": output}
      Description: CloudFrontログを変換します
      FunctionName: realtimeLogsTransformer
      Handler: index.lambda_handler
      MemorySize: 512
      Role: !GetAtt IAMRoleForLambda.Arn
      Runtime: python3.8
      Timeout: 60
      TracingConfig:
        Mode: Active
  LambdaLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties: 
      LogGroupName: !Sub /aws/lambda/${Lambda}
      RetentionInDays: 60

接下来,如果从Firehose到Elasticsearch Service的传输失败,我们也会预先创建一个用于存储实时日志的S3存储桶作为替代方案。

Resources:
  S3ForKinesisFirehose:
    Type: 'AWS::S3::Bucket'
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration: 
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName: !Sub ${ElasticSearchDomainName}-${AWS::Region}-${AWS::AccountId}
      LifecycleConfiguration:
        Rules:
          - Id: ExpirationInDays
            ExpirationInDays: 60
            Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

此外,将在IAM角色中指定授予Firehose的权限。

  IAMRoleForKinesisFirehose:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: firehose.amazonaws.com
            Action: 'sts:AssumeRole'
      Description: A role required for KinesisFirehose to access Glue, S3, Lambda, CloudWatch Logs, Kinesis and KMS.
      Policies:
        - PolicyName: !Sub '${AWS::StackName}-FirehoseDelivery-${AWS::Region}'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 's3:AbortMultipartUpload'
                  - 's3:GetBucketLocation'
                  - 's3:GetObject'
                  - 's3:ListBucket'
                  - 's3:ListBucketMultipartUploads'
                  - 's3:PutObject'
                Resource:
                  - !Sub 'arn:aws:s3:::${S3ForKinesisFirehose}'
                  - !Sub 'arn:aws:s3:::${S3ForKinesisFirehose}/*'
              - Effect: Allow
                Action:
                  - 'kms:Decrypt'
                  - 'kms:GenerateDataKey'
                Resource:
                  - !GetAtt KMSKey.Arn
                Condition:
                  StringEquals:
                    'kms:ViaService': s3.region.amazonaws.com
                  StringLike:
                    'kms:EncryptionContext:aws:s3:arn': !Sub 'arn:aws:s3:::${S3ForKinesisFirehose}/*'
              - Effect: Allow
                Action:
                  - 'es:DescribeElasticsearchDomain'
                  - 'es:DescribeElasticsearchDomains'
                  - 'es:DescribeElasticsearchDomainConfig'
                  - 'es:ESHttpPost'
                  - 'es:ESHttpPut'
                Resource:
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/*'
              - Effect: Allow
                Action:
                  - 'es:ESHttpGet'
                Resource:
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/_all/_settings'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/_cluster/stats'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/realtime*/_mapping/*'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/_nodes'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/_nodes/stats'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/_nodes/*/stats'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/_stats'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSearchDomainName}/realtime*/_stats'
              - Effect: Allow
                Action:
                  - 'kinesis:DescribeStream'
                  - 'kinesis:GetShardIterator'
                  - 'kinesis:GetRecords'
                  - 'kinesis:ListShards'
                Resource: !GetAtt Kinesis.Arn
              - Effect: Allow
                Action:
                  - 'logs:PutLogEvents'
                Resource:
                  - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudWatchLogsGroupForFirehose}:log-stream:${CloudWatchLogsStreamForFirehoseElasticSearch}'
                  - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudWatchLogsGroupForFirehose}:log-stream:${CloudWatchLogsStreamForFirehoseS3}'
              - Effect: Allow
                Action:
                  - 'lambda:InvokeFunction'
                  - 'lambda:GetFunctionConfiguration'
                Resource:
                  - !GetAtt Lambda.Arn
              - Effect: Allow
                Action:
                  - 'kms:Decrypt'
                Resource:
                  - !GetAtt KMSKey.Arn
                Condition:
                  StringEquals:
                    'kms:ViaService': kinesis.ap-northeast-1.amazonaws.com
                  StringLike:
                    'kms:EncryptionContext:aws:kinesis:arn': !GetAtt Kinesis.Arn
      RoleName: !Sub '${AWS::StackName}-Firehose-${AWS::Region}'

最后,我们将创建一个Firehose,指定Elasticsearch Service和S3作为实时日志的传送目标,并提供Lambda函数来进行数据转换。为了尽可能减少向Elasticsearch Service传送数据时的延迟,我们将设置BufferingHints为可配置的最小值。此外,通过将S3BackupMode设置为FailedDocumentsOnly,仅在将日志传送到Elasticsearch Service失败时,将实时日志保存到S3中。AWS::KinesisFirehose::DeliveryStream的某些属性在更新时的行为为Replacement,也就是在更新时需要重新创建Firehose资源并生成新的物理ID。因此,如果要更新这些属性的值,请同时更新DeliveryStreamName的值。

Resources:
  KinesisFirehose:
    Type: 'AWS::KinesisFirehose::DeliveryStream'
    Properties:
      DeliveryStreamName: !Sub ${AWS::StackName}-${KinesisFirehoseStreamNameSuffix}
      DeliveryStreamType: KinesisStreamAsSource
      KinesisStreamSourceConfiguration:
        KinesisStreamARN: !GetAtt Kinesis.Arn
        RoleARN: !GetAtt IAMRoleForKinesisFirehose.Arn
      ElasticsearchDestinationConfiguration:
        BufferingHints:
          IntervalInSeconds: 60
          SizeInMBs: 1
        CloudWatchLoggingOptions: 
          Enabled: true
          LogGroupName: !Ref CloudWatchLogsGroupForFirehose
          LogStreamName: !Ref CloudWatchLogsStreamForFirehoseS3
        DomainARN: !GetAtt ElasticSearchDomain.DomainArn
        IndexName: realtime
        IndexRotationPeriod: NoRotation
        ProcessingConfiguration: 
          Enabled: true
          Processors: 
            - Parameters: 
                - ParameterName: LambdaArn
                  ParameterValue: !GetAtt Lambda.Arn
              Type: Lambda
        RetryOptions: 
          DurationInSeconds: 300
        RoleARN: !GetAtt IAMRoleForKinesisFirehose.Arn
        S3BackupMode: FailedDocumentsOnly
        S3Configuration: 
          BucketARN: !GetAtt S3ForKinesisFirehose.Arn
          CloudWatchLoggingOptions:
            Enabled: true
            LogGroupName: !Ref CloudWatchLogsGroupForFirehose
            LogStreamName: !Ref CloudWatchLogsStreamForFirehoseElasticSearch
          RoleARN: !GetAtt IAMRoleForKinesisFirehose.Arn
        TypeName: ''
  CloudWatchLogsGroupForFirehose:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub '/aws/kinesisfirehose/${AWS:StackName}'
      RetentionInDays: 60
  CloudWatchLogsStreamForFirehoseS3:
    Type: 'AWS::Logs::LogStream'
    Properties:
      LogGroupName: !Ref CloudWatchLogsGroupForFirehose
      LogStreamName: S3
  CloudWatchLogsStreamForFirehoseElasticSearch:
    Type: 'AWS::Logs::LogStream'
    Properties:
      LogGroupName: !Ref CloudWatchLogsGroupForFirehose
      LogStreamName: ElasticSearch

以上是完成了所有必需的资源设置,以可视化CloudFront的实时日志。通过执行此CloudFormation模板,每个资源将被部署。

另外,请监控以下CloudWatch指标以确认Firehose是否正常工作。您可以通过此链接,批量启用基于这些指标的CloudWatch警报。

ネームスペースメトリクス閾値AWS/FirehoseDeliveryToElasticsearch.DataFreshnessテンプレートで指定した値AWS/FirehoseThrottledGetShardIterator1分間に1回以上AWS/FirehoseThrottledGetRecords1分間に1回以上AWS/FirehoseDeliveryToElasticsearch.Success1より小さい場合

Kibana的配置

一旦上述资源的部署完成后,将为Elasticsearch中导入的数据指定索引,并进行使用Kibana进行可视化的设置。关于此步骤的详细信息,请参见此处,并与本文一起阅读。通过执行以下操作,Firehose将能够向Elasticsearch投放数据,并自动创建必要的图表和仪表板以进行可视化。

Security の Roles を選択します。

kibana_12.png

+ アイコンをクリックして新しいロールを追加します。
作成したロールに firehose という名前をつけます。

Kibana

Cluster Permissions タブの Cluster-wide permissions で cluster_composite_ops cluster_monitor グループを追加します。

Kibana

Index Permissions タブの Add index permissions から Index Patterns を選んで realtime* を入力します。Permissions: Action Groups で crud create_index manage アクショングループを追加します。

Kibana

Save Role Definition をクリックします。

Security の Role Mappings を選択します。

kibana_11.png

Add Backend Role をクリックします。
先ほど作成した firehoseを選択します。
Backend roles に Kinesis Data Firehose が Amazon ES および S3 に書き込むために使用する IAM ロールの ARN を入力します。

Kibana

Submit をクリックします。

Dev Tools を選択します。

timestamp フィールドを date タイプと認識させるために、以下のコマンドを入力して実行します。

PUT _template/custom_template
{
    "template": "realtime*",
    "mappings": {
        "properties": {
            "timestamp": {
                "type": "date",
                "format": "epoch_second"
            }
        }
    }
}
Kibana

インデックス および visualizes と dashboard の設定ファイル をインポートします。

我們成功在AWS上搭建了一個使用Kibana來實時查看CloudFront日誌的環境。

实时仪表盘

当您完成上述设置后,您可以登录Kibana并实时查看下列数据的图表等信息。

仪表板

可以通过一屏幕查看所有已创建的图表。

Screenshot_2021-01-23 Default - Elastic.png

展现形象

每个图形如下:

每秒请求数

Screenshot_2021-01-23 Requests by timestamp - Elastic.png

国家

Screenshot_2021-01-23 Requests by country - Elastic.png

响应时间

Screenshot_2021-01-23 Response time - Elastic.png

内容类型

Screenshot_2021-01-23 Result type - Elastic.png

响应码

Screenshot_2021-01-23 Response code - Elastic.png

结果类型

Screenshot_2021-01-23 Result type - Elastic.png

通过提取实时日志的任意字段,我们可以可视化各种数据,并实时确认其变化。这将使得网站的运营监控体系更加灵活和细致,我认为可以建立起比以往更高效的运维监控体系。

相关链接

    1. 使用CloudFormation一键搭建分发基础设施-通过CloudFormation轻松实现网站托管

 

    1. 使用CloudFormation将WAF与CloudFront进行关联-通过CloudFormation轻松实现网站托管

 

    1. 使用CloudFormation定期监测特定URL-通过CloudFormation轻松实现网站托管

使用CloudFormation将CloudFront的实时日志在Kibana中进行可视化-通过CloudFormation轻松实现网站托管

bannerAds