📚

AWS CDK(Python)でコンテナをサクッとたてる

2023/02/17に公開

はじめに

タイトルの通りです。
コンテナをサクッとたてるためのテンプレが欲しくて作りました。
コンテナイメージにはOWASP Juice Shopを指定していますが、コンテナイメージとポートを変更すればおそらく動くはずです。

コード

以下に置いてあります。
https://github.com/raihalea/cdk-owasp-juice

構成

主なリソースはECS/Fargate、ALB、WAFです。
特にECSとALBに代表されるようなよくあるパターンであれば、ECS patternsを利用すると少ない記述量で必要なリソースを作成してくれます。

今回はApplicationLoadBalancedFargateServiceを利用しています。これだけでALBとECS、Fargateのセットを構築します。例えばvpcはOptionalですが、記載しなければCDKが必要なVPCやサブネット、ルートテーブルを作ってくれます。(私がVPCを設定しているのではNAT GWの数をコントロールしたいからです。)

追加でAWS WAFで指定したIPアドレス以外からのアクセスをブロックしています。
セキュリティグループでブロックしてもいいですが、AWS WAFのログを見たかったのでこちらで設定しています。
構成図

簡単な説明

VPC

subnet_typeでそのサブネットのタイプを決定します。
ここではec2.SubnetType.PUBLICec2.SubnetType.PRIVATE_WITH_EGRESSを指定してます。
前者はPublicサブネットとして扱う、後者はPrivateサブネットとして扱いたい場合に指定します。
他にはインターネットに接続しないec2.SubnetType.PRIVATE_ISOLATED、現在は非推奨であるec2.SubnetType.PRIVATE_WITH_NATがあります。
https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_ec2/SubnetType.html

以下の設定では、以下のリソースが作成されます。感覚としてはマネジメントコンソールから作成するのと似ていると思います。

  • VPC x1
  • 2AZ(max_azsは利用する最大のAZ数を指定するもの)
  • Publicサブネット x2
  • Privateサブネット x2
  • NAT GW x1
  • ルートテーブル x4

明記していないもの(例えばCIDRや各リソースの名前)は自動で設定・生成されます。

        vpc = ec2.Vpc(
            self,
            "vpc",
            max_azs=2,
            nat_gateways=1,
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    name="public", subnet_type=ec2.SubnetType.PUBLIC
                ),
                ec2.SubnetConfiguration(
                    name="private", subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
                ),
            ],
        )

ECS

今回はECSに指定のVPCを利用したいためClusterを作成しています。
task_image_optionsでコンテナイメージやポートを指定してますが、from_registryはDockerHubからイメージを取得する場合に使うもので、ECRから取得する場合にはfrom_ecr_repositoryという別の関数があるので注意してください。

        # ECS
        cluster = ecs.Cluster(self, "ecs-cluster", cluster_name="ecs-cluster", vpc=vpc)

        # ECS pattern
        ecsp_service = ecsp.ApplicationLoadBalancedFargateService(
            self,
            "ecs_service",
            cluster=cluster,
            task_image_options=ecsp.ApplicationLoadBalancedTaskImageOptions(
                image=ecs.ContainerImage.from_registry("bkimminich/juice-shop"),
                container_port=3000
            ),
            public_load_balancer=True
        )

WAF

IPSet

AWS WAFのルールで利用するためのIPSetです。単純にIPアドレスのリストです。
addressesのIPアドレスに変更・追加すると、そのIPアドレスからアクセスできるようになります。

        # WAF IPSet
        ipset = wafv2.CfnIPSet(
            self,
            "ipset",
            addresses=["198.51.100.0/24"],
            ip_address_version="IPV4",
            scope="REGIONAL",
            name="myIP",
        )

rules

AWS WAFに設定するルールです。
wafv2.CfnWebACL.RulePropertyで一つのルールを表しています。ルールは複数のステートメントから構成されることもあるのでもっと長くなったり、マネージドルールの場合は記載が変わったりします。
statementがルールのマッチ条件になります。wafv2.CfnWebACL.NotStatementPropertyを間にいれることで、IPSetにマッチしなかったらという条件を作ることができます。次のactionでブロックに設定しているので、IPSetにマッチしない送信元はブロックされます。

            rules=[
                wafv2.CfnWebACL.RuleProperty(
                    name="IPblock",
                    priority=10,
                    visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty(
                        cloud_watch_metrics_enabled=True,
                        metric_name="owasp-waf-ipbloack",
                        sampled_requests_enabled=True,
                    ),
                    statement=wafv2.CfnWebACL.StatementProperty(
                        not_statement=wafv2.CfnWebACL.NotStatementProperty(
                            statement=wafv2.CfnWebACL.StatementProperty(
                                ip_set_reference_statement={"arn": ipset.attr_arn}
                            )
                        )
                    ),
                    action=wafv2.CfnWebACL.RuleActionProperty(
                        block=wafv2.CfnWebACL.BlockActionProperty(
                            custom_response=wafv2.CfnWebACL.CustomResponseProperty(
                                response_code=403
                            )
                        )
                    ),
                )
            ],

Output

ALBのarnを出力しているだけです。特に何か使っているわけではないです。
なお、ALBのDNS名とスキーマがついたURLはECS patternsによって自動的に出力されるようになっています。

        CfnOutput(self, "albarn", value=ecsp_service.load_balancer.load_balancer_arn)

おわりに

試しにWebサーバがたてたい時にはECS patternsが役に立つのではないでしょうか?
ついでにPythonでCDKの情報とAWS WAFをCDKで書いてるサンプルとしても役に立てばうれしいです。
FargateのSpotインスタンス指定は時間ができたら追加しようと思います。

記事中でも述べていますが、AWS WAFはCDK以外の方法で管理するか、aws-samplesを利用するのがよさそうです。

Discussion