AWS CDKでリソース作成 第8回: CloudWatch編

こんにちは。 エキサイト株式会社で内定者アルバイトをしています。

今回も、実際のリソースの作成を行いたいと思います。第8回は、CloudWatchです。

前の記事

tech.excite.co.jp

CloudWatchとは?

CloudWatchはアプリケーションをモニタリングし、システム全体におけるパフォーマンスの変化に対応して、リソース使用率の最適化を行うためのデータと実用的な情報を提供してくれます。

メトリクスの収集や可視化が主な機能ですが、アラームを設定してアクション(EC2をオートスケーリングしたり、インスタンスを停止したり)を自動で開始する設定を行うこともできるようです。

実際に作成してみる

今回は、ダッシュボードと呼ばれるメトリクスとログをモニタリングできるホームページと、アラームおよびそれに対するアクションを作成します。

ダッシュボードには以下の設定のようなグラフウィジットを配置します。

ダッシュボードに配置するグラフウィジット

項目 設定
タイトル CPU Utilization Average
対象リソース EC2インスタンス
メトリック名 CPU使用率
統計量 平均と最大値
平均の方は12、最大値の方は8

また、アラームとしてEC2インスタンスのCPU使用率が95%を超えると設定したメールアドレスにメールが届くようなものを作成します。

前回までのコード

前回はOpenSearchドメインを作成しました。しかし、今回はそれらのリソースは参照しませんので、EC2インスタンスを作成する部分までを再掲します。

package com.myorg;

import software.amazon.awscdk.CfnTag;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.services.ec2.CfnInternetGateway;
import software.amazon.awscdk.services.ec2.CfnVPCGatewayAttachment;
import software.amazon.awscdk.services.ec2.IMachineImage;
import software.amazon.awscdk.services.ec2.Instance;
import software.amazon.awscdk.services.ec2.InstanceClass;
import software.amazon.awscdk.services.ec2.InstanceSize;
import software.amazon.awscdk.services.ec2.InstanceType;
import software.amazon.awscdk.services.ec2.MachineImage;
import software.amazon.awscdk.services.ec2.Peer;
import software.amazon.awscdk.services.ec2.Port;
import software.amazon.awscdk.services.ec2.PrivateSubnet;
import software.amazon.awscdk.services.ec2.PublicSubnet;
import software.amazon.awscdk.services.ec2.SecurityGroup;
import software.amazon.awscdk.services.ec2.SubnetSelection;
import software.amazon.awscdk.services.ec2.Vpc;
import software.constructs.Construct;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;

import java.util.List;

