Skip to content

WebApp Stack

The WebApp class is the main CDK stack for deploying containerized web applications.

Overview

WebApp is a complete AWS CDK Stack that creates and configures all resources needed to run a containerized web application, including:

  • ECS Fargate cluster and service
  • Application Load Balancer with HTTPS
  • Route53 DNS records and ACM certificates
  • Optional Cognito authentication
  • WAF integration
  • CloudWatch logging

WebApp

gds_idea_cdk_constructs.web_app.stack.WebApp

Bases: BaseWebStack

A configurable web application stack with a simplified API for authentication.

Source code in src/gds_idea_cdk_constructs/web_app/stack.py
class WebApp(BaseWebStack):
    """
    A configurable web application stack with a simplified API for authentication.
    """

    def __init__(
        self,
        scope: Construct,
        deployment_config: DeploymentConfig,
        app_config: AppConfig,
        authentication: AuthType = AuthType.COGNITO,
        docker_context_path: str = ".",
        dockerfile_path: str = "app_src/Dockerfile",
        container_props: WebAppContainerProperties | None = None,
        task_role: iam.Role | None = None,
        disable_waf: bool = False,
        cross_account_access: bool = False,
        enable_usage_dashboard: bool = True,
        dashboard_properties: DashboardProperties | None = None,
    ) -> None:
        """Initialize a WebApp stack with containerized application infrastructure.

        Creates a complete web application deployment including ECS Fargate service,
        Application Load Balancer with HTTPS, Route53 DNS records, ACM certificate,
        and optional Cognito authentication.

        Args:
            scope: The CDK app or stack to create this stack within.
            deployment_config: Environment-specific configuration including VPC,
                domain name, and AWS resource identifiers.
            app_config: Application configuration including name, framework, and
                health check settings.
            authentication: Authentication strategy to use. Defaults to
                AuthType.COGNITO. Options: COGNITO, INTERNAL_ACCESS, or NONE.
            docker_context_path: Path to the Docker build context directory.
                Defaults to current directory (".").
            dockerfile_path: Path to the Dockerfile relative to docker_context_path.
                Defaults to "app_src/Dockerfile".
            container_props: Custom container configuration (CPU, memory, count, etc.).
                If None, uses default values from WebAppContainerProperties.
            task_role: Custom IAM role for the ECS task. If None, a minimal
                role will be created with permissions required by the
                authentication strategy. If provided, the strategy will augment
                it with necessary permissions.
            disable_waf: Disable WAF association with the ALB. Defaults to False.
                When True, the Web Application Firewall will NOT be associated with
                the Application Load Balancer. **WARNING: This should ONLY be used
                for short-term debugging when WAF rules are blocking legitimate traffic.
                Never use in production. Disabling WAF removes critical security
                protections against common web exploits.**
            cross_account_access: Enable cross-account access to production resources.
                Defaults to False. When True and deploying to a non-production
                environment, grants the task role sts:AssumeRole permission on
                the cross-account role and injects CROSS_ACCOUNT_ROLE_ARN as a
                container environment variable.
            enable_usage_dashboard: When ``True`` (default), create a standard
                CloudWatch usage dashboard for this app. See
                :class:`AppUsageDashboard` for the widgets included and
                :class:`DashboardProperties` for user-tunable knobs.
            dashboard_properties: Optional overrides for the usage dashboard —
                name, per-user email disclosure, log filter pattern and extra
                widgets. See :class:`DashboardProperties`. Ignored when
                ``enable_usage_dashboard`` is ``False``.

        Example:
            Basic usage with Cognito authentication::

                app = App()
                deployment_config = DeploymentConfig(cdk_env)
                app_config = AppConfig.from_pyproject()

                WebApp(
                    app,
                    deployment_config,
                    app_config,
                    authentication=AuthType.COGNITO,
                    docker_context_path=".",
                    dockerfile_path="Dockerfile",
                )

        Note:
            The stack automatically creates all required infrastructure including
            VPC subnets lookup, DNS hosted zone, SSL certificate, load balancer,
            ECS cluster lookup, Fargate task definition and service, and optional
            Cognito user pool client configuration.
        """
        super().__init__(scope, deployment_config, app_config, authentication)

        self.container_props = (
            container_props or WebAppContainerProperties()
        )  # Load the default values

        # Configure task role based on whether a custom role is provided
        if task_role:
            # Custom role provided - augment it with auth-specific permissions
            self.task_role = task_role
            self._auth_strategy.configure_role_permissions(self.task_role)
        else:
            # No custom role - let strategy create a properly configured one
            self.task_role = self._auth_strategy.get_minimal_role()

        # Let users assume the role if we are deploying in dev.
        if self.deployment_config.environment == DeploymentEnvironment.DEVELOPMENT:
            self._add_assume_policy_for_dev()

        # Cross-account access to production resources from non-prod environments
        self._cross_account_env: dict[str, str] = {}
        if cross_account_access:
            self._setup_cross_account_access()

        logger.info(
            f"Creating web app: {self.app_name} with authentication: {authentication}"
        )
        logger.info(f"Domain: {self.alb_domain_name}")

        self._import_existing_resources()
        self._setup_dns_and_certificate()
        self._setup_acm_clean_up()
        self._setup_ecs_resources(docker_context_path, dockerfile_path)
        self._setup_load_balancer()
        self._setup_dns_record()

        if disable_waf:
            logging.warning(
                "WAF is disabled. This should only be used for short-term debugging. "
                "Never use in production."
            )
        else:
            self._associate_waf()

        self.usage_dashboard: AppUsageDashboard | None = None
        if enable_usage_dashboard:
            self._setup_usage_dashboard(
                properties=dashboard_properties or DashboardProperties(),
            )

        self._create_outputs()

    def _setup_cross_account_access(self) -> None:
        """Grant the task role permission to assume the cross-account role
        and inject the role ARN as a container environment variable."""
        role_arn = self.deployment_config.cross_account_role_arn
        if role_arn is None:
            logger.info(
                "Cross-account access enabled but no role configured "
                "for this environment — skipping"
            )
            return

        self.task_role.add_to_policy(
            iam.PolicyStatement(
                actions=["sts:AssumeRole"],
                resources=[role_arn],
            )
        )
        self._cross_account_env = {"CROSS_ACCOUNT_ROLE_ARN": role_arn}
        logger.info(f"Cross-account access enabled: {role_arn}")

    def _setup_ecs_resources(
        self, docker_context_path: str, dockerfile_path: str
    ) -> None:
        """Create ECS Fargate task definition, container, and service."""
        cpu = self.container_props.cpu
        memory = self.container_props.memory_limit_mib
        desired_count = self.container_props.desired_count
        container_port = self.container_props.container_port
        environment = self.container_props.environment_variables
        health_check_grace_period = self.container_props.health_check_grace_period
        min_healthy_percent = self.container_props.min_healthy_percent

        # Look up the cluster
        self.cluster = ecs.Cluster.from_cluster_attributes(
            self,
            id="Cluster",
            cluster_name=self.deployment_config.cluster_name,
            vpc=self.vpc,
        )

        self.task_definition = ecs.FargateTaskDefinition(
            self,
            "TaskDefinition",
            memory_limit_mib=memory,
            cpu=cpu,
            task_role=self.task_role,
        )

        self.log_group = logs.LogGroup(
            self,
            "ContainerLogGroup",
            retention=logs.RetentionDays.ONE_YEAR,
            removal_policy=RemovalPolicy.DESTROY,
        )

        self.container = self.task_definition.add_container(
            "Container",
            image=ecs.ContainerImage.from_asset(
                docker_context_path, file=dockerfile_path, platform=Platform.LINUX_AMD64
            ),
            port_mappings=[ecs.PortMapping(container_port=container_port)],
            logging=ecs.LogDrivers.aws_logs(
                stream_prefix=f"{self.app_name}-app",
                log_group=self.log_group,
            ),
            environment={
                **self._auth_strategy.get_environment_variables(),
                **self._cross_account_env,
                **environment,
            },
        )

        self.fargate_service = ecs.FargateService(
            self,
            "FargateService",
            cluster=self.cluster,
            task_definition=self.task_definition,
            desired_count=desired_count,
            vpc_subnets=ec2.SubnetSelection(subnets=self.vpc.private_subnets),
            assign_public_ip=True,
            circuit_breaker=ecs.DeploymentCircuitBreaker(enable=True, rollback=True),
            health_check_grace_period=Duration.seconds(health_check_grace_period),
            min_healthy_percent=min_healthy_percent,
        )

    def _setup_load_balancer(self) -> None:
        """Create target group for ECS service and set up ALB with listeners."""
        health_check_path = (
            self.container_props.health_check_path or self.app_config.health_check_path
        )

        self.target_group = elbv2.ApplicationTargetGroup(
            self,
            "TargetGroup",
            vpc=self.vpc,
            port=80,
            protocol=elbv2.ApplicationProtocol.HTTP,
            targets=[self.fargate_service],
            health_check={"path": health_check_path},
        )

        self._setup_alb_and_listeners(self.target_group)

    def _create_outputs(self) -> None:
        """Create base outputs and delegate to the strategy for specific outputs."""
        CfnOutput(
            self,
            "ApplicationURL",
            value=f"https://{self.alb_domain_name}",
            description=f"Application URL for {self.app_name}",
        )

        CfnOutput(
            self,
            "TaskRoleARN",
            value=f"{self.task_role.role_arn}",
            description="Role assumed by the task container. If DEV can be assumed",
        )

        self._auth_strategy.create_outputs()

    def _setup_usage_dashboard(
        self,
        *,
        properties: DashboardProperties,
    ) -> None:
        """Create a CloudWatch usage dashboard for this app, reading its own ALB
        metrics and container authentication logs."""
        self.usage_dashboard = AppUsageDashboard(
            self,
            "UsageDashboard",
            app_name=self.app_name,
            stage=self.deployment_config.environment.name.lower(),
            load_balancer=self.load_balancer,
            log_groups=[self.log_group],
            properties=properties,
        )

