実践!AWS CDK #14 ネットワーク ACL

題字・息子たち
2021.06.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

今回はネットワーク ACL を作成します。
これが完成すればネットワークまわりの構築は一旦完了です。

前回の記事はこちら。

AWS 構成図

1

今度も前回と変わらず。構成図には書きません。

設計

ネットワーク ACL は VPC 作成時に自動生成されるデフォルトのものと同じ設定にします。基本的にアクセス制御はセキュリティグループで行い、ネットワーク ACL は利用しないという思想です。

作成するネットワーク ACL は 3 つ。各層のサブネットに紐付け、AZ で区別はしません。

プロパティは以下の通りです。

リソース名 関連付けるサブネット
devio-stg-nacl-public devio-stg-subnet-public-1a
devio-stg-subnet-public-1c
devio-stg-nacl-app devio-stg-subnet-app-1a
devio-stg-subnet-app-1c
devio-stg-nacl-db devio-stg-subnet-db-1a
devio-stg-subnet-db-1c

すべてのネットワーク ACL は次のように設定します。(デフォルトネットワーク ACL と同じ)

インバウンドルール

ルール番号 タイプ プロトコル ポート範囲 送信元 許可/拒否
100 すべてのトラフィック すべて すべて 0.0.0.0/0 Allow
* すべてのトラフィック すべて すべて 0.0.0.0/0 Deny

アウトバウンドルール

ルール番号 タイプ プロトコル ポート範囲 送信先 許可/拒否
100 すべてのトラフィック すべて すべて 0.0.0.0/0 Allow
* すべてのトラフィック すべて すべて 0.0.0.0/0 Deny

ルール番号 * はネットワーク ACL を作成すると自動で付与されるので、ルール番号 100 のみ実装します。

実装

ネットワーク ACL に関する処理を行うクラスはこちら。

lib/resource/networkAcl.ts

import * as cdk from '@aws-cdk/core';
import { CfnNetworkAcl, CfnNetworkAclEntry, CfnSubnetNetworkAclAssociation, CfnVPC, CfnSubnet } from '@aws-cdk/aws-ec2';
import { Resource } from './abstract/resource';

interface AssociationInfo {
    readonly id: string;
    readonly subnetId: () => string;
}

interface ResourceInfo {
    readonly id: string;
    readonly resourceName: string;
    readonly entryIdInbound: string;
    readonly entryIdOutbound: string;
    readonly associations: AssociationInfo[];
    readonly assign: (networkAcl: CfnNetworkAcl) => void;
}

export class NetworkAcl extends Resource {
    public public: CfnNetworkAcl;
    public app: CfnNetworkAcl;
    public db: CfnNetworkAcl;

    private readonly vpc: CfnVPC;
    private readonly subnetPublic1a: CfnSubnet;
    private readonly subnetPublic1c: CfnSubnet;
    private readonly subnetApp1a: CfnSubnet;
    private readonly subnetApp1c: CfnSubnet;
    private readonly subnetDb1a: CfnSubnet;
    private readonly subnetDb1c: CfnSubnet;
    private readonly resources: ResourceInfo[] = [
        {
            id: 'NetworkAclPublic',
            resourceName: 'nacl-public',
            entryIdInbound: 'NetworkAclEntryInboundPublic',
            entryIdOutbound: 'NetworkAclEntryOutboundPublic',
            associations: [
                {
                    id: 'NetworkAclAssociationPublic1a',
                    subnetId: () => this.subnetPublic1a.ref
                },
                {
                    id: 'NetworkAclAssociationPublic1c',
                    subnetId: () => this.subnetPublic1c.ref
                }
            ],
            assign: networkAcl => this.public = networkAcl
        },
        {
            id: 'NetworkAclApp',
            resourceName: 'nacl-app',
            entryIdInbound: 'NetworkAclEntryInboundApp',
            entryIdOutbound: 'NetworkAclEntryOutboundApp',
            associations: [
                {
                    id: 'NetworkAclAssociationApp1a',
                    subnetId: () => this.subnetApp1a.ref
                },
                {
                    id: 'NetworkAclAssociationApp1c',
                    subnetId: () => this.subnetApp1c.ref
                }
            ],
            assign: networkAcl => this.app = networkAcl
        },
        {
            id: 'NetworkAclDb',
            resourceName: 'nacl-db',
            entryIdInbound: 'NetworkAclEntryInboundDb',
            entryIdOutbound: 'NetworkAclEntryOutboundDb',
            associations: [
                {
                    id: 'NetworkAclAssociationDb1a',
                    subnetId: () => this.subnetDb1a.ref
                },
                {
                    id: 'NetworkAclAssociationDb1c',
                    subnetId: () => this.subnetDb1c.ref
                }
            ],
            assign: networkAcl => this.db = networkAcl
        }
    ];