public class MyProjectStack extends Stack {
    public MyProjectStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public MyProjectStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);

        Vpc vpc = Vpc.Builder.create(this, "SampleVPC")
                .vpcName("sample-vpc")
                .cidr("cidr")
                .subnetConfiguration(List.of())
                .build();

        // Availability zone: ap-northeast-1a
        PrivateSubnet privateSubnetA = PrivateSubnet.Builder.create(this, "PrivateSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetA = PublicSubnet.Builder.create(this, "PublicSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // Availability zone: ap-northeast-1c
        PrivateSubnet privateSubnetB = PrivateSubnet.Builder.create(this, "PrivateSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetB = PublicSubnet.Builder.create(this, "PublicSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // NAT gatewayの追加
        publicSubnetA.addNatGateway();
        publicSubnetB.addNatGateway();

        // Internet Gateway
        CfnInternetGateway internetGateway = CfnInternetGateway.Builder.create(this, "InternetGateway")
                .tags(List.of(CfnTag.builder()
                        .key("Name")
                        .value("sample-internet-gateway")
                        .build()))
                .build();

        // Internet gatewayをVPCにattachする
        CfnVPCGatewayAttachment.Builder.create(this, "VpcGateAwayAttachment")
                .vpcId(vpc.getVpcId())
                .internetGatewayId(internetGateway.getAttrInternetGatewayId())
                .build();

        SecurityGroup securityGroup = SecurityGroup.Builder.create(this, "SampleSecurityGroup")
                .securityGroupName("sample-security-group")
                .vpc(vpc)
                .build();

        IMachineImage machineImage = MachineImage.latestAmazonLinux();

        Instance instance01 = Instance.Builder.create(this, "ec2-instance-01")
                .instanceName("ec2-instance-01")
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetA)).build())
                .machineImage(machineImage)
                .vpc(vpc)
                .availabilityZone("ap-northeast-1a")
                .instanceType(InstanceType.of(InstanceClass.T2, InstanceSize.SMALL))
                .securityGroup(securityGroup)
                .build();

        Instance instance02 = Instance.Builder.create(this, "ec2-instance-02")
                .instanceName("ec2-instance-02")
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetB)).build())
                .machineImage(machineImage)
                .vpc(vpc)
                .availabilityZone("ap-northeast-1c")
                .instanceType(InstanceType.of(InstanceClass.T2, InstanceSize.SMALL))
                .securityGroup(securityGroup)
                .build();

        // Load Balancer
        // Load Balancer用のSecurity Group
        SecurityGroup securityGroupEC2LB = SecurityGroup.Builder.create(this, "SecurityGroupEC2LoadBalancer")
                .securityGroupName("ec2-lb-security-group")
                .vpc(vpc)
                .description("security-group-of-ec2-load-balancer")
                .allowAllOutbound(true)
                .build();

        securityGroupEC2LB.addIngressRule(Peer.ipv4("0.0.0.0/0"), Port.tcp(80));

        ApplicationLoadBalancer applicationLoadBalancer = ApplicationLoadBalancer.Builder.create(this, "SampleEC2ApplicationLoadBalancer")
                .loadBalancerName("sample-ec2-load-balancer")
                .deletionProtection(false)
                .vpc(vpc)
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetA, privateSubnetB)).build())
                .securityGroup(securityGroupEC2LB)
                .ipAddressType(IpAddressType.IPV4)
                .idleTimeout(Duration.seconds(120))
                .http2Enabled(true)
                .build();

        ApplicationTargetGroup ec2TargetGroup = ApplicationTargetGroup.Builder.create(this, "EC2TargetGroup")
                .targetGroupName("ec2-target-group")
                .targetType(TargetType.INSTANCE)
                .port(80)
                .vpc(vpc)
                .targets(List.of(new InstanceTarget(instance01, 80), new InstanceTarget(instance02, 80)))
                .protocol(ApplicationProtocol.HTTP)
                .protocolVersion(ApplicationProtocolVersion.HTTP1)
                .healthCheck(HealthCheck.builder()
                        .enabled(true)
                        .path("/")
                        .healthyThresholdCount(3)
                        .unhealthyThresholdCount(2)
                        .interval(Duration.seconds(60))
                        .timeout(Duration.seconds(5))
                        .protocol(Protocol.HTTP)
                        .port("80")
                        .build())
                .build();

        ApplicationListener applicationListener = ApplicationListener.Builder.create(this, "ApplicationListener")
                .loadBalancer(applicationLoadBalancer)
                .protocol(ApplicationProtocol.HTTP)
                .port(80)
                .defaultAction(ListenerAction.forward(List.of(ec2TargetGroup)))
                .build();
    }
}

詳細は以下の記事をご覧ください。

tech.excite.co.jp

