diff --git a/.kamal/secrets b/.kamal/secrets-common similarity index 95% rename from .kamal/secrets rename to .kamal/secrets-common index 9d28e1e..728b94c 100644 --- a/.kamal/secrets +++ b/.kamal/secrets-common @@ -17,6 +17,5 @@ # KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD # Improve security by using a password manager. Never check config/master.key into git! -RAILS_MASTER_KEY=$(cat config/credentials/production.key) KAMAL_REGISTRY_PASSWORD=$(aws ecr get-login-password --region us-east-2) diff --git a/.kamal/secrets.production b/.kamal/secrets.production new file mode 100644 index 0000000..78c9ae1 --- /dev/null +++ b/.kamal/secrets.production @@ -0,0 +1,3 @@ +# Secrets for `kamal -d production`. Shared secrets live in .kamal/secrets-common. +# DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. +RAILS_MASTER_KEY=$(cat config/credentials/production.key) diff --git a/.kamal/secrets.staging b/.kamal/secrets.staging new file mode 100644 index 0000000..b7c5f7e --- /dev/null +++ b/.kamal/secrets.staging @@ -0,0 +1,3 @@ +# Secrets for `kamal -d staging`. Shared secrets live in .kamal/secrets-common. +# DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. +RAILS_MASTER_KEY=$(cat config/credentials/staging.key) diff --git a/config/cable.yml b/config/cable.yml index b9adc5a..74d2b53 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -8,10 +8,13 @@ development: test: adapter: test -production: +production: &solid_cable adapter: solid_cable connects_to: database: writing: cable polling_interval: 0.1.seconds message_retention: 1.day + +staging: + <<: *solid_cable diff --git a/config/cache.yml b/config/cache.yml index 19d4908..344c2cc 100644 --- a/config/cache.yml +++ b/config/cache.yml @@ -14,3 +14,7 @@ test: production: database: cache <<: *default + +staging: + database: cache + <<: *default diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc new file mode 100644 index 0000000..1779ce1 --- /dev/null +++ b/config/credentials/staging.yml.enc @@ -0,0 +1 @@ +eeoCByo6ZKfHPIAiUs+RWlMG6ctONoh7PtOa+YhVOHmJ+Cyx/g8veArB8vviyXaXJKDSnTVKGtmN4iFkRmv8U80LVXuIrguyLPU71VhTVjN6D/Z3msO8lE4EuFVqmm2nSgwxqv3kNsnzq0PvUaFka/gLMGYiJc1lxnqNaa49uLPPoGO6ou8HwarmXNfUsiga7P6NIgltzccC6kEqxbfriBophSo4vzLdYCo1jfkb01IFa1TALIB3r6HZ68dFXLiKaXdjEu0f1u0gL0wOajGbl0ov8MEhGMr0sQUV/BjjEy65JNSYU/95OCoZxbljVgYpSX+DYuCrEdSDqOVL6d/icFGSrQZ3SnRWHtE4xi/38m4J77WYwO8m8VE1tcWCOgLNfXKQcy+lQIBWOThypclncYLTuCXZYGucKwd6GtRpL2pWcskP0vKwNSXtDauLp/mdOCiOA3JXr8rkxsEiwKtH9PlBHDQnSczjW+x+aFk5M36/7JkPLgNKQXfJ--1UqKYQAZ+lgiu89x--T/GJd6zjnSiOqNo1NLjbsw== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index 302d638..1b63afd 100644 --- a/config/database.yml +++ b/config/database.yml @@ -38,3 +38,20 @@ production: <<: *default database: storage/production_cable.sqlite3 migrations_paths: db/cable_migrate + +staging: + primary: + <<: *default + database: storage/staging.sqlite3 + cache: + <<: *default + database: storage/staging_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/staging_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/staging_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml new file mode 100644 index 0000000..d83beaa --- /dev/null +++ b/config/deploy.staging.yml @@ -0,0 +1,14 @@ +# Staging destination. Deploy with: bin/kamal deploy -d staging +# Values here are merged over config/deploy.yml. + +servers: + web: + - 18.117.181.111 + +ssh: + user: ec2-user + keys: ["~/.ssh/2026rfg-staging.pem"] + +env: + clear: + RAILS_ENV: staging diff --git a/config/deploy.yml b/config/deploy.yml index d4a5554..ff89f6d 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -4,14 +4,17 @@ service: endsideout # Name of the container image (use your-user/app-name on external registries). image: endsideout -# Deploy to these servers. -servers: - web: - - 18.117.181.111 - # job: - # hosts: - # - 192.168.0.1 - # cmd: bin/jobs +# Always deploy to a named destination (config/deploy..yml), for example `bin/kamal deploy -d staging`. +require_destination: true + +# Servers are set per destination. +# servers: +# web: +# - 192.168.0.1 +# job: +# hosts: +# - 192.168.0.1 +# cmd: bin/jobs # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. # If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. @@ -97,10 +100,7 @@ builder: # - GITHUB_TOKEN - RAILS_MASTER_KEY -# Use a different ssh user than root -ssh: - user: ec2-user - keys: ["~/.ssh/2026rfg-staging.pem"] +# The ssh user and keys are set per destination. # Use accessory services (secrets come from .kamal/secrets). diff --git a/config/environments/production.rb b/config/environments/production.rb index bed8ce3..fd34108 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -58,7 +58,7 @@ # config.action_mailer.raise_delivery_errors = false # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = { host: "staging-classroom.endsideout.org", protocol: "https" } + config.action_mailer.default_url_options = { host: "classroom.endsideout.org", protocol: "https" } # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. # config.action_mailer.smtp_settings = { @@ -81,7 +81,7 @@ # Enable DNS rebinding protection and other `Host` header attacks. config.hosts = [ - "staging-classroom.endsideout.org" + "classroom.endsideout.org" ] # Skip DNS rebinding protection for the default health check endpoint. diff --git a/config/environments/staging.rb b/config/environments/staging.rb new file mode 100644 index 0000000..3980b85 --- /dev/null +++ b/config/environments/staging.rb @@ -0,0 +1,8 @@ +require_relative "production" + +Rails.application.configure do + staging_host = "staging-classroom.endsideout.org" + + config.action_mailer.default_url_options = { host: staging_host, protocol: "https" } + config.hosts = [ staging_host ] +end diff --git a/config/queue.yml b/config/queue.yml index 9eace59..8d1d6b7 100644 --- a/config/queue.yml +++ b/config/queue.yml @@ -16,3 +16,6 @@ test: production: <<: *default + +staging: + <<: *default diff --git a/config/recurring.yml b/config/recurring.yml index b4207f9..3a12138 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -9,7 +9,10 @@ # priority: 2 # schedule: at 5am every day -production: +production: &production clear_solid_queue_finished_jobs: command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" schedule: every hour at minute 12 + +staging: + <<: *production diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..03213dd --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,29 @@ +# Deployment + +## Configuring AWS Instance +Kamal's deployment model includes a proxy that runs on the instance and handles the app via a blue/green deployment model. +During deployment the current version of the app is running while the new version is being deployed. +During the deployment process, the proxy runs status checks on the new version of the app to ensure it is healthy before switching traffic over to it. +This approach means that during deployment, two versions of the app are running at the same time, which can cause memory issues if the instance is too small. +When choosing an instance you either need to ensure the instance has enough memory to run two versions of the app at the same time, or you need to add swap space to the instance to ensure it doesn't run out of memory during deployment. +If you choose to add swap space, you need to ensure the instance has a big enough volume to add swap space. +*AWS defaults to 8GB volumes, which is realistically not enough to add swap space (or honestly to not run into other issues).* Be sure that you choose a volume size that is big enough to add swap space. 20 GB is a good minimum. + +## Secrets and Credentials +### Rails Credentials +The encryption keys for Rails credentials are stored in AWS Secrets Manager. +In order to deploy you need to create a key file in the config/credentials folder such as `config/credentials/staging.key` with the value from secrets manager. +*These files should remain gitignored and not be checked into source control.* + +### SSH Keys +Kamal uses SSH to connect to the instance and deploy the application. This key is stored in AWS Secrets Manager and needs to be downloaded and placed in the `~/.ssh/` folder with permissions set to 600. +Note you should use the same file name referenced for the key in the environment specific deployment configuration. For example [deploy.staging.yml](../config/deploy.staging.yml) + +## AWS CLI +You need to be logged into the AWS CLI to deploy. You can use `aws configure` to set up your credentials. +You can make a named profile for this AWS account if you have multiple AWS accounts using `aws configure --profile your_profile_name`. +To ensure that kamal uses the correct profile, run EXPORT AWS_PROFILE=your_profile_name to set the profile for the current terminal session. + +## Important Gotchas +- We have configured kamal to use explicit deploy targets. Therefore you always need to specify the target environment when using any kamal command. For example, `bin/kamal deploy -d staging` +- Kamal only deploys committed code. If you have uncommitted changes, they will not be deployed. \ No newline at end of file