こんにちは。 エキサイト株式会社で内定者アルバイトをしています。
今回も、実際のリソースの作成を行いたいと思います。第8回は、CloudWatchです。
前の記事
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(); } }
詳細は以下の記事をご覧ください。
ダッシュボードを作成してウィジットを追加するコード
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();
いくつか解説しておくべき設定項目があるので、以下それらについて説明していきます。
まず、GraphWidget
のwidth
についてCloudWatchのダッシュボードの横幅は24となっています。
ですので、GraphWidget
に限らずウィジットを作成する際にはwidth
は1以上24の整数を指定します。25以上の値を指定するとエラーになります。
left
には左側のy軸に表示するメトリックを指定します。同様にright
には右側のy軸に表示するメトリックを指定します。
メトリックの指定についても説明しておきます。メトリックを表現するクラスはMetric
であり、以下のようなものを設定します。
namespace
- そのメトリックの名前空間を指定します。EC2で定義されたメトリックなら
AWS/EC2
、RDSで定義されたメトリックならAWS/RDS
です。 - 詳細は以下を参照してください
- そのメトリックの名前空間を指定します。EC2で定義されたメトリックなら
metricName
- メトリックの名前を指定します。
- ここでは、CPU使用率を示す
CPUUtilization
を指定しています。 - 使用できるメトリックについては以下を参照してください
statistic
- 統計量を指定します。例えば、最大を表す
max
や平均を表すavg
、合計を表すsum
などが使えます。
- 統計量を指定します。例えば、最大を表す
dimensionsMap
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を、comparisonOperator
とthreshold
がDを表しています。当然、metric
はCです。
では、Aの期間はどのように決定されるかというと、メトリックで定義したperiod
とevaluationPeriods
の積です。この例では、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での管理面はともかく、作成についてはあまり向いていないのかもしれません。
では、また次回。
次の記事