__init__

__init__(
    scope: Construct,
    deployment_config: DeploymentConfig,
    app_config: AppConfig,
    authentication: AuthType = AuthType.COGNITO,
    docker_context_path: str = ".",
    dockerfile_path: str = "app_src/Dockerfile",
    container_props: WebAppContainerProperties
    | None = None,
    task_role: Role | None = None,
    disable_waf: bool = False,
    cross_account_access: bool = False,
    enable_usage_dashboard: bool = True,
    dashboard_properties: DashboardProperties | None = None,
) -> None

Initialize a WebApp stack with containerized application infrastructure.

Creates a complete web application deployment including ECS Fargate service, Application Load Balancer with HTTPS, Route53 DNS records, ACM certificate, and optional Cognito authentication.

Parameters:

Name Type Description Default
scope Construct

The CDK app or stack to create this stack within.

required
deployment_config DeploymentConfig

Environment-specific configuration including VPC, domain name, and AWS resource identifiers.

required
app_config AppConfig

Application configuration including name, framework, and health check settings.

required
authentication AuthType

Authentication strategy to use. Defaults to AuthType.COGNITO. Options: COGNITO, INTERNAL_ACCESS, or NONE.

COGNITO
docker_context_path str

Path to the Docker build context directory. Defaults to current directory (".").

