実践!AWS CDK #16 セキュリティグループ

題字・息子たち
2021.07.05

はじめに

今回はセキュリティグループを作成していきましょう。
インスタンスの作成まではあと少し。

前回の記事はこちら。

AWS 構成図

こちらは未来の構成図です。

1

これを実現するために ALB, EC2, RDS 用のセキュリティグループをそれぞれ作成します。

設計

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

セキュリティグループ名
devio-stg-sg-alb
devio-stg-sg-ec2
devio-stg-sg-rds

アウトバウンドルールはデフォルトとし、すべての通信を許可します。
インバウンドルールのみ設定していきます。

以下詳細。

devio-stg-sg-alb

タイプ プロトコル ポート範囲 ソース
HTTP TCP 80 0.0.0.0/0
HTTPS TCP 443 0.0.0.0/0

ALB に関連付けるセキュリティグループ。
任意の IPv4 アドレスからの HTTP, HTTPS アクセスを許可します。

devio-stg-sg-ec2

タイプ プロトコル ポート範囲 ソース
HTTP TCP 80 devio-stg-sg-alb

EC2 に関連付けるセキュリティグループ。
ALB からの HTTP アクセスのみを許可します。

devio-stg-sg-rds

タイプ プロトコル ポート範囲 ソース
MYSQL/Aurora TCP 3306 devio-stg-sg-ec2

RDS に関連付けるセキュリティグループ。
EC2 からのアクセスのみを許可します。

実装

セキュリティグループに関する処理を行うクラスはこちら。

lib/resource/securityGroup.ts

import * as cdk from '@aws-cdk/core';
import { CfnSecurityGroup, CfnSecurityGroupIngress, CfnSecurityGroupIngressProps, CfnVPC } from '@aws-cdk/aws-ec2';
import { Resource } from './abstract/resource';

interface IngressInfo {
    readonly id: string;
    readonly securityGroupIngressProps: CfnSecurityGroupIngressProps;
    readonly groupId: () => string;
    readonly sourceSecurityGroupId?: () => string;
}

interface ResourceInfo {
    readonly id: string;
    readonly groupDescription: string;
    readonly ingresses: IngressInfo[];
    readonly resourceName: string;
    readonly assign: (securityGroup: CfnSecurityGroup) => void;
}

export class SecurityGroup extends Resource {
    public alb: CfnSecurityGroup;
    public ec2: CfnSecurityGroup;
    public rds: CfnSecurityGroup;

    private readonly vpc: CfnVPC;
    private readonly resources: ResourceInfo[] = [
        {
            id: 'SecurityGroupAlb',
            groupDescription: 'for ALB',
            ingresses: [
                {
                    id: 'SecurityGroupIngressAlb1',
                    securityGroupIngressProps: {
                        ipProtocol: 'tcp',
                        cidrIp: '0.0.0.0/0',
                        fromPort: 80,
                        toPort: 80
                    },
                    groupId: () => this.alb.attrGroupId
                },
                {
                    id: 'SecurityGroupIngressAlb2',
                    securityGroupIngressProps: {
                        ipProtocol: 'tcp',
                        cidrIp: '0.0.0.0/0',
                        fromPort: 443,
                        toPort: 443
                    },
                    groupId: () => this.alb.attrGroupId
                }
            ],
            resourceName: 'sg-alb',
            assign: securityGroup => this.alb = securityGroup
        },
        {
            id: 'SecurityGroupEc2',
            groupDescription: 'for EC2',
            ingresses: [
                {
                    id: 'SecurityGroupIngressEc21',
                    securityGroupIngressProps: {
                        ipProtocol: 'tcp',
                        fromPort: 80,
                        toPort: 80
                    },
                    groupId: () => this.ec2.attrGroupId,
                    sourceSecurityGroupId: () => this.alb.attrGroupId,
                }
            ],
            resourceName: 'sg-ec2',
            assign: securityGroup => this.ec2 = securityGroup
        },
        {
            id: 'SecurityGroupRds',
            groupDescription: 'for RDS',
            ingresses: [
                {
                    id: 'SecurityGroupIngressRds1',
                    securityGroupIngressProps: {
                        ipProtocol: 'tcp',
                        fromPort: 3306,
                        toPort: 3306
                    },
                    groupId: () => this.rds.attrGroupId,
                    sourceSecurityGroupId: () => this.ec2.attrGroupId,
                }
            ],
            resourceName: 'sg-rds',
            assign: securityGroup => this.rds = securityGroup
        }
    ];