    constructor(
        vpc: CfnVPC,
        subnetPublic1a: CfnSubnet,
        subnetPublic1c: CfnSubnet,
        subnetApp1a: CfnSubnet,
        subnetApp1c: CfnSubnet,
        subnetDb1a: CfnSubnet,
        subnetDb1c: CfnSubnet
    ) {
        super();
        this.vpc = vpc;
        this.subnetPublic1a = subnetPublic1a;
        this.subnetPublic1c = subnetPublic1c;
        this.subnetApp1a = subnetApp1a;
        this.subnetApp1c = subnetApp1c;
        this.subnetDb1a = subnetDb1a;
        this.subnetDb1c = subnetDb1c;
    }

    createResources(scope: cdk.Construct) {
        for (const resourceInfo of this.resources) {
            const networkAcl = this.createNetworkAcl(scope, resourceInfo);
            resourceInfo.assign(networkAcl);
        }
    }

    private createNetworkAcl(scope: cdk.Construct, resourceInfo: ResourceInfo): CfnNetworkAcl {
        const networkAcl = new CfnNetworkAcl(scope, resourceInfo.id, {
            vpcId: this.vpc.ref,
            tags: [{
                key: 'Name',
                value: this.createResourceName(scope, resourceInfo.resourceName)
            }]
        });

        this.createEntry(scope, resourceInfo.entryIdInbound, networkAcl, false);
        this.createEntry(scope, resourceInfo.entryIdOutbound, networkAcl, true);

        for (const associationInfo of resourceInfo.associations) {
            this.createAssociation(scope, associationInfo, networkAcl);
        }

        return networkAcl;
    }

    private createEntry(scope: cdk.Construct, id: string, networkAcl: CfnNetworkAcl, egress: boolean) {
        const entry = new CfnNetworkAclEntry(scope, id, {
            networkAclId: networkAcl.ref,
            protocol: -1,
            ruleAction: 'allow',
            ruleNumber: 100,
            cidrBlock: '0.0.0.0/0'
        });

        if (egress) entry.egress = true;
    }

    private createAssociation(scope: cdk.Construct, associationInfo: AssociationInfo, networkAcl: CfnNetworkAcl) {
        new CfnSubnetNetworkAclAssociation(scope, associationInfo.id, {
            networkAclId: networkAcl.ref,
            subnetId: associationInfo.subnetId()
        });
    }
}

今回も前回のルートテーブルと同様、リソース情報(ResourceInfo)の中に別のリソース情報(AssociationInfo)が含まれています。

目的のネットワーク ACL を作成するためには以下の 3 つのリソースが必要となります。

  • CfnNetworkAcl(AWS::EC2::NetworkAcl)
    • ネットワーク ACL 本体
  • CfnNetworkAclEntry(AWS::EC2::NetworkAclEntry)
    • ネットワーク ACL に設定するルール
  • CfnSubnetNetworkAclAssociation(AWS::EC2::SubnetNetworkAclAssociation)
    • サブネットの関連付け

インバウンドルールとアウトバウンドルールはどちらも CfnNetworkAclEntry クラスを利用して生成します。
このクラスが持つプロパティ egresstrue ならばアウトバウンド、false ならばインバウンドのルールとなります。
よって生成時のメソッドに boolean 型の egress パラメータを持たせ、その値によって処理を分岐させます。(以下ハイライト部分)

private createEntry(scope: cdk.Construct, id: string, networkAcl: CfnNetworkAcl, egress: boolean) {
    const entry = new CfnNetworkAclEntry(scope, id, {
        networkAclId: networkAcl.ref,
        protocol: -1,
        ruleAction: 'allow',
        ruleNumber: 100,
        cidrBlock: '0.0.0.0/0'
    });

    if (egress) entry.egress = true;
}

