AWS CDKでWindows Updateへの通信のみ許可するNetwork Firewallを設定してみた

Network Firewallでプロキシサーバ on EC2を無くしていきたい。 ついでにCDKを使って簡単に環境構築をしたい。
2021.04.30

はじめに

こんにちは、のん です。

皆さんはプロキシサーバの運用をしたことありますか? 私はあります。 私はプロキシサーバの運用をする時に以下のようなところに苦しめられました。

  • プロキシサーバの可用性を担保するのが大変
  • ドメインフィルタリング機能のルールの設定が大変

今までプロキシサーバを構築する際は、EC2インスタンス内にミドルウェアをインストールされていたパターンが多いかと思いますが、 昨年、ドメインフィルタリングができるAWS Network Firewallという、AWSのマネージドサービスがGAになりました。

また、「NAT Gatewayは使いたいけど、プライベートサブネットにあるEC2インスタンスから必要以上にインターネットへの通信は制限したい」といった要望は受けたことはありませんか? 私はあります。

この記事では、そういったプロキシサーバ運用のつらみと、NAT Gatewayのつらみを、Network Firewallを使って解決していきたいと思います。

今回の検証の想定としては、Windows Updateは実施したいけど、Windows Update以外のインターネットに抜ける通信はさせたくないといった用件で検証をしていきます。

いきなりまとめ

  • SSM Patch Managerだろうと、SSM RunCommandだろうと、Windows Updateをする際にはインターネットへの経路が必要
  • NAT Gatewayと組み合わせれば、プライベートサブネットのEC2インスタンスもドメインフィルタリングすることができる
  • Reachability Analyzer上から経路の到達確認をすると、EC2インスタンスからインターネットへの経路は到達不能になる。
  • Network Firewallは結構お金がかかる

今回の検証の構成

今回の検証の構成は以下図の通りです。 2つのAZにNetwork Firewall用のFirewallサブネット、NAT GatewayがいるPublicサブネット、EC2インスタンスがいるPrivateサブネットがあります。

その他、構成図には記載していませんが以下のようにVPCエンドポイントも作成しています。

  • EC2インスタンスでSSMのセッションマネージャーをするために、以下VPCエンドポイントを作成している
    • ssm
    • ssm-messages
    • ec2
    • ec2-messages
    • s3
  • EC2インスタンスのイベントログやメトリクスを取得するために以下VPCエンドポイントを作成している
    • monitoring
    • logs

Network Firewall自体の細かい解説は以下のブログが参考になるかと思います。

やってみた

CDK周りのコードの確認

私は手動で真心を込めて構築するタイプではないので、AWS CDKでリソースをデプロイしていきたいと思います。 CDKを実行して、作成されるリソースは図の赤枠の箇所です。

CDKでリソースを作成するにあたって、こちらのGitHubが大変参考になりました。 CDKのメインのコードは以下の通りです。

./lib/app-stack.ts

import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as logs from "@aws-cdk/aws-logs";
import * as iam from "@aws-cdk/aws-iam";
import * as networkfirewall from "@aws-cdk/aws-networkfirewall";
import * as ssm from "@aws-cdk/aws-ssm";
import * as fs from "fs";