'.'
dockerfile_path str

Path to the Dockerfile relative to docker_context_path. Defaults to "app_src/Dockerfile".

'app_src/Dockerfile'
container_props WebAppContainerProperties | None

Custom container configuration (CPU, memory, count, etc.). If None, uses default values from WebAppContainerProperties.

None
task_role Role | None

Custom IAM role for the ECS task. If None, a minimal role will be created with permissions required by the authentication strategy. If provided, the strategy will augment it with necessary permissions.

None
disable_waf bool

Disable WAF association with the ALB. Defaults to False. When True, the Web Application Firewall will NOT be associated with the Application Load Balancer. WARNING: This should ONLY be used for short-term debugging when WAF rules are blocking legitimate traffic. Never use in production. Disabling WAF removes critical security protections against common web exploits.

False
cross_account_access bool

Enable cross-account access to production resources. Defaults to False. When True and deploying to a non-production environment, grants the task role sts:AssumeRole permission on the cross-account role and injects CROSS_ACCOUNT_ROLE_ARN as a container environment variable.

False
enable_usage_dashboard bool

When True (default), create a standard CloudWatch usage dashboard for this app. See :class:AppUsageDashboard for the widgets included and :class:DashboardProperties for user-tunable knobs.

True
dashboard_properties DashboardProperties | None