[お詫び]

すみません。書いたあとに気づいたんですが、パラメータの egress はコンストラクタに直接渡して問題ありません。(ボケてました、ごめんなさい)

private createEntry(scope: cdk.Construct, id: string, networkAcl: CfnNetworkAcl, egress: boolean) {
    new CfnNetworkAclEntry(scope, id, {
        networkAclId: networkAcl.ref,
        protocol: -1,
        ruleAction: 'allow',
        ruleNumber: 100,
        cidrBlock: '0.0.0.0/0',
        egress: egress
    });
}

ソースコードはある程度書き溜めているので修正ができず、今回の GitHub では変な形のままとなっています。次回までには修正しておきますのでお許しを。


protocol の -1すべてのプロトコル を表します。

You can specify -1 for all protocols.

メインのプログラムはこちら。
ハイライト部分を追記しました。

lib/devio-stack.ts

import * as cdk from '@aws-cdk/core';
import { Vpc } from './resource/vpc';
import { Subnet } from './resource/subnet';
import { InternetGateway } from './resource/internetGateway';
import { ElasticIp } from './resource/elasticIp';
import { NatGateway } from './resource/natGateway';
import { RouteTable } from './resource/routeTable';
import { NetworkAcl } from './resource/networkAcl';

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

    // VPC
    const vpc = new Vpc();
    vpc.createResources(this);

    // Subnet
    const subnet = new Subnet(vpc.vpc);
    subnet.createResources(this);

    // Internet Gateway
    const internetGateway = new InternetGateway(vpc.vpc);
    internetGateway.createResources(this);

    // Elastic IP
    const elasticIp = new ElasticIp();
    elasticIp.createResources(this);

    // NAT Gateway
    const natGateway = new NatGateway(
      subnet.public1a,
      subnet.public1c,
      elasticIp.ngw1a,
      elasticIp.ngw1c
    );
    natGateway.createResources(this);

    // Route Table
    const routeTable = new RouteTable(
      vpc.vpc,
      subnet.public1a,
      subnet.public1c,
      subnet.app1a,
      subnet.app1c,
      subnet.db1a,
      subnet.db1c,
      internetGateway.igw,
      natGateway.ngw1a,
      natGateway.ngw1c
    );
    routeTable.createResources(this);

    // Network ACL
    const networkAcl = new NetworkAcl(
      vpc.vpc,
      subnet.public1a,
      subnet.public1c,
      subnet.app1a,
      subnet.app1c,
      subnet.db1a,
      subnet.db1c
    );
    networkAcl.createResources(this);
  }
}

こっちも長くなってきましたね。

ご注意

今回作成したプログラムは変更に弱いため、あまりいいものではありません。「一部のネットワーク ACL のアウトバウンドルールのこの部分だけ変更したい」という要件には対応不可です。(一箇所を変更したらすべてのネットワーク ACL に影響するので)

今回は ネットワーク ACL は基本的に利用しない という思想で共通のプロパティを持つリソースにしましたが、ネットワーク ACL を個別に設定してガンガン利用したい という場合は(長くなっちゃいますが)以下のようにインタフェースを作成し、柔軟に対応してください。

インタフェースの定義。

interface EntryInfo {
    readonly id: string;
    readonly protocol: number;
    readonly ruleAction: string;
    readonly ruleNumber: number;
    readonly cidrBlock: string;
    readonly egress: boolean;
}

interface ResourceInfo {
    readonly id: string;
    readonly resourceName: string;
    readonly entries: EntryInfo[];
    readonly associations: AssociationInfo[];
    readonly assign: (networkAcl: CfnNetworkAcl) => void;
}

値の設定。