export class AppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Create CloudWatch Logs for VPC Flow Logs
    const flowLogsLogGroup = new logs.LogGroup(this, "FlowLogsLogGroup", {
      retention: logs.RetentionDays.ONE_WEEK,
    });

    // Create CloudWatch Logs for Network Firewall Logs
    const networkFirewallFlowLogsLogGroup = new logs.LogGroup(
      this,
      "NetworkFirewallFlowLogsLogGroup",
      {
        retention: logs.RetentionDays.ONE_WEEK,
      }
    );

    // Create VPC Flow Logs IAM role
    const flowLogsIamrole = new iam.Role(this, "FlowLogsIamrole", {
      assumedBy: new iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"),
    });

    // Create SSM IAM role
    const ssmIamRole = new iam.Role(this, "SsmIamRole", {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
        iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMPatchAssociation"),
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "CloudWatchAgentAdminPolicy"
        ),
      ],
    });

    // Create VPC Flow Logs IAM Policy
    const flowLogsIamPolicy = new iam.Policy(this, "FlowLogsIamPolicy", {
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ["iam:PassRole"],
          resources: [flowLogsIamrole.roleArn],
        }),
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: [
            "logs:CreateLogStream",
            "logs:PutLogEvents",
            "logs:DescribeLogStreams",
          ],
          resources: [flowLogsLogGroup.logGroupArn],
        }),
      ],
    });

    // Atach VPC Flow Logs IAM Policy
    flowLogsIamrole.attachInlinePolicy(flowLogsIamPolicy);

    // Create VPC
    const vpc = new ec2.Vpc(this, "Vpc", {
      cidr: "10.0.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      natGateways: 2,
      maxAzs: 2,
      subnetConfiguration: [
        { name: "Firewall", subnetType: ec2.SubnetType.PRIVATE, cidrMask: 28 },
        { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
        { name: "Private", subnetType: ec2.SubnetType.PRIVATE, cidrMask: 24 },
      ],
    });

    // Setting VPC Flow Logs
    new ec2.CfnFlowLog(this, "FlowLogToLogs", {
      resourceId: vpc.vpcId,
      resourceType: "VPC",
      trafficType: "ALL",
      deliverLogsPermissionArn: flowLogsIamrole.roleArn,
      logDestination: flowLogsLogGroup.logGroupArn,
      logDestinationType: "cloud-watch-logs",
      logFormat:
        "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}",
      maxAggregationInterval: 60,
    });

    // Create SSM Privatelink
    new ec2.InterfaceVpcEndpoint(this, "SsmVpcEndpoint", {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.SSM,
      subnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
    });

    // Create SSM MESSAGES Privatelink
    new ec2.InterfaceVpcEndpoint(this, "SsmMessagesVpcEndpoint", {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
      subnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
    });

    // Create EC2 MESSAGES Privatelink
    new ec2.InterfaceVpcEndpoint(this, "Ec2VpcEndpoint", {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.EC2,
      subnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
    });

    // Create EC2 MESSAGES Privatelink
    new ec2.InterfaceVpcEndpoint(this, "Ec2MessagesVpcEndpoint", {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES,
      subnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
    });

    // Create CloudWatch Privatelink
    new ec2.InterfaceVpcEndpoint(this, "CloudwatchVpcEndpoint", {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH,
      subnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
    });

    // Create CloudWatch Privatelink
    new ec2.InterfaceVpcEndpoint(this, "CloudwatchLogsVpcEndpoint", {
      vpc: vpc,
      service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
      subnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
    });

    // // Create S3 Gateway
    new ec2.GatewayVpcEndpoint(this, "S3GatewayVpcEndpoint", {
      vpc: vpc,
      service: ec2.GatewayVpcEndpointAwsService.S3,
    });

    // Get Network Firewall Subnet ID
    const firewallSubnetId = new Array();
    vpc
      .selectSubnets({ subnetGroupName: "Firewall" })
      .subnets.forEach((subnet) => {
        firewallSubnetId.push({ subnetId: subnet.subnetId });
      });

    // Create EC2 instance
    vpc
      .selectSubnets({ subnetGroupName: "Private" })
      .subnets.forEach((subnet, index) => {
        new ec2.Instance(this, `Ec2Instance${index}`, {
          machineImage: ec2.MachineImage.latestWindows(
            ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_BASE
          ),
          instanceType: new ec2.InstanceType("t3.micro"),
          vpc: vpc,
          keyName: this.node.tryGetContext("key-pair"),
          role: ssmIamRole,
          vpcSubnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
        });
      });

    // Create Network Firewall rule group
    const networkfirewallRuleGroup = new networkfirewall.CfnRuleGroup(
      this,
      "NetworkFirewallRuleGroup",
      {
        capacity: 100,
        ruleGroupName: "WindowsUpdateRuleGroup",
        type: "STATEFUL",
        ruleGroup: {
          rulesSource: {
            rulesSourceList: {
              generatedRulesType: "ALLOWLIST",
              targetTypes: ["TLS_SNI", "HTTP_HOST"],
              targets: [
                ".update.microsoft.com",
                ".download.microsoft.com",
                ".windowsupdate.com",
                ".windowsupdate.microsoft.com",
                ".mp.microsoft.com",
                "wustat.windows.com",
                "ntservicepack.microsoft.com",
                "login.live.com",
              ],
            },
          },
        },
      }
    );

    // Create Network Firewall policy
    const networkfirewallPolicy = new networkfirewall.CfnFirewallPolicy(
      this,
      "NetworkFirewallPolicy",
      {
        firewallPolicyName: "WindowsUpdatePolicy",
        firewallPolicy: {
          statelessDefaultActions: ["aws:forward_to_sfe"],
          statelessFragmentDefaultActions: ["aws:forward_to_sfe"],
          statefulRuleGroupReferences: [
            {
              resourceArn: networkfirewallRuleGroup.attrRuleGroupArn,
            },
          ],
        },
      }
    );

    // Create Network Firewall
    const networkFirewall = new networkfirewall.CfnFirewall(
      this,
      "NetworkFirewall",
      {
        firewallName: "NetworkFirewall",
        firewallPolicyArn: networkfirewallPolicy.attrFirewallPolicyArn,
        vpcId: vpc.vpcId,
        subnetMappings: firewallSubnetId,
      }
    );

    // // Setting Network Firewall logs
    new networkfirewall.CfnLoggingConfiguration(
      this,
      "NetworkFirewallFlowLogsToLogs",
      {
        firewallArn: networkFirewall.ref,
        loggingConfiguration: {
          logDestinationConfigs: [
            {
              logDestination: {
                logGroup: networkFirewallFlowLogsLogGroup.logGroupName,
              },
              logDestinationType: "CloudWatchLogs",
              logType: "FLOW",
            },
          ],
        },
      }
    );

    // Routing NAT Gateway to Network Firewall
    vpc
      .selectSubnets({ subnetGroupName: "Public" })
      .subnets.forEach((subnet, index) => {
        const route = subnet.node.children.find(
          (child) => child.node.id == "DefaultRoute"
        ) as ec2.CfnRoute;
        route.addDeletionOverride("Properties.GatewayId");
        route.addOverride(
          "Properties.VpcEndpointId",
          cdk.Fn.select(
            1,
            cdk.Fn.split(
              ":",
              cdk.Fn.select(index, networkFirewall.attrEndpointIds)
            )
          )
        );
      });

    // Routing Network Firewall to Internet Gateway
    vpc
      .selectSubnets({ subnetGroupName: "Firewall" })
      .subnets.forEach((subnet, index) => {
        const route = subnet.node.children.find(
          (child) => child.node.id == "DefaultRoute"
        ) as ec2.CfnRoute;
        route.addDeletionOverride("Properties.NatGatewayId");
        route.addOverride("Properties.GatewayId", vpc.internetGatewayId);
      });

    // Internet Gateway RouteTable
    const igwRouteTable = new ec2.CfnRouteTable(this, "IgwRouteTable", {
      vpcId: vpc.vpcId,
    });

    // Routing Internet Gateway to Network Firewall
    vpc
      .selectSubnets({ subnetGroupName: "Public" })
      .subnets.forEach((subnet, index) => {
        const igwRouteToFirewall = new ec2.CfnRoute(
          this,
          `IgwRouteTableToFirewall${index}`,
          {
            routeTableId: igwRouteTable.ref,
            destinationCidrBlock: subnet.ipv4CidrBlock,
            vpcEndpointId: cdk.Fn.select(
              1,
              cdk.Fn.split(
                ":",
                cdk.Fn.select(index, networkFirewall.attrEndpointIds)
              )
            ),
          }
        );
      });

    // Association Internet Gateway RouteTable
    new ec2.CfnGatewayRouteTableAssociation(this, "IgwRouteTableAssociation", {
      gatewayId: <string>vpc.internetGatewayId,
      routeTableId: igwRouteTable.ref,
    });

    // CloudWatch parameters for Windows OS
    const stringValue = fs.readFileSync(
      "./AmazonCloudWatch-windows.json",
      "utf8"
    );

    // Create a new SSM Parameter
    new ssm.StringParameter(this, "StringParameter", {
      description: "CloudWatch parameters for Windows OS",
      parameterName: "AmazonCloudWatch-windows",
      stringValue: stringValue,
    });
  }
}

