From 31b13dc09b3497d5e0e2f5c45f464f356188938a Mon Sep 17 00:00:00 2001 From: Erik Stambaugh Date: Thu, 18 Jan 2024 18:14:17 -0800 Subject: [PATCH] First version that works, or at least gets all the way through ansible config --- .gitignore | 6 ++ Makefile | 13 ++- ansible/Makefile | 18 ++-- ansible/inventory.tmpl.yaml | 19 +++- ansible/roles/mastodon/tasks/main.yaml | 83 +++++++++++++++++- .../templates/docker-compose.mastodon.yaml | 20 ++--- .../roles/mastodon/templates/env.production | 87 +++++++++++++++++++ .../mastodon/templates/mastodon_secrets.yaml | 5 ++ config.mk.in | 9 +- terraform/Makefile | 12 ++- terraform/config.tf.in | 2 +- terraform/main.tf | 32 +------ terraform/outputs.tf | 16 +++- terraform/route53.tf | 9 +- terraform/s3.tf | 42 +++++++++ 15 files changed, 302 insertions(+), 71 deletions(-) create mode 100644 ansible/roles/mastodon/templates/env.production create mode 100644 ansible/roles/mastodon/templates/mastodon_secrets.yaml create mode 100644 terraform/s3.tf diff --git a/.gitignore b/.gitignore index fe7d8e3..b75cf19 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,9 @@ terraform.tfstate* config.tf privkey pubkey +.toolcheck +.s3_iam_credentials +.s3_id +.s3_secret +ansible/credentials +ansible/mastodon_secrets.yaml diff --git a/Makefile b/Makefile index b4cb3cc..b33962d 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,10 @@ endef default: terraform ansible -terraform ansible: config.mk +ansible: config.mk .s3_iam_credentials + @$(MAKE) -C $@ + +terraform: config.mk @$(MAKE) -C $@ ssh: config.mk @@ -21,10 +24,16 @@ ssh: config.mk #ansible: # @$(MAKE) -C ansible -config.mk: +config.mk: config.mk.in @ $(info $(CONFIG_MSG) ) @ exit 1 +.s3_iam_credentials: terraform/.s3_id terraform/.s3_secret + cat $^ > $@ + +terraform/.s3_id terraform/.s3_secret: terraform + + clean: rm -f config.mk @$(MAKE) -C terraform clean diff --git a/ansible/Makefile b/ansible/Makefile index 4e8371c..339b636 100644 --- a/ansible/Makefile +++ b/ansible/Makefile @@ -2,8 +2,8 @@ include ../config.mk include ../terraform/terraform.mk -# XXX parameterize -AWS_REGION = us-west-2 +# I don't remember why I had this at all: +#AWS_REGION = $(AWS_REGION) SSH := ssh -o "StrictHostKeyChecking=no" -o UserKnownHostsFile=/dev/null -o ProxyCommand="sh -c \"aws --region $(AWS_REGION) ssm start-session --target %h --document-name AWS-StartSSHSession --parameters portNumber=%p\"" -i ../terraform/privkey -l ubuntu @@ -21,7 +21,7 @@ inventory.yaml: inventory.tmpl.yaml sedline SEDLINE = -sedline: terraform_sedline config_sedline +sedline: terraform_sedline config_sedline s3_sedline config_sedline: $(addprefix __sed_,$(shell grep '^[0-9A-Z_]' ../config.mk | awk '{print $$1}')) @@ -30,13 +30,10 @@ terraform_sedline: $(addprefix __sed_,$(shell grep '^[0-9A-Z_]' ../terraform/ter __sed_%: $(eval SEDLINE := $$(SEDLINE) -e 's/{{$*}}/$($*)/') - - -# -#TF_OUTPUTS = -# -#tf_outputs: ../terraform/terraform.tfstate -# +# FIXME: this is awful because it's all in the clear +s3_sedline: + $(eval SEDLINE := $$(SEDLINE) -e 's/{{S3_IAM_ID}}/$(shell head -1 ../.s3_iam_credentials)/') + $(eval SEDLINE := $$(SEDLINE) -e 's/{{S3_IAM_SECRET}}/$(shell tail -1 ../.s3_iam_credentials)/') # FIXME: DRY this target @@ -57,5 +54,6 @@ toolcheck: fi @echo + mkdir -p credentials/mastodon diff --git a/ansible/inventory.tmpl.yaml b/ansible/inventory.tmpl.yaml index 148bef2..babf63e 100644 --- a/ansible/inventory.tmpl.yaml +++ b/ansible/inventory.tmpl.yaml @@ -7,7 +7,24 @@ social: hostname: social vars: ansible_ssh_common_args: -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="sh -c \"aws --region {{AWS_REGION}} ssm start-session --target %h --document-name AWS-StartSSHSession --parameters portNumber=%p\"" - public_ip: "{{PUBLIC_IP}}" + alternate_domains: {{ALTERNATE_DOMAINS}} + aws_region: {{AWS_REGION}} + #db_password: + domain_name: {{DOMAIN_NAME}} mastodon_sidekiq_count: {{MASTODON_SIDEKIQ_COUNT}} mastodon_sidekiq_threads: {{MASTODON_SIDEKIQ_THREADS}} + public_ip: "{{PUBLIC_IP}}" + s3_bucket_name: "{{S3_BUCKET_NAME}}" + #s3_endpoint: + s3_hostname: "s3.{{AWS_REGION}}.amazonaws.com" + s3_iam_id: {{S3_IAM_ID}} + s3_iam_secret: {{S3_IAM_SECRET}} + #smtp_server: + + otp_secret: + secret_key_base: + vapid_secrets: + #vapid_private_key: + #vapid_public_key: + diff --git a/ansible/roles/mastodon/tasks/main.yaml b/ansible/roles/mastodon/tasks/main.yaml index d32bff4..41b515d 100644 --- a/ansible/roles/mastodon/tasks/main.yaml +++ b/ansible/roles/mastodon/tasks/main.yaml @@ -7,22 +7,97 @@ - docker-compose-v2 - git -- name: Mastodon path +- name: base path file: path: "/srv/mastodon" state: directory recurse: true -- name: mastodon source +- name: source git: repo: "https://tea.entar.net/teh/mastodon.git" dest: /srv/mastodon/src -- name: mastodon docker-compose +- name: docker-compose file template: src: templates/docker-compose.mastodon.yaml dest: /srv/mastodon/docker-compose.yaml + register: compose -# add env file +## generate a secrets file if we need one +# FIXME: what's in the mastodon_secrets.yaml file should be in credential lookup like db_password is + +- name: check mastodon secrets var file + delegate_to: localhost + become: false + stat: + path: mastodon_secrets.yaml + register: mastosecrets + +- name: env file stub + template: + src: templates/env.production + dest: /srv/mastodon/.env.production + vars: + db_password: "{{ lookup('ansible.builtin.password', 'credentials/mastodon/postgres', length=15) }}" + alternate_domains: "mastodon_web" + when: mastosecrets.stat.exists != true + +- name: get SECRET_KEY_BASE + shell: docker compose run --rm mastodon_web rake secret 2>/dev/null | tail -1 + args: + chdir: /srv/mastodon + register: skb + when: mastosecrets.stat.exists != true + +- name: get OTP_SECRET + shell: docker compose run --rm mastodon_web rake secret 2>/dev/null | tail -1 + args: + chdir: /srv/mastodon + register: otp + when: mastosecrets.stat.exists != true + +- name: get vapid secrets + command: docker compose run --rm mastodon_web rake mastodon:webpush:generate_vapid_key + args: + chdir: /srv/mastodon + register: vapid + when: mastosecrets.stat.exists != true + +- name: create mastodon secrets file + delegate_to: localhost + become: false + template: + src: templates/mastodon_secrets.yaml + dest: mastodon_secrets.yaml + when: mastosecrets.stat.exists != true + + ## now that we have a secrets file, read it in and make the env file again + +- name: read env secret vars + include_vars: + file: mastodon_secrets.yaml + +- name: env file + template: + src: templates/env.production + dest: /srv/mastodon/.env.production + vars: + db_password: "{{ lookup('ansible.builtin.password', 'credentials/mastodon/postgres', length=15) }}" + alternate_domains: "mastodon_web" + register: envfile + +## finally, let's launch mastodon + +- name: launch mastodon + command: docker compose up -d + args: + chdir: /srv/mastodon + +- name: restart mastodon + command: docker compose restart + args: + chdir: /srv/mastodon + when: envfile.changed or compose.changed diff --git a/ansible/roles/mastodon/templates/docker-compose.mastodon.yaml b/ansible/roles/mastodon/templates/docker-compose.mastodon.yaml index 7ddbc58..f58432e 100644 --- a/ansible/roles/mastodon/templates/docker-compose.mastodon.yaml +++ b/ansible/roles/mastodon/templates/docker-compose.mastodon.yaml @@ -43,7 +43,7 @@ services: # - "thread_pool.write.queue_size=1000" # networks: # - mastodon -# - nginx +# #- nginx # healthcheck: # test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"] # volumes: @@ -67,7 +67,7 @@ services: command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" networks: - mastodon - - nginx + #- nginx healthcheck: # prettier-ignore test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1'] @@ -78,7 +78,7 @@ services: - mastodon_redis # - es volumes: - - ./public/system:/mastodon/public/system + - ./src/public/system:/mastodon/public/system mastodon_streaming: container_name: mastodon_streaming @@ -91,7 +91,7 @@ services: command: node ./streaming networks: - mastodon - - nginx + #- nginx healthcheck: # prettier-ignore test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1'] @@ -101,9 +101,9 @@ services: - mastodon_db - mastodon_redis - {% for i in range(mastodon_sidekiq_count) %} +{% for i in range(mastodon_sidekiq_count) %} mastodon_sidekiq_{{i}}: - container_name: mastodon_sidekiq + container_name: mastodon_sidekiq_{{i}} build: src image: ghcr.io/mastodon/mastodon:v4.2.1 restart: always @@ -116,13 +116,13 @@ services: - mastodon_redis networks: - mastodon - - nginx + #- nginx volumes: - - ./public/system:/mastodon/public/system + - ./src/public/system:/mastodon/public/system healthcheck: test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"] - {% endfor %} +{% endfor %} ## Uncomment to enable federation with tor instances along with adding the following ENV variables ## http_proxy=http://privoxy:8118 @@ -150,7 +150,7 @@ services: command: "--statsd.mapping-config=/statsd-mapping.yaml" volumes: - - ./statsd-mapping.yaml:/statsd-mapping.yaml + - ./src/statsd-mapping.yaml:/statsd-mapping.yaml networks: - mastodon diff --git a/ansible/roles/mastodon/templates/env.production b/ansible/roles/mastodon/templates/env.production new file mode 100644 index 0000000..eba929e --- /dev/null +++ b/ansible/roles/mastodon/templates/env.production @@ -0,0 +1,87 @@ +# Note that this file accepts slightly different syntax depending on whether +# you are using `docker-compose` or not. In particular, if you use +# `docker-compose`, the value of each declared variable will be taken verbatim, +# including surrounding quotes. +# See: https://github.com/mastodon/mastodon/issues/16895 + +# Federation +# ---------- +# This identifies your server and cannot be changed safely later +# ---------- +LOCAL_DOMAIN={{domain_name}} +ALTERNATE_DOMAINS={{alternate_domains}} + +# Redis +# ----- +REDIS_HOST=mastodon_redis +REDIS_PORT=6379 + +# PostgreSQL +# ---------- +DB_HOST=mastodon_db +DB_USER=postgres +DB_NAME=mastodon_production +DB_PASS={{db_password}} +DB_PORT=5432 + +POSTGRES_USER=postgres +POSTGRES_PASSWORD={{db_password}} +POSTGRES_DB=mastodon_production + + +## Elasticsearch (optional) +## ------------------------ +#ES_ENABLED=true +#ES_HOST=localhost +#ES_PORT=9200 +## Authentication for ES (optional) +#ES_USER=elastic +#ES_PASS=password + +# Secrets +# ------- +# Make sure to use `rake secret` to generate secrets +# ------- +SECRET_KEY_BASE={{secret_key_base}} +OTP_SECRET={{otp_secret}} +{{vapid_secrets}} + +# Sending mail +# ------------ +#SMTP_SERVER=#{#{smtp_server#}#} +SMTP_PORT=25 +SMTP_FROM_ADDRESS=Mastodon +SMTP_AUTH_METHOD=none +SMTP_OPENSSL_VERIFY_MODE=none + +## File storage (optional) +## ----------------------- +# teh-entar-net-mastodon-media.us-southeast-1.linodeobjects.com +S3_ENABLED=true +S3_BUCKET={{s3_bucket_name}} +AWS_ACCESS_KEY_ID={{s3_iam_id}} +AWS_SECRET_ACCESS_KEY={{s3_iam_secret}} +#S3_ALIAS_HOST= +S3_REGION={{aws_region}} +S3_PROTOCOL=https +#S3_HOSTNAME=teh-entar-net-mastodon-media.us-southeast-1.linodeobjects.com +S3_HOSTNAME={{s3_hostname}} +#S3_ENDPOINT=#{#{s3_endpoint#}#} +S3_OVERRIDE_PATH_STYLE=true + +# IP and session retention +# ----------------------- +# Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml +# to be less than daily if you lower IP_RETENTION_PERIOD below two days (172800). +# ----------------------- +IP_RETENTION_PERIOD=31556952 +SESSION_RETENTION_PERIOD=31556952 + +STATSD_ADDR=statsd:9125 + +ACTIVITY_API_ENABLED=false +PEERS_API_ENABLED=false + +RAILS_LOG_TO_STDOUT=enabled +RAILS_LOG_LEVEL=info + diff --git a/ansible/roles/mastodon/templates/mastodon_secrets.yaml b/ansible/roles/mastodon/templates/mastodon_secrets.yaml new file mode 100644 index 0000000..e457384 --- /dev/null +++ b/ansible/roles/mastodon/templates/mastodon_secrets.yaml @@ -0,0 +1,5 @@ +--- +secret_key_base: {{skb.stdout}} +otp_secret: {{otp.stdout}} +vapid_secrets: | + {{vapid.stdout | indent(2)}} diff --git a/config.mk.in b/config.mk.in index 2d84ae5..40227ea 100644 --- a/config.mk.in +++ b/config.mk.in @@ -21,11 +21,16 @@ AWS_SINGLE_INSTANCE_TYPE = t4g.small # Paste (one-line!) the root SSH public key for the AWS instance, or leave blank to generate new private/public keys AWS_INSTANCE_PUBLIC_KEY = -# What is the DNS subdomain to be delegated for these services? Leave blank to skip. -AWS_ROUTE53_ZONE = + +# What is the domain name that will be used for these services? Don't be silly and use the default in production +DOMAIN_NAME = burn.social + +# What alternate domains will this use? Leave blank for none. +ALTERNATE_DOMAINS = + # TODO: more detailed sidekiq tuning per https://thomas-leister.de/en/scaling-up-mastodon/ # How many sidekiq containers should Mastodon have? diff --git a/terraform/Makefile b/terraform/Makefile index 8f046d0..1ae760d 100644 --- a/terraform/Makefile +++ b/terraform/Makefile @@ -5,7 +5,7 @@ default: terraform # I hate sed too and I am so sorry for what I'm about to do terraform: terraform-check *.tf - terraform init + terraform init || terraform init -upgrade terraform apply terraform output | sed \ -e 's/\(.*\) = /\U\1 = /' \ @@ -15,7 +15,7 @@ terraform: terraform-check *.tf -e 's/ \+$$//' \ > terraform.mk -terraform-check: toolcheck pubkey +terraform-check: .toolcheck pubkey $(eval AWS_INSTANCE_PUBLIC_KEY := $(shell sed -e 's/\//\\\//g' pubkey)) @@ -38,7 +38,8 @@ __sed_%: CHECK_TOOLS = terraform aws -toolcheck: +# this relies on your credentials file, so that if you edit it, it will run again +.toolcheck: ~/.aws/credentials @echo @echo "Checking applications..." @ FAIL=""; \ @@ -55,6 +56,11 @@ toolcheck: @echo @echo "Checking AWS configuration..." aws iam get-user + touch .toolcheck + +# ...but if you don't have a credentials file, that's ok, it just runs the check every time +~/.aws/credentials: + true pubkey: if test -n "$(AWS_INSTANCE_PUBLIC_KEY)"; then \ diff --git a/terraform/config.tf.in b/terraform/config.tf.in index 8f67d11..40044bb 100644 --- a/terraform/config.tf.in +++ b/terraform/config.tf.in @@ -2,7 +2,7 @@ locals { public_key = "{{AWS_INSTANCE_PUBLIC_KEY}}" instance_type = "{{AWS_SINGLE_INSTANCE_TYPE}}" - route53_zone = "{{AWS_ROUTE53_ZONE}}" aws_region = "{{AWS_REGION}}" + domain_name = "{{DOMAIN_NAME}}" } diff --git a/terraform/main.tf b/terraform/main.tf index f5b13d9..660f8e9 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -1,40 +1,10 @@ -# [X] aws provider -# [/] random pet -# not needed w/o s3 bucket -# [/] s3 bucket -# [/] use pet name! -# n/a -# [X] vpc -# [/] tls private key -# [X] aws key pair -# [/] aws key local file -# [X] instance -# [ ] "myip" -# [X] sg -# [X] EIP -# [X] iam_instance_profile -# [X] iam_role -# [X] policydoc -# [X] policy -# [X] policy attachment -# [X] iam policy data -# [ ] route53 records -# [/] adminpass for nextcloud -# [ ] outputs: -# [ ] instance ID -# [ ] public IP -# [ ] name servers -# [ ] bucket -# [ ] myip - - provider "aws" { region = local.aws_region } -#resource "random_pet" "name" () +resource "random_pet" "name" {} module "vpc" { source = "terraform-aws-modules/vpc/aws" diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 5c51f17..1f0be17 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -6,10 +6,20 @@ output "public_ip" { value = aws_instance.social.public_ip } output "nameservers" { - value = length(module.zone) == 0 ? "" : module.zone.0.route53_zone_name_servers + #value = length(module.zone) == 0 ? "" : module.zone.0.route53_zone_name_servers + value = module.zone.route53_zone_name_servers +} +output "s3_bucket_name" { + value = module.s3_bucket.s3_bucket_id } -#output "bucket" { -#} #output "myip" { #} +#output "aws_route53_zone" { +# value = local.route53_zone +#} + +output "aws_route53_nameservers" { + value = module.zone.route53_zone_name_servers +} + diff --git a/terraform/route53.tf b/terraform/route53.tf index 05b8047..e609610 100644 --- a/terraform/route53.tf +++ b/terraform/route53.tf @@ -1,21 +1,22 @@ module "zone" { - count = local.route53_zone == "" ? 0 : 1 + # count = local.route53_zone == "" ? 0 : 1 source = "terraform-aws-modules/route53/aws//modules/zones" version = "~> 2.0" zones = { - "${local.route53_zone}" = { comment = "${local.route53_zone}" } + "${local.domain_name}" = { comment = "${local.domain_name}" } } } module "records" { - count = local.route53_zone == "" ? 0 : 1 + # count = local.route53_zone == "" ? 0 : 1 source = "terraform-aws-modules/route53/aws//modules/records" version = "~> 2.0" - zone_name = keys(module.zone.0.route53_zone_zone_id)[0] + #zone_name = keys(module.zone.0.route53_zone_zone_id)[0] + zone_name = keys(module.zone.route53_zone_zone_id)[0] records = [ { diff --git a/terraform/s3.tf b/terraform/s3.tf new file mode 100644 index 0000000..4f72635 --- /dev/null +++ b/terraform/s3.tf @@ -0,0 +1,42 @@ + +module "s3_bucket" { + source = "terraform-aws-modules/s3-bucket/aws" + + bucket = "mastodon-${random_pet.name.id}" +# acl = "private" + + versioning = { + enabled = false + } + +# server_side_encryption_configuration = { +# rule = { +# apply_server_side_encryption_by_default = { +# sse_algorithm = "AES256" +# } +# +# bucket_key_enabled = true +# } +# } + +} + +resource "aws_iam_access_key" "s3" { + user = aws_iam_user.s3.name +} + +resource "aws_iam_user" "s3" { + name = "mastodon-s3-${random_pet.name.id}" + path = "/system/" +} + +resource "local_file" "s3_secret" { + filename = ".s3_secret" + content = "${aws_iam_access_key.s3.secret}\n" +} + +resource "local_file" "s3_id" { + filename = ".s3_id" + content = "${aws_iam_access_key.s3.id}\n" +} +