private readonly resources: ResourceInfo[] = [
    {
        id: 'NetworkAclPublic',
        resourceName: 'nacl-public',
        entries: [
            {
                id: 'NetworkAclEntryInboundPublic',
                protocol: -1,
                ruleAction: 'allow',
                ruleNumber: 100,
                cidrBlock: '0.0.0.0/0',
                egress: false
            },
            {
                id: 'NetworkAclEntryOutboundPublic',
                protocol: -1,
                ruleAction: 'deny',
                ruleNumber: 101,
                cidrBlock: '0.0.0.0/0',
                egress: true
            }
        ],
        associations: [
            {
                id: 'NetworkAclAssociationPublic1a',
                subnetId: () => this.subnetPublic1a.ref
            },
            {
                id: 'NetworkAclAssociationPublic1c',
                subnetId: () => this.subnetPublic1c.ref
            }
        ],
        assign: networkAcl => this.public = networkAcl
    },
    ~ 省略 ~

ルールの生成メソッドはこんな感じで。

private createEntry(scope: cdk.Construct, entryInfo: EntryInfo, networkAcl: CfnNetworkAcl) {
    new CfnNetworkAclEntry(scope, entryInfo.id, {
        networkAclId: networkAcl.ref,
        protocol: entryInfo.protocol,
        ruleAction: entryInfo.ruleAction,
        ruleNumber: entryInfo.ruleNumber,
        cidrBlock: entryInfo.cidrBlock,
        egress: entryInfo.egress
    });
}

ループで呼び出します。

private createNetworkAcl(scope: cdk.Construct, resourceInfo: ResourceInfo): CfnNetworkAcl {
    const networkAcl = new CfnNetworkAcl(scope, resourceInfo.id, {
        vpcId: this.vpc.ref,
        tags: [{
            key: 'Name',
            value: this.createResourceName(scope, resourceInfo.resourceName)
        }]
    });

    for (const entryInfo of resourceInfo.entries) {
        this.createEntry(scope, entryInfo, networkAcl);
    }

    for (const associationInfo of resourceInfo.associations) {
        this.createAssociation(scope, associationInfo, networkAcl);
    }

    return networkAcl;
}

その時やりたいことに合わせてカスタマイズしていきましょう。

テスト

テストコードはこちら。

test/resource/networkAcl.test.ts

import { expect, countResources, haveResource, anything } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import * as Devio from '../../lib/devio-stack';

test('NetworkAcl', () => {
    const app = new cdk.App();
    const stack = new Devio.DevioStack(app, 'DevioStack');

    expect(stack).to(countResources('AWS::EC2::NetworkAcl', 3));
    expect(stack).to(haveResource('AWS::EC2::NetworkAcl', {
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-nacl-public' }]
    }));
    expect(stack).to(haveResource('AWS::EC2::NetworkAcl', {
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-nacl-app' }]
    }));
    expect(stack).to(haveResource('AWS::EC2::NetworkAcl', {
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-nacl-db' }]
    }));

    expect(stack).to(countResources('AWS::EC2::NetworkAclEntry', 6));
    expect(stack).to(haveResource('AWS::EC2::NetworkAclEntry', {
        NetworkAclId: anything(),
        Protocol: -1,
        RuleAction: 'allow',
        RuleNumber: 100,
        CidrBlock: '0.0.0.0/0'
    }));
    expect(stack).to(haveResource('AWS::EC2::NetworkAclEntry', {
        NetworkAclId: anything(),
        Protocol: -1,
        RuleAction: 'allow',
        RuleNumber: 100,
        CidrBlock: '0.0.0.0/0',
        Egress: true
    }));

    expect(stack).to(countResources('AWS::EC2::SubnetNetworkAclAssociation', 6));
    expect(stack).to(haveResource('AWS::EC2::SubnetNetworkAclAssociation', {
        NetworkAclId: anything(),
        SubnetId: anything()
    }));
});

以下を確認しています。

  • ネットワーク ACL リソースが 3 つあること
  • ルールが 6 つあること
  • サブネットとネットワーク ACL の関連付けリソースが 6 つあること
  • 各リソースのプロパティが正しいこと

確認

マネジメントコンソール上でリソースを確認してみましょう。

2

できてますよ。

インバウンドルール、アウトバウンドルールも OK。

3

4

CloudFormation 版

今回のコードを CFn で書くと以下のようになります。

NetworkAclPublic:
  Type: AWS::EC2::NetworkAcl
  Properties:
    VpcId:
      Ref: Vpc
    Tags:
      - Key: Name
        Value: devio-stg-nacl-public
NetworkAclEntryInboundPublic:
  Type: AWS::EC2::NetworkAclEntry
  Properties:
    NetworkAclId:
      Ref: NetworkAclPublic
    Protocol: -1
    RuleAction: allow
    RuleNumber: 100
    CidrBlock: 0.0.0.0/0
NetworkAclEntryOutboundPublic:
  Type: AWS::EC2::NetworkAclEntry
  Properties:
    NetworkAclId:
      Ref: NetworkAclPublic
    Protocol: -1
    RuleAction: allow
    RuleNumber: 100
    CidrBlock: 0.0.0.0/0
    Egress: true
NetworkAclAssociationPublic1a:
  Type: AWS::EC2::SubnetNetworkAclAssociation
  Properties:
    NetworkAclId:
      Ref: NetworkAclPublic
    SubnetId:
      Ref: SubnetPublic1a
NetworkAclAssociationPublic1c:
  Type: AWS::EC2::SubnetNetworkAclAssociation
  Properties:
    NetworkAclId:
      Ref: NetworkAclPublic
    SubnetId:
      Ref: SubnetPublic1c
NetworkAclApp:
  Type: AWS::EC2::NetworkAcl
  Properties:
    VpcId:
      Ref: Vpc
    Tags:
      - Key: Name
        Value: devio-stg-nacl-app
NetworkAclEntryInboundApp:
  Type: AWS::EC2::NetworkAclEntry
  Properties:
    NetworkAclId:
      Ref: NetworkAclApp
    Protocol: -1
    RuleAction: allow
    RuleNumber: 100
    CidrBlock: 0.0.0.0/0
NetworkAclEntryOutboundApp:
  Type: AWS::EC2::NetworkAclEntry
  Properties:
    NetworkAclId:
      Ref: NetworkAclApp
    Protocol: -1
    RuleAction: allow
    RuleNumber: 100
    CidrBlock: 0.0.0.0/0
    Egress: true
NetworkAclAssociationApp1a:
  Type: AWS::EC2::SubnetNetworkAclAssociation
  Properties:
    NetworkAclId:
      Ref: NetworkAclApp
    SubnetId:
      Ref: SubnetApp1a
NetworkAclAssociationApp1c:
  Type: AWS::EC2::SubnetNetworkAclAssociation
  Properties:
    NetworkAclId:
      Ref: NetworkAclApp
    SubnetId:
      Ref: SubnetApp1c
NetworkAclDb:
  Type: AWS::EC2::NetworkAcl
  Properties:
    VpcId:
      Ref: Vpc
    Tags:
      - Key: Name
        Value: devio-stg-nacl-db
NetworkAclEntryInboundDb:
  Type: AWS::EC2::NetworkAclEntry
  Properties:
    NetworkAclId:
      Ref: NetworkAclDb
    Protocol: -1
    RuleAction: allow
    RuleNumber: 100
    CidrBlock: 0.0.0.0/0
NetworkAclEntryOutboundDb:
  Type: AWS::EC2::NetworkAclEntry
  Properties:
    NetworkAclId:
      Ref: NetworkAclDb
    Protocol: -1
    RuleAction: allow
    RuleNumber: 100
    CidrBlock: 0.0.0.0/0
    Egress: true
NetworkAclAssociationDb1a:
  Type: AWS::EC2::SubnetNetworkAclAssociation
  Properties:
    NetworkAclId:
      Ref: NetworkAclDb
    SubnetId:
      Ref: SubnetDb1a
NetworkAclAssociationDb1c:
  Type: AWS::EC2::SubnetNetworkAclAssociation
  Properties:
    NetworkAclId:
      Ref: NetworkAclDb
    SubnetId:
      Ref: SubnetDb1c

記録更新です。合計 15 のリソースを作成しました。

なげぇ

GitHub

今回のソースコードは コチラ です。

おわりに

ネットワーク、おしまい!

長かったですね。
以下の順序で作成してきました。(依存関係も書いておきます)

No リソース 必要なリソース
1 VPC なし
2 サブネット VPC
3 インターネットゲートウェイ VPC
4 Elastic IP なし
5 NAT ゲートウェイ サブネット, Elastic IP
6 ルートテーブル VPC, サブネット, インターネットゲートウェイ, NAT ゲートウェイ
7 ネットワーク ACL VPC, サブネット

今後の予定はこちら。

  1. IAM ロール
  2. セキュリティグループ
  3. EC2
  4. ALB
  5. RDS

楽しみですなぁ

リンク