ダッシュボードを作成してウィジットを追加するコード

        GraphWidget widget1 = GraphWidget.Builder.create()
                .title("CPU Utilization Average")
                .width(12)
                .region("ap-northeast-1")
                .left(List.of(Metric.Builder.create()
                                // 統計量
                                .statistic("avg")
                                // 名前空間
                                .namespace("AWS/EC2")
                                .metricName("CPUUtilization")
                                // ここに対象となるリソースなどを記述する
                                .dimensionsMap(Map.of("InstanceId", "ec2-instance-01"))
                                .label("sample-instance-01")
                                .build(),
                        Metric.Builder.create()
                                .statistic("avg")
                                .namespace("AWS/EC2")
                                .metricName("CPUUtilization")
                                .dimensionsMap(Map.of("InstanceId", "ec2-instance-02"))
                                .label("sample-instance-02")
                                .build()))
                .period(Duration.seconds(300))
                .view(GraphWidgetView.TIME_SERIES)
                .stacked(false)
                .legendPosition(LegendPosition.BOTTOM)
                .build();

        GraphWidget widget2 = GraphWidget.Builder.create()
                .title("CPU Utilization Average")
                .width(8)
                .region("ap-northeast-1")
                .right(List.of(Metric.Builder.create()
                                .statistic("max")
                                .namespace("AWS/EC2")
                                .metricName("CPUUtilization")
                                .dimensionsMap(Map.of("InstanceId", Objects.requireNonNull(instance01.getInstanceId())))
                                .label("sample-instance-01")
                                .build(),
                        Metric.Builder.create()
                                .statistic("max")
                                .namespace("AWS/EC2")
                                .metricName("CPUUtilization")
                                .dimensionsMap(Map.of("InstanceId", Objects.requireNonNull(instance02.getInstanceId())))
                                .label("sample-instance-02")
                                .build()))
                .period(Duration.seconds(300))
                .view(GraphWidgetView.TIME_SERIES)
                .stacked(false)
                .legendPosition(LegendPosition.BOTTOM)
                .build();

        Dashboard ec2Dashboard = Dashboard.Builder.create(this, "EC2Dashboard")
                .dashboardName("sample-ec2-dashboard")
                .widgets(List.of(List.of(new Column(widget1), new Column(widget2))))
                .build();

いくつか解説しておくべき設定項目があるので、以下それらについて説明していきます。

まず、GraphWidgetwidthについてCloudWatchのダッシュボードの横幅は24となっています。
ですので、GraphWidgetに限らずウィジットを作成する際にはwidthは1以上24の整数を指定します。25以上の値を指定するとエラーになります。

leftには左側のy軸に表示するメトリックを指定します。同様にrightには右側のy軸に表示するメトリックを指定します。

メトリックの指定についても説明しておきます。メトリックを表現するクラスはMetricであり、以下のようなものを設定します。

  • namespace

  • metricName

  • statistic

    • 統計量を指定します。例えば、最大を表すmaxや平均を表すavg、合計を表すsumなどが使えます。
  • dimensionsMap

    • ディメンションは、メトリクスのアイデンティティの一部である名前と値のペアだそうです。いろいろ指定できるようですが、ここではコード例のようにInstanceIdインスタンスのIDを指定することで、インスタンスの統計を取得できることがわかっていれば十分だと思います。
  • label

    • 凡例などで使用されるラベルを指定します。
  • period

    • この統計量を取得する範囲を指定します。例えば、period(Duration.seconds(300))のように設定すると、300秒(5分)の間の統計量(平均値、最大値など)を表示するようになります。
    • なお、コード例ではここで指定するのではなく、ウィジット全体の設定として全てのメトリクスに適用されるようにGraphWidgetのプロパティとして指定しています。

詳しくはAmazon CloudWatch の概念 - Amazon CloudWatchが参考になると思います。

最後に、ダッシュボード本体を作成し、ウィジットをダッシュボードに配置しています。
widgetsに指定したListの各要素が行を表します。各行内にさらにColumnで列を作成しその中にウィジットを配置していきます。
上記のコード例では、一行に2つのウィジットを左詰めで配置しています。
なお、マネジメントコンソール上で作成する場合と異なり、完全に自由にウィジットを配置することはできないようです(実は、ウィジットにはpositionというメソッドがあり、x座標y座標を指定できるようになっていますが、これを指定しても特に何も変化しません...)。