Optional overrides for the usage dashboard — name, per-user email disclosure, log filter pattern and extra widgets. See :class:DashboardProperties. Ignored when enable_usage_dashboard is False.

None
Example

Basic usage with Cognito authentication::

app = App()
deployment_config = DeploymentConfig(cdk_env)
app_config = AppConfig.from_pyproject()

WebApp(
    app,
    deployment_config,
    app_config,
    authentication=AuthType.COGNITO,
    docker_context_path=".",
    dockerfile_path="Dockerfile",
)
Note

The stack automatically creates all required infrastructure including VPC subnets lookup, DNS hosted zone, SSL certificate, load balancer, ECS cluster lookup, Fargate task definition and service, and optional Cognito user pool client configuration.

Source code in src/gds_idea_cdk_constructs/web_app/stack.py
def __init__(
    self,
    scope: Construct,
    deployment_config: DeploymentConfig,
    app_config: AppConfig,
    authentication: AuthType = AuthType.COGNITO,
    docker_context_path: str = ".",
    dockerfile_path: str = "app_src/Dockerfile",
    container_props: WebAppContainerProperties | None = None,
    task_role: iam.Role | None = None,
    disable_waf: bool = False,
    cross_account_access: bool = False,
    enable_usage_dashboard: bool = True,
    dashboard_properties: DashboardProperties | None = None,
) -> None:
    """Initialize a WebApp stack with containerized application infrastructure.

    Creates a complete web application deployment including ECS Fargate service,
    Application Load Balancer with HTTPS, Route53 DNS records, ACM certificate,
    and optional Cognito authentication.

    Args:
        scope: The CDK app or stack to create this stack within.
        deployment_config: Environment-specific configuration including VPC,
            domain name, and AWS resource identifiers.
        app_config: Application configuration including name, framework, and
            health check settings.
        authentication: Authentication strategy to use. Defaults to
            AuthType.COGNITO. Options: COGNITO, INTERNAL_ACCESS, or NONE.
        docker_context_path: Path to the Docker build context directory.
            Defaults to current directory (".").
        dockerfile_path: Path to the Dockerfile relative to docker_context_path.
            Defaults to "app_src/Dockerfile".
        container_props: Custom container configuration (CPU, memory, count, etc.).
            If None, uses default values from WebAppContainerProperties.
        task_role: Custom IAM role for the ECS task. If None, a minimal
            role will be created with permissions required by the
            authentication strategy. If provided, the strategy will augment
            it with necessary permissions.
        disable_waf: Disable WAF association with the ALB. Defaults to False.
            When True, the Web Application Firewall will NOT be associated with
            the Application Load Balancer. **WARNING: This should ONLY be used
            for short-term debugging when WAF rules are blocking legitimate traffic.
            Never use in production. Disabling WAF removes critical security
            protections against common web exploits.**
        cross_account_access: Enable cross-account access to production resources.
            Defaults to False. When True and deploying to a non-production
            environment, grants the task role sts:AssumeRole permission on
            the cross-account role and injects CROSS_ACCOUNT_ROLE_ARN as a
            container environment variable.
        enable_usage_dashboard: When ``True`` (default), create a standard
            CloudWatch usage dashboard for this app. See
            :class:`AppUsageDashboard` for the widgets included and
            :class:`DashboardProperties` for user-tunable knobs.
        dashboard_properties: Optional overrides for the usage dashboard —
            name, per-user email disclosure, log filter pattern and extra
            widgets. See :class:`DashboardProperties`. Ignored when
            ``enable_usage_dashboard`` is ``False``.

    Example:
        Basic usage with Cognito authentication::

            app = App()
            deployment_config = DeploymentConfig(cdk_env)
            app_config = AppConfig.from_pyproject()

            WebApp(
                app,
                deployment_config,
                app_config,
                authentication=AuthType.COGNITO,
                docker_context_path=".",
                dockerfile_path="Dockerfile",
            )

    Note:
        The stack automatically creates all required infrastructure including
        VPC subnets lookup, DNS hosted zone, SSL certificate, load balancer,
        ECS cluster lookup, Fargate task definition and service, and optional
        Cognito user pool client configuration.
    """
    super().__init__(scope, deployment_config, app_config, authentication)

    self.container_props = (
        container_props or WebAppContainerProperties()
    )  # Load the default values

    # Configure task role based on whether a custom role is provided
    if task_role:
        # Custom role provided - augment it with auth-specific permissions
        self.task_role = task_role
        self._auth_strategy.configure_role_permissions(self.task_role)
    else:
        # No custom role - let strategy create a properly configured one
        self.task_role = self._auth_strategy.get_minimal_role()

    # Let users assume the role if we are deploying in dev.
    if self.deployment_config.environment == DeploymentEnvironment.DEVELOPMENT:
        self._add_assume_policy_for_dev()

    # Cross-account access to production resources from non-prod environments
    self._cross_account_env: dict[str, str] = {}
    if cross_account_access:
        self._setup_cross_account_access()

    logger.info(
        f"Creating web app: {self.app_name} with authentication: {authentication}"
    )
    logger.info(f"Domain: {self.alb_domain_name}")

    self._import_existing_resources()
    self._setup_dns_and_certificate()
    self._setup_acm_clean_up()
    self._setup_ecs_resources(docker_context_path, dockerfile_path)
    self._setup_load_balancer()
    self._setup_dns_record()

    if disable_waf:
        logging.warning(
            "WAF is disabled. This should only be used for short-term debugging. "
            "Never use in production."
        )
    else:
        self._associate_waf()

    self.usage_dashboard: AppUsageDashboard | None = None
    if enable_usage_dashboard:
        self._setup_usage_dashboard(
            properties=dashboard_properties or DashboardProperties(),
        )

    self._create_outputs()