    constructor(vpc: CfnVPC) {
        super();
        this.vpc = vpc;
    };

    createResources(scope: cdk.Construct) {
        for (const resourceInfo of this.resources) {
            const securityGroup = this.createSecurityGroup(scope, resourceInfo);
            resourceInfo.assign(securityGroup);

            this.createSecurityGroupIngress(scope, resourceInfo);
        }
    }

    private createSecurityGroup(scope: cdk.Construct, resourceInfo: ResourceInfo): CfnSecurityGroup {
        const resourceName = this.createResourceName(scope, resourceInfo.resourceName);
        const securityGroup = new CfnSecurityGroup(scope, resourceInfo.id, {
            groupDescription: resourceInfo.groupDescription,
            groupName: resourceName,
            vpcId: this.vpc.ref,
            tags: [{
                key: 'Name',
                value: resourceName
            }]
        });

        return securityGroup;
    }

    private createSecurityGroupIngress(scope: cdk.Construct, resourceInfo: ResourceInfo) {
        for (const ingress of resourceInfo.ingresses) {
            const securityGroupIngress = new CfnSecurityGroupIngress(scope, ingress.id, ingress.securityGroupIngressProps);
            securityGroupIngress.groupId = ingress.groupId();

            if (ingress.sourceSecurityGroupId) {
                securityGroupIngress.sourceSecurityGroupId = ingress.sourceSecurityGroupId();
            }
        }
    }
}

インバウンドルールの実装が少し冗長になってしまいました。
CfnSecurityGroup でセキュリティグループを作成したのち、CfnSecurityGroupIngress を利用してインバウンドルールの設定を行っています。

private createSecurityGroupIngress(scope: cdk.Construct, resourceInfo: ResourceInfo) {
    for (const ingress of resourceInfo.ingresses) {
        const securityGroupIngress = new CfnSecurityGroupIngress(scope, ingress.id, ingress.securityGroupIngressProps);
        securityGroupIngress.groupId = ingress.groupId();

        if (ingress.sourceSecurityGroupId) {
            securityGroupIngress.sourceSecurityGroupId = ingress.sourceSecurityGroupId();
        }
    }
}

ハイライト部分で、作成したセキュリティグループのグループ ID を指定することで関連付けをしています。
本当は CfnSecurityGroup のインスタンス作成時に CfnSecurityGroupPropssecurityGroupIngress というプロパティでルールの指定を行いたかったのですが、現在の私のプログラム構成では上手くできませんでした。無念

今回使用した CfnSecurityGroupIngress は CFn の AWS::EC2::SecurityGroupIngress に対応し、こっちでやりたかったなぁという IngressProperty は CFn の AWS::EC2::SecurityGroup Ingress に対応しています。
色々試したおかげで、セキュリティグループのインバウンドルール設定方法は 2 パターンあるということがわかりました。よかった

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

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';
import { IamRole } from './resource/iamRole';
import { SecurityGroup } from './resource/securityGroup';

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);

    ~ 省略 ~

    // Security Group
    const securityGroup = new SecurityGroup(vpc.vpc);
    securityGroup.createResources(this);
  }
}

テスト

テストコードはこちら。

