├── .gitignore ├── README.md ├── datasources └── tfc │ └── workspaces.csv ├── modules └── pleroma │ ├── .gitignore │ ├── README.md │ ├── acm.tf │ ├── cloudfront.tf │ ├── data.tf │ ├── iam.tf │ ├── lb.tf │ ├── main.tf │ ├── outputs.tf │ ├── postgres.tf │ ├── s3.tf │ ├── s3_backup.tf │ ├── s3_logs.tf │ ├── ses.tf │ ├── sg.tf │ ├── terraform.tf │ └── variables.tf ├── snippets └── provider_aws.tf └── workspaces ├── manhole ├── .terraform.lock.hcl ├── data.tf ├── main.tf ├── pleroma_bot.tf ├── provider_aws.tf └── terraform.tf ├── pleroma ├── .terraform.lock.hcl ├── data.tf ├── files │ └── keybase.txt ├── gl.tf ├── keybase.tf ├── logstash.tf ├── main.tf ├── provider_aws.tf └── terraform.tf ├── route53 ├── .terraform.lock.hcl ├── main.tf ├── provider_aws.tf └── terraform.tf ├── tfc ├── .terraform.lock.hcl ├── csv.tf ├── main.tf └── terraform.tf └── vpc ├── .terraform.lock.hcl ├── main.tf ├── outputs_vpc.tf ├── provider_aws.tf └── terraform.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | # example.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform code for dabr.ca 2 | 3 | This repository contains Terraform code for [dabr.ca](https://dabr.ca/), a microblogging site powered by [Pleroma](https://pleroma.social/). 4 | 5 | In the event that the administrator of dabr.ca is unable to fulfil their responsibilities, anyone interested can use this repo to step up as a successor. If you want to run your own instance, you may need to do a find-and-replace for hard-coded values such as domain names. 6 | 7 | ## Setup 8 | 9 | 1. Have an AWS account ready. 10 | 2. Register a domain and add it to Route 53. 11 | 3. [Create a VPC](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest). 12 | 4. Use the module in this repository to create infrastructure resources. 13 | 5. Run [Ansible playbook](https://github.com/dabr-ca/config) to install and configure Pleroma on the instance. 14 | 15 | ## Components 16 | 17 | * EC2 instance with ELB for TLS termination 18 | * RDS instance with PostgresSQL engine 19 | * S3 bucket with CloudFront for storing user-uploaded files 20 | * Security groups, IAM roles, ACM certificates, Route 53 records, etc 21 | 22 | Check [module README](./modules/pleroma/README.md) for details. 23 | 24 | ## Cost 25 | 26 | Most of the AWS resources are eligible for 12-month Free Tier, [after which cost is approximately 500 USD/y](https://calculator.aws/#/estimate?id=45a11934bdf6900573ad46263707edfc2ad4d44c). You can utilize [EC2 Instance Savings Plan](https://aws.amazon.com/savingsplans/compute-pricing/) and [RDS Reserved Instances](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithReservedDBInstances.html) to **cut the price down by more than half** if you are committed to run your site on AWS for more than 1 year. 27 | -------------------------------------------------------------------------------- /datasources/tfc/workspaces.csv: -------------------------------------------------------------------------------- 1 | name,auto_apply,global_remote_state,modules 2 | pleroma,true,false,pleroma 3 | route53,true,false, 4 | tfc,true,false, 5 | vpc,true,true,vpc 6 | -------------------------------------------------------------------------------- /modules/pleroma/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform.lock.hcl 2 | -------------------------------------------------------------------------------- /modules/pleroma/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Requirements 3 | 4 | | Name | Version | 5 | |------|---------| 6 | | [terraform](#requirement\_terraform) | >= 1.2.9 | 7 | | [aws](#requirement\_aws) | ~> 4.45 | 8 | | [random](#requirement\_random) | ~> 3.4 | 9 | 10 | ## Providers 11 | 12 | | Name | Version | 13 | |------|---------| 14 | | [aws](#provider\_aws) | 4.67.0 | 15 | | [aws.us-east-1](#provider\_aws.us-east-1) | 4.67.0 | 16 | | [random](#provider\_random) | 3.5.1 | 17 | 18 | ## Modules 19 | 20 | No modules. 21 | 22 | ## Resources 23 | 24 | | Name | Type | 25 | |------|------| 26 | | [aws_acm_certificate.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate) | resource | 27 | | [aws_acm_certificate.us-east-1](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate) | resource | 28 | | [aws_acm_certificate_validation.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation) | resource | 29 | | [aws_acm_certificate_validation.us-east-1](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation) | resource | 30 | | [aws_cloudfront_distribution.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution) | resource | 31 | | [aws_cloudfront_origin_access_control.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_control) | resource | 32 | | [aws_db_instance.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance) | resource | 33 | | [aws_db_subnet_group.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_subnet_group) | resource | 34 | | [aws_eip.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip) | resource | 35 | | [aws_iam_instance_profile.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | 36 | | [aws_iam_role.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 37 | | [aws_iam_role_policy.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 38 | | [aws_instance.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource | 39 | | [aws_lb.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb) | resource | 40 | | [aws_lb_listener.main_http](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | 41 | | [aws_lb_listener.main_https](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | 42 | | [aws_lb_target_group.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_target_group) | resource | 43 | | [aws_lb_target_group_attachment.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_target_group_attachment) | resource | 44 | | [aws_route53_record.acm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | 45 | | [aws_route53_record.dkim](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | 46 | | [aws_route53_record.files](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | 47 | | [aws_route53_record.spf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | 48 | | [aws_route53_record.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | 49 | | [aws_s3_bucket.backup](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | 50 | | [aws_s3_bucket.logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | 51 | | [aws_s3_bucket.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | 52 | | [aws_s3_bucket_acl.logs_cloudfront](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_acl) | resource | 53 | | [aws_s3_bucket_lifecycle_configuration.backup](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource | 54 | | [aws_s3_bucket_lifecycle_configuration.logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource | 55 | | [aws_s3_bucket_policy.logs_elb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | 56 | | [aws_s3_bucket_policy.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | 57 | | [aws_s3_bucket_public_access_block.backup](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | 58 | | [aws_s3_bucket_public_access_block.logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | 59 | | [aws_s3_bucket_public_access_block.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | 60 | | [aws_s3_object.healthcheck](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_object) | resource | 61 | | [aws_security_group.backend](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | 62 | | [aws_security_group.db](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | 63 | | [aws_security_group.lb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | 64 | | [aws_ses_domain_dkim.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ses_domain_dkim) | resource | 65 | | [aws_ses_domain_identity.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ses_domain_identity) | resource | 66 | | [aws_ses_domain_identity_verification.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ses_domain_identity_verification) | resource | 67 | | [aws_ssm_parameter.postgres_address](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 68 | | [aws_ssm_parameter.postgres_heartbeat_url](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 69 | | [aws_ssm_parameter.postgres_name](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 70 | | [aws_ssm_parameter.postgres_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 71 | | [aws_ssm_parameter.postgres_username](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 72 | | [aws_ssm_parameter.s3_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 73 | | [aws_ssm_parameter.s3_bucket_backup](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 74 | | [aws_vpc_security_group_egress_rule.backend_all](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | 75 | | [aws_vpc_security_group_egress_rule.lb_all](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | 76 | | [aws_vpc_security_group_ingress_rule.backend_lb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 77 | | [aws_vpc_security_group_ingress_rule.backend_mosh](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 78 | | [aws_vpc_security_group_ingress_rule.backend_ssh](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 79 | | [aws_vpc_security_group_ingress_rule.db_backend](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 80 | | [aws_vpc_security_group_ingress_rule.lb_http](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 81 | | [aws_vpc_security_group_ingress_rule.lb_https](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 82 | | [aws_vpc_security_group_ingress_rule.lb_icmp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 83 | | [random_id.s3_bucket_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | 84 | | [random_password.postgres](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | 85 | | [aws_ami.ubuntu](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | 86 | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 87 | | [aws_canonical_user_id.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/canonical_user_id) | data source | 88 | | [aws_cloudfront_cache_policy.managed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_cache_policy) | data source | 89 | | [aws_cloudfront_log_delivery_canonical_user_id.cloudfront](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_log_delivery_canonical_user_id) | data source | 90 | | [aws_cloudfront_origin_request_policy.managed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_origin_request_policy) | data source | 91 | | [aws_elb_service_account.elb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/elb_service_account) | data source | 92 | | [aws_iam_policy_document.bucket_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 93 | | [aws_iam_policy_document.ec2-assume-role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 94 | | [aws_iam_policy_document.logs_bucket_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 95 | | [aws_iam_policy_document.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 96 | | [aws_key_pair.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/key_pair) | data source | 97 | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 98 | | [aws_route53_zone.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | 99 | | [aws_subnet.ec2](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | 100 | 101 | ## Inputs 102 | 103 | | Name | Description | Type | Default | Required | 104 | |------|-------------|------|---------|:--------:| 105 | | [domain](#input\_domain) | Domain of the Pleroma instance. This domain points to ELB. The domain must already exist in Route 53. | `string` | n/a | yes | 106 | | [ec2\_instance\_type](#input\_ec2\_instance\_type) | n/a | `string` | `"t3.micro"` | no | 107 | | [ec2\_key\_name](#input\_ec2\_key\_name) | Name of key pair to log into the EC2 instance. The key pair must already exist. | `string` | n/a | yes | 108 | | [files\_domain](#input\_files\_domain) | Domain for serving user-uploaded files. This domain points to CloudFront, whose origin is an S3 bucket. | `string` | n/a | yes | 109 | | [name](#input\_name) | Name of the service. It will be used to name EC2, ELB, and RDS instances. | `string` | `"pleroma"` | no | 110 | | [private\_subnet\_ids](#input\_private\_subnet\_ids) | List of IDs of subnets to create private resources (e.g. databases) in. | `list(string)` | n/a | yes | 111 | | [public\_subnet\_ids](#input\_public\_subnet\_ids) | List of IDs of subnets to create public resources (e.g. load balancers) in. | `list(string)` | n/a | yes | 112 | | [rds\_allocated\_storage](#input\_rds\_allocated\_storage) | n/a | `number` | `20` | no | 113 | | [rds\_instance\_class](#input\_rds\_instance\_class) | n/a | `string` | `"db.t4g.micro"` | no | 114 | | [rds\_storage\_type](#input\_rds\_storage\_type) | n/a | `string` | `"gp2"` | no | 115 | | [vpc\_id](#input\_vpc\_id) | ID of the VPC where the resources are created. | `string` | n/a | yes | 116 | 117 | ## Outputs 118 | 119 | | Name | Description | 120 | |------|-------------| 121 | | [bucket\_logs](#output\_bucket\_logs) | S3 bucket for storing CloudFront and ELB logs. | 122 | | [bucket\_main](#output\_bucket\_main) | S3 bucket for storing user-uploaded files. | 123 | | [instance](#output\_instance) | The main EC2 instance. | 124 | | [lb](#output\_lb) | The Application Load Balancer. | 125 | -------------------------------------------------------------------------------- /modules/pleroma/acm.tf: -------------------------------------------------------------------------------- 1 | resource "aws_acm_certificate" "main" { 2 | domain_name = data.aws_route53_zone.main.name 3 | validation_method = "DNS" 4 | key_algorithm = "EC_secp384r1" 5 | 6 | subject_alternative_names = [ 7 | "*.${data.aws_route53_zone.main.name}", 8 | ] 9 | 10 | lifecycle { 11 | create_before_destroy = true 12 | } 13 | } 14 | 15 | resource "aws_acm_certificate_validation" "main" { 16 | certificate_arn = aws_acm_certificate.main.arn 17 | validation_record_fqdns = [for record in aws_route53_record.acm : record.fqdn] 18 | } 19 | 20 | # CloudFront requires ACM to be in us-east-1, so duplicate the resources. 21 | resource "aws_acm_certificate" "us-east-1" { 22 | provider = aws.us-east-1 23 | 24 | domain_name = data.aws_route53_zone.main.name 25 | validation_method = "DNS" 26 | # CloudFront only supports P-256 27 | key_algorithm = "EC_prime256v1" 28 | 29 | subject_alternative_names = [ 30 | "*.${data.aws_route53_zone.main.name}", 31 | ] 32 | 33 | lifecycle { 34 | create_before_destroy = true 35 | } 36 | } 37 | 38 | resource "aws_acm_certificate_validation" "us-east-1" { 39 | provider = aws.us-east-1 40 | 41 | certificate_arn = aws_acm_certificate.us-east-1.arn 42 | validation_record_fqdns = [for record in aws_route53_record.acm : record.fqdn] 43 | } 44 | 45 | resource "aws_route53_record" "acm" { 46 | for_each = { 47 | for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => { 48 | name = dvo.resource_record_name 49 | record = dvo.resource_record_value 50 | type = dvo.resource_record_type 51 | } 52 | } 53 | 54 | allow_overwrite = true 55 | 56 | zone_id = data.aws_route53_zone.main.zone_id 57 | name = each.value.name 58 | records = [each.value.record] 59 | ttl = 60 60 | type = each.value.type 61 | } 62 | -------------------------------------------------------------------------------- /modules/pleroma/cloudfront.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudfront_distribution" "main" { 2 | aliases = [var.files_domain] 3 | enabled = true 4 | http_version = "http2and3" 5 | is_ipv6_enabled = true 6 | price_class = "PriceClass_All" 7 | retain_on_delete = true 8 | wait_for_deployment = false 9 | 10 | default_cache_behavior { 11 | target_origin_id = aws_s3_bucket.main.bucket_regional_domain_name 12 | 13 | compress = true 14 | viewer_protocol_policy = "redirect-to-https" 15 | allowed_methods = ["GET", "HEAD"] 16 | cached_methods = ["GET", "HEAD"] 17 | cache_policy_id = data.aws_cloudfront_cache_policy.managed["CachingOptimized"].id 18 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.managed["CORS-S3Origin"].id 19 | } 20 | 21 | ordered_cache_behavior { 22 | path_pattern = "proxy/*" 23 | target_origin_id = aws_lb.main.dns_name 24 | 25 | compress = true 26 | viewer_protocol_policy = "redirect-to-https" 27 | allowed_methods = ["GET", "HEAD"] 28 | cached_methods = ["GET", "HEAD"] 29 | cache_policy_id = data.aws_cloudfront_cache_policy.managed["CachingOptimized"].id 30 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.managed["CORS-CustomOrigin"].id 31 | } 32 | 33 | origin { 34 | origin_id = aws_s3_bucket.main.bucket_regional_domain_name 35 | domain_name = aws_s3_bucket.main.bucket_regional_domain_name 36 | origin_access_control_id = aws_cloudfront_origin_access_control.main.id 37 | } 38 | 39 | origin { 40 | origin_id = aws_lb.main.dns_name 41 | domain_name = data.aws_route53_zone.main.name 42 | 43 | custom_origin_config { 44 | origin_protocol_policy = "https-only" 45 | http_port = 80 46 | https_port = 443 47 | origin_ssl_protocols = ["TLSv1.2"] 48 | } 49 | } 50 | 51 | restrictions { 52 | geo_restriction { 53 | restriction_type = "none" 54 | } 55 | } 56 | 57 | viewer_certificate { 58 | acm_certificate_arn = aws_acm_certificate_validation.us-east-1.certificate_arn 59 | minimum_protocol_version = "TLSv1.2_2021" 60 | ssl_support_method = "sni-only" 61 | } 62 | 63 | logging_config { 64 | bucket = aws_s3_bucket.logs.bucket_regional_domain_name 65 | prefix = "cloudfront/" 66 | } 67 | } 68 | 69 | resource "aws_cloudfront_origin_access_control" "main" { 70 | name = var.files_domain 71 | description = var.files_domain 72 | origin_access_control_origin_type = "s3" 73 | signing_behavior = "always" 74 | signing_protocol = "sigv4" 75 | } 76 | 77 | resource "aws_route53_record" "files" { 78 | for_each = toset(["A", "AAAA"]) 79 | 80 | zone_id = data.aws_route53_zone.main.id 81 | name = var.files_domain 82 | type = each.key 83 | 84 | alias { 85 | zone_id = aws_cloudfront_distribution.main.hosted_zone_id 86 | name = aws_cloudfront_distribution.main.domain_name 87 | evaluate_target_health = false 88 | } 89 | } 90 | 91 | # Managed policies 92 | locals { 93 | managed_cache_policies = [ 94 | "Amplify", 95 | "CachingDisabled", 96 | "CachingOptimized", 97 | "CachingOptimizedForUncompressedObjects", 98 | "Elemental-MediaPackage", 99 | ] 100 | managed_origin_request_policies = [ 101 | "AllViewer", 102 | "CORS-CustomOrigin", 103 | "CORS-S3Origin", 104 | "Elemental-MediaTailor-PersonalizedManifests", 105 | "UserAgentRefererHeaders", 106 | ] 107 | } 108 | 109 | data "aws_cloudfront_cache_policy" "managed" { 110 | for_each = toset(local.managed_cache_policies) 111 | 112 | name = "Managed-${each.key}" 113 | } 114 | 115 | data "aws_cloudfront_origin_request_policy" "managed" { 116 | for_each = toset(local.managed_origin_request_policies) 117 | 118 | name = "Managed-${each.key}" 119 | } 120 | -------------------------------------------------------------------------------- /modules/pleroma/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | data "aws_caller_identity" "current" {} 4 | 5 | data "aws_key_pair" "main" { 6 | key_name = var.ec2_key_name 7 | } 8 | 9 | data "aws_ami" "ubuntu" { 10 | most_recent = true 11 | owners = ["099720109477"] # Canonical 12 | 13 | filter { 14 | name = "name" 15 | values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] 16 | } 17 | } 18 | 19 | data "aws_route53_zone" "main" { 20 | name = var.domain 21 | } 22 | -------------------------------------------------------------------------------- /modules/pleroma/iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "main" { 2 | name = local.name 3 | assume_role_policy = data.aws_iam_policy_document.ec2-assume-role.json 4 | } 5 | 6 | data "aws_iam_policy_document" "ec2-assume-role" { 7 | statement { 8 | actions = ["sts:AssumeRole"] 9 | 10 | principals { 11 | type = "Service" 12 | identifiers = ["ec2.amazonaws.com"] 13 | } 14 | } 15 | } 16 | 17 | resource "aws_iam_instance_profile" "main" { 18 | name = aws_iam_role.main.name 19 | role = aws_iam_role.main.name 20 | } 21 | 22 | resource "aws_iam_role_policy" "main" { 23 | role = aws_iam_role.main.name 24 | policy = data.aws_iam_policy_document.main.json 25 | } 26 | 27 | data "aws_iam_policy_document" "main" { 28 | # Allow accessing S3 buckets 29 | statement { 30 | actions = [ 31 | "s3:ListAllMyBuckets" 32 | ] 33 | resources = ["*"] 34 | } 35 | statement { 36 | actions = [ 37 | "s3:ListBucket", 38 | "s3:PutObject", 39 | "s3:PutObjectAcl", # required by Ansible aws_s3 module 40 | "s3:GetObject", 41 | "s3:DeleteObject", 42 | ] 43 | resources = [ 44 | aws_s3_bucket.main.arn, 45 | aws_s3_bucket.backup.arn, 46 | "${aws_s3_bucket.main.arn}/*", 47 | "${aws_s3_bucket.backup.arn}/*", 48 | ] 49 | } 50 | # Allow reading from parameter store 51 | # https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html 52 | statement { 53 | actions = [ 54 | "ssm:GetParameter", 55 | "ssm:GetParameters", 56 | "ssm:GetParameterHistory", 57 | "ssm:GetParametersByPath", 58 | ] 59 | resources = [ 60 | "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.id}:parameter/${local.name}/*" 61 | ] 62 | } 63 | statement { 64 | actions = [ 65 | "ssm:DescribeParameters", 66 | ] 67 | resources = ["*"] 68 | } 69 | # Allow sending emails 70 | # SednRawEmail is required by Swoosh 71 | # https://hexdocs.pm/swoosh/Swoosh.Adapters.AmazonSES.html 72 | statement { 73 | actions = [ 74 | "ses:SendRawEmail" 75 | ] 76 | resources = [ 77 | aws_ses_domain_identity.main.arn 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /modules/pleroma/lb.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lb" "main" { 2 | name = local.name 3 | subnets = var.public_subnet_ids 4 | security_groups = [aws_security_group.lb.id] 5 | 6 | access_logs { 7 | enabled = true 8 | bucket = aws_s3_bucket.logs.id 9 | prefix = "elb" 10 | } 11 | } 12 | 13 | resource "aws_lb_listener" "main_http" { 14 | load_balancer_arn = aws_lb.main.arn 15 | port = 80 16 | protocol = "HTTP" 17 | 18 | default_action { 19 | type = "redirect" 20 | redirect { 21 | port = "443" 22 | protocol = "HTTPS" 23 | status_code = "HTTP_301" 24 | } 25 | } 26 | } 27 | 28 | resource "aws_lb_listener" "main_https" { 29 | load_balancer_arn = aws_lb.main.arn 30 | port = 443 31 | protocol = "HTTPS" 32 | certificate_arn = aws_acm_certificate_validation.main.certificate_arn 33 | ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" 34 | 35 | default_action { 36 | type = "forward" 37 | target_group_arn = aws_lb_target_group.main.arn 38 | } 39 | } 40 | 41 | resource "aws_lb_target_group" "main" { 42 | name = local.name 43 | port = 4000 44 | protocol = "HTTP" 45 | vpc_id = var.vpc_id 46 | } 47 | 48 | resource "aws_lb_target_group_attachment" "main" { 49 | target_group_arn = aws_lb_target_group.main.arn 50 | target_id = aws_instance.main.id 51 | } 52 | 53 | resource "aws_route53_record" "web" { 54 | zone_id = data.aws_route53_zone.main.id 55 | name = var.domain 56 | type = "A" 57 | 58 | alias { 59 | zone_id = aws_lb.main.zone_id 60 | name = aws_lb.main.dns_name 61 | evaluate_target_health = false 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /modules/pleroma/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | name = var.name 3 | } 4 | 5 | # Place EC2 in the same AZ as the RDS to avoid cross-AZ data charge 6 | data "aws_subnet" "ec2" { 7 | filter { 8 | name = "availability-zone" 9 | values = [local.db_az] 10 | } 11 | filter { 12 | name = "subnet-id" 13 | values = var.public_subnet_ids 14 | } 15 | } 16 | 17 | resource "aws_instance" "main" { 18 | ami = data.aws_ami.ubuntu.id 19 | instance_type = var.ec2_instance_type 20 | subnet_id = data.aws_subnet.ec2.id 21 | key_name = data.aws_key_pair.main.key_name 22 | iam_instance_profile = aws_iam_instance_profile.main.name 23 | 24 | vpc_security_group_ids = [aws_security_group.backend.id] 25 | 26 | root_block_device { 27 | volume_type = "gp3" 28 | tags = { 29 | Name = "${local.name}-root" 30 | } 31 | } 32 | 33 | tags = { 34 | Name = local.name 35 | } 36 | 37 | lifecycle { 38 | ignore_changes = [ami] 39 | } 40 | } 41 | 42 | resource "aws_eip" "main" { 43 | instance = aws_instance.main.id 44 | } 45 | -------------------------------------------------------------------------------- /modules/pleroma/outputs.tf: -------------------------------------------------------------------------------- 1 | output "instance" { 2 | description = "The main EC2 instance." 3 | value = { 4 | id = aws_instance.main.id 5 | arn = aws_instance.main.arn 6 | public_ip = aws_eip.main.public_ip 7 | private_ip = aws_instance.main.private_ip 8 | } 9 | } 10 | 11 | output "lb" { 12 | description = "The Application Load Balancer." 13 | value = aws_lb.main 14 | } 15 | 16 | output "bucket_main" { 17 | description = "S3 bucket for storing user-uploaded files." 18 | value = aws_s3_bucket.main 19 | } 20 | 21 | output "bucket_logs" { 22 | description = "S3 bucket for storing CloudFront and ELB logs." 23 | value = aws_s3_bucket.logs 24 | } 25 | -------------------------------------------------------------------------------- /modules/pleroma/postgres.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # AWS: DBName must begin with a letter and contain only alphanumeric characters 3 | postgres_name = "pleroma" 4 | postgres_username = "pleroma" 5 | } 6 | 7 | resource "aws_db_instance" "main" { 8 | engine = "postgres" 9 | engine_version = "13.7" # default as of 2022-11 10 | parameter_group_name = "default.postgres13" 11 | identifier = local.name 12 | db_name = local.postgres_name 13 | instance_class = var.rds_instance_class 14 | storage_type = var.rds_storage_type 15 | allocated_storage = var.rds_allocated_storage 16 | multi_az = false 17 | db_subnet_group_name = aws_db_subnet_group.main.name 18 | vpc_security_group_ids = [aws_security_group.db.id] 19 | 20 | backup_retention_period = 7 21 | performance_insights_enabled = true 22 | 23 | apply_immediately = true 24 | deletion_protection = true 25 | 26 | username = local.postgres_username 27 | password = random_password.postgres.result 28 | 29 | lifecycle { 30 | # FIXME 31 | ignore_changes = [engine_version, allocated_storage] 32 | } 33 | } 34 | 35 | locals { 36 | # Indirectly referencing via a local variable avoids force reading of data 37 | # source. 38 | db_az = aws_db_instance.main.availability_zone 39 | } 40 | 41 | resource "aws_db_subnet_group" "main" { 42 | name = local.name 43 | subnet_ids = var.private_subnet_ids 44 | } 45 | 46 | resource "random_password" "postgres" { 47 | length = 16 48 | special = false 49 | } 50 | 51 | resource "aws_ssm_parameter" "postgres_address" { 52 | name = "/${local.name}/postgres/address" 53 | type = "String" 54 | value = aws_db_instance.main.address 55 | } 56 | 57 | resource "aws_ssm_parameter" "postgres_name" { 58 | name = "/${local.name}/postgres/name" 59 | type = "String" 60 | value = local.postgres_name 61 | } 62 | 63 | resource "aws_ssm_parameter" "postgres_username" { 64 | name = "/${local.name}/postgres/username" 65 | type = "String" 66 | value = local.postgres_username 67 | } 68 | 69 | resource "aws_ssm_parameter" "postgres_password" { 70 | name = "/${local.name}/postgres/password" 71 | type = "SecureString" 72 | value = random_password.postgres.result 73 | } 74 | 75 | resource "aws_ssm_parameter" "postgres_heartbeat_url" { 76 | name = "/${local.name}/postgres/heartbeat_url" 77 | type = "String" 78 | value = "." # populate this value manually 79 | 80 | lifecycle { 81 | ignore_changes = [value] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /modules/pleroma/s3.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "s3_bucket_suffix" { 2 | byte_length = 4 3 | } 4 | 5 | resource "aws_s3_bucket" "main" { 6 | bucket = "${var.name}-${random_id.s3_bucket_suffix.hex}" 7 | } 8 | 9 | resource "aws_s3_bucket_public_access_block" "main" { 10 | bucket = aws_s3_bucket.main.id 11 | 12 | # https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html 13 | block_public_acls = false # needed by Pleroma S3 uploader 14 | ignore_public_acls = true # ignore public ACL set by the Pleroma so that direct access is blocked 15 | block_public_policy = true 16 | restrict_public_buckets = true 17 | } 18 | 19 | resource "aws_s3_bucket_policy" "main" { 20 | bucket = aws_s3_bucket.main.id 21 | policy = data.aws_iam_policy_document.bucket_policy.json 22 | } 23 | 24 | data "aws_iam_policy_document" "bucket_policy" { 25 | # Allow CloudFront to read from the bucket 26 | statement { 27 | principals { 28 | type = "Service" 29 | identifiers = [ 30 | "cloudfront.amazonaws.com" 31 | ] 32 | } 33 | actions = [ 34 | "s3:GetObject" 35 | ] 36 | resources = [ 37 | "${aws_s3_bucket.main.arn}/*", 38 | ] 39 | condition { 40 | test = "StringEquals" 41 | variable = "AWS:SourceArn" 42 | values = [aws_cloudfront_distribution.main.arn] 43 | } 44 | } 45 | } 46 | 47 | resource "aws_ssm_parameter" "s3_bucket" { 48 | name = "/${local.name}/s3_bucket" 49 | type = "String" 50 | value = aws_s3_bucket.main.bucket 51 | } 52 | 53 | resource "aws_s3_object" "healthcheck" { 54 | bucket = aws_s3_bucket.main.id 55 | key = "healthcheck" 56 | content = "OK" 57 | content_type = "text/plain" 58 | } 59 | -------------------------------------------------------------------------------- /modules/pleroma/s3_backup.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | pg_dump_prefix = "pg_dump/" 3 | } 4 | 5 | resource "aws_s3_bucket" "backup" { 6 | bucket = "${var.name}-backup-${random_id.s3_bucket_suffix.hex}" 7 | } 8 | 9 | resource "aws_s3_bucket_public_access_block" "backup" { 10 | bucket = aws_s3_bucket.backup.id 11 | 12 | # https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html 13 | block_public_acls = true 14 | ignore_public_acls = true 15 | block_public_policy = true 16 | restrict_public_buckets = true 17 | } 18 | 19 | resource "aws_s3_bucket_lifecycle_configuration" "backup" { 20 | bucket = aws_s3_bucket.backup.id 21 | 22 | rule { 23 | id = "ArchiveDatabaseDumps" 24 | status = "Enabled" 25 | 26 | filter { 27 | prefix = local.pg_dump_prefix 28 | } 29 | 30 | transition { 31 | days = 30 # min = 30 32 | storage_class = "STANDARD_IA" 33 | } 34 | } 35 | 36 | rule { 37 | id = "DeleteDatabaseDumps" 38 | status = "Enabled" 39 | 40 | filter { 41 | prefix = local.pg_dump_prefix 42 | } 43 | 44 | expiration { 45 | days = 365 46 | } 47 | } 48 | } 49 | 50 | resource "aws_ssm_parameter" "s3_bucket_backup" { 51 | name = "/${local.name}/s3_bucket_backup" 52 | type = "String" 53 | value = aws_s3_bucket.backup.bucket 54 | } 55 | -------------------------------------------------------------------------------- /modules/pleroma/s3_logs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "logs" { 2 | bucket = "${var.name}-logs-${random_id.s3_bucket_suffix.hex}" 3 | } 4 | 5 | resource "aws_s3_bucket_public_access_block" "logs" { 6 | bucket = aws_s3_bucket.logs.id 7 | 8 | # https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html 9 | block_public_acls = true 10 | ignore_public_acls = true 11 | block_public_policy = true 12 | restrict_public_buckets = true 13 | } 14 | 15 | resource "aws_s3_bucket_lifecycle_configuration" "logs" { 16 | bucket = aws_s3_bucket.logs.id 17 | 18 | rule { 19 | id = "ExpireOldLogs" 20 | status = "Enabled" 21 | 22 | filter {} 23 | 24 | expiration { 25 | days = 365 26 | } 27 | } 28 | } 29 | 30 | # Allow CloudFront to deliver logs to the bucket 31 | # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#AccessLogsBucketAndFileOwnership 32 | # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_log_delivery_canonical_user_id 33 | 34 | data "aws_cloudfront_log_delivery_canonical_user_id" "cloudfront" {} 35 | 36 | data "aws_canonical_user_id" "current" {} 37 | 38 | resource "aws_s3_bucket_acl" "logs_cloudfront" { 39 | bucket = aws_s3_bucket.logs.id 40 | 41 | access_control_policy { 42 | grant { 43 | grantee { 44 | id = data.aws_cloudfront_log_delivery_canonical_user_id.cloudfront.id 45 | type = "CanonicalUser" 46 | } 47 | permission = "FULL_CONTROL" 48 | } 49 | owner { 50 | id = data.aws_canonical_user_id.current.id 51 | } 52 | } 53 | } 54 | 55 | # Allow ELB to deliver logs to the bucket 56 | # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html 57 | # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/elb_service_account 58 | 59 | resource "aws_s3_bucket_policy" "logs_elb" { 60 | bucket = aws_s3_bucket.logs.id 61 | policy = data.aws_iam_policy_document.logs_bucket_policy.json 62 | } 63 | 64 | data "aws_elb_service_account" "elb" {} 65 | 66 | data "aws_iam_policy_document" "logs_bucket_policy" { 67 | statement { 68 | principals { 69 | type = "AWS" 70 | identifiers = [ 71 | data.aws_elb_service_account.elb.arn 72 | ] 73 | } 74 | actions = [ 75 | "s3:PutObject", 76 | ] 77 | resources = [ 78 | "${aws_s3_bucket.logs.arn}/*" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /modules/pleroma/ses.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ses_domain_identity" "main" { 2 | domain = data.aws_route53_zone.main.name 3 | } 4 | 5 | resource "aws_ses_domain_dkim" "main" { 6 | domain = aws_ses_domain_identity.main.domain 7 | } 8 | 9 | resource "aws_ses_domain_identity_verification" "main" { 10 | domain = aws_ses_domain_identity.main.domain 11 | } 12 | 13 | resource "aws_route53_record" "dkim" { 14 | count = 3 15 | 16 | zone_id = data.aws_route53_zone.main.id 17 | name = "${element(aws_ses_domain_dkim.main.dkim_tokens, count.index)}._domainkey" 18 | type = "CNAME" 19 | ttl = 600 20 | records = ["${element(aws_ses_domain_dkim.main.dkim_tokens, count.index)}.dkim.amazonses.com"] 21 | } 22 | 23 | resource "aws_route53_record" "spf" { 24 | zone_id = data.aws_route53_zone.main.id 25 | name = aws_ses_domain_identity.main.domain 26 | type = "TXT" 27 | ttl = 600 28 | records = [var.domain_spf_record] 29 | } 30 | -------------------------------------------------------------------------------- /modules/pleroma/sg.tf: -------------------------------------------------------------------------------- 1 | # Backend 2 | resource "aws_security_group" "backend" { 3 | name = local.name 4 | vpc_id = var.vpc_id 5 | } 6 | 7 | resource "aws_vpc_security_group_ingress_rule" "backend_ssh" { 8 | security_group_id = aws_security_group.backend.id 9 | ip_protocol = "tcp" 10 | from_port = 22 11 | to_port = 22 12 | cidr_ipv4 = "0.0.0.0/0" 13 | description = "SSH from everywhere" 14 | } 15 | 16 | resource "aws_vpc_security_group_ingress_rule" "backend_mosh" { 17 | security_group_id = aws_security_group.backend.id 18 | ip_protocol = "udp" 19 | from_port = 60000 20 | to_port = 61000 21 | cidr_ipv4 = "0.0.0.0/0" 22 | description = "Mosh from everywhere" 23 | } 24 | 25 | resource "aws_vpc_security_group_ingress_rule" "backend_lb" { 26 | security_group_id = aws_security_group.backend.id 27 | ip_protocol = "all" 28 | referenced_security_group_id = aws_security_group.lb.id 29 | description = "All from load balancer" 30 | } 31 | 32 | resource "aws_vpc_security_group_egress_rule" "backend_all" { 33 | security_group_id = aws_security_group.backend.id 34 | ip_protocol = "all" 35 | cidr_ipv4 = "0.0.0.0/0" 36 | description = "All to everywhere" 37 | } 38 | 39 | # Load balancer 40 | resource "aws_security_group" "lb" { 41 | name = "${local.name}-lb" 42 | vpc_id = var.vpc_id 43 | } 44 | 45 | resource "aws_vpc_security_group_ingress_rule" "lb_icmp" { 46 | security_group_id = aws_security_group.lb.id 47 | ip_protocol = "icmp" 48 | from_port = -1 49 | to_port = -1 50 | cidr_ipv4 = "0.0.0.0/0" 51 | description = "ICMP from everywhere" 52 | } 53 | 54 | resource "aws_vpc_security_group_ingress_rule" "lb_http" { 55 | security_group_id = aws_security_group.lb.id 56 | ip_protocol = "tcp" 57 | from_port = 80 58 | to_port = 80 59 | cidr_ipv4 = "0.0.0.0/0" 60 | description = "HTTP from everywhere" 61 | } 62 | 63 | resource "aws_vpc_security_group_ingress_rule" "lb_https" { 64 | security_group_id = aws_security_group.lb.id 65 | ip_protocol = "tcp" 66 | from_port = 443 67 | to_port = 443 68 | cidr_ipv4 = "0.0.0.0/0" 69 | description = "HTTPS from everywhere" 70 | } 71 | 72 | resource "aws_vpc_security_group_egress_rule" "lb_all" { 73 | security_group_id = aws_security_group.lb.id 74 | ip_protocol = "all" 75 | cidr_ipv4 = "0.0.0.0/0" 76 | description = "All to everywhere" 77 | } 78 | 79 | # Database 80 | resource "aws_security_group" "db" { 81 | name = "${local.name}-db" 82 | vpc_id = var.vpc_id 83 | } 84 | 85 | resource "aws_vpc_security_group_ingress_rule" "db_backend" { 86 | security_group_id = aws_security_group.db.id 87 | ip_protocol = "all" 88 | referenced_security_group_id = aws_security_group.backend.id 89 | description = "All from backend" 90 | } 91 | -------------------------------------------------------------------------------- /modules/pleroma/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.9" 3 | required_providers { 4 | # gp3 requires > 4.45 5 | # https://github.com/hashicorp/terraform-provider-aws/issues/27702 6 | aws = { 7 | source = "hashicorp/aws" 8 | version = "~> 4.45" 9 | configuration_aliases = [aws.us-east-1] 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.4" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/pleroma/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "Name of the service. It will be used to name EC2, ELB, and RDS instances." 3 | type = string 4 | default = "pleroma" 5 | } 6 | 7 | variable "domain" { 8 | description = "Domain of the Pleroma instance. This domain points to ELB. The domain must already exist in Route 53." 9 | type = string 10 | } 11 | 12 | variable "files_domain" { 13 | description = "Domain for serving user-uploaded files. This domain points to CloudFront, whose origin is an S3 bucket." 14 | type = string 15 | } 16 | 17 | variable "vpc_id" { 18 | description = "ID of the VPC where the resources are created." 19 | type = string 20 | } 21 | 22 | variable "private_subnet_ids" { 23 | description = "List of IDs of subnets to create private resources (e.g. databases) in." 24 | type = list(string) 25 | validation { 26 | condition = length(var.private_subnet_ids) >= 3 27 | error_message = "At least 3 private subnets are required." 28 | } 29 | } 30 | 31 | variable "public_subnet_ids" { 32 | description = "List of IDs of subnets to create public resources (e.g. load balancers) in." 33 | type = list(string) 34 | validation { 35 | condition = length(var.public_subnet_ids) >= 3 36 | error_message = "At least 3 public subnets are required." 37 | } 38 | } 39 | 40 | variable "ec2_instance_type" { 41 | type = string 42 | default = "t3.micro" 43 | } 44 | 45 | variable "ec2_key_name" { 46 | description = "Name of key pair to log into the EC2 instance. The key pair must already exist." 47 | type = string 48 | } 49 | 50 | variable "rds_instance_class" { 51 | type = string 52 | default = "db.t4g.micro" 53 | } 54 | 55 | variable "rds_storage_type" { 56 | type = string 57 | default = "gp2" # gp3 is no eligible for free tier 58 | } 59 | 60 | variable "rds_allocated_storage" { 61 | type = number 62 | default = 20 63 | } 64 | 65 | variable "domain_spf_record" { 66 | description = "Customize domain SPF record. By default only SES are allowed to send mails from the domain." 67 | type = string 68 | default = "v=spf1 include:amazonses.com -all" 69 | } 70 | -------------------------------------------------------------------------------- /snippets/provider_aws.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.67" 6 | } 7 | } 8 | } 9 | 10 | variable "region" { 11 | type = string 12 | default = "us-west-2" 13 | description = "AWS region. Default to us-west-2." 14 | } 15 | 16 | provider "aws" { 17 | region = var.region 18 | } 19 | 20 | provider "aws" { 21 | alias = "us-east-1" 22 | region = "us-east-1" 23 | } 24 | -------------------------------------------------------------------------------- /workspaces/manhole/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.67.0" 6 | constraints = "~> 4.67" 7 | hashes = [ 8 | "h1:dCRc4GqsyfqHEMjgtlM1EympBcgTmcTkWaJmtd91+KA=", 9 | "zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060", 10 | "zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6", 11 | "zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183", 12 | "zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1", 13 | "zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29", 14 | "zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7", 15 | "zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043", 16 | "zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362", 17 | "zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b", 18 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 19 | "zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf", 20 | "zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b", 21 | "zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c", 22 | "zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c", 23 | "zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /workspaces/manhole/data.tf: -------------------------------------------------------------------------------- 1 | data "terraform_remote_state" "vpc" { 2 | backend = "remote" 3 | 4 | config = { 5 | organization = "dabr-ca" 6 | workspaces = { 7 | name = "vpc" 8 | } 9 | } 10 | } 11 | 12 | data "aws_route53_zone" "main" { 13 | name = local.domain 14 | } 15 | 16 | data "aws_lb" "main" { 17 | name = local.name 18 | } 19 | 20 | data "aws_lb_listener" "main_https" { 21 | load_balancer_arn = data.aws_lb.main.arn 22 | port = 443 23 | } 24 | 25 | data "aws_ami" "ubuntu22" { 26 | most_recent = true 27 | owners = ["099720109477"] # Canonical 28 | 29 | filter { 30 | name = "name" 31 | values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /workspaces/manhole/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | name = "dabr-ca" 3 | domain = "dabr.ca" 4 | } 5 | 6 | data "aws_instance" "main" { 7 | filter { 8 | name = "tag:Name" 9 | values = [local.name] 10 | } 11 | } 12 | 13 | resource "aws_instance" "manhole" { 14 | ami = data.aws_ami.ubuntu22.id 15 | instance_type = data.aws_instance.main.instance_type 16 | subnet_id = data.aws_instance.main.subnet_id 17 | key_name = data.aws_instance.main.key_name 18 | iam_instance_profile = data.aws_instance.main.iam_instance_profile 19 | vpc_security_group_ids = data.aws_instance.main.vpc_security_group_ids 20 | 21 | root_block_device { 22 | volume_type = "gp3" 23 | } 24 | 25 | tags = { 26 | Name = "manhole" 27 | } 28 | 29 | lifecycle { 30 | ignore_changes = [ami] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /workspaces/manhole/pleroma_bot.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ssm_parameter" "pleroma_bot_token" { 2 | name = "/${local.name}/pleroma_bot_token" 3 | type = "SecureString" 4 | value = "." # populate this value manually 5 | 6 | lifecycle { 7 | ignore_changes = [value] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /workspaces/manhole/provider_aws.tf: -------------------------------------------------------------------------------- 1 | ../../snippets/provider_aws.tf -------------------------------------------------------------------------------- /workspaces/manhole/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.9" 3 | } 4 | -------------------------------------------------------------------------------- /workspaces/pleroma/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.67.0" 6 | constraints = "~> 4.45, ~> 4.67" 7 | hashes = [ 8 | "h1:dCRc4GqsyfqHEMjgtlM1EympBcgTmcTkWaJmtd91+KA=", 9 | ] 10 | } 11 | 12 | provider "registry.terraform.io/hashicorp/random" { 13 | version = "3.5.1" 14 | constraints = "~> 3.4" 15 | hashes = [ 16 | "h1:VSnd9ZIPyfKHOObuQCaKfnjIHRtR7qTw19Rz8tJxm+k=", 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /workspaces/pleroma/data.tf: -------------------------------------------------------------------------------- 1 | data "terraform_remote_state" "vpc" { 2 | backend = "remote" 3 | 4 | config = { 5 | organization = "dabr-ca" 6 | workspaces = { 7 | name = "vpc" 8 | } 9 | } 10 | } 11 | 12 | data "aws_route53_zone" "main" { 13 | name = local.domain 14 | } 15 | 16 | data "aws_lb_listener" "main_https" { 17 | load_balancer_arn = module.pleroma.lb.arn 18 | port = 443 19 | } 20 | -------------------------------------------------------------------------------- /workspaces/pleroma/files/keybase.txt: -------------------------------------------------------------------------------- 1 | ================================================================== 2 | https://keybase.io/wzyboy 3 | -------------------------------------------------------------------- 4 | 5 | I hereby claim: 6 | 7 | * I am an admin of https://dabr.ca 8 | * I am wzyboy (https://keybase.io/wzyboy) on keybase. 9 | * I have a public key with fingerprint 34C2 38A1 522D BD63 9B33 957B 8532 9745 B099 9CAB 10 | 11 | To do so, I am signing this object: 12 | 13 | { 14 | "body": { 15 | "key": { 16 | "eldest_kid": "01168f2e1b2877c6c6013d03ecb213b801a17f5576419ccf6ae444adebe94359def40a", 17 | "fingerprint": "34c238a1522dbd639b33957b85329745b0999cab", 18 | "host": "keybase.io", 19 | "key_id": "85329745b0999cab", 20 | "kid": "01168f2e1b2877c6c6013d03ecb213b801a17f5576419ccf6ae444adebe94359def40a", 21 | "uid": "9977be95ac45cc41069f690be38b6600", 22 | "username": "wzyboy" 23 | }, 24 | "service": { 25 | "hostname": "dabr.ca", 26 | "protocol": "https:" 27 | }, 28 | "type": "web_service_binding", 29 | "version": 1 30 | }, 31 | "ctime": 1717554735, 32 | "expire_in": 157680000, 33 | "prev": "759176828d70996d50bad18c6a1ee15cfd2b5f1d30687ef30ac145705770e7d2", 34 | "seqno": 29, 35 | "tag": "signature" 36 | } 37 | 38 | which yields the signature: 39 | 40 | -----BEGIN PGP MESSAGE----- 41 | 42 | owGtUj1oFEEY3ZgoeCAIwjUi6mhjOOP8z85ZWdhYRtDOdX6+vSxJds/dvcTziIUi 43 | WKgEQaKohbWKqQUjitgIgoWWglgJYmFhJzp7JJ2l0wzzvvfevO/jW901GbXa1+ja 44 | yfV7Dx5PvPtmozR5T0fIFn6IuiM0D+MLFjxUdTKfedRFmBAZpxSIpbFSTjqJCfOY 45 | gbOUMBtjYohKhVCSE+1cKg1wzo0HC5ozoT2kHBvUQWmW96Dsl1leB1vGHWWxIYJS 46 | b71k2jKmhbKxYFQrLizWWjtjg3CuqBpFCGdNBTNZEbDwSMbx/sH/z7kHYzutlQoV 47 | YRwXznGCpU6lxhZYbKXEuCFWUOZmEQJ7+dLQFkO00kEBW8ocNGNt+tise2PLGde4 48 | 98uiLlyxEMC5uu5X3UZUD/tjF7DJpj6xWe7DAINiCcoqK3LUJYHp6qwxJIooIbhi 49 | ooPgYj8rIckaRuguxuE0/8BSsFRCk4DR2KswsFnpBbbGk9hJQwCIcKmnVqTEMyxj 50 | BSnDxhEuFBZKYVCeoqalC3mBulSHoKYXTKusl5t6UAJaaV0fTEXtVrSvfXCKv44f 51 | zR594e6zO6Otpdu+rdm4qLVz9xZyik1EP4pk+syHWzOD7pObL9eevb16483HQzuO 52 | nHt++MvGgVdXPk9Ev5PbX+Xynk8/zx9/uHdx9eyv6WNP7574s//7xvppd9lO/gU= 53 | =QPyp 54 | -----END PGP MESSAGE----- 55 | 56 | And finally, I am proving ownership of this host by posting or 57 | appending to this document. 58 | 59 | View my publicly-auditable identity here: https://keybase.io/wzyboy 60 | 61 | ================================================================== 62 | 63 | ================================================================== 64 | https://keybase.io/wzyboy 65 | -------------------------------------------------------------------- 66 | 67 | I hereby claim: 68 | 69 | * I am an admin of https://dabr.ca 70 | * I am wzyboy (https://keybase.io/wzyboy) on keybase. 71 | * I have a public key with fingerprint CDEF 6ED1 A4C7 ADE1 D6AB C4DB AE9C 2216 8EBA 0BDB 72 | 73 | To do so, I am signing this object: 74 | 75 | { 76 | "body": { 77 | "key": { 78 | "eldest_kid": "0101d8255c38b754f928ecb171d81a32fd30ef33c01fb0ef40df702189cadc88c23c0a", 79 | "fingerprint": "cdef6ed1a4c7ade1d6abc4dbae9c22168eba0bdb", 80 | "host": "keybase.io", 81 | "key_id": "ae9c22168eba0bdb", 82 | "kid": "0101d8255c38b754f928ecb171d81a32fd30ef33c01fb0ef40df702189cadc88c23c0a", 83 | "uid": "9977be95ac45cc41069f690be38b6600", 84 | "username": "wzyboy" 85 | }, 86 | "service": { 87 | "hostname": "dabr.ca", 88 | "protocol": "https:" 89 | }, 90 | "type": "web_service_binding", 91 | "version": 1 92 | }, 93 | "ctime": 1671753572, 94 | "expire_in": 157680000, 95 | "prev": "073de9d05f74d170f894c844412f9aac765d99d88eb5ae0cd9046bebb1b52dec", 96 | "seqno": 24, 97 | "tag": "signature" 98 | } 99 | 100 | which yields the signature: 101 | 102 | -----BEGIN PGP MESSAGE----- 103 | 104 | owGtUmtMFFcY3QU2XeSlphKlUmEQJAo6MzvPLVKrojSIBGg1hQLOnXtnGWF3cXdF 105 | Hl1RK4IkAgI/WpCCldoAPni2BbGptQrWIiFQWqVqbUBLMEFICoXYxwzRf/3Z++fe 106 | e75zzv3uyVfu5arRay+dwXxLuz1GtbcngEZsmDLlY8AKczFjPpaBFjeUCZHdkZYh 107 | Q8yI4QROQI6kadHAAZamJJ7kkAgIVkEJwUBK0IAjyWAQcUICyonCocTiJMHxogBF 108 | jhNJpSRgYZgkW0zIlmWTLQ7FVoRIYhAkBEpkBYgIyAhApCAQEC+SJMFwCAg4gEAR 109 | plvtqkJpDgh2tFG2KphySVts7z/4/3PfBxfteJ5lAeJpQaRoUaQInOElhscBUtwZ 110 | BsdVoh3ZLIIZKexDebnAmos5wzAFy5ZFpMaq/uNFHQrAtlFU3bNsVodVtGYqYLrD 111 | kWU3qiJHbtaiCwJpL/RpQLZAJUBFkY1sdtlqwYyEwhQdsmpIMCzB0gaaJcMwlJMl 112 | 21CarDJoluFwZanvoGw1FdYAEQ9xWmIpSCSwuMTxlMhRFEWQEi8IIsvQkOchp+RJ 113 | CwgXIY9TDEAAEIAmIRIx9UsHLFbMSFJKo4JJMbXLJovgOGhDmHPJSReDm0ar1/j7 114 | Brr1T1kGG3ovPhpp/m705dTpXNSR0yxxX/oS2T2yVFN2uOvKpX3LFib02n+Czrcd 115 | KTi9YCYKOjcjkMAUn5zON5/X/+KvP7DcxUu7IufpruP2sg9rbrZ81Pf+ztdKhauD 116 | PXDg18f6wR+jN9XOz8jg4ZPOy8fueleWh37sHIv6jKwIfHYvcd3zq6kxhtpt7Xvc 117 | VlM/3No+EdxxtlDr4pwZYUff9fBMtufsi6gedsTtTNB61KS7PrBgk0ENicNkfMfx 118 | G3VjbXHr7ww21c+7tizMiYXbn9QHPF9WUhSKbdlh9zyRTAy3aqtD7nf13fbdkdHt 119 | U3h4M722q/DrkKQx+/pzeMnAdIdvWPnvfl9ePGX+OZ9vD4ifGy6K3XMn0mP2+6GI 120 | o/HVPUkTATHRpcEVmTrdgF+QVLy3PDtrhbcuvrBxw43xvmMjjYmnU9zX/PGT19xM 121 | c9WFR2vO5s1Wen1zrz5vfFVKoPnTrTGReUVP+3trP/gtInzoftP1c1WNumBP3e7V 122 | 7V9EG7Qkt+umf0J47KqhQw91j1sfbHH/iql6O/b1KZP+grfNNyY0+VqSa42u4L3P 123 | 24YuR9Q56ZlO52zCKy1xlmbjurUBbm9MRsGjGbd6zQOQtWzqG02Sl/+5YeW8Dk99 124 | 1mq70lhh1r/qVny3xCcDjYd3R/mgv8dStgVXvhMWsxfNTeh79uvKvr2W79epd6au 125 | dG6NPDXpcn3a9NeRiDf3N4W8Zew/E1/3yb8= 126 | =xt0T 127 | -----END PGP MESSAGE----- 128 | 129 | And finally, I am proving ownership of this host by posting or 130 | appending to this document. 131 | 132 | View my publicly-auditable identity here: https://keybase.io/wzyboy 133 | 134 | ================================================================== 135 | -------------------------------------------------------------------------------- /workspaces/pleroma/gl.tf: -------------------------------------------------------------------------------- 1 | # Set up glitch-lily as an alternative FE. This is not considered a part of 2 | # Pleroma so it's not codified in the module. 3 | 4 | locals { 5 | gl_domain = "gl.${local.domain}" 6 | } 7 | 8 | resource "aws_lb_listener_rule" "gl" { 9 | listener_arn = data.aws_lb_listener.main_https.arn 10 | 11 | condition { 12 | host_header { 13 | values = [local.gl_domain] 14 | } 15 | } 16 | 17 | action { 18 | type = "forward" 19 | target_group_arn = aws_lb_target_group.gl.arn 20 | } 21 | } 22 | 23 | resource "aws_lb_target_group" "gl" { 24 | name = "${local.name}-gl" 25 | port = 4080 26 | protocol = "HTTP" 27 | vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id 28 | } 29 | 30 | resource "aws_lb_target_group_attachment" "gl" { 31 | target_group_arn = aws_lb_target_group.gl.arn 32 | target_id = module.pleroma.instance.id 33 | } 34 | 35 | resource "aws_route53_record" "gl" { 36 | zone_id = data.aws_route53_zone.main.id 37 | name = local.gl_domain 38 | type = "A" 39 | 40 | alias { 41 | zone_id = module.pleroma.lb.zone_id 42 | name = module.pleroma.lb.dns_name 43 | evaluate_target_health = false 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /workspaces/pleroma/keybase.tf: -------------------------------------------------------------------------------- 1 | # Set up Keybase verification. 2 | 3 | resource "aws_lb_listener_rule" "keybase" { 4 | listener_arn = data.aws_lb_listener.main_https.arn 5 | 6 | condition { 7 | host_header { 8 | values = [local.domain] 9 | } 10 | } 11 | condition { 12 | path_pattern { 13 | values = ["/keybase.txt"] 14 | } 15 | } 16 | 17 | action { 18 | type = "redirect" 19 | redirect { 20 | host = "files.${local.domain}" 21 | status_code = "HTTP_302" 22 | } 23 | } 24 | } 25 | 26 | # ALB does not allow fixed-response body to be > 1024 bytes. 27 | resource "aws_s3_object" "keybase" { 28 | bucket = module.pleroma.bucket_main.bucket 29 | key = "keybase.txt" 30 | content = file("./files/keybase.txt") 31 | content_type = "text/plain" 32 | } 33 | -------------------------------------------------------------------------------- /workspaces/pleroma/logstash.tf: -------------------------------------------------------------------------------- 1 | # Set up a static IAM user so that Logstash can read logs from a non-AWS environment 2 | 3 | resource "aws_iam_user" "logstash" { 4 | name = "logstash" 5 | } 6 | 7 | resource "aws_iam_user_policy" "logstash" { 8 | user = aws_iam_user.logstash.name 9 | policy = data.aws_iam_policy_document.logstash.json 10 | } 11 | 12 | resource "aws_iam_access_key" "logstash" { 13 | user = aws_iam_user.logstash.name 14 | } 15 | 16 | data "aws_iam_policy_document" "logstash" { 17 | statement { 18 | actions = ["s3:ListBucket"] 19 | resources = [module.pleroma.bucket_logs.arn] 20 | } 21 | statement { 22 | actions = ["s3:GetObject"] 23 | resources = ["${module.pleroma.bucket_logs.arn}/*"] 24 | } 25 | } 26 | 27 | output "logstash" { 28 | description = "Information for setting up Logstash." 29 | value = { 30 | bucket = module.pleroma.bucket_logs.id 31 | access_key_id = aws_iam_access_key.logstash.id 32 | secret_access_key = aws_iam_access_key.logstash.secret 33 | } 34 | sensitive = true 35 | } 36 | -------------------------------------------------------------------------------- /workspaces/pleroma/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | name = "dabr-ca" 3 | domain = "dabr.ca" 4 | } 5 | 6 | module "pleroma" { 7 | source = "../../modules/pleroma" 8 | providers = { 9 | aws.us-east-1 = aws.us-east-1 10 | } 11 | 12 | name = local.name 13 | domain = local.domain 14 | files_domain = "files.${local.domain}" 15 | vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id 16 | private_subnet_ids = data.terraform_remote_state.vpc.outputs.subnet_ids.private[*] 17 | public_subnet_ids = data.terraform_remote_state.vpc.outputs.subnet_ids.public[*] 18 | ec2_key_name = "wzyboy@tarball" 19 | 20 | rds_instance_class = "db.m6g.large" 21 | rds_storage_type = "gp3" 22 | rds_allocated_storage = 25 23 | 24 | domain_spf_record = "v=spf1 include:amazonses.com include:spf.messagingengine.com -all" 25 | } 26 | -------------------------------------------------------------------------------- /workspaces/pleroma/provider_aws.tf: -------------------------------------------------------------------------------- 1 | ../../snippets/provider_aws.tf -------------------------------------------------------------------------------- /workspaces/pleroma/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.9" 3 | cloud { 4 | organization = "dabr-ca" 5 | workspaces { 6 | name = "pleroma" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /workspaces/route53/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.67.0" 6 | constraints = "~> 4.67" 7 | hashes = [ 8 | "h1:dCRc4GqsyfqHEMjgtlM1EympBcgTmcTkWaJmtd91+KA=", 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /workspaces/route53/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | domain = "dabr.ca" 3 | } 4 | 5 | resource "aws_route53_zone" "main" { 6 | name = local.domain 7 | } 8 | 9 | # Set up MX to receive emails 10 | resource "aws_route53_record" "mx" { 11 | zone_id = aws_route53_zone.main.id 12 | 13 | name = local.domain 14 | type = "MX" 15 | ttl = 300 16 | records = [ 17 | "10 in1-smtp.messagingengine.com.", 18 | "20 in2-smtp.messagingengine.com.", 19 | ] 20 | } 21 | 22 | # Set up DKIM for FastMail 23 | # NOTE: SPF is set up in ../pleroma 24 | resource "aws_route53_record" "dkim" { 25 | for_each = toset(["fm1", "fm2", "fm3"]) 26 | 27 | zone_id = aws_route53_zone.main.id 28 | name = "${each.key}._domainkey.${local.domain}." 29 | type = "CNAME" 30 | ttl = "300" 31 | records = ["${each.key}.${local.domain}.dkim.fmhosted.com."] 32 | } 33 | 34 | # Status page 35 | resource "aws_route53_record" "status" { 36 | zone_id = aws_route53_zone.main.id 37 | 38 | name = "status.${local.domain}" 39 | type = "CNAME" 40 | ttl = 300 41 | records = ["statuspage.betteruptime.com"] 42 | } 43 | -------------------------------------------------------------------------------- /workspaces/route53/provider_aws.tf: -------------------------------------------------------------------------------- 1 | ../../snippets/provider_aws.tf -------------------------------------------------------------------------------- /workspaces/route53/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.5.5" 3 | cloud { 4 | organization = "dabr-ca" 5 | workspaces { 6 | name = "route53" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /workspaces/tfc/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/tfe" { 5 | version = "0.39.0" 6 | constraints = "~> 0.39.0" 7 | hashes = [ 8 | "h1:6gbiHfdJAUopCXoqj3khdv4eSMpgQSy8HuCwVqSPJic=", 9 | "zh:084f4de9d660780bc326d86fa93ecd383455dc040f52931766a5360f13791bea", 10 | "zh:09d12b42d7bbf9fd11663aa98266eae20d6df4f989f4fca05d4a15b917572a00", 11 | "zh:0e6da14680a01c3ad4c53ea375edb28ca15927b5ad0fa775efc8dc17b684c68b", 12 | "zh:29e6c0725c84576d7533eff03c6af71f48decfce79ffcbe655111870f6c712b9", 13 | "zh:32bbc784492251b3dac028fa8b95ad87fb9828ba52a7f6df85a1c3e00c444918", 14 | "zh:605f46c284ea2eac591ec42b58259761dbe5130d02d31bc0c9388d36c5b74c9f", 15 | "zh:6b7082dc3608b05540aee00c9fe1a31e196d49f1422c8cc9da03b252ee1f9d9c", 16 | "zh:8293aa197bf70f75f56ca9e51b168d7e7b18e0339a2d86bdf5926727b34e85d8", 17 | "zh:d115d053faa140f45bd99156be6c5a629c61257c7811715793f6d8512cc09a80", 18 | "zh:d95ec293fa70e946b6cd657912b33155f8be3413e6128ed2bfa5a493f788e439", 19 | "zh:e89c4a0eff9350265084c8200802a68de6aeeeeb74aa776be1d62f36a0f32552", 20 | "zh:f79757b6c54c4c80e9c797fd1bac9e535bb3e106318ea590d85b3effff87b19f", 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /workspaces/tfc/csv.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | default_terraform_version = "1.5.5" 3 | 4 | csv = csvdecode(file("../../datasources/tfc/workspaces.csv")) 5 | workspaces = { 6 | for workspace in local.csv : 7 | workspace["name"] => { 8 | name = trimspace(workspace["name"]) 9 | auto_apply = tobool(workspace["auto_apply"]) 10 | global_remote_state = tobool(workspace["global_remote_state"]) 11 | modules = compact(split(":", trimspace(workspace["modules"]))) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /workspaces/tfc/main.tf: -------------------------------------------------------------------------------- 1 | # This workspace uses tfe provider to manages all workspaces on Terraform Cloud, 2 | # including itself. 3 | 4 | data "tfe_organization" "dabr-ca" { 5 | name = "dabr-ca" 6 | } 7 | 8 | locals { 9 | # https://app.terraform.io/app/dabr-ca/settings/version-control 10 | gh_oauth_token_id = "ot-YGoaidJBNjpHQTj3" 11 | } 12 | 13 | resource "tfe_workspace" "workspaces" { 14 | for_each = local.workspaces 15 | 16 | name = each.key 17 | organization = data.tfe_organization.dabr-ca.name 18 | terraform_version = local.default_terraform_version 19 | allow_destroy_plan = false 20 | auto_apply = each.value.auto_apply 21 | execution_mode = "remote" 22 | global_remote_state = each.value.global_remote_state 23 | 24 | trigger_prefixes = flatten([ 25 | "/datasources/${each.key}", 26 | [for m in each.value.modules : "/modules/${m}"], 27 | ]) 28 | 29 | vcs_repo { 30 | identifier = "dabr-ca/infra" 31 | oauth_token_id = local.gh_oauth_token_id 32 | } 33 | working_directory = "/workspaces/${each.key}" 34 | } 35 | -------------------------------------------------------------------------------- /workspaces/tfc/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | tfe = { 4 | source = "hashicorp/tfe" 5 | version = "~> 0.39.0" 6 | } 7 | } 8 | 9 | cloud { 10 | organization = "dabr-ca" 11 | workspaces { 12 | name = "tfc" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /workspaces/vpc/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.67.0" 6 | constraints = ">= 3.63.0, ~> 4.67" 7 | hashes = [ 8 | "h1:dCRc4GqsyfqHEMjgtlM1EympBcgTmcTkWaJmtd91+KA=", 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /workspaces/vpc/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | name = "main" 3 | cidr_block = "10.31.0.0/16" 4 | subnets = cidrsubnets(local.cidr_block, 4, 4, 4, 4, 4, 4) 5 | subnet_groups = chunklist(local.subnets, 3) 6 | } 7 | 8 | module "vpc" { 9 | source = "terraform-aws-modules/vpc/aws" 10 | version = "4.0.2" 11 | 12 | name = local.name 13 | cidr = local.cidr_block 14 | 15 | azs = ["${var.region}a", "${var.region}b", "${var.region}c"] 16 | private_subnets = local.subnet_groups[0] 17 | public_subnets = local.subnet_groups[1] 18 | 19 | enable_nat_gateway = false 20 | single_nat_gateway = true 21 | map_public_ip_on_launch = true 22 | } 23 | 24 | resource "aws_key_pair" "tarball" { 25 | key_name = "wzyboy@tarball" 26 | public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDB3eYUem12rVaP+2ijbGqFqTM4bfnYcYmHjDq7j6IjT wzyboy@tarball" 27 | } 28 | -------------------------------------------------------------------------------- /workspaces/vpc/outputs_vpc.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | value = module.vpc.vpc_id 3 | } 4 | 5 | output "vpc_cidr_block" { 6 | value = module.vpc.vpc_cidr_block 7 | } 8 | 9 | output "subnet_ids" { 10 | description = "Map of subnet type => [id]" 11 | value = { 12 | private = module.vpc.private_subnets 13 | public = module.vpc.public_subnets 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /workspaces/vpc/provider_aws.tf: -------------------------------------------------------------------------------- 1 | ../../snippets/provider_aws.tf -------------------------------------------------------------------------------- /workspaces/vpc/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | cloud { 3 | organization = "dabr-ca" 4 | workspaces { 5 | name = "vpc" 6 | } 7 | } 8 | } 9 | --------------------------------------------------------------------------------