CDKのポイント

VPC Flow Logs

VPC Flow LogsのコードってL2で書けるよね?と思った方。その通りです。

L2で書けば。以下のように簡潔に書くことができます。

    new ec2.FlowLog(this, "FlowLogToLogs", {
      resourceType: ec2.FlowLogResourceType.fromVpc(vpc),
      destination: ec2.FlowLogDestination.toCloudWatchLogs(flowLogsLogGroup),
    });

今回L2ではなく、以下のようにL1で記載した理由はログのフォーマットや、ログの収集間隔をカスタマイズするためです。

./lib/app-stack.ts

    // Setting VPC Flow Logs
    new ec2.CfnFlowLog(this, "FlowLogToLogs", {
      resourceId: vpc.vpcId,
      resourceType: "VPC",
      trafficType: "ALL",
      deliverLogsPermissionArn: flowLogsIamrole.roleArn,
      logDestination: flowLogsLogGroup.logGroupArn,
      logDestinationType: "cloud-watch-logs",
      logFormat:
        "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}",
      maxAggregationInterval: 60,
    });

AWS公式のCDKのリファレンスでL2でVPC Flow Logsを設定する箇所を確認すると、ログのフォーマットや、ログの収集間隔に関するプロパティが見つかりません。

デフォルトではログの収集間隔は10分間であるため、ログが出力されるのに10分待つ必要があり、ログのレベルも物足りない状態になります。 特に私はとてもせっかちなので、10分はとても待てないと思い、L1で記載しました。

L1で記載する際のコツとしては、CloudFormationのリファレンスを隣に置いておくことだと思います。

例えば、VPC Flow LogsをL1で記述する際は、CfnFlowLogというクラスを使います。コーディングするためにリファレンスを開くと、CfnFlowLogクラス内のプロパティは以下のように型名CloudFormationで言う所のこのプロパティだよ!というところしか記載されていません。

そのため、Stringで書けば良いのは分かったけど、具体的にどんな値を書けば良いのか分からん状態になります。

でも安心してください。CloudFormationで言う所のこのプロパティだよ!のという通り、CloudFormationにヒントが書かれています。
実際に、CloudFormationのリファレンスを開くと、プロパティの詳細な説明が記載されています。

今回の場合、ResourceIdには、VPC Flow logsを取得する元の、サブネットもしくは、ENI、VPCのIDを記述すれば良いことが分かります。

このようにL1で書く必要があるタイミングがある際は、リファレンスを上手く活用すると、詰まらずに設定できると思います。

Network Firewallのルール

Windows Updateの通信先はMicrosoftのドキュメントに記載があります。 通信先を列挙すると以下の通りです。

  • http://windowsupdate.microsoft.com
  • http://*.windowsupdate.microsoft.com
  • https://*.windowsupdate.microsoft.com
  • http://*.update.microsoft.com
  • https://*.update.microsoft.com
  • http://*.windowsupdate.com
  • http://download.windowsupdate.com
  • https://download.microsoft.com
  • http://*.download.windowsupdate.com
  • http://wustat.windows.com
  • http://ntservicepack.microsoft.com
  • http://go.microsoft.com
  • http://dl.delivery.mp.microsoft.com
  • https://dl.delivery.mp.microsoft.com

Network Firewallは.がワイルドカードとして使えるので、上述した通信先から省略して書くことができます。

Domain list – List of strings specifying the domain names that you want to match. A packet must match one of the domain specifications in the >list to be a match for the rule group. Valid domain name specifications are the following:

Explicit names. For example, abc.example.com matches only the domain abc.example.com.

Names that use a domain wildcard, which you indicate with an initial '.'. For example,.example.com matches example.com and matches all >subdomains of example.com, such as abc.example.com and www.example.com.

Stateful domain list rule groups in AWS Network Firewall - Match settings

プロトコルも別の条件と記載することができるので、CDKで書くと以下のようになります。

./lib/app-stack.ts

    // Create Network Firewall rule group
    const networkfirewallRuleGroup = new networkfirewall.CfnRuleGroup(
      this,
      "NetworkFirewallRuleGroup",
      {
        capacity: 100,
        ruleGroupName: "WindowsUpdateRuleGroup",
        type: "STATEFUL",
        ruleGroup: {
          rulesSource: {
            rulesSourceList: {
              generatedRulesType: "ALLOWLIST",
              targetTypes: ["TLS_SNI", "HTTP_HOST"],
              targets: [
                ".update.microsoft.com",
                ".download.microsoft.com",
                ".windowsupdate.com",
                ".windowsupdate.microsoft.com",
                ".mp.microsoft.com",
                "wustat.windows.com",
                "ntservicepack.microsoft.com",
                "login.live.com",
              ],
            },
          },
        },
      }
    );

Ingress Route Table

Network Firewallがいる影響で、Internet Gatewayにアタッチする Route Tableを編集する必要があります。

インスタンスからWindows Updateの通信は以下の通りです。

インスタンス -> NAT Gateway -> Network Firewall -> Internet Gateway -> Windows Update通信先(構成図の赤い太い線)

問題はレスポンスの通信です。

本来のあるべきレスポンスの通信経路は、リクエストの通信と逆で、構成図の青い太い線のように、Windows Update通信先 -> Internet Gateway -> Network Firewall -> NAT Gateway -> インスタンスです。

