Terraform module to deploy BookStack on AWS with enterprise-grade security and monitoring.
This module deploys a highly available BookStack installation on AWS with:
- Auto-scaling EC2 instances managed by an Application Load Balancer
- Multi-AZ RDS MySQL database with automated backups
- Encrypted EFS for shared file storage (uploads, images)
- SES integration for email notifications with automatic IAM key rotation
- CloudWatch alarms for SES reputation and RDS metrics
- Google OAuth authentication support
- Full encryption at rest for all data stores
- Encryption by Default: RDS and EFS are always encrypted (AWS managed keys by default, custom KMS keys supported)
- IAM Key Rotation: Automatic rotation of SES SMTP credentials every 45 days (configurable 30-90 days)
- Least Privilege IAM: SES permissions restricted to sending from verified domain only
- Secrets Management: All credentials stored in AWS Secrets Manager with automatic rotation support
- Network Isolation: Database and EFS restricted to VPC traffic only
- Email Notifications: SNS topic with email subscriptions for all alarms
- SES Reputation Monitoring: CloudWatch alarms for bounce rate (5%) and complaint rate (0.1%)
- Comprehensive RDS Monitoring:
- Core Metrics: CPU utilization (80%), storage space (5GB), database connections (80)
- Performance: Disk queue depth (10), read/write latency (25ms)
- Memory: Freeable memory (100MB), swap usage (256MB)
- Burst Credits: CPU credit balance for t3 instances (20 credits) - prevents throttling
- All thresholds fully configurable
- RDS CloudWatch Logs: Error, general, and slow query logs exported to CloudWatch (365-day retention by default)
- Performance Insights: Advanced RDS performance monitoring enabled by default (7-day free tier retention)
- Integration Ready: Support for external SNS topics (PagerDuty, Slack, etc.)
- Smart Alerting: Multi-period evaluation to reduce false positives (2-3 periods for most alarms)
- Multi-AZ Database: RDS instance with automated backups and snapshots
- Auto-Scaling: EC2 instances distributed across availability zones
- Load Balancing: Application Load Balancer with health checks
- Shared Storage: EFS for consistent file storage across instances
- AWS Provider Compatibility: Supports both AWS provider v5 and v6
- Terraform 1.5+: Modern Terraform version support
- InfraHouse Standards: Follows InfraHouse module conventions and patterns
┌─────────────────┐
│ Route 53 │
│ DNS Records │
└────────┬────────┘
│
┌────────▼────────┐
│ Application │
│ Load Balancer │
└────────┬────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌───────▼────────┐ ┌────────▼───────┐ ┌─────────▼──────┐
│ BookStack │ │ BookStack │ │ BookStack │
│ EC2 (AZ-A) │ │ EC2 (AZ-B) │ │ EC2 (AZ-C) │
└───────┬────────┘ └────────┬───────┘ └─────────┬──────┘
│ │ │
└───────────────────┼───────────────────┘
│
┌───────────────────┼──────────────────┐
│ │ │
┌───────▼────────┐ ┌───────▼────────┐ ┌──────▼─────────┐
│ RDS MySQL │ │ EFS Shared │ │ AWS Secrets │
│ (Multi-AZ) │ │ Storage │ │ Manager │
│ (Encrypted) │ │ (Encrypted) │ │ │
└────────────────┘ └────────────────┘ └────────────────┘
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Amazon SES │ │ SNS Topic │ │ CloudWatch │
│ (Email Sending)│ │ (Alert Emails) │ │ Alarms │
└────────────────┘ └────────────────┘ └────────────────┘
module "bookstack" {
source = "registry.infrahouse.com/infrahouse/bookstack/aws"
version = "3.4.0"
# Network Configuration
backend_subnet_ids = ["subnet-abc123", "subnet-def456"]
lb_subnet_ids = ["subnet-public1", "subnet-public2"]
internet_gateway_id = "igw-xyz789"
# DNS Configuration
zone_id = "Z1234567890ABC"
service_name = "wiki" # Will be accessible at wiki.yourdomain.com
# Required Secrets
google_oauth_client_secret = "google-oauth-bookstack" # AWS Secrets Manager secret name
# Monitoring (REQUIRED)
alarm_emails = [
"[email protected]",
"[email protected]"
]
providers = {
aws = aws
aws.dns = aws # Can be different account for DNS
}
}module "bookstack" {
source = "registry.infrahouse.com/infrahouse/bookstack/aws"
version = "3.4.0"
# Network Configuration
backend_subnet_ids = ["subnet-abc123", "subnet-def456", "subnet-ghi789"]
lb_subnet_ids = ["subnet-public1", "subnet-public2", "subnet-public3"]
internet_gateway_id = "igw-xyz789"
# DNS Configuration
zone_id = "Z1234567890ABC"
service_name = "docs"
dns_a_records = ["docs", "wiki", "knowledge"] # Multiple DNS names
# Instance Configuration
instance_type = "t3.small"
db_instance_type = "db.t3.small"
asg_min_size = 2
asg_max_size = 6
# Encryption with Custom KMS Keys
storage_encryption_key_arn = aws_kms_key.rds.arn
efs_encryption_key_arn = aws_kms_key.efs.arn
# SMTP Key Rotation
smtp_key_rotation_days = 30 # Rotate every 30 days
# Monitoring Configuration
alarm_emails = ["[email protected]"]
alarm_topic_arns = [
"arn:aws:sns:us-east-1:123456789012:pagerduty-critical"
]
# Custom Alarm Thresholds
ses_bounce_rate_threshold = 0.03 # 3% instead of default 5%
ses_complaint_rate_threshold = 0.0005 # 0.05% instead of default 0.1%
rds_cpu_threshold = 70 # 70% instead of default 80%
rds_storage_threshold_gb = 10 # 10GB instead of default 5GB
rds_connections_threshold = 100 # 100 connections instead of default 80
# RDS Monitoring Configuration
enable_rds_cloudwatch_logs = true
rds_cloudwatch_logs_retention_days = 731 # 2 years instead of default 1 year
enable_rds_performance_insights = true
rds_performance_insights_retention_days = 731 # 2 years (additional cost) instead of 7-day free tier
# Google OAuth
google_oauth_client_secret = "google-oauth-bookstack"
# Database Configuration
deletion_protection = true
skip_final_snapshot = false
providers = {
aws = aws.main
aws.dns = aws.dns_account # Separate account for DNS
}
}alarm_emailsis now REQUIRED: You must provide at least one email address for alarm notifications- Encryption Always Enabled: RDS and EFS encryption is now mandatory (was optional in v2.x)
- EFS Data Migration: Upgrading from v2.x will recreate the EFS filesystem - backup your data first!
⚠️ RDS Identifier Change: The module now uses fixedidentifierinstead ofidentifier_prefixto prevent CloudWatch log group race conditions. Causes brief downtime during in-place rename (can be avoided withdb_identifiervariable) - see migration instructions below.
The module now uses a fixed identifier instead of identifier_prefix. Good news: AWS RDS allows in-place identifier changes!
You have two options when upgrading:
Option 1: Let the identifier change (Recommended)
# Just upgrade - Terraform will rename the identifier in-place
# Old: bookstack-encrypted20251109012345678900000001
# New: bookstack-encrypted
terraform plan # Review the identifier change
terraform apply # Apply the change
# ⚠️ IMPORTANT: This causes brief RDS downtime during the rename
# AWS will reboot the instance to apply the new identifier
# Plan this during a maintenance windowOption 2: Keep the old identifier (Avoid downtime)
If you cannot tolerate any downtime:
# 1. Get your current RDS identifier
terraform state show 'module.bookstack.aws_db_instance.db' | grep '^\s*identifier\s*='
# Example output: identifier = "bookstack-encrypted20251109012345678900000001"
# 2. Set db_identifier in your module configuration
module "bookstack" {
# ... other config ...
db_identifier = "bookstack-encrypted20251109012345678900000001" # Use YOUR actual identifier
}
# 3. Verify no changes to RDS
terraform plan # Should show no identifier change
# ⚠️ WARNING: Once you set db_identifier, it is PERMANENT!
# Removing it later will trigger an identifier change and cause downtime
# This option locks in the old naming pattern foreverRecommendation: Choose Option 1 for clean naming. The brief downtime is worth having a clean, predictable identifier.
If you're upgrading from v2.x and have an existing unencrypted EFS:
# 1. Backup EFS data
aws efs describe-file-systems --file-system-id fs-xxxxx
# Mount and backup: rsync -av /mnt/efs/ /backup/bookstack-uploads/
# 2. Update module version
# In your terraform code, update to version ~> 3.0
# 3. Apply (will recreate EFS)
terraform apply
# 4. Restore data
# Mount new EFS and restore: rsync -av /backup/bookstack-uploads/ /mnt/efs/The module automatically creates and rotates SES SMTP credentials:
- Rotation Schedule: Every 45 days by default (configurable 30-90 days)
- Zero Downtime: New key created before old key deleted
- Automatic Restart: Puppet automatically restarts services when credentials rotate
- Manual Trigger: Run
terraform applyto trigger rotation on schedule
Monitor rotation status via outputs:
output "next_rotation" {
value = module.bookstack.smtp_credentials_next_rotation
}
output "last_rotated" {
value = module.bookstack.smtp_credentials_last_rotated
}- SES Sending Limits: Ensure your AWS account is out of SES sandbox mode
- DNS Verification: Verify your domain in SES before deployment
- Google OAuth Setup: Configure OAuth credentials in Google Cloud Console
- Secrets Manager: Store Google OAuth credentials in AWS Secrets Manager
- VPC Configuration: Ensure proper VPC, subnet, and internet gateway setup
⚠️ TLS Private Key in Terraform State: This module generates SSH keys usingtls_private_keyresource. The private key will be stored in Terraform state. Ensure your state backend is encrypted and access-controlled (e.g., S3 with encryption and restrictive IAM policies). For enhanced security, consider generating SSH keys externally and passing them viakey_pair_namevariable instead.
- Use
t3.microinstances for development environments - Set
deletion_protection = falseandskip_final_snapshot = truefor non-production - Use AWS managed KMS keys (default) instead of custom keys for lower costs
- Configure appropriate
asg_min_sizeandasg_max_sizefor your workload
AWS EC2 has a 16KB limit for userdata (after base64 encoding). The module tracks userdata size and provides recommendations:
After applying, check the userdata size:
terraform output userdata_size_infoOutput example:
{
"compression_enabled": false,
"base64_kb": "12.45",
"aws_limit_kb": "16.00",
"utilization_pct": "77.8%",
"status": "✓ OK",
"recommendation": "Size is within safe limits"
}If userdata approaches the 16KB limit (>12KB), enable compression:
module "bookstack" {
source = "registry.infrahouse.com/infrahouse/bookstack/aws"
# ... other configuration ...
compress_userdata = true # Enable gzip compression
}Compression typically reduces userdata size by 60-70%, allowing more packages and configuration.
If compression isn't enough, consider:
- Move large scripts to S3 and download them during boot
- Reduce the number of packages in
var.packages - Minimize content in
var.extra_files - Store large configuration in Parameter Store/Secrets Manager
The module automatically validates userdata size during tests and will fail if the limit is exceeded.
The module provides several useful outputs:
# BookStack URLs
module.bookstack.bookstack_urls # ["https://wiki.example.com"]
# Infrastructure ARNs
module.bookstack.bookstack_load_balancer_arn
module.bookstack.bookstack_instance_role_arn
module.bookstack.rds_instance_identifier
# Monitoring
module.bookstack.sns_topic_arn
module.bookstack.smtp_credentials_next_rotation
module.bookstack.smtp_credentials_last_rotated- Terraform: ~> 1.5
- AWS Provider: >= 5.11, < 7.0
- AWS Account: With SES verified domain
- VPC: With public and private subnets
- Route53: Hosted zone for your domain
Contributions are welcome! Please:
- Open an issue to discuss proposed changes
- Follow InfraHouse Terraform module standards
- Include tests for new features
- Update documentation
Apache 2.0 Licensed. See LICENSE for full details.
- Issues: https://github.com/infrahouse/terraform-aws-bookstack/issues
- InfraHouse: https://infrahouse.com
| Name | Version |
|---|---|
| terraform | ~> 1.5 |
| aws | >= 5.11, < 7.0 |
| random | ~> 3.6 |
| time | ~> 0.13 |
| tls | ~> 4.0 |
| Name | Version |
|---|---|
| aws | >= 5.11, < 7.0 |
| aws.dns | >= 5.11, < 7.0 |
| random | ~> 3.6 |
| time | ~> 0.13 |
| tls | ~> 4.0 |
| Name | Source | Version |
|---|---|---|
| bookstack | registry.infrahouse.com/infrahouse/website-pod/aws | 5.8.2 |
| bookstack-userdata | registry.infrahouse.com/infrahouse/cloud-init/aws | 2.2.2 |
| bookstack_app_key | registry.infrahouse.com/infrahouse/secret/aws | 1.1.0 |
| db_user | registry.infrahouse.com/infrahouse/secret/aws | 1.1.0 |
| ses_smtp_password | registry.infrahouse.com/infrahouse/secret/aws | 1.1.0 |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| access_log_force_destroy | Destroy S3 bucket with access logs even if non-empty | bool |
false |
no |
| alarm_emails | List of email addresses to receive alarm notifications for SES bounce rate, RDS issues, etc. AWS will send confirmation emails that must be accepted. At least one email is required. |
list(string) |
n/a | yes |
| alarm_topic_arns | List of existing SNS topic ARNs to send alarms to. Use for advanced integrations like PagerDuty, Slack, etc. |
list(string) |
[] |
no |
| asg_ami | Image for EC2 instances | string |
null |
no |
| asg_health_check_grace_period | ASG will wait up to this number of minutes for instance to become healthy | number |
600 |
no |
| asg_max_size | Maximum number of instances in ASG | number |
null |
no |
| asg_min_size | Minimum number of instances in ASG | number |
null |
no |
| backend_subnet_ids | List of subnet ids where the webserver and database instances will be created | list(string) |
n/a | yes |
| compress_userdata | Compress userdata with gzip to reduce size and work around AWS 16KB limit. When enabled, userdata is gzip-compressed before being sent to EC2 instances. AWS automatically decompresses it before execution. This can reduce userdata size by 60-70%, allowing more packages, files, and configuration. Recommended: Enable if userdata_size_info shows approaching limit (>12KB). Requirements: gzip command must be available on the system running terraform. |
bool |
false |
no |
| db_identifier | RDS instance identifier. If not provided, defaults to '{var.service_name}-encrypted'. DOWNTIME AVOIDANCE: When upgrading from v2.x, RDS will rename the identifier in-place (brief downtime). To prevent this, set this variable to your existing identifier. WARNING: Once set, this value is PERMANENT - removing it will trigger the rename. Most users should leave this unset and accept the brief downtime for clean naming. |
string |
null |
no |
| db_instance_type | Instance type to run the database instances | string |
"db.t3.micro" |
no |
| deletion_protection | Specifies whether to enable deletion protection for the DB instance. | bool |
true |
no |
| dns_a_records | A list of A records the BookStack application will be accessible at. E.g. ["wiki"] or ["bookstack", "docs"]. By default, it will be [var.service_name]. |
list(string) |
null |
no |
| efs_encryption_key_arn | KMS key ARN to encrypt EFS file system. If not provided, AWS managed key will be used. EFS encryption is always enabled. |
string |
null |
no |
| enable_rds_alarms | Enable CloudWatch alarms for RDS metrics | bool |
true |
no |
| enable_rds_burst_balance_alarm | Enable CPU credit balance alarm for burstable RDS instances (t2/t3). CRITICAL for t3.micro: When CPU credits reach 0, CPU is throttled to baseline (10%). This causes severe performance degradation. Enable this if using t2/t3 instance classes. Disable if using non-burstable instances (m5, r5, etc). |
bool |
true |
no |
| enable_rds_cloudwatch_logs | Enable CloudWatch logs export for RDS. Exports error, general, and slow query logs to CloudWatch. |
bool |
true |
no |
| enable_rds_latency_alarms | Enable read/write latency alarms for RDS. Monitors average query latency. High latency indicates: - I/O bottlenecks - Insufficient instance resources - Query optimization needed - Network issues |
bool |
true |
no |
| enable_rds_performance_insights | Enable Performance Insights for RDS. Provides advanced database performance monitoring and analysis. |
bool |
true |
no |
| enable_ses_alarms | Enable CloudWatch alarms for SES bounce/complaint rates | bool |
true |
no |
| environment | Name of environment. | string |
"development" |
no |
| extra_files | Additional files to create on an instance. Consider storing large scripts in S3 and downloading them instead. Check the userdata_size_info output after applying to monitor usage. |
list( |
[] |
no |
| extra_instance_profile_permissions | A JSON with a permissions policy document. The policy will be attached to the ASG instance profile. |
string |
null |
no |
| extra_repos | Additional APT repositories to configure on an instance. | map( |
{} |
no |
| google_oauth_client_secret | AWS secretsmanager secret name with a Google Oauth 'client id' and 'client secret'. | string |
n/a | yes |
| instance_type | Instance type to run the webserver instances | string |
"t3.micro" |
no |
| internet_gateway_id | Not used, but AWS Internet Gateway must be present. Ensure by passing its id. | string |
n/a | yes |
| key_pair_name | SSH keypair name to be deployed in EC2 instances | string |
null |
no |
| lb_subnet_ids | List of subnet ids where the load balancer will be created | list(string) |
n/a | yes |
| packages | List of packages to install when the instance bootstraps. The module already includes mysql-client and nfs-common by default. Check the userdata_size_info output after applying to monitor usage. |
list(string) |
[] |
no |
| puppet_debug_logging | Enable debug logging if true. | bool |
false |
no |
| puppet_hiera_config_path | Path to hiera configuration file. | string |
"{root_directory}/environments/{environment}/hiera.yaml" |
no |
| puppet_module_path | Path to common puppet modules. | string |
"{root_directory}/modules" |
no |
| puppet_root_directory | Path where the puppet code is hosted. | string |
"/opt/puppet-code" |
no |
| rds_cloudwatch_logs_retention_days | Number of days to retain RDS CloudWatch logs. Default is 365 days (1 year). Set to 0 for never expire. Valid values: 0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653 |
number |
365 |
no |
| rds_connections_threshold | RDS database connections threshold for alarms. Default is 80 - alarm triggers when connection count exceeds this value. Adjust based on your instance type's max_connections setting. |
number |
80 |
no |
| rds_cpu_credit_balance_threshold | RDS CPU credit balance threshold for alarms (t2/t3 instances only). Default is 20 credits - alarm triggers when credit balance drops below this. t3.micro accumulates credits at 12 credits/hour and can hold up to 288 credits. At 20 credits remaining, you have ~1.67 hours before throttling (if no credits earned). Lower threshold = less warning time before throttling. Higher threshold = more false positives during normal bursting. |
number |
20 |
no |
| rds_cpu_threshold | RDS CPU utilization percentage threshold for alarms. Default is 80% - alarm triggers when CPU exceeds this value. |
number |
80 |
no |
| rds_disk_queue_depth_threshold | RDS disk queue depth threshold for alarms. Default is 10 - alarm triggers when average queue depth exceeds this value. High queue depth indicates sustained I/O bottleneck. Recommendations: - 0-10: Normal operation - 10-64: Monitor - may need to upgrade storage or instance - >64: Critical - severe I/O bottleneck |
number |
10 |
no |
| rds_freeable_memory_threshold_percentage | RDS freeable memory threshold as a percentage of total instance RAM. Default is 10% - alarm triggers when free memory drops below this percentage. The actual MB threshold is calculated based on the instance type: - db.t3.micro (1GB): 10% = 100MB - db.t3.small (2GB): 10% = 200MB - db.t3.medium (4GB): 10% = 400MB This scales automatically when you change instance types. Low memory causes performance degradation and potential OOM kills. |
number |
10 |
no |
| rds_performance_insights_retention_days | Number of days to retain Performance Insights data. Valid values: 7 (free tier) or 731 (2 years, additional cost). Default is 7 days. |
number |
7 |
no |
| rds_read_latency_threshold_ms | RDS read latency threshold in milliseconds for alarms. Default is 25ms - alarm triggers when average read latency exceeds this. Typical latencies: - <5ms: Excellent (SSD, good indexes) - 5-25ms: Good (normal operation) - 25-100ms: Acceptable (may need optimization) - >100ms: Poor (investigate immediately) |
number |
25 |
no |
| rds_storage_threshold_gb | RDS free storage space threshold in gigabytes (GB) for alarms. Default is 5GB - alarm triggers when free space drops below this value. |
number |
5 |
no |
| rds_swap_usage_threshold_mb | RDS swap usage threshold in megabytes (MB) for alarms. Default is 256MB - alarm triggers when swap usage exceeds this value. High swap usage indicates memory pressure and will cause performance degradation. Ideally, swap usage should be 0. Any sustained swap usage is a sign to upgrade instance. |
number |
256 |
no |
| rds_write_latency_threshold_ms | RDS write latency threshold in milliseconds for alarms. Default is 25ms - alarm triggers when average write latency exceeds this. Write latency is typically higher than read latency due to fsync requirements. Typical latencies: - <10ms: Excellent - 10-25ms: Good - 25-100ms: Acceptable - >100ms: Poor (investigate immediately) |
number |
25 |
no |
| service_name | DNS hostname for the service. It's also used to name some resources like EC2 instances. | string |
"bookstack" |
no |
| ses_bounce_rate_threshold | SES bounce rate percentage threshold (AWS recommends keeping below 5%) | number |
0.05 |
no |
| ses_complaint_rate_threshold | SES complaint rate percentage threshold (AWS recommends keeping below 0.1%) | number |
0.001 |
no |
| skip_final_snapshot | Specifies whether to skip the final snapshot when the DB instance is deleted. | bool |
false |
no |
| smtp_credentials_secret | AWS secret name with SMTP credentials. The secret must contain a JSON with user and password keys. |
string |
null |
no |
| smtp_key_rotation_days | Number of days between SMTP credential rotations | number |
45 |
no |
| sns_topic_alarm_arn | ARN of SNS topic for Cloudwatch alarms on base EC2 instance. | string |
null |
no |
| sns_topic_name | Name for the SNS topic. If not provided, defaults to '<service_name>-alarms' | string |
null |
no |
| ssh_cidr_block | CIDR range that is allowed to SSH into the backend instances. Format is a.b.c.d/. | string |
null |
no |
| storage_encryption_key_arn | KMS key ARN to encrypt RDS instance storage. If not provided, AWS managed key will be used. RDS encryption is always enabled. |
string |
null |
no |
| ubuntu_codename | Ubuntu version to use for the elasticsearch node | string |
"jammy" |
no |
| zone_id | Domain name zone ID where the website will be available | string |
n/a | yes |
| Name | Description |
|---|---|
| autoscaling_group_name | Name of the Auto Scaling Group for BookStack instances |
| bookstack_instance_role_arn | IAM role ARN assigned to bookstack EC2 instances. |
| bookstack_load_balancer_arn | ARN of the load balancer for the BookStack website pod. |
| bookstack_urls | List of URLs where bookstack is available. |
| database_address | Address of the RDS database instance |
| database_name | Name of the database |
| database_port | Port of the RDS database instance |
| database_secret_name | Name of the secret containing database credentials |
| rds_instance_identifier | Identifier of the RDS instance. |
| smtp_credentials_last_rotated | When SMTP credentials were last rotated (creation date of current key) |
| smtp_credentials_next_rotation | Next SMTP credential rotation date (RFC3339 format) |
| sns_topic_arn | ARN of the SNS topic for alarms |
| userdata_size_info | Userdata size information for launch template validation. AWS limit is 16KB (16384 bytes) after base64 encoding. If size exceeds limit, EC2 launch will fail. |