From 04652fb1cd777191bdd810de1a335d6eee067476 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Tue, 17 Oct 2023 17:15:02 +0200 Subject: [PATCH] Add Superconfig for Environment and Vault in the Backend (#273) adds superconfig in the same way as the monolith --- .env.development | 7 ++ .env.test | 6 ++ Gemfile | 6 +- Gemfile.lock | 12 ++- app/models/registration.rb | 8 +- app/worker/env_config.rb | 23 ++++++ app/worker/queue_poller.rb | 13 +--- app_secrets.rb | 42 ++++++++++ config/application.rb | 2 + config/database.yml | 9 +-- config/environments/development.rb | 2 +- config/environments/production.rb | 9 +-- config/initializers/_vault.rb | 87 --------------------- config/initializers/aws.rb | 4 +- config/initializers/dynamoid.rb | 4 +- config/initializers/jwt.rb | 2 +- config/initializers/prometheus.rb | 18 ++--- config/secrets.yml | 6 +- db/seeds.rb | 121 +++++++++++++++-------------- docker-compose.dev.yml | 20 +---- docker-compose.test.yml | 17 +--- env_config.rb | 26 +++++++ infra/handler/main.tf | 10 ++- infra/handler/variables.tf | 7 +- infra/shared/dynamodb.tf | 2 +- infra/staging/main.tf | 36 ++++++++- infra/staging/variables.tf | 8 +- infra/worker/main.tf | 8 +- infra/worker/variables.tf | 5 +- vault_config.rb | 21 +++++ 30 files changed, 297 insertions(+), 244 deletions(-) create mode 100644 .env.development create mode 100644 .env.test create mode 100644 app/worker/env_config.rb create mode 100644 app_secrets.rb delete mode 100644 config/initializers/_vault.rb create mode 100644 env_config.rb create mode 100644 vault_config.rb diff --git a/.env.development b/.env.development new file mode 100644 index 00000000..046876be --- /dev/null +++ b/.env.development @@ -0,0 +1,7 @@ +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID=fake-key +AWS_SECRET_ACCESS_KEY=fake-access-key +DYNAMO_REGISTRATIONS_TABLE=registrations-development +RAILS_LOG_TO_STDOUT=true +JWT_SECRET=jwt-test-secret +SECRET_KEY_BASE=a003fdc6f113ff7d295596a02192c7116a76724ba6d3071043eefdd16f05971be0dc58f244e67728757b2fb55ae7a41e1eb97c1fe247ddaeb6caa97cea32120c diff --git a/.env.test b/.env.test new file mode 100644 index 00000000..10884e7e --- /dev/null +++ b/.env.test @@ -0,0 +1,6 @@ +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID=fake-key +AWS_SECRET_ACCESS_KEY=fake-access-key +DYNAMO_REGISTRATIONS_TABLE=registrations-development +JWT_SECRET=jwt-test-secret +SECRET_KEY_BASE=a003fdc6f113ff7d295596a02192c7116a76724ba6d3071043eefdd16f05971be0dc58f244e67728757b2fb55ae7a41e1eb97c1fe247ddaeb6caa97cea32120c diff --git a/Gemfile b/Gemfile index be5eb6a8..2d8e4c0b 100644 --- a/Gemfile +++ b/Gemfile @@ -53,7 +53,11 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'prometheus_exporter' # vault for secrets management -gem 'vault-rails' +gem 'vault' + +# for environment variable management +gem 'dotenv-rails', require: 'dotenv/rails-now' +gem 'superconfig' group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem diff --git a/Gemfile.lock b/Gemfile.lock index 5fb11ec2..c68ab71a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,10 @@ GEM irb (>= 1.5.0) reline (>= 0.3.1) diff-lcs (1.5.0) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) dynamoid (3.8.0) activemodel (>= 4) aws-sdk-dynamodb (~> 1.0) @@ -273,6 +277,7 @@ GEM ruby-progressbar (1.13.0) sqlite3 (1.6.3-aarch64-linux) sqlite3 (1.6.3-x86_64-linux) + superconfig (2.1.1) thor (1.2.2) timeout (0.4.0) tzinfo (2.0.6) @@ -280,9 +285,6 @@ GEM unicode-display_width (2.5.0) vault (0.18.1) aws-sigv4 - vault-rails (0.9.0) - activesupport (>= 5.0) - vault (~> 0.18) webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -303,6 +305,7 @@ DEPENDENCIES bootsnap connection_pool debug + dotenv-rails dynamoid (= 3.8.0) factory_bot_rails hiredis @@ -322,8 +325,9 @@ DEPENDENCIES rubocop ruby-prof sqlite3 (~> 1.4) + superconfig tzinfo-data - vault-rails + vault webmock RUBY VERSION diff --git a/app/models/registration.rb b/app/models/registration.rb index faba8d90..d78cfd61 100644 --- a/app/models/registration.rb +++ b/app/models/registration.rb @@ -4,12 +4,8 @@ class Registration include Dynamoid::Document - # We autoscale dynamodb in production - if ENV.fetch('CODE_ENVIRONMENT', 'development') == 'staging' - table name: 'registrations-staging', read_capacity: 5, write_capacity: 5, key: :attendee_id - else - table name: 'registrations', capacity_mode: nil, key: :attendee_id - end + # We autoscale dynamodb + table name: EnvConfig.DYNAMO_REGISTRATIONS_TABLE, capacity_mode: nil, key: :attendee_id REGISTRATION_STATES = %w[pending waiting_list accepted cancelled].freeze ADMIN_ONLY_STATES = %w[pending waiting_list accepted].freeze # Only admins are allowed to change registration state to one of these states diff --git a/app/worker/env_config.rb b/app/worker/env_config.rb new file mode 100644 index 00000000..54869c5e --- /dev/null +++ b/app/worker/env_config.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'superconfig' + +EnvConfig = SuperConfig.new do + # We don't have RAILS_ENV in ruby + if ENV.fetch('CODE_ENVIRONMENT', 'development') == 'development' + mandatory :LOCALSTACK_ENDPOINT, :string + # Have to be the same as in localstack to simulate authentication + mandatory :AWS_ACCESS_KEY_ID, :string + mandatory :AWS_SECRET_ACCESS_KEY, :string + optional :WCA_HOST, :string, '' + optional :CODE_ENVIRONMENT, 'development' + else + mandatory :QUEUE_URL, :string + mandatory :WCA_HOST, :string + mandatory :CODE_ENVIRONMENT, :string + end + # We even need the AWS_REGION in dev because we fake authenticate with localstack + mandatory :AWS_REGION, :string + mandatory :PROMETHEUS_EXPORTER, :string + mandatory :DYNAMO_REGISTRATIONS_TABLE, :string +end diff --git a/app/worker/queue_poller.rb b/app/worker/queue_poller.rb index 99a6a26c..95f0b461 100644 --- a/app/worker/queue_poller.rb +++ b/app/worker/queue_poller.rb @@ -6,6 +6,7 @@ require 'prometheus_exporter/instrumentation' require 'prometheus_exporter/metric' require_relative 'registration_processor' +require_relative 'env_config' class QueuePoller # Wait for 1 second so we can start work on 10 messages at at time @@ -16,15 +17,9 @@ class QueuePoller def self.perform PrometheusExporter::Client.default = PrometheusExporter::Client.new(host: ENV.fetch('PROMETHEUS_EXPORTER'), port: 9091) # Instrumentation of the worker process is currently disabled per https://github.com/discourse/prometheus_exporter/issues/282 - if ENV.fetch('CODE_ENVIRONMENT', 'dev') == 'staging' - # PrometheusExporter::Instrumentation::Process.start(type: "wca-registration-worker-staging", labels: { process: "1" }) - @suffix = '-staging' - else - # PrometheusExporter::Instrumentation::Process.start(type: "wca-registration-worker", labels: { process: "1" }) - @suffix = '' - end - registrations_counter = PrometheusExporter::Client.default.register('counter', "registrations_counter-#{@suffix}", 'The number of Registrations processed') - error_counter = PrometheusExporter::Client.default.register('counter', "worker_error_counter-#{@suffix}", 'The number of Errors in the worker') + suffix = "-#{ENV.fetch("CODE_ENVIRONMENT", "development")}" + registrations_counter = PrometheusExporter::Client.default.register('counter', "registrations_counter-#{suffix}", 'The number of Registrations processed') + error_counter = PrometheusExporter::Client.default.register('counter', "worker_error_counter-#{suffix}", 'The number of Errors in the worker') @sqs ||= if ENV['LOCALSTACK_ENDPOINT'] Aws::SQS::Client.new(endpoint: ENV['LOCALSTACK_ENDPOINT']) diff --git a/app_secrets.rb b/app_secrets.rb new file mode 100644 index 00000000..398e531a --- /dev/null +++ b/app_secrets.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'superconfig' + +require_relative 'env_config' + +SuperConfig::Base.class_eval do + # The skeleton is stolen from the source code of the `superconfig` gem, file lib/superconfig.rb:104 + # (method SuperConfig::Base#credential). The inner Vault fetching logic is custom-written :) + def vault(secret_name, &block) + define_singleton_method(secret_name) do + @__cache__["_vault_#{secret_name}".to_sym] ||= begin + value = self.vault_read(secret_name)[:value] + block ? block.call(value) : value + end + end + end + + private def vault_read(secret_name) + Vault.with_retries(Vault::HTTPConnectionError, Vault::HTTPError) do |attempt, e| + puts "Received exception #{e} from Vault - attempt #{attempt}" if e.present? + + secret = Vault.logical.read("kv/data/#{EnvConfig.VAULT_APPLICATION}/#{secret_name}") + raise "Tried to read #{secret_name}, but doesn't exist" unless secret.present? + + secret.data[:data] + end + end +end + +AppSecrets = SuperConfig.new do + if Rails.env.production? + require_relative 'vault_config' + + vault :JWT_SECRET + vault :SECRET_KEY_BASE + + else + mandatory :JWT_SECRET, :string + mandatory :SECRET_KEY_BASE, :string + end +end diff --git a/config/application.rb b/config/application.rb index 17695ce0..beb107c6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -7,6 +7,8 @@ # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) +require_relative '../env_config' +require_relative '../app_secrets' module WcaRegistration class Application < Rails::Application diff --git a/config/database.yml b/config/database.yml index fcba57f1..9b11356c 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,12 +1,7 @@ -# SQLite. Versions 3.8.0 and up are supported. -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem "sqlite3" -# +# We currently have no database configured default: &default adapter: sqlite3 - pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + pool: 5 timeout: 5000 development: diff --git a/config/environments/development.rb b/config/environments/development.rb index 4e171623..d8776721 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -20,7 +20,7 @@ config.server_timing = true # Caching with Redis even in Development to speed up calls to the external services - config.cache_store = :redis_cache_store, { url: ENV.fetch('REDIS_URL', nil) } + config.cache_store = :redis_cache_store, { url: EnvConfig.REDIS_URL } # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local diff --git a/config/environments/production.rb b/config/environments/production.rb index b04828f6..dc5ce16d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -4,12 +4,7 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - code_env = ENV.fetch('CODE_ENVIRONMENT', nil) - if code_env == 'production' - config.hosts << 'registration.worldcubeassociation.org' - elsif code_env == 'staging' - config.hosts << 'staging.registration.worldcubeassociation.org' - end + config.hosts = EnvConfig.HOST # Exclude requests for the /healthcheck/ path from host checking Rails.application.config.host_authorization = { @@ -63,7 +58,7 @@ # Use a different cache store in production. config.cache_store = :redis_cache_store, { - url: ENV.fetch('REDIS_URL', nil), + url: EnvConfig.REDIS_URL, connect_timeout: 30, # Defaults to 20 seconds read_timeout: 0.2, # Defaults to 1 second write_timeout: 0.2, # Defaults to 1 second diff --git a/config/initializers/_vault.rb b/config/initializers/_vault.rb deleted file mode 100644 index b0218af8..00000000 --- a/config/initializers/_vault.rb +++ /dev/null @@ -1,87 +0,0 @@ -# This file starts with _ because it has to be the first one run -# frozen_string_literal: true - -require 'vault/rails' - -Vault.configure do |vault| - # Use Vault in transit mode for encrypting and decrypting data. If - # disabled, vault-rails will encrypt data in-memory using a similar - # algorithm to Vault. The in-memory store uses a predictable encryption - # which is great for development and test, but should _never_ be used in - # production. Default: ENV["VAULT_RAILS_ENABLED"]. - vault.enabled = Rails.env.production? - - # The name of the application. All encrypted keys in Vault will be - # prefixed with this application name. If you change the name of the - # application, you will need to migrate the encrypted data to the new - # key namespace. Default: ENV["VAULT_RAILS_APPLICATION"]. - env = ENV.fetch('CODE_ENVIRONMENT', 'development') - if env == 'production' - @vault_application = 'wca-registration' - elsif env == 'staging' - @vault_application = 'wca-registration-staging' - else - @vault_application = 'wca-registration-development' - end - - vault.application = @vault_application - - # The address of the Vault server, also read as ENV["VAULT_ADDR"] - vault.address = ENV.fetch('VAULT_ADDR') - - # The token to authenticate with Vault, for prod auth is done via AWS - if Rails.env.production? - # Assume the correct role - # this is needed because otherwise we will assume the role of the underlying instance instead - role_credentials = Aws::ECSCredentials.new(retries: 3) - - Vault.auth.aws_iam(ENV.fetch('TASK_ROLE', nil), role_credentials, nil, "https://sts.#{ENV.fetch('AWS_REGION', nil)}.amazonaws.com") - else - vault.token = ENV.fetch('VAULT_DEV_ROOT_TOKEN_ID', nil) - end - - # Use SSL verification, also read as ENV["VAULT_SSL_VERIFY"] - vault.ssl_verify = false - - # Timeout the connection after a certain amount of time (seconds), also read - # as ENV["VAULT_TIMEOUT"] - vault.timeout = 30 - - # It is also possible to have finer-grained controls over the timeouts, these - # may also be read as environment variables - vault.ssl_timeout = 5 - vault.open_timeout = 5 - vault.read_timeout = 30 -end - -# Read a secret from Vault. -def read_secret(secret_name) - Vault.with_retries(Vault::HTTPConnectionError, Vault::HTTPError) do |attempt, e| - if e - puts "Received exception #{e} from Vault - attempt #{attempt}" - end - secret = Vault.logical.read("secret/data/#{@vault_application}/#{secret_name}") - if secret.present? - secret.data[:data][:value] - else # TODO: should we hard error out here? - puts "Tried to read #{secret_name}, but doesn´t exist" - end - end -end - -def create_secret(secret_name, value) - Vault.with_retries(Vault::HTTPConnectionError) do - Vault.logical.write("secret/data/#{@vault_application}/#{secret_name}", data: { value: value }) - end -end - -# Initialize secrets for dev and test -def init - create_secret('SECRET_KEY_BASE', - 'a003fdc6f113ff7d295596a02192c7116a76724ba6d3071043eefdd16f05971be0dc58f244e67728757b2fb55ae7a41e1eb97c1fe247ddaeb6caa97cea32120c') - # Make sure this development secret is the same across this and the monolith - create_secret('JWT_SECRET', - 'jwt-test-secret') -end - -init unless Rails.env.production? diff --git a/config/initializers/aws.rb b/config/initializers/aws.rb index 270755ec..67648653 100644 --- a/config/initializers/aws.rb +++ b/config/initializers/aws.rb @@ -7,6 +7,6 @@ $sqs = Aws::SQS::Client.new else # We are using localstack to emulate AWS in dev - $dynamodb = Aws::DynamoDB::Client.new(endpoint: ENV.fetch('LOCALSTACK_ENDPOINT', nil)) - $sqs = Aws::SQS::Client.new(endpoint: ENV.fetch('LOCALSTACK_ENDPOINT', nil)) + $dynamodb = Aws::DynamoDB::Client.new(endpoint: EnvConfig.LOCALSTACK_ENDPOINT) + $sqs = Aws::SQS::Client.new(endpoint: EnvConfig.LOCALSTACK_ENDPOINT) end diff --git a/config/initializers/dynamoid.rb b/config/initializers/dynamoid.rb index 35d6acb3..4a4e3b44 100644 --- a/config/initializers/dynamoid.rb +++ b/config/initializers/dynamoid.rb @@ -3,11 +3,11 @@ require 'dynamoid' Dynamoid.configure do |config| - config.region = ENV.fetch('AWS_REGION', 'us-west-2') + config.region = EnvConfig.AWS_REGION config.namespace = nil if Rails.env.production? config.credentials = Aws::ECSCredentials.new(retries: 3) else - config.endpoint = ENV.fetch('LOCALSTACK_ENDPOINT', nil) + config.endpoint = EnvConfig.LOCALSTACK_ENDPOINT end end diff --git a/config/initializers/jwt.rb b/config/initializers/jwt.rb index 63537f7f..d0e4321d 100644 --- a/config/initializers/jwt.rb +++ b/config/initializers/jwt.rb @@ -2,7 +2,7 @@ require_relative '../../app/helpers/jwt_options' -JwtOptions.secret = read_secret('JWT_SECRET') +JwtOptions.secret = AppSecrets.JWT_SECRET # Default algorithm for Devise-jwt JwtOptions.algorithm = 'HS256' # The expiry time we define in the monolith diff --git a/config/initializers/prometheus.rb b/config/initializers/prometheus.rb index bbd0a866..97d57ab0 100644 --- a/config/initializers/prometheus.rb +++ b/config/initializers/prometheus.rb @@ -8,12 +8,12 @@ PrometheusExporter::Client.default = PrometheusExporter::Client.new(host: ENV.fetch('PROMETHEUS_EXPORTER'), port: 9091) -if ENV.fetch('CODE_ENVIRONMENT', 'dev') == 'staging' +if Rails.env.production? && !EnvConfig.REGISTRATION_LIVE_SITE PrometheusExporter::Instrumentation::Process.start(type: 'wca-registration-handler-staging', labels: { process: '1' }) - @suffix = '-staging' + suffix = '-staging' else PrometheusExporter::Instrumentation::Process.start(type: 'wca-registration-handler', labels: { process: '1' }) - @suffix = '' + suffix = '' end unless Rails.env.test? @@ -24,9 +24,9 @@ end # Create our Metric Counters -Metrics.registration_dynamodb_errors_counter = PrometheusExporter::Client.default.register('counter', "registration_dynamodb_errors_counter#{@suffix}", 'The number of times interacting with dynamodb fails') -Metrics.registration_competition_api_error_counter = PrometheusExporter::Client.default.register('counter', "registration_competition_api_error_counter#{@suffix}", 'The number of times interacting with the competition API failed') -Metrics.registration_competitor_api_error_counter = PrometheusExporter::Client.default.register('counter', "registration_competitor_api_error_counter#{@suffix}", 'The number of times interacting with the user API failed') -Metrics.registration_validation_errors_counter = PrometheusExporter::Client.default.register('counter', "registration_validation_errors_counter#{@suffix}", 'The number of times validation fails when an attendee tries to register') -Metrics.jwt_verification_error_counter = PrometheusExporter::Client.default.register('counter', "jwt_verification_error_counter#{@suffix}", 'The number of times JWT verification failed') -Metrics.registration_impersonation_attempt_counter = PrometheusExporter::Client.default.register('counter', "registration_impersonation_attempt_counter#{@suffix}", 'The number of times a Person tries to register as someone else') +Metrics.registration_dynamodb_errors_counter = PrometheusExporter::Client.default.register('counter', "registration_dynamodb_errors_counter#{suffix}", 'The number of times interacting with dynamodb fails') +Metrics.registration_competition_api_error_counter = PrometheusExporter::Client.default.register('counter', "registration_competition_api_error_counter#{suffix}", 'The number of times interacting with the competition API failed') +Metrics.registration_competitor_api_error_counter = PrometheusExporter::Client.default.register('counter', "registration_competitor_api_error_counter#{suffix}", 'The number of times interacting with the user API failed') +Metrics.registration_validation_errors_counter = PrometheusExporter::Client.default.register('counter', "registration_validation_errors_counter#{suffix}", 'The number of times validation fails when an attendee tries to register') +Metrics.jwt_verification_error_counter = PrometheusExporter::Client.default.register('counter', "jwt_verification_error_counter#{suffix}", 'The number of times JWT verification failed') +Metrics.registration_impersonation_attempt_counter = PrometheusExporter::Client.default.register('counter', "registration_impersonation_attempt_counter#{suffix}", 'The number of times a Person tries to register as someone else') diff --git a/config/secrets.yml b/config/secrets.yml index 28664e32..e3c05c23 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -11,10 +11,10 @@ # if you're sharing your code publicly. development: - secret_key_base: <%= read_secret("SECRET_KEY_BASE") %> + secret_key_base: <%= AppSecrets.SECRET_KEY_BASE %> test: - secret_key_base: <%= read_secret("SECRET_KEY_BASE") %> + secret_key_base: <%= AppSecrets.SECRET_KEY_BASE %> production: - secret_key_base: <%= read_secret("SECRET_KEY_BASE") %> + secret_key_base: <%= AppSecrets.SECRET_KEY_BASE %> diff --git a/db/seeds.rb b/db/seeds.rb index e757fb6c..2b53be18 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,64 +1,67 @@ # frozen_string_literal: true -# Create the DynamoDB Tables -table_name = 'registrations' -key_schema = [ - { attribute_name: 'attendee_id', key_type: 'HASH' }, -] -attribute_definitions = [ - { attribute_name: 'attendee_id', attribute_type: 'S' }, - { attribute_name: 'user_id', attribute_type: 'S' }, - { attribute_name: 'competition_id', attribute_type: 'S' }, -] -provisioned_throughput = { - read_capacity_units: 5, - write_capacity_units: 5, -} -global_secondary_indexes = [ - { - index_name: 'registrations_index_competition_id', - key_schema: [ - { attribute_name: 'competition_id', key_type: 'HASH' }, - ], - projection: { - projection_type: 'ALL', +# Failsafe so this is never run in production (the task doesn't have permissions to create tables anyway) +unless Rails.env.production? + # Create the DynamoDB Tables + table_name = EnvConfig.DYNAMO_REGISTRATIONS_TABLE + key_schema = [ + { attribute_name: 'attendee_id', key_type: 'HASH' }, + ] + attribute_definitions = [ + { attribute_name: 'attendee_id', attribute_type: 'S' }, + { attribute_name: 'user_id', attribute_type: 'S' }, + { attribute_name: 'competition_id', attribute_type: 'S' }, + ] + provisioned_throughput = { + read_capacity_units: 5, + write_capacity_units: 5, + } + global_secondary_indexes = [ + { + index_name: "#{table_name}_index_competition_id", + key_schema: [ + { attribute_name: 'competition_id', key_type: 'HASH' }, + ], + projection: { + projection_type: 'ALL', + }, + provisioned_throughput: { + read_capacity_units: 5, + write_capacity_units: 5, + }, }, - provisioned_throughput: { - read_capacity_units: 5, - write_capacity_units: 5, + { + index_name: "#{table_name}_index_user_id", + key_schema: [ + { attribute_name: 'user_id', key_type: 'HASH' }, + ], + projection: { + projection_type: 'ALL', + }, + provisioned_throughput: { + read_capacity_units: 5, + write_capacity_units: 5, + }, }, - }, - { - index_name: 'registrations_index_user_id', - key_schema: [ - { attribute_name: 'user_id', key_type: 'HASH' }, - ], - projection: { - projection_type: 'ALL', - }, - provisioned_throughput: { - read_capacity_units: 5, - write_capacity_units: 5, - }, - }, -] -begin - $dynamodb.create_table({ - table_name: table_name, - key_schema: key_schema, - attribute_definitions: attribute_definitions, - provisioned_throughput: provisioned_throughput, - global_secondary_indexes: global_secondary_indexes, - }) -rescue Aws::DynamoDB::Errors::ResourceInUseException - puts 'Database Already exists' -end + ] + begin + $dynamodb.create_table({ + table_name: table_name, + key_schema: key_schema, + attribute_definitions: attribute_definitions, + provisioned_throughput: provisioned_throughput, + global_secondary_indexes: global_secondary_indexes, + }) + rescue Aws::DynamoDB::Errors::ResourceInUseException + puts 'Database Already exists' + end -# Create SQS Queue -queue_name = 'registrations.fifo' -$sqs.create_queue({ - queue_name: queue_name, - attributes: { - FifoQueue: 'true', - }, - }) + # Create SQS Queue + queue_name = 'registrations.fifo' + $sqs.create_queue({ + queue_name: queue_name, + attributes: { + FifoQueue: 'true', + }, + }) +end diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 10d914aa..434560a4 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,14 +9,9 @@ services: - "3001:3000" environment: LOCALSTACK_ENDPOINT: "http://localstack:4566" - AWS_REGION: "us-east-1" - AWS_ACCESS_KEY_ID: "fake-key" - AWS_SECRET_ACCESS_KEY: "fake-access-key" - VAULT_ADDR: "http://wca_vault:8200" - VAULT_DEV_ROOT_TOKEN_ID: aDGdUASDGIUASGDKI PROMETHEUS_EXPORTER: "prometheus_exporter" REDIS_URL: "redis://redis:6379" - RAILS_LOG_TO_STDOUT: "true" + RAILS_ENV: development volumes: - .:/app - gems_volume_handler:/usr/local/bundle @@ -30,7 +25,6 @@ services: - wca-registration depends_on: - localstack - - wca_vault - prometheus_exporter - redis @@ -57,7 +51,7 @@ services: AWS_ACCESS_KEY_ID: "fake-key" AWS_SECRET_ACCESS_KEY: "fake-access-key" PROMETHEUS_EXPORTER: "prometheus_exporter" - + DYNAMO_REGISTRATIONS_TABLE: "registrations-development" volumes: - .:/app - gems_volume_worker:/usr/local/bundle @@ -105,16 +99,6 @@ services: networks: - wca-registration - wca_vault: - container_name: vault - image: hashicorp/vault - environment: - - VAULT_DEV_ROOT_TOKEN_ID=aDGdUASDGIUASGDKI - ports: - - "8200:8200" - networks: - - wca-registration - # Frontend frontend: container_name: frontend diff --git a/docker-compose.test.yml b/docker-compose.test.yml index a4793c9b..5dda8dc9 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -8,12 +8,8 @@ services: - "3001:3000" environment: LOCALSTACK_ENDPOINT: "http://localstack:4566" - AWS_REGION: "us-east-1" - AWS_ACCESS_KEY_ID: "fake-key" - AWS_SECRET_ACCESS_KEY: "fake-access-key" - VAULT_ADDR: "http://wca_vault:8200" - VAULT_DEV_ROOT_TOKEN_ID: aDGdUASDGIUASGDKI PROMETHEUS_EXPORTER: "prometheus_exporter" + RAILS_ENV: test volumes: - .:/app - gems_volume_handler:/usr/local/bundle @@ -25,17 +21,8 @@ services: - wca-registration depends_on: - localstack - - wca_vault - prometheus_exporter - wca_vault: - image: hashicorp/vault - environment: - - VAULT_DEV_ROOT_TOKEN_ID=aDGdUASDGIUASGDKI - ports: - - "8200:8200" - networks: - - wca-registration prometheus_exporter: build: @@ -56,7 +43,7 @@ services: AWS_REGION: "us-east-1" AWS_ACCESS_KEY_ID: "fake-key" AWS_SECRET_ACCESS_KEY: "fake-access-key" - + DYNAMO_REGISTRATIONS_TABLE: "registrations-test" volumes: - .:/app - gems_volume_worker:/usr/local/bundle diff --git a/env_config.rb b/env_config.rb new file mode 100644 index 00000000..ef13e9a2 --- /dev/null +++ b/env_config.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'superconfig' + +EnvConfig = SuperConfig.new do + if Rails.env.production? + mandatory :HOST, :string + mandatory :VAULT_ADDR, :string + mandatory :TASK_ROLE, :string + mandatory :REGISTRATION_LIVE_SITE, :bool + mandatory :QUEUE_URL, :string + mandatory :WCA_HOST, :string + mandatory :REDIS_URL, :string + else + mandatory :LOCALSTACK_ENDPOINT, :string + # Have to be the same as in localstack to simulate authentication + mandatory :AWS_ACCESS_KEY_ID, :string + mandatory :AWS_SECRET_ACCESS_KEY, :string + optional :WCA_HOST, :string, '' + optional :REDIS_URL, :string, '' + end + # We even need the AWS_REGION in dev because we fake authenticate with localstack + mandatory :AWS_REGION, :string + mandatory :PROMETHEUS_EXPORTER, :string + mandatory :DYNAMO_REGISTRATIONS_TABLE, :string +end diff --git a/infra/handler/main.tf b/infra/handler/main.tf index 7b4bb7f4..35f9fc81 100644 --- a/infra/handler/main.tf +++ b/infra/handler/main.tf @@ -20,13 +20,17 @@ locals { name = "VAULT_ADDR" value = var.vault_address }, + { + name = "REGISTRATION_LIVE_SITE", + value = true + }, { name = "TASK_ROLE" value = aws_iam_role.task_role.name }, { - name = "CODE_ENVIRONMENT" - value = "production" + name = "DYNAMO_REGISTRATIONS_TABLE", + value = var.shared_resources.dynamo_registration_table.name }, { name = "QUEUE_URL", @@ -90,7 +94,7 @@ data "aws_iam_policy_document" "task_policy" { "dynamodb:DeleteItem", "dynamodb:DescribeTable", ] - resources = [var.shared_resources.dynamo_registration_table, "${var.shared_resources.dynamo_registration_table}/*"] + resources = [var.shared_resources.dynamo_registration_table.arn, "${var.shared_resources.dynamo_registration_table.arn}/*"] } statement { effect = "Allow" diff --git a/infra/handler/variables.tf b/infra/handler/variables.tf index 1a5ff28d..69a8384f 100644 --- a/infra/handler/variables.tf +++ b/infra/handler/variables.tf @@ -37,7 +37,7 @@ variable "availability_zones" { variable "host" { type = string description = "The host for generating absolute URLs in the application" - default = "register.worldcubeassociation.org" + default = "registration.worldcubeassociation.org" } variable "wca_host" { @@ -49,7 +49,10 @@ variable "wca_host" { variable "shared_resources" { description = "All the resources that the two Modules both use" type = object({ - dynamo_registration_table: string, + dynamo_registration_table: object({ + name: string, + arn: string + }), queue: object({ arn: string, url: string diff --git a/infra/shared/dynamodb.tf b/infra/shared/dynamodb.tf index d95f2d3f..bfbf2366 100644 --- a/infra/shared/dynamodb.tf +++ b/infra/shared/dynamodb.tf @@ -45,7 +45,7 @@ resource "aws_dynamodb_table" "registrations" { } output "dynamo_registration_table" { - value = aws_dynamodb_table.registrations.arn + value = aws_dynamodb_table.registrations } # Add autoscaling diff --git a/infra/staging/main.tf b/infra/staging/main.tf index 458d84f5..997326ee 100644 --- a/infra/staging/main.tf +++ b/infra/staging/main.tf @@ -3,11 +3,41 @@ resource "aws_cloudwatch_log_group" "this" { } locals { + worker_environment = [ + { + name = "PROMETHEUS_EXPORTER" + value = var.prometheus_address + }, + { + name = "AWS_REGION" + value = var.region + }, + { + name = "DYNAMO_REGISTRATIONS_TABLE", + value = aws_dynamodb_table.registrations.name + }, + { + name = "QUEUE_URL", + value = aws_sqs_queue.this.url + }, + { # We don't have access to RAILS_ENV as the worker uses plain ruby + name = "CODE_ENVIRONMENT", + value = "staging" + } + ] app_environment = [ { name = "WCA_HOST" value = var.wca_host }, + { + name = "REGISTRATION_LIVE_SITE", + value = false + }, + { + name = "HOST", + value = var.host + }, { name = "AWS_REGION" value = var.region @@ -25,8 +55,8 @@ locals { value = aws_iam_role.task_role.name }, { - name = "CODE_ENVIRONMENT" - value = "staging" + name = "DYNAMO_REGISTRATIONS_TABLE", + value = aws_dynamodb_table.registrations.name }, { name = "PROMETHEUS_EXPORTER" @@ -204,7 +234,7 @@ resource "aws_ecs_task_definition" "this" { awslogs-stream-prefix = "${var.name_prefix}-worker" } } - environment = local.app_environment + environment = local.worker_environment healthCheck = { command = ["CMD-SHELL", "pgrep ruby || exit 1"] interval = 30 diff --git a/infra/staging/variables.tf b/infra/staging/variables.tf index 7b16e275..7a42e157 100644 --- a/infra/staging/variables.tf +++ b/infra/staging/variables.tf @@ -4,6 +4,12 @@ variable "env" { default = "staging" } +variable "host" { + type = string, + description = "The address of the service" + default = "staging.registration.worldcubeassociation.org" +} + variable "name_prefix" { type = string description = "Prefix for naming resources" @@ -36,7 +42,7 @@ variable "availability_zones" { variable "wca_host" { type = string - description = "The host for generating absolute URLs in the application" + description = "The host for the WCA Monolith" default = "staging.worldcubeassociation.org" } diff --git a/infra/worker/main.tf b/infra/worker/main.tf index 1b591a68..d5014b1e 100644 --- a/infra/worker/main.tf +++ b/infra/worker/main.tf @@ -23,7 +23,11 @@ locals { { name = "PROMETHEUS_EXPORTER" value = var.prometheus_address - } + }, + { + name = "DYNAMO_REGISTRATIONS_TABLE", + value = var.shared_resources.dynamo_registration_table.name + }, ] } @@ -74,7 +78,7 @@ data "aws_iam_policy_document" "task_policy" { "dynamodb:DeleteItem", "dynamodb:DescribeTable", ] - resources = [var.shared_resources.dynamo_registration_table, "${var.shared_resources.dynamo_registration_table}/*"] + resources = [var.shared_resources.dynamo_registration_table.arn, "${var.shared_resources.dynamo_registration_table.arn}/*"] } statement { effect = "Allow" diff --git a/infra/worker/variables.tf b/infra/worker/variables.tf index bf90087d..84cc843b 100644 --- a/infra/worker/variables.tf +++ b/infra/worker/variables.tf @@ -36,7 +36,10 @@ variable "wca_host" { variable "shared_resources" { description = "All the resources that the two Modules both use" type = object({ - dynamo_registration_table: string, + dynamo_registration_table: object({ + name: string, + arn: string + }), queue: object({ arn: string, url: string diff --git a/vault_config.rb b/vault_config.rb new file mode 100644 index 00000000..0c3eba7a --- /dev/null +++ b/vault_config.rb @@ -0,0 +1,21 @@ +# This file starts with _ because it has to be the first one run +# frozen_string_literal: true + +require 'vault' + +Vault.configure do |vault| + role_credentials = Aws::ECSCredentials.new + Vault.auth.aws_iam(EnvConfig.TASK_ROLE, role_credentials, nil, "https://sts.#{EnvConfig.AWS_REGION}.amazonaws.com") + + vault.ssl_verify = false + + # Timeout the connection after a certain amount of time (seconds), also read + # as ENV["VAULT_TIMEOUT"] + vault.timeout = 30 + + # It is also possible to have finer-grained controls over the timeouts, these + # may also be read as environment variables + vault.ssl_timeout = 5 + vault.open_timeout = 5 + vault.read_timeout = 30 +end