Usage Examples

Minimal Example

from aws_cdk import App
from gds_idea_cdk_constructs.config import DeploymentConfig, AppConfig
from gds_idea_cdk_constructs.web_app import WebApp, AuthType

app = App()

deployment_config = DeploymentConfig(cdk_env)
app_config = AppConfig(app_name="simple-app", framework="streamlit")

WebApp(
    app,
    deployment_config=deployment_config,
    app_config=app_config,
    authentication=AuthType.INTERNAL_ACCESS,
    docker_context_path=".",
    dockerfile_path="Dockerfile",
)

app.synth()

Public Application

WebApp(
    app,
    deployment_config=deployment_config,
    app_config=app_config,
    authentication=AuthType.NONE,  # No authentication
    docker_context_path="./app",
    dockerfile_path="app/Dockerfile",
)

Advanced Configuration

from gds_idea_cdk_constructs.web_app import WebAppContainerProperties
import aws_cdk.aws_iam as iam

# Custom container configuration
container_props = WebAppContainerProperties(
    cpu=512,
    memory_limit_mib=1024,
    desired_count=2,
    container_port=8501,
    health_check_path="/_stcore/health",
    environment_variables={
        "LOG_LEVEL": "INFO",
        "DATABASE_URL": "postgresql://...",
    },
)
class MyBackEnd(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs):
        super().__init__(scope, construct_id, **kwargs)

    # ... creation of backend

    # Create custom role
    self.task_role = iam.Role(
        self,
        "CustomTaskRole",
        assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
    )

    # Grant S3 access
    task_role.add_to_policy(
        iam.PolicyStatement(
            actions=["s3:GetObject", "s3:PutObject"],
            resources=["arn:aws:s3:::my-bucket/*"],
        )
    )

backend = MyBackEnd(app, "MyBackEnd")

# Create stack with advanced options
WebApp(
    app,
    deployment_config=deployment_config,
    app_config=app_config,
    authentication=AuthType.INTERNAL_ACCESS,
    docker_context_path=".",
    dockerfile_path="Dockerfile",
    container_props=container_props,
    task_role=task_role,
)