test/resource/securityGroup.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('SecurityGroup', () => {
    const app = new cdk.App();
    const stack = new Devio.DevioStack(app, 'DevioStack');

    expect(stack).to(countResources('AWS::EC2::SecurityGroup', 3));
    expect(stack).to(haveResource('AWS::EC2::SecurityGroup', {
        GroupDescription: 'for ALB',
        GroupName: 'undefined-undefined-sg-alb',
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-sg-alb' }]
    }));
    expect(stack).to(haveResource('AWS::EC2::SecurityGroup', {
        GroupDescription: 'for EC2',
        GroupName: 'undefined-undefined-sg-ec2',
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-sg-ec2' }]
    }));
    expect(stack).to(haveResource('AWS::EC2::SecurityGroup', {
        GroupDescription: 'for RDS',
        GroupName: 'undefined-undefined-sg-rds',
        VpcId: anything(),
        Tags: [{ 'Key': 'Name', 'Value': 'undefined-undefined-sg-rds' }]
    }));

    expect(stack).to(countResources('AWS::EC2::SecurityGroupIngress', 4));
    expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', {
        IpProtocol: 'tcp',
        CidrIp: '0.0.0.0/0',
        FromPort: 80,
        ToPort: 80,
        GroupId: anything()
    }));
    expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', {
        IpProtocol: 'tcp',
        CidrIp: '0.0.0.0/0',
        FromPort: 443,
        ToPort: 443,
        GroupId: anything()
    }));
    expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', {
        IpProtocol: 'tcp',
        FromPort: 80,
        ToPort: 80,
        GroupId: anything(),
        SourceSecurityGroupId: anything()
    }));
    expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', {
        IpProtocol: 'tcp',
        FromPort: 3306,
        ToPort: 3306,
        GroupId: anything(),
        SourceSecurityGroupId: anything()
    }));
});

以下を確認しています。

  • セキュリティグループリソースが 3 つあること
  • インバウンドルールリソースが 4 つあること
  • 各リソースのプロパティが正しいこと

確認

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

2

バッチリできております。

ALB のインバウンドルールも OK。

3

アウトバウンドルールもすべて許可(デフォルト)になっています。

4

EC2 のインバウンドルールは ALB からの 80 番ポートのみ許可。

5

RDS も OK です。

6

CloudFormation 版

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

SecurityGroupAlb:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: for ALB
    GroupName: devio-stg-sg-alb
    Tags:
      - Key: Name
        Value: devio-stg-sg-alb
    VpcId:
      Ref: Vpc
SecurityGroupIngressAlb1:
  Type: AWS::EC2::SecurityGroupIngress
  Properties:
    IpProtocol: tcp
    CidrIp: 0.0.0.0/0
    FromPort: 80
    GroupId:
      Fn::GetAtt:
        - SecurityGroupAlb
        - GroupId
    ToPort: 80
SecurityGroupIngressAlb2:
  Type: AWS::EC2::SecurityGroupIngress
  Properties:
    IpProtocol: tcp
    CidrIp: 0.0.0.0/0
    FromPort: 443
    GroupId:
      Fn::GetAtt:
        - SecurityGroupAlb
        - GroupId
    ToPort: 443
SecurityGroupEc2:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: for EC2
    GroupName: devio-stg-sg-ec2
    Tags:
      - Key: Name
        Value: devio-stg-sg-ec2
    VpcId:
      Ref: Vpc
SecurityGroupIngressEc21:
  Type: AWS::EC2::SecurityGroupIngress
  Properties:
    IpProtocol: tcp
    FromPort: 80
    GroupId:
      Fn::GetAtt:
        - SecurityGroupEc2
        - GroupId
    SourceSecurityGroupId:
      Fn::GetAtt:
        - SecurityGroupAlb
        - GroupId
    ToPort: 80
SecurityGroupRds:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: for RDS
    GroupName: devio-stg-sg-rds
    Tags:
      - Key: Name
        Value: devio-stg-sg-rds
    VpcId:
      Ref: Vpc
SecurityGroupIngressRds1:
  Type: AWS::EC2::SecurityGroupIngress
  Properties:
    IpProtocol: tcp
    FromPort: 3306
    GroupId:
      Fn::GetAtt:
        - SecurityGroupRds
        - GroupId
    SourceSecurityGroupId:
      Fn::GetAtt:
        - SecurityGroupEc2
        - GroupId
    ToPort: 3306

GitHub

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

おわりに

ようやく下準備が完了しました。
これからはインスタンスまわりを作成していきましょう!

次回のお題は「EC2」です。

リンク