ウィジットを作成するのは結構面倒であったり、自由にうまく配置できなかったりもするため、CloudWatchのダッシュボードだけはCDKで管理するのではなく、マネジメントコンソール上で作成するのも選択肢に入るかもしれません。

アラームを作成するコード

        // ① アラームの対象となるメトリックを定義
        Metric ec2AvgCpuUtilizationMetric = Metric.Builder.create()
                .statistic("avg")
                .namespace("AWS/EC2")
                .metricName("CPUUtilization")
                .dimensionsMap(Map.of("InstanceId", Objects.requireNonNull(instance01.getInstanceId())))
                .label("ec2-instance-01")
                .period(Duration.minutes(5))
                .build();

        // ② アラームを定義
        Alarm alarm = Alarm.Builder.create(this, "SampleAlarm")
                .alarmName("ec2-cpu-utilization-alarm")
                .metric(ec2AvgCpuUtilizationMetric)
                .comparisonOperator(ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD)
                .threshold(95)
                .evaluationPeriods(2)
                .datapointsToAlarm(2)
                .build();

        // ③ アクションで使用するトピックを定義
        Topic topic = Topic.Builder.create(this, "SampleTopic")
                .topicName("sample-topic")
                .displayName("sample-topic")
                .fifo(false)
                .build();

        EmailSubscription emailSubscription = EmailSubscription.Builder.create("youremail@example.com")
                .build();

        topic.addSubscription(emailSubscription);

        // ④ アラーム時のアクションをトピックから生成
        SnsAction snsAction = new SnsAction(topic);

        alarm.addAlarmAction(snsAction);

①では、アラームの対象となるメトリックを定義しています。
メトリックは先ほどのコードと同様に、EC2インスタンスのCPU使用率ですが、ここではperiodで5分を指定しています。すなわち、5分間の平均が対象となります。

②では、実際にアラームを作成しています。アラームは「A内のBデータポイントのCがDである」の条件が満たされるとアラーム状態になります。Aは期間、Bはデータポイントの数、Cはメトリック、Dは閾値を表しています。
ここで指定しているdatapointsToAlarmがBを、comparisonOperatorthresholdがDを表しています。当然、metricはCです。
では、Aの期間はどのように決定されるかというと、メトリックで定義したperiodevaluationPeriodsの積です。この例では、5分間の平均を2ピリオドだけ評価するので、10分間となります。
以上の結果をまとめますと、10分の間には(5分間の平均のメトリックなので)2ピリオドが存在し、その中の2ピリオド(すなわち全部のピリオド)でCPU使用率が95%以上であれば、アラーム状態になるということになります。

③では、トピックを生成しています。このトピックはAWS SNSの要素であり、通信チャネルとして機能する論理アクセスポイントとされています。複数のエンドポイント (AWS Lambda、Amazon SQS、HTTP/S、E メールアドレスなど) をグループにまとめることができるそうです。
ここで、作成しているトピックは非常に単純で、指定したメールアドレスへのEメールで通知を受け取るためのトピックとなっています。

④ではトピックからアクションを作成しています。アクションはアラーム状態になった時に自動的に引き起こされます。ここでは、アラーム状態になるとメールで通知がきます。

デプロイする

ここまで記述できたら、あとはデプロイするだけです。

cdk deploy

エラーにならずにデプロイできればOKです!

なお、上の画像において、保留中の確認とあるのは、コードで指定したメールアドレスについて、確認作業が済んでいないことを表しています。
実際に運用する際にはサブスクリプション確認の E メールを探して確認作業を行う必要があります。

終わりに

今回は、AWS CDKを用いてCloudWatchのダッシュボードとアラームを作成しました。

ダッシュボードについては、現時点ではAWS CDKでの管理面はともかく、作成についてはあまり向いていないのかもしれません。

では、また次回。

次の記事

tech.excite.co.jp

参考文献