しかし、リクエストの通信の際にNetwork FirewallはパブリックIPアドレスを持っていないために、Windows Update通信先 -> Internet Gateway -> NAT Gateway -> インスタンスとなり、非対称なルーティングになってしまいます。 このようなことを防ぐために、Ingress Route Tableを使用します。

Ingress Route Tableを使用して、NAT Gatewayに対しての通信が来た場合は、Network Firewallのエンドポイントに向くように変更します。これでリクエスト/レスポンスで対称なルーティングとなります。

Ingress Route Tableについては以下ブログが非常に参考になるかと思います。

コードの確認に戻ります。

./lib/app-stack.ts内で読み込んでいる./AmazonCloudWatch-windows.jsonは以下の通りです。 これはCloudWatch Agentインストール時に併せてインストールされるamazon-cloudwatch-agent-config-wizard.exeの出力結果をベースにしたものです。 (なんとなくログとかメトリクスとか取りたくなるのは私だけでしょうか??)

./AmazonCloudWatch-windows.json

{
    "logs": {
        "logs_collected": {
            "windows_events": {
                "collect_list": [{
                        "event_format": "xml",
                        "event_levels": [
                            "INFORMATION",
                            "WARNING",
                            "ERROR",
                            "CRITICAL"
                        ],
                        "event_name": "System",
                        "log_group_name": "/WindowsEvents/System/"
                    },
                    {
                        "event_format": "xml",
                        "event_levels": [
                            "INFORMATION",
                            "WARNING",
                            "ERROR",
                            "CRITICAL"
                        ],
                        "event_name": "Security",
                        "log_group_name": "/WindowsEvents/Security/"
                    },
                    {
                        "event_format": "xml",
                        "event_levels": [
                            "INFORMATION",
                            "WARNING",
                            "ERROR",
                            "CRITICAL"
                        ],
                        "event_name": "Application",
                        "log_group_name": "/WindowsEvents/Application/"
                    }
                ]
            }
        }
    },
    "metrics": {
        "append_dimensions": {
            "AutoScalingGroupName": "${aws:AutoScalingGroupName}",
            "ImageId": "${aws:ImageId}",
            "InstanceId": "${aws:InstanceId}",
            "InstanceType": "${aws:InstanceType}"
        },
        "metrics_collected": {
            "LogicalDisk": {
                "measurement": [
                    "% Free Space"
                ],
                "metrics_collection_interval": 60,
                "resources": [
                    "*"
                ]
            },
            "Memory": {
                "measurement": [
                    "% Committed Bytes In Use"
                ],
                "metrics_collection_interval": 60
            },
            "Paging File": {
                "measurement": [
                    "% Usage"
                ],
                "metrics_collection_interval": 60,
                "resources": [
                    "*"
                ]
            },
            "PhysicalDisk": {
                "measurement": [
                    "% Disk Time",
                    "Disk Write Bytes/sec",
                    "Disk Read Bytes/sec",
                    "Disk Writes/sec",
                    "Disk Reads/sec"
                ],
                "metrics_collection_interval": 60,
                "resources": [
                    "*"
                ]
            },
            "Processor": {
                "measurement": [
                    "% User Time",
                    "% Idle Time",
                    "% Interrupt Time"
                ],
                "metrics_collection_interval": 60,
                "resources": [
                    "*"
                ]
            },
            "TCPv4": {
                "measurement": [
                    "Connections Established"
                ],
                "metrics_collection_interval": 60
            },
            "TCPv6": {
                "measurement": [
                    "Connections Established"
                ],
                "metrics_collection_interval": 60
            },
            "statsd": {
                "metrics_aggregation_interval": 60,
                "metrics_collection_interval": 10,
                "service_address": ":8125"
            }
        }
    }
}

また、特に面白いものはインストールしていませんが、package.jsonは以下の通りです。 今回、CDKのバージョンは1.101.0です。

./package.json