Debugging with WAF Disabled

Security Warning

Only for debugging - Never use disable_waf=True in production environments. This removes critical security protections against web exploits.

When troubleshooting WAF rule blocks during development, you can temporarily disable WAF:

# TEMPORARY DEBUGGING ONLY
WebApp(
    app,
    deployment_config=deployment_config,
    app_config=app_config,
    authentication=AuthType.INTERNAL_ACCESS,
    docker_context_path=".",
    dockerfile_path="Dockerfile",
    disable_waf=True,  # WARNING: Removes security protections!
)

Use cases: - Debugging legitimate traffic being blocked by WAF rules - Isolating whether issues are caused by WAF vs application code - Short-term testing during WAF rule development

Best practices: - Time-box usage (e.g., "disable for 1 hour while testing") - Never commit disable_waf=True to version control - Re-enable immediately after debugging - Consider adjusting WAF rules instead of disabling entirely

Created Resources

When you create a WebApp, the following AWS resources are automatically created:

Networking

  • VPC (imported) - Uses existing VPC from deployment config
  • Subnets - Uses public subnets for Fargate tasks
  • Security Groups - Automatically configured for ALB and ECS

Compute

  • ECS Cluster - Fargate cluster for running containers
  • Task Definition - Container configuration
  • Fargate Service - Managed service with desired count

Load Balancing

  • Application Load Balancer - Internet-facing ALB
  • Target Group - Routes traffic to ECS tasks
  • Listeners:
    • HTTP (port 80) - Redirects to HTTPS
    • HTTPS (port 443) - Forwards to target group

DNS & TLS

  • Route53 Hosted Zone - Subdomain for your application
  • NS Record - Links subdomain to parent hosted zone
  • A Record - Points domain to load balancer
  • ACM Certificate - TLS certificate with DNS validation

Authentication (Cognito only)

  • User Pool Client - OAuth2 client for ALB authentication
  • Secrets Manager Secret - Stores client credentials

Security

  • WAF Association (optional) - By default, links the environment's WAF Web ACL to the Application Load Balancer, providing protection against common web exploits including:

    • SQL injection attacks
    • Cross-site scripting (XSS)
    • HTTP floods and DDoS attempts
    • Known malicious IP addresses

    The WAF can be temporarily disabled using disable_waf=True for debugging purposes. Never disable WAF in production environments.

  • IAM Roles:

    • Task Role - For application permissions (can be custom or auto-generated)
    • Execution Role - For ECS to pull images and write logs
  • TLS/HTTPS Enforcement - All HTTP traffic is automatically redirected to HTTPS

  • Security Groups - Automatically configured with least-privilege access

Monitoring

  • CloudWatch Log Group - Container logs
  • S3 Access Logs - ALB access logs

CloudFormation Outputs

The stack creates the following outputs:

  • ApplicationURL - HTTPS URL for your application
  • TaskRoleARN - ARN of the ECS task role (can be assumed in DEV)
  • CognitoClientId (Cognito only) - OAuth2 client ID

Development Features

Dev Environment Assume Role

In the development environment, the task role can be assumed by developers with *-poweraccess or *-admin roles. This enables:

  • Local testing with AWS credentials
  • Debugging with production-like IAM permissions
  • Development without modifying production policies

This feature is disabled in production environments.

Cross-Account Access

When cross_account_access=True is set, the construct enables the ECS task to assume a cross-account role for accessing production data resources from non-production environments.

WebApp(
    app,
    deployment_config=deployment_config,
    app_config=app_config,
    cross_account_access=True,
)

What it does (non-production only):

  • Grants sts:AssumeRole permission to the task role for the cross-account role
  • Injects CROSS_ACCOUNT_ROLE_ARN environment variable into the container

In production: No action is taken regardless of the flag value — the task role should have direct access to resources via IAM policies.

App-side usage: Applications should read CROSS_ACCOUNT_ROLE_ARN from the environment and assume the role when creating boto3 sessions. When the variable is not set (production), a default session with direct credentials is used instead.