# terraform-internal-spa

## Overview

This module provides functionality to host Single Page Applications (SPAs) on AWS S3 with private access through an Application Load Balancer and VPC endpoints. It can be used for internal web applications that require private access within a VPC, whose bundles are themselves not publicly accessible.

This module follows a few conventions:

- Services will be named `var.environment-var.service_name` (e.g., dev-application-name)
- S3 buckets will be named using `var.fully_qualified_domain_name` (e.g., application-name.dev.theorchard.io)
- Security groups will be named `var.environment-var.service_name-load-balancer-security-group`
- The module uses existing S3 interface VPC endpoints rather than creating new ones

## Resources

#### The module manages the following:

- S3 buckets for SPA hosting (via terraform-s3 module)
- Application Load Balancer (internal)
  - HTTPS listener with SSL/TLS termination
  - Target group pointing to S3 VPC endpoint ENIs
  - Listener rules for static paths and URL rewriting
- Security groups for load balancer access control
- WAF association for web application protection
- Route53 DNS records (optional)
- URL rewriting for client-side routing support

#### The module does not manage the following:

- VPC endpoints (assumes existing S3 interface endpoint)
- SSL/TLS certificates (uses existing ACM certificates)
- Subnets (assumes they already exist and are specified by `var.load_balancer_subnet_ids`)
- Deployments of SPA files to S3 (handled externally)

## Workflow

### Providers

The module requires an explicitly defined second AWS provider to create DNS records in the shared networking account or in the same account, depending on your setup.
This provider must be defined in the same directory as the module. If you are creating records in the networking account, use the `networking` profile. If you are creating records in the same account, you can use the default provider.

The DNS provider should be defined as follows:

```hcl
provider "aws" {
  region = var.aws_region
}

provider "aws" {
  region  = var.aws_region
  alias   = "networking"
  profile = "networking"
}

module "internal_spa" {
  source = "git@github.com:theorchard/terraform-internal-spa.git?ref=1.0.0"

  providers = {
    aws.dns = aws.networking
  }
  
  environment                   = "dev"
  service_name                  = "application-name"
  application_family            = "web"
  vpc_id                        = "vpc-12345678"
  load_balancer_subnet_ids      = ["subnet-11111111", "subnet-22222222"]
  fully_qualified_domain_name   = "application-name.somedomain.com"
  certificate_domain            = "*.somedomain.com"
  ingress_prefix_list_names     = ["vpn-ny-users"]
  
  # Optional Route53 record creation
  route53_record_enabled = true
  route53_zone_name      = "somedomain.com"
  
  // other settings
}
```

Alternatively, if you are creating records in the same account, you can use the default provider as follows:

```hcl
provider "aws" {
  region = var.aws_region
}

module "internal_spa" {
  source = "git@github.com:theorchard/terraform-internal-spa.git?ref=1.0.0"

  providers = {
    aws.dns = aws
  }
  
  environment                   = "dev"
  service_name                  = "application-name"
  application_family            = "web"
  vpc_id                        = "vpc-12345678"
  load_balancer_subnet_ids      = ["subnet-11111111", "subnet-22222222"]
  fully_qualified_domain_name   = "application-name.dev.theorchard.io"
  certificate_domain            = "*.dev.theorchard.io"
  ingress_prefix_list_names     = ["vpn-ny-users"]
  
  // other settings
}
```

If you are not sure which account to create records in, please contact the DevOps team.

### Variables

There are several variables that must be provided for the module to function properly:

```
* environment
* service_name
* application_family
* vpc_id
* load_balancer_subnet_ids
* fully_qualified_domain_name (used for S3 bucket name and Route53 record)
* certificate_domain (ACM certificate domain pattern)
```

Similarly, there are a number of variables for which you should likely provide values specific to the AWS account and region:

```
* ingress_cidr_blocks or ingress_prefix_list_names (at least one is likely required)
* route53_zone_name (if route53_record_enabled is true)
* custom_waf_name (optional, defaults to environment-based WAF)
```

The default values for load balancer settings are configured for internal applications and don't need to be explicitly specified. However, they can be customized:

```
* load_balancer_is_internal (default: true)
* load_balancer_listener_port (default: 443)
* load_balancer_ssl_policy (default: ELBSecurityPolicy-TLS13-1-2-2021-06)
```

This module will create security groups for the load balancer, limiting external access based on your CIDR blocks and prefix lists. You can control access with:

```
* ingress_cidr_blocks
* ingress_prefix_list_names
* allow_vpc_private_subnets (default: false)
```

Finally, there are variables related to SPA behavior and URL rewriting:

```
* static_path_patterns (paths to forward directly without rewriting)
* spa_rewrite_path_patterns (paths to apply URL rewriting to)
* spa_rewrite_regex (regex pattern for URL rewriting)
* spa_rewrite_target (target path for rewritten URLs, typically /index.html)
```

See variables.tf for a full list of variables and settings.

### VPC Endpoint Discovery

By default, the module discovers the S3 interface VPC endpoint in your VPC automatically. If you have multiple S3 interface endpoints and need to specify a particular one, use the `vpc_endpoint_name` variable to filter by the endpoint's Name tag:

```hcl
vpc_endpoint_name = "primary-s3-endpoint"
```

### URL Rewriting for SPA Routing

This module supports client-side routing for SPAs through ALB's URL rewrite feature. Two types of listener rules are created:

1. **Static Path Forwarding**: Specified paths are forwarded directly to S3 without modification
2. **URL Rewriting**: Other paths are rewritten to `/index.html` (or custom target) while preserving the browser URL

Example configuration:

```hcl
static_path_patterns      = ["/frontend-app-name/*, "/assets/*", "/favicon.ico", "/index.html"]
spa_rewrite_path_patterns = ["/*"]
spa_rewrite_regex         = "^/.*$"
spa_rewrite_target        = "/index.html"
```

This allows your SPA's client-side router (React Router, Vue Router, etc.) to handle navigation without server-side redirects.

### S3 Bucket Configuration

The module uses the terraform-s3 module to create the S3 bucket. You can pass through various S3-specific configurations:

```
* s3_bucket_policy_overrides
* s3_apply_replication_configuration
* s3_custom_logging_bucket
* s3_lifecycle_rules_* (various lifecycle rule options)
* s3_read_only_policy
* s3_bucket_cors_rule
```

See the S3 module variables section in variables.tf for the complete list.

### Health Checks

The module configures health checks on the S3 VPC endpoint target group. These can be customized:

```
* health_check_interval (default: 10 seconds)
* health_check_timeout (default: 5 seconds)
* health_check_healthy_threshold (default: 3)
* health_check_unhealthy_threshold (default: 3)
* health_check_path (default: /)
* health_check_matcher (default: 200)
```

## Examples

See the `tests/dev/` directory for a working example.

## Caveats

The S3 bucket name is determined by the `fully_qualified_domain_name` variable. Bucket names must be globally unique and follow S3 naming conventions. In this model, the bucket name *must* match the FQDN on which it will be served.

The module passes the `fully_qualified_domain_name` directly to the S3 module as the custom bucket name, so ensure it meets these requirements.

## Testing

The module includes tests using the Terraform test framework:

- **Unit Tests**: `tests/unit.tftest.hcl` - Mock provider tests for validation
- **Integration Tests**: `tests/integration.tftest.hcl` - Real AWS environment tests (requires valid credentials)

Run tests with:
```bash
terraform test
```

For unit tests only:
```bash
terraform test -filter=tests/unit.tftest.hcl
```

For integration tests only (will create real AWS resources):
```bash
terraform test -filter=tests/integration.tftest.hcl
```