{
  "name": "app",
  "version": "0.1.0",
  "bin": {
    "app": "bin/app.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
  "devDependencies": {
    "@aws-cdk/assert": "1.101.0",
    "@types/jest": "^26.0.23",
    "@types/node": "10.17.27",
    "aws-cdk": "1.101.0",
    "jest": "^26.4.2",
    "ts-jest": "^26.2.0",
    "ts-node": "^9.0.0",
    "typescript": "~3.9.7"
  },
  "dependencies": {
    "@aws-cdk/aws-ec2": "^1.101.0",
    "@aws-cdk/aws-iam": "^1.101.0",
    "@aws-cdk/aws-networkfirewall": "^1.101.0",
    "@aws-cdk/aws-s3": "^1.101.0",
    "@aws-cdk/aws-ssm": "^1.101.0",
    "@aws-cdk/core": "1.101.0",
    "fs": "0.0.1-security",
    "source-map-support": "^0.5.16"
  }
}

リソースのデプロイ

CDKを使ってリソースをデプロイしていきます。私はCDKをグローバルでインストールしていないので、npxを使用しています。 CDKをグローバルでインストールされている方は先頭のnpxを無視して読み替えてください。

// CDKのバージョン確認
> npx cdk --version
1.101.0 (build 149f0fc)

// リソースのデプロイ
> npx cdk deploy
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────────────┬────────┬────────────────────────────────┬──────────────────────────────────┬───────────┐
│   │ Resource                       │ Effect │ Action                         │ Principal                        │ Condition │
├───┼────────────────────────────────┼────────┼────────────────────────────────┼──────────────────────────────────┼───────────┤
│ + │ ${FlowLogsIamrole.Arn}         │ Allow  │ sts:AssumeRole                 │ Service:vpc-flow-logs.amazonaws. │           │
│   │                                │        │                                │ com                              │           │
│ + │ ${FlowLogsIamrole.Arn}         │ Allow  │ iam:PassRole                   │ AWS:${FlowLogsIamrole}           │           │
├───┼────────────────────────────────┼────────┼────────────────────────────────┼──────────────────────────────────┼───────────┤
│ + │ ${FlowLogsLogGroup.Arn}        │ Allow  │ logs:CreateLogStream           │ AWS:${FlowLogsIamrole}           │           │
│   │                                │        │ logs:DescribeLogStreams        │                                  │           │
│   │                                │        │ logs:PutLogEvents              │                                  │           │
├───┼────────────────────────────────┼────────┼────────────────────────────────┼──────────────────────────────────┼───────────┤
│ + │ ${SsmIamRole.Arn}              │ Allow  │ sts:AssumeRole                 │ Service:ec2.${AWS::URLSuffix}    │           │
└───┴────────────────────────────────┴────────┴────────────────────────────────┴──────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬───────────────┬────────────────────────────────────────────────────────────────────┐
│   │ Resource      │ Managed Policy ARN                                                 │
├───┼───────────────┼────────────────────────────────────────────────────────────────────┤
│ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore │
│ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMPatchAssociation    │
│ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentAdminPolicy   │
└───┴───────────────┴────────────────────────────────────────────────────────────────────┘
Security Group Changes
┌───┬────────────────────────────────────────────────────┬─────┬────────────┬──────────────────┐
│   │ Group                                              │ Dir │ Protocol   │ Peer             │
├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤
│ + │ ${CloudwatchLogsVpcEndpoint/SecurityGroup.GroupId} │ In  │ TCP 443    │ ${Vpc.CidrBlock} │
│ + │ ${CloudwatchLogsVpcEndpoint/SecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4)  │
├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤
│ + │ ${CloudwatchVpcEndpoint/SecurityGroup.GroupId}     │ In  │ TCP 443    │ ${Vpc.CidrBlock} │
│ + │ ${CloudwatchVpcEndpoint/SecurityGroup.GroupId}     │ Out │ Everything │ Everyone (IPv4)  │
├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤
│ + │ ${Ec2Instance0/InstanceSecurityGroup.GroupId}      │ Out │ Everything │ Everyone (IPv4)  │
├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤
│ + │ ${Ec2Instance1/InstanceSecurityGroup.GroupId}      │ Out │ Everything │ Everyone (IPv4)  │
├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤
│ + │ ${Ec2MessagesVpcEndpoint/SecurityGroup.GroupId}    │ In  │ TCP 443    │ ${Vpc.CidrBlock} │
│ + │ ${Ec2MessagesVpcEndpoint/SecurityGroup.GroupId}    │ Out │ Everything │ Everyone (IPv4)  │
├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤
│ + │ ${Ec2VpcEndpoint/SecurityGroup.GroupId}            │ In  │ TCP 443    │ ${Vpc.CidrBlock} │
│ + │ ${Ec2VpcEndpoint/SecurityGroup.GroupId}            │ Out │ Everything │ Everyone (IPv4)  │
├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤
│ + │ ${SsmMessagesVpcEndpoint/SecurityGroup.GroupId}    │ In  │ TCP 443    │ ${Vpc.CidrBlock} │
│ + │ ${SsmMessagesVpcEndpoint/SecurityGroup.GroupId}    │ Out │ Everything │ Everyone (IPv4)  │
├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤
│ + │ ${SsmVpcEndpoint/SecurityGroup.GroupId}            │ In  │ TCP 443    │ ${Vpc.CidrBlock} │
│ + │ ${SsmVpcEndpoint/SecurityGroup.GroupId}            │ Out │ Everything │ Everyone (IPv4)  │
└───┴────────────────────────────────────────────────────┴─────┴────────────┴──────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
AppStack: deploying...
AppStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (67/67)



 ✅  AppStack

Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/AppStack/47c7a540-a956-11eb-a52f-1245efb4d2e3

リソースの確認

ここで一旦デプロイされたリソースを確認してみます。

サブネットを確認してみると、以下のようにキチンと各AZにデプロイされていることが分かります。

Ingress Route Tableも確認してみると、正しく、NAT Gateway行きだった場合は、Network Firewallのエンドポイントルーティングするようになっています。

Network Firewallも確認してみます。 Network Firewallのルールを確認すると、CDKで記載した通りにドメインとプロトコルが許可されています。

また、Reachability Analyzerでインスタンスからインターネットに通信できるか確認してみます。 送信元をインスタンス、送信先をInternet Gatewayを指定すると、以下のように到達不可能と出力されます。

Privateサブネット、Publicサブネット、Firewallサブネットと正しくルートテーブルは設定していますが、Firewall EndpointがNAT GatewayとInternet Gatewayの間に存在すると正しく経路が認識されない挙動のようです。挙動としては不思議ですが、このまま作業を継続します。

動作確認

SSMセッションマネージャーでインスタンスに接続して、以下URLにアクセスしてみました。

  1. https://dev.classmethod.jp
  2. http://www.google.com/
  3. http://update.microsoft.com
  4. http://windowsupdate.microsoft.com

1~2の2つのURLにアクセスすると拒否され、3~4のURLは正常に通信できる想定です。 結果は以下の通り、Windows Updateで使用されるURLへの通信は許可され、許可されていないURLへの通信はタイムアウトになることが確認できました。

Windows PowerShell
Copyright (C) 2014 Microsoft Corporation. All rights reserved.

PS C:\Windows\system32> Invoke-WebRequest https://dev.classmethod.jp -UseBasicParsing
Invoke-WebRequest : 接続が切断されました: 送信時に、予期しないエラーが発生しました。。
発生場所 行:1 文字:1
+ Invoke-WebRequest https://dev.classmethod.jp -UseBasicParsing
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

PS C:\Windows\system32> Invoke-WebRequest http://www.google.com/ -UseBasicParsing
Invoke-WebRequest : 接続が切断されました: 受信時に予期しないエラーが発生しました。
発生場所 行:1 文字:1
+ Invoke-WebRequest http://www.google.com/ -UseBasicParsing
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

PS C:\Windows\system32> Invoke-WebRequest http://update.microsoft.com -UseBasicParsing


StatusCode        : 200
StatusDescription : OK
Content           :
                        <html>
                        <head>
                            <!-- Mimic Internet Explorer 7 -->
                            <meta http-equiv='X-UA-Compatible' content='IE=5; requiresActiveX=true' />
                            <meta http-equiv="PICS-Label" content='(PICS-1.1 "http://www.r...
RawContent        : HTTP/1.1 200 OK
                    Vary: *
                    Content-Length: 2048
                    Cache-Control: public, max-age=0
                    Content-Type: text/html; charset=utf-8
                    Date: Thu, 29 Apr 2021 07:44:22 GMT
                    Expires: Thu, 29 Apr 2021 07:44:23 GMT
                    L...
Forms             :
Headers           : {[Vary, *], [Content-Length, 2048], [Cache-Control, public, max-age=0], [Content-Type, text/html; charset=utf-8]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        :
RawContentLength  : 2048



PS C:\Windows\system32> Invoke-WebRequest http://windowsupdate.microsoft.com -UseBasicParsing


StatusCode        : 200
StatusDescription : OK
Content           :
                        <html>
                        <head>
                            <!-- Mimic Internet Explorer 7 -->
                            <meta http-equiv='X-UA-Compatible' content='IE=5; requiresActiveX=true' />
                            <meta http-equiv="PICS-Label" content='(PICS-1.1 "http://www.r...
RawContent        : HTTP/1.1 200 OK
                    Vary: *
                    Content-Length: 2048
                    Cache-Control: public, max-age=56
                    Content-Type: text/html; charset=utf-8
                    Date: Thu, 29 Apr 2021 07:44:28 GMT
                    Expires: Thu, 29 Apr 2021 07:45:25 GMT
                    ...
Forms             :
Headers           : {[Vary, *], [Content-Length, 2048], [Cache-Control, public, max-age=56], [Content-Type, text/html; charset=utf-8]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        :
RawContentLength  : 2048

PS C:\Windows\system32>

VPC Flow Logsで通信の様子を確認してみましょう。 NAT GatewayのENIのVPC Flow Logsを確認すると以下のようになっていました。

| ${version} | ${account-id} | ${interface-id}       | ${srcaddr} | ${dstaddr} | ${srcport} | ${dstport} | ${protocol} | ${packets} | ${bytes} | ${start}   | ${end}     | ${action} | ${log-status} | ${vpc-id}             | ${subnet-id}             | ${instance-id} | ${tcp-flags} | ${type} | ${pkt-srcaddr} | ${pkt-dstaddr} | ${region} | ${az-id} | ${sublocation-type} | ${sublocation-id} | ${pkt-src-aws-service} | ${pkt-dst-aws-service} | ${flow-direction} | ${traffic-path} | 
| ---------- | ------------- | --------------------- | ---------- | ---------- | ---------- | ---------- | ----------- | ---------- | -------- | ---------- | ---------- | --------- | ------------- | --------------------- | ------------------------ | -------------- | ------------ | ------- | -------------- | -------------- | --------- | -------- | ------------------- | ----------------- | ---------------------- | ---------------------- | ----------------- | --------------- | 
| 5          | <AWSアカウントID>| eni-08e9a808001d52a42 | 10.0.1.17  | 10.0.0.8   | 55430      | 80         | 6           | 2          | 80       | 1619748444 | 1619748453 | ACCEPT    | OK            | vpc-015f0a3a497792ace | subnet-01e159902b87d0c83 | -              | 1            | IPv4    | 10.0.1.17      | 52.137.90.34   | us-east-1 | use1-az6 | -                   | -                 | -                      | -                      | ingress           | -               | 
| 5          | <AWSアカウントID>| eni-08e9a808001d52a42 | 10.0.0.8   | 10.0.1.17  | 80         | 55430      | 6           | 1          | 40       | 1619748444 | 1619748453 | ACCEPT    | OK            | vpc-015f0a3a497792ace | subnet-01e159902b87d0c83 | -              | 1            | IPv4    | 52.137.90.34   | 10.0.1.17      | us-east-1 | use1-az6 | -                   | -                 | -                      | -                      | egress            | 1               |

3行目の${dstaddr}と、${pkt-dstaddr}を見比べてると分かる通り、送信先のIPアドレスが変換されてます。
本来の送信元(${pkt-dstaddr})は、52.137.90.34となっています。このIPアドレスを逆引きすると、Microsoftがゾーン情報を管理していることからWindows Updateに使用されるIPアドレスであることが分かります。

> dig -x 52.137.90.34

; <<>> DiG 9.10.6 <<>> -x 52.137.90.34
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 35412
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;34.90.137.52.in-addr.arpa.	IN	PTR

;; AUTHORITY SECTION:
90.137.52.in-addr.arpa.	60	IN	SOA	prd1.azuredns-cloud.net. msnhst.microsoft.com. 1 900 300 604800 60

${dstaddr}10.0.0.8ですが、このIPアドレスはNetwork FirewallのVPCエンドポイントのIPアドレスです。そのため、Network Firewallにルーティングするために、AWS側でIPアドレスを書き換われていることが分かります。

このことから、3行目がEC2インスタンスからWindows UpdateへのURLへのリクエストの通信で、4行目がそのレスポンスの通信であることが分かります。

また、Network FirewallのENIのVPC Flow Logsを確認すると以下のようになっていました。

| ${version} | ${account-id} | ${interface-id}       | ${srcaddr} | ${dstaddr}   | ${srcport} | ${dstport} | ${protocol} | ${packets} | ${bytes} | ${start}   | ${end}     | ${action} | ${log-status} | ${vpc-id}             | ${subnet-id}             | ${instance-id} | ${tcp-flags} | ${type} | ${pkt-srcaddr} | ${pkt-dstaddr} | ${region} | ${az-id} | ${sublocation-type} | ${sublocation-id} | ${pkt-src-aws-service} | ${pkt-dst-aws-service} | ${flow-direction} | ${traffic-path} | 
| ---------- | ------------- | --------------------- | ---------- | ------------ | ---------- | ---------- | ----------- | ---------- | -------- | ---------- | ---------- | --------- | ------------- | --------------------- | ------------------------ | -------------- | ------------ | ------- | -------------- | -------------- | --------- | -------- | ------------------- | ----------------- | ---------------------- | ---------------------- | ----------------- | --------------- | 
| 5          | <AWSアカウントID>| eni-08e9a808001d52a42 | 10.0.0.8   | 52.137.90.34 | 55430      | 80         | 6           | 4          | 332      | 1619748319 | 1619748324 | ACCEPT    | OK            | vpc-015f0a3a497792ace | subnet-01e159902b87d0c83 | -              | 2            | IPv4    | 10.0.1.17      | 52.137.90.34   | us-east-1 | use1-az6 | -                   | -                 | -                      | -                      | egress            | 8               |

${dstaddr}を確認すると、52.137.90.34となっています。このことから、Network Firewallを通過する際には、NAT Gateway通過時に変更されたIPアドレスが、本来の宛先IPアドレスに戻っていることが分かります。

このように、Network Firewallや、Gateway Load Balancerなどネットワークの通信経路に影響を与えるサービスを使用する際は、VPC Flow Logsの内容は詳細に表示するようにしておいた方が良いかと思います。

VPC Flow Logsで取得可能なフィールドについては、AWS公式ドキュメントに記載があります。

続いて、Network Firewallのモニタリングも確認してみます。ご覧の通り、どの程度の通信を許可したのか、拒否したのかが分かりやすく表示されています。

Network Firewallのログは以下の通りです。JSON形式で保存されるため、VPC Flow Logsよりも見やすいかもしれませんね。

CloudWatch Agentのインストール・設定

作成したEC2インスタンスにCloudWatch Agentをインストールしてイベントログや、メモリなどのメトリクスを取得します。

まずは、CloudWatch Agentのインストールです。
インストールはSSM RunCommandを使用して行います。

SSMのコンソールより、RunCommandを選択して、Run commandをクリックします。

CloudWatch AgentをEC2インスタンスにインストールするため、実行するドキュメントはAWS-ConfigureAWSPackageです。AWS-ConfigureAWSPackageは指定されたパッケージをEC2インストール/アンインストールする際に使用されるドキュメントです。
パラメーターも以下の通り、Action: InstallName: AmazonCloudWatchAgentと指定します。

実行対象のEC2インスタンスと、ログの出力先を指定します。ログの出力先はCloudWatch Logsの場合、/aws/ssm/AWS-ConfigureAWSPackageというロググループに出力されます。
問題がなければ、右下の実行をクリックすると、CloudWatch Agentのインストールが始まります。

実行が完了すると以下のような画面になります。どちらのEC2インスタンスもステータスが成功になっており、全体的なステータスも成功となっています。

RunCommandを実行した際に指定したCloudWatch Logsにログが出力されているので、確認してみます。
Successfully installed arn:aws:ssm:::package/AmazonCloudWatchAgent 1.247347.6b250880となっているので、正常にインストールされていることを確認できます。

続いて、CloudWatch Agentの設定をしていきます。

まずは、CDKで設定したSSM Parameter storeに、CloudWatchの設定JSONが保存されているか確認します。 パラメーターストアを確認すると、意図したJSONが保存されていることが確認できました。

CloudWatch Agentの設定もRunCommandで行います。 以下のように入力します。Option Restart: yesとすることで、設定を更新するためにCloudWatch Agentを再起動してくれます。

実行対象のEC2インスタンスと、ログの出力先の指定は、CloudWatch Agentをインストールした時と同様です。実行をクリックすると、CloudWatch Agentの設定が開始されます。

実行が完了すると以下のような画面になります。どちらのEC2インスタンスもステータスが成功になっており、全体的なステータスも成功となっています。

RunCommandを実行した際に指定したCloudWatch Logsにログが出力されているので、確認してみます。
Successfully fetched the config and saved in C:\ProgramData\Amazon\AmazonCloudWatchAgent\Configs\ssm_AmazonCloudWatch-windows.tmpとあることからSSMパラメーターストアからJSONをダウンロードできていることが分かります。
その後Configuration validation succeededと設定の検証もされ、サービスの再起動も行われているとなっているので、正常に設定されたことを確認できます。

それではイベントログや、メトリクスが取得できているか確認します。まずはWindowsのイベントログの確認からです。 設定の通り、/WindowsEvents/.*/の形式でロググループが作成されているようです。

Systemイベントのログを確認すると以下の通り、正常に出力されていることが確認できました。

イベントログが取得できていることが確認できたので、続いてメトリクスです。 CloudWatchメトリクスを確認すると、カスタム名前空間としてCWAgentが追加されています。

デプロイされた2つのEC2インスタンスのMemory % Committed Bytes In Useを選択すると、きれいにメモリの使用率のメトリクスが表示されていました。

SSM Patch ManagerによるWindows Update

それでは、SSM Patch Managerを使ってWindows Updateをしていきます。

SSM Patch Manager自体の詳細な説明については以下ブログが参考になるかと思います。

まずは、Windows Updateをするにあたって、実行するパッチベースラインを指定していきます。今回はAWS-WindowsPredefinedPatchBaseline-OSをパッチベースラインとして設定します。
AWS-WindowsPredefinedPatchBaseline-OSの説明は以下、公式ドキュメントにある通り、重要度の高いパッチを適用するものになります。

分類が「CriticalUpdates」または「SecurityUpdates」で、MSRC 重要度が「非常事態」または「重要」のすべての Windows Server オペレーティングシステムパッチを承認します。パッチはリリースから 7 日後に自動承認されます。

事前定義されたパッチベースラインおよびカスタムパッチベースラインについて

続いてパッチグループの設定をしていきます。パッチグループの変更をクリックします。

パッチグループ名を入力します。今回は対象のEC2インスタンスがWindows Server 2012 R2だったので、Windows Server 2012 R2にしました。 変更後は閉じるをクリックします。

パッチグループ名をWindows Server 2012 R2にしたので、対象のEC2インスタンスの2台にPatch Group: Windows Server 2012 R2のタグを付与します。

続いて、実際にWindows Updateを実施していきます。 適用するインスタンスや、スケジュール、操作を指定しています。

  • パッチを適用するインスタンスは先ほど作成したWindows Server 2012 R2を指定します。
  • パッチ適用スケジュールは今回一度だけ実行したいので、スケジュール作成とインスタンスへのバッチ適用をスキップするを指定します。
  • パッチ適用操作はインストールまで実施して欲しいので、スキャンとインストールを指定します。

問題がなければ、パッチ適用を設定をクリックします。

パッチマネージャーを確認すると、AWS-RunPatchBaselineのStatusがInProgressになっています。

しばらく待つと、StatusがSuccessになっており、Windows Updateが完了したようです!

パッチマネージャーでInstallをクリックすると、RunCommandの画面に遷移しました。Patch Managerも実際はRunCommandが動いているようです。

次にログの確認をします。 対象のインスタンスを選択して、出力の表示をクリックすると以下のような画面に遷移します。

画面では見切れていたので、ログを以下に記載します。 InstalledCount: 42となっていることから、パッチベースラインで定義された42個のパッチがインスタンスにインストールされているようです。

Preparing to download PatchBaselineOperations PowerShell module from S3.

Downloading PatchBaselineOperations PowerShell module from https://s3.amazonaws.com/aws-ssm-us-east-1/patchbaselineoperations/Amazon.PatchBaselineOperations-1.31.zip to C:\ProgramData\Amazon\SSM\InstanceData\i-0f027dd30cbffa0cc\document\orchestration\4bb232d5-ad74-4ec7-8195-202a01af4a40\PatchWindows\Amazon.PatchBaselineOperations-1.31.zip.

Extracting PatchBaselineOperations zip file contents to temporary folder.

Verifying SHA 256 of the PatchBaselineOperations PowerShell module files.

Successfully downloaded and installed the PatchBaselineOperations PowerShell module.


Patch Summary for i-0f027dd30cbffa0cc
PatchGroup          : Windows Server 2012 R2
BaselineId          : pb-096d816473f2bdb03
Baseline            : {"AccountId":"075727635805","BaselineId":"pb-096d816473f2bdb03","Name":"AWS-WindowsPredefinedPatchBaseline-OS","GlobalFilters":{"Filters":[{"Key":"PRODUCT","Values":["*"]}]},"ApprovalRules":{"Rules":[{"ApproveAfterDays":7,"FilterGroup":{"Filters":[{"Key":"PATCH_SET","Values":["OS"]},{"Key":"CLASSIFICATION","Values":["CriticalUpdates","SecurityUpdates"]},{"Key":"MSRC_SEVERITY","Values":["Critical","Important"]}]}}]},"ApprovedPatches":[],"RejectedPatches":[],"RejectedPatchesAction":"ALLOW_AS_DEPENDENCY","CreatedTime":1557253943.887,"ModifiedTime":1557253943.887,"Description":"Approves all Windows Server operating system patches that are classified as CriticalUpdates or SecurityUpdates and that have an MSRC severity of Critical or Important. Patches are auto-approved seven days after release."}
SnapshotId          : 859a9f98-70b5-4a51-a7d7-0eb9af0e4999
ExecutionId         : 4bb232d5-ad74-4ec7-8195-202a01af4a40
RebootOption        : RebootIfNeeded
OwnerInformation    : 
OperationType       : Install
OperationStartTime  : 2021-04-29T08:40:12.1789065Z
OperationEndTime    : 2021-04-29T08:44:01.7313056Z
InstalledCount      : 42
InstalledRejectedCount : 0
InstalledPendingRebootCount : 0
InstalledOtherCount : 167
FailedCount         : 0
MissingCount        : 0
CriticalNonCompliantCount : 0
SecurityNonCompliantCount : 0
OtherNonCompliantCount : 0
NotApplicableCount  : 1927
UnreportedNotApplicableCount : 0

WIN-CD0UU99DGCT - PatchBaselineOperations Installation Results - 2021-04-29T09:03:34.038

KbArticleId Installed   Message
----------- ----------- -----------

リソースのお片付け

無事Network Firewallを経由してWindows Updateができたので、環境のお片付けをします。 以下のコマンドでCDKでデプロイされたリソースは削除されます。なお、CloudWatch Logsのロググループは削除されないので、手動で削除が必要です。

> npx cdk destroy 
Are you sure you want to delete: AppStack (y/n)? y
AppStack: destroying...
11:25:08 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack                 | AppStack
11:28:49 | DELETE_IN_PROGRESS   | AWS::NetworkFirewall::RuleGroup            | NetworkFirewallRuleGroup




 ✅  AppStack: destroyed

課金のお話

Network Firewallの料金は以下の通りです。詳細は公式ドキュメントをご覧ください。

Resource Type Price
Network Firewall Endpoint $0.395/hr
Network Firewall Traffic Processing $0.065/GB
NAT gateway Pricing Use one hour & one GB of NAT gateway at no additional cost for every hour & GB charged for Network Firewall endpoints.

AZ単位で課金が発生するので1AZでも1ヶ月フル(24時間×30日)で使うとなると$284.4程度かかります。通常Multi-AZで運用すると思うので、$568.8と案外なお値段です。 私は検証でかなりの時間格闘したので、Network Firewallだけで$32.09ほどかかってしまいました...

皆さんも検証用途で作成される際は、削除忘れにお気をつけください。

Network Firewallを使ってみよう!

Network Firewallを使うことによって今までできなかった細かいアクセス制御ができそうです!
お値段は少々しますが、マネージドサービスなのでプロキシサーバの可用性やOSの運用コストなどが減るので、実運用する身としてはかなり助かります。

今回はドメインフィルタリングとありそうでなかった機能の検証でしたが、今度はIPS機能の記事でも書いてみようと思います。 みなさんが私と同様のつまずきをしたときの助けになれば嬉しいです。

以上、東京オフィスの のん(@non____97)でした!