diff --git a/.dockerignore b/.dockerignore index e8eeafe2b..4ebe09659 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,6 +11,12 @@ coverage/ .ruby-version vendor/bundle +/storage/* + +/public/packs +/public/packs-test +/node_modules + .dockerignore docker-compose.yml docker-compose.lightweight.yml diff --git a/.env_deployment_sample b/.env_deployment_sample index 2943b54e0..84e004d8c 100644 --- a/.env_deployment_sample +++ b/.env_deployment_sample @@ -1,7 +1,6 @@ #Rails application env variables RAILS_ENV=uat DATABASE_URL=postgresql://jupiter:mysecretpassword@postgres:5432/ -FCREPO_URL=http://fcrepo:8080/fcrepo/rest SOLR_URL=http://solr:8983/solr/jupiter-uat REDIS_URL=redis://redis/1 SECRET_KEY_BASE=33d83bc35b707a1f70ac9475cdaabd4224fca0d3edc07e0ce6db13515d1c1e3aaf4c3b8bf580d9ea6198ec90bae2005ca807fa46a9aee5906bd64be99cc2e065 @@ -10,8 +9,6 @@ SAML_CERTIFICATE= ROLLBAR_TOKEN= GOOGLE_ANALYTICS_TOKEN= RAILS_LOG_TO_STDOUT=true -# Comma delimited string of rack attack safelisted IPs -RACK_ATTACK_SAFELISTED_IPS= TLD_LENGTH=3 GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= @@ -19,7 +16,6 @@ GOOGLE_DEVELOPER_KEY= ERA_HOST=era.uat.library.ualberta.ca DIGITIZATION_HOST=digitalcollections.uat.library.ualberta.ca - # Postgres environment variables PGDATA=/var/lib/postgresql/data/pgdata POSTGRES_PASSWORD=mysecretpassword diff --git a/Gemfile b/Gemfile index 4e0341c64..c53273da2 100644 --- a/Gemfile +++ b/Gemfile @@ -80,6 +80,10 @@ gem 'google-api-client', gem 'builder_deferred_tagging', github: 'ualbertalib/builder_deferred_tagging', tag: 'v0.01' gem 'oaisys', github: 'ualbertalib/oaisys', tag: 'v1.0.3' +group :uat do + gem 'azure-storage-blob', require: false +end + # Seeds group :development, :test, :uat do gem 'faker', require: false diff --git a/Gemfile.lock b/Gemfile.lock index adcd6bcd2..6984a7db9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,6 +113,14 @@ GEM public_suffix (>= 2.0.2, < 5.0) amazing_print (1.4.0) ast (2.4.2) + azure-storage-blob (2.0.3) + azure-storage-common (~> 2.0) + nokogiri (~> 1, >= 1.10.8) + azure-storage-common (2.0.4) + faraday (~> 1.0) + faraday_middleware (~> 1.0, >= 1.0.0.rc1) + net-http-persistent (~> 4.0) + nokogiri (~> 1, >= 1.10.8) bcrypt (3.1.16) better_errors (2.9.1) coderay (>= 1.0.0) @@ -233,6 +241,8 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) + faraday_middleware (1.2.0) + faraday (~> 1.0) ffi (1.15.5) flipper (0.23.1) flipper-active_record (0.23.0) @@ -620,6 +630,7 @@ DEPENDENCIES active_link_to acts_as_rdfable! addressable (~> 2.8.0) + azure-storage-blob bcrypt (>= 3.1.13) better_errors (>= 2.3.0) binding_of_caller diff --git a/README.md b/README.md index 9a236d74a..72b327ca1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ **Requirements:** -- **Ruby** 2.5+ +- **Ruby** 2.6+ - **PostgreSQL** - **Redis** - **Solr** diff --git a/config/puma.rb b/config/puma.rb index afcbbd0c2..059371274 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -4,8 +4,8 @@ # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -max_threads_count = ENV.fetch('PUMA_MAX_THREADS', 5) -min_threads_count = ENV.fetch('PUMA_MIN_THREADS') { max_threads_count } +max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) +min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } threads min_threads_count, max_threads_count # Specifies the `worker_timeout` threshold that Puma will use to wait before @@ -39,14 +39,5 @@ # # preload_app! -if ENV['RAILS_ENV'] == 'uat' - workers ENV.fetch('WEB_CONCURRENCY', 3) - preload_app! - on_worker_boot do - ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.establish_connection - end - end -end -# Allow puma to be restarted by `rails restart` command. +# Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart diff --git a/config/secrets.yml b/config/secrets.yml index a13c9d18f..59df88f58 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -36,7 +36,6 @@ shared: allow_crawlers: <%= ENV['RAILS_ALLOW_CRAWLERS'] || false %> preservation_queue_name: <%= ENV['PMPY_QUEUE_NAME'] || 'prod:pmpy_queue' %> fits_path: <%= ENV['FITS_PATH'] || 'fits.sh' %> - rack_attack_safelisted_ips: <%= ENV['RACK_ATTACK_SAFELISTED_IPS'] || '""' %> system_user_api_key: <%= ENV['SYSTEM_USER_API_KEY'] %> tld_length: <%= ENV['TLD_LENGTH'] || 1 %> @@ -63,11 +62,6 @@ development: saml_idp_cert: '' saml_idp_sso_target_url: '' - fcrepo_url: <%= ENV['FCREPO_URL'] || 'http://localhost:8080/fcrepo/rest' %> - fcrepo_user: fedoraAdmin - fcrepo_password: fedoraAdmin - fcrepo_base_path: /dev - preservation_queue_name: <%= ENV['PMPY_QUEUE_NAME'] || 'dev:pmpy_queue'%> system_user_api_key: <%= ENV['SYSTEM_USER_API_KEY'] || '3eeb395e-63b7-11ea-bc55-0242ac130003' %> @@ -88,11 +82,6 @@ test: saml_idp_cert: '' saml_idp_sso_target_url: '' - fcrepo_url: <%= ENV['FCREPO_URL'] || 'http://localhost:8080/fcrepo/rest' %> - fcrepo_user: fedoraAdmin - fcrepo_password: fedoraAdmin - fcrepo_base_path: /test - preservation_queue_name: <%= ENV['PMPY_QUEUE_NAME'] || 'test:pmpy_queue' %> system_user_api_key: <%= ENV['SYSTEM_USER_API_KEY'] || '3eeb395e-63b7-11ea-bc55-0242ac130003' %> @@ -117,10 +106,6 @@ uat: saml_idp_sso_target_url: 'https://login-uat.ualberta.ca/saml2/idp/SSOService.php' database_url: <%= ENV['DATABASE_URL'] %> - fcrepo_user: <%= ENV['FCREPO_USER'] %> - fcrepo_password: <%= ENV['FCREPO_PASSWORD'] %> - fcrepo_url: <%= ENV['FCREPO_URL'] %> - fcrepo_base_path: /uat solr_url: <%= ENV['SOLR_URL'] %> staging: @@ -135,10 +120,6 @@ staging: saml_idp_sso_target_url: 'https://login-uat.ualberta.ca/saml2/idp/SSOService.php' database_url: <%= ENV['DATABASE_URL'] %> - fcrepo_user: <%= ENV['FCREPO_USER'] %> - fcrepo_password: <%= ENV['FCREPO_PASSWORD'] %> - fcrepo_url: <%= ENV['FCREPO_URL'] %> - fcrepo_base_path: /prod solr_url: <%= ENV['SOLR_URL'] %> production: @@ -154,10 +135,6 @@ production: saml_idp_sso_target_url: 'https://login.ualberta.ca/saml2/idp/SSOService.php' database_url: <%= ENV['DATABASE_URL'] %> - fcrepo_user: <%= ENV['FCREPO_USER'] %> - fcrepo_password: <%= ENV['FCREPO_PASSWORD'] %> - fcrepo_url: <%= ENV['FCREPO_URL'] %> - fcrepo_base_path: /prod solr_url: <%= ENV['SOLR_URL'] %> skylight_authentication: <%= ENV['SKYLIGHT_AUTHENTICATION'] %> diff --git a/config/storage.yml b/config/storage.yml index 8e709a69c..1c853a034 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -24,11 +24,11 @@ local: # bucket: your_own_bucket # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) -# microsoft: -# service: AzureStorage -# storage_account_name: your_account_name -# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> -# container: your_container_name +microsoft: + service: AzureStorage + storage_account_name: <%= ENV["AZURE_STORAGE_ACCOUNT_NAME"] %> + storage_access_key: <%= ENV["AZURE_STORAGE_ACCESS_KEY"] %> + container: <%= ENV["AZURE_STORAGE_CONTAINER"] %> # mirror: # service: Mirror diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 000000000..8bd3bf2b6 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,47 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sentitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +# +*.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +kubeconfig diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 000000000..2f7b3effd --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,94 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "2.90.0" + constraints = "2.90.0" + hashes = [ + "h1:j2SkWgabdECJo8vGy5Q/cTLF3yVaWR1kRaO/Nopb2VY=", + "zh:11e484585bc324e56ece3d75bd1d8371fc1d56501fb7f15542be0185eaf2c05e", + "zh:189f11bba0665a45405d12b66ac789de87582d23863f0599016bcc28f399a4e3", + "zh:1e4103ab153f16b614c98898d4f4d46c953687a35a0381e05b4a2958b27165b8", + "zh:1f8bb4631f8206736e1ff560b59453d9cc6d7dd0598072feaba10b6833cda1c4", + "zh:424acd64ee42cf3cc78919dc32b07a8dc4e7e9cc103eca37cce2f256a48f5e6d", + "zh:4ce7c5469e1fbed3066bb05c3c4ad8a194b76a4d01e2bbd7093537aa3eafeb0a", + "zh:6b4b592f1f3684a4df9fad146b4cbe125fffbadc2239f7011c0390199094676f", + "zh:8525bb868cd3e117fb1278e54c899a22ab6533c134779a64c224e651983fc253", + "zh:9873ee69ee94059af70ee544e0b27b68064dcc21e4d2912aa7186f46acca47c1", + "zh:af305aa28b532c78a1c51cf707a02bd1993ff1b8e7b5d656db8043c3eb484eab", + "zh:f96ecf6c3f208ad50af0959042e5fe1730b2bf26582430a61aac5b2a970917c7", + ] +} + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.4.1" + constraints = "2.4.1" + hashes = [ + "h1:CLb4n9f/hLyqqq0zbc+h5SuNOB7KnO65qOOb+ohwsKA=", + "zh:07517b24ea2ce4a1d3be3b88c3efc7fb452cd97aea8fac93ca37a08a8ec06e14", + "zh:11ef6118ed03a1b40ff66adfe21b8707ece0568dae1347ddfbcff8452c0655d5", + "zh:1ae07e9cc6b088a6a68421642c05e2fa7d00ed03e9401e78c258cf22a239f526", + "zh:1c5b4cd44033a0d7bf7546df930c55aa41db27b70b3bca6d145faf9b9a2da772", + "zh:256413132110ddcb0c3ea17c7b01123ad2d5b70565848a77c5ccc22a3f32b0dd", + "zh:4ab46fd9aadddef26604382bc9b49100586647e63ef6384e0c0c3f010ff2f66e", + "zh:5a35d23a9f08c36fceda3cef7ce2c7dc5eca32e5f36494de695e09a5007122f0", + "zh:8e9823a1e5b985b63fe283b755a821e5011a58112447d42fb969c7258ed57ed3", + "zh:8f79722eba9bf77d341edf48a1fd51a52d93ec31d9cac9ba8498a3a061ea4a7f", + "zh:b2ea782848b10a343f586ba8ee0cf4d7ff65aa2d4b144eea5bbd8f9801b54c67", + "zh:e72d1ccf8a75d8e8456c6bb4d843fd4deb0e962ad8f167fa84cf17f12c12304e", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.7.1" + hashes = [ + "h1:/zifejk3MfLSDQr5J6sc3EHrnFwAVEDH9LrewWMRqe4=", + "zh:0da320fd81ece6696f7cceda35e459ee97cae8955088af38fc7f2feab1dce924", + "zh:37d304b8b992518c9c12e8f10437b9d4a0cc5a823c9421ac794ad2347c4d1122", + "zh:3d4e12fb9588c3b2e782d392fea758c6982e5d653154bec951e949155bcbc169", + "zh:6bb32b8d5cccf3e3ae7c124ed27df76dc7653ca760c132addeee15272630c930", + "zh:94775153b90e285876fc17261e8f5338a1ff732f4133336cc68754acb74570b6", + "zh:a665d1336765cdf8620a8797fd4e7e3cecf789e96e59ba80634336a4390df377", + "zh:aa8b35e9958cb89f01c115e8866a07d5468fb53f1c227d673e94f7ee8fb76242", + "zh:b7a571336387d773a74ed6eefa3843ff78d3662f2745c99c95008002a1341662", + "zh:c50d661782175d50ea4952fe943b0e4a3e33c27aa69e5ff21b3cbfa513e90d0a", + "zh:e0999b349cc772c75876adbc2a13b5dc256d3ecd7e4aa91baee5fdfcecaa7465", + "zh:e1399aec06a7aa98e9b0f64b4281697247f338a8a40b79f5f6ebfd43bf4ce1e2", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "2.1.0" + hashes = [ + "h1:KfieWtVyGWwplSoLIB5usKAUnrIkDQBkWaR5TI+4WYg=", + "zh:0f1ec65101fa35050978d483d6e8916664b7556800348456ff3d09454ac1eae2", + "zh:36e42ac19f5d68467aacf07e6adcf83c7486f2e5b5f4339e9671f68525fc87ab", + "zh:6db9db2a1819e77b1642ec3b5e95042b202aee8151a0256d289f2e141bf3ceb3", + "zh:719dfd97bb9ddce99f7d741260b8ece2682b363735c764cac83303f02386075a", + "zh:7598bb86e0378fd97eaa04638c1a4c75f960f62f69d3662e6d80ffa5a89847fe", + "zh:ad0a188b52517fec9eca393f1e2c9daea362b33ae2eb38a857b6b09949a727c1", + "zh:c46846c8df66a13fee6eff7dc5d528a7f868ae0dcf92d79deaac73cc297ed20c", + "zh:dc1a20a2eec12095d04bf6da5321f535351a594a636912361db20eb2a707ccc4", + "zh:e57ab4771a9d999401f6badd8b018558357d3cbdf3d33cc0c4f83e818ca8e94b", + "zh:ebdcde208072b4b0f8d305ebf2bfdc62c926e0717599dcf8ec2fd8c5845031c3", + "zh:ef34c52b68933bedd0868a13ccfd59ff1c820f299760b3c02e008dc95e2ece91", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.1.0" + hashes = [ + "h1:rKYu5ZUbXwrLG1w81k7H3nce/Ys6yAxXhWcbtk36HjY=", + "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", + "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", + "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", + "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", + "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", + "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", + "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", + "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", + "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", + "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", + "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", + ] +} diff --git a/terraform/kubernetes.tf b/terraform/kubernetes.tf new file mode 100644 index 000000000..d2dee3b24 --- /dev/null +++ b/terraform/kubernetes.tf @@ -0,0 +1,335 @@ +resource "kubernetes_namespace" "namespace" { + metadata { + name = "${var.app-name}" + } +} + +resource "helm_release" "ingress-nginx" { + depends_on = [local_file.kubeconfig, kubernetes_namespace.namespace] + name = "${var.app-name}-ingress-nginx" + repository = "https://kubernetes.github.io/ingress-nginx" + chart = "ingress-nginx" + namespace = "${var.app-name}" +} + +resource "kubernetes_config_map" "config" { + depends_on = [azurerm_redis_cache.redis, azurerm_postgresql_database.postgresql-db, azurerm_storage_container.storage_container] + + metadata { + name = "${var.app-name}-config" + namespace = "${var.app-name}" + } + +# Rails application env variables + data = { + RAILS_ENV = "uat" + RAILS_LOG_TO_STDOUT = "true" + RAILS_SERVE_STATIC_FILES = "true" + DATABASE_URL = "postgresql://${urlencode("${var.postgresql-admin-login}@${azurerm_postgresql_server.db.name}")}:${urlencode(var.postgresql-admin-password)}@${azurerm_postgresql_server.db.fqdn}:5432/${azurerm_postgresql_database.postgresql-db.name}" + SOLR_URL = "http://${var.app-name}-solr:8983/solr/jupiter-uat" + REDIS_URL = "redis://:${urlencode(azurerm_redis_cache.redis.primary_access_key)}@${azurerm_redis_cache.redis.hostname}:${azurerm_redis_cache.redis.port}/0" + SECRET_KEY_BASE = "${var.rails-secret-key}" + SAML_PRIVATE_KEY = "" + SAML_CERTIFICATE = "" + ROLLBAR_TOKEN = "" + GOOGLE_ANALYTICS_TOKEN = "" + TLD_LENGTH = "3" + GOOGLE_CLIENT_ID = "" + GOOGLE_CLIENT_SECRET = "" + GOOGLE_DEVELOPER_KEY = "" + ERA_HOST = "era.uat.library.ualberta.ca" + DIGITIZATION_HOST = "digitalcollections.uat.library.ualberta.ca" + SKYLIGHT_AUTHENTICATION = "secretauthenticationtoken" + ACTIVE_STORAGE_SERVICE = "microsoft" + AZURE_STORAGE_ACCOUNT_NAME = azurerm_storage_account.blob_account.name + AZURE_STORAGE_ACCESS_KEY = azurerm_storage_account.blob_account.primary_access_key + AZURE_STORAGE_CONTAINER = azurerm_storage_container.storage_container.name + } +} + + +resource "kubernetes_config_map" "solr-config" { + metadata { + name = "${var.app-name}-solr-config" + namespace = "${var.app-name}" + } + + data = { + "schema.xml" = "${file("${path.module}/../solr/config/schema.xml")}" + "solrconfig.xml" = "${file("${path.module}/../solr/config/solrconfig.xml")}" + } +} + +resource "kubernetes_deployment" "solr" { + metadata { + name = "${var.app-name}-solr" + labels = { + app = "${var.app-name}" + } + namespace = "${var.app-name}" + } + + spec { + replicas = 1 + + selector { + match_labels = { + app = "${var.app-name}-solr" + } + } + + template { + metadata { + name = "${var.app-name}-solr" + labels = { + app = "${var.app-name}-solr" + } + } + + spec { + container { + image = "solr:6.6" + image_pull_policy = "Always" + name = "${var.app-name}-solr" + command = ["docker-entrypoint.sh", "solr-precreate", "jupiter-uat", "/config"] + port { + container_port = 8983 + } + volume_mount { + name = "solr-config" + mount_path = "/config" + } + resources { + limits = { + cpu = "250m" + memory = "512Mi" + } + requests = { + cpu = "150m" + memory = "128Mi" + } + } + } + volume { + name = "solr-config" + config_map { + name = "${var.app-name}-solr-config" + } + } + restart_policy = "Always" + } + } + } +} + +resource "kubernetes_service" "solr-service" { + metadata { + name = "${var.app-name}-solr" + namespace = "${var.app-name}" + } + + spec { + port { + port = 8983 + target_port = 8983 + } + + selector = { + app = "${var.app-name}-solr" + } + } +} + +resource "kubernetes_deployment" "app" { + depends_on = [kubernetes_config_map.config] + + metadata { + name = "${var.app-name}-app" + labels = { + app = "${var.app-name}" + } + namespace = "${var.app-name}" + } + + spec { + replicas = 2 + + selector { + match_labels = { + app = "${var.app-name}" + } + } + + template { + metadata { + name = "${var.app-name}" + labels = { + app = "${var.app-name}" + } + } + + spec { + init_container { + image = "murny/jupiter:latest" + image_pull_policy = "Always" + name = "${var.app-name}-init" + command = ["rake", "db:migrate"] + env_from { + config_map_ref { + name = "${var.app-name}-config" + } + } + } + container { + image = "murny/jupiter:latest" + image_pull_policy = "Always" + name = "${var.app-name}" + port { + container_port = 3000 + } + env_from { + config_map_ref { + name = "${var.app-name}-config" + } + } + # TODO: Figure healthcheck out, seems to be failing on HTTP/HTTPS issue + # readiness_probe { + # http_get { + # path = "/healthcheck" + # port = 3000 + # scheme = "HTTPS" + # } + # initial_delay_seconds = 10 + # period_seconds = 10 + # timeout_seconds = 2 + # } + resources { + limits = { + cpu = "250m" + memory = "512Mi" + } + requests = { + cpu = "150m" + memory = "128Mi" + } + } + } + restart_policy = "Always" + } + } + } +} + +resource "kubernetes_deployment" "worker" { + depends_on = [kubernetes_config_map.config] + + metadata { + name = "${var.app-name}-workers" + namespace = "${var.app-name}" + + } + spec { + replicas = 1 + + selector { + match_labels = { + app = "${var.app-name}-workers" + } + } + + template { + metadata { + name = "${var.app-name}-workers" + labels = { + app = "${var.app-name}-workers" + } + } + + spec { + container { + name = "${var.app-name}-workers" + image = "murny/jupiter:latest" + image_pull_policy = "Always" + command = ["bundle", "exec", "sidekiq"] + + env_from { + config_map_ref { + name = "${var.app-name}-config" + } + } + resources { + limits = { + cpu = "250m" + memory = "512Mi" + } + requests = { + cpu = "150m" + memory = "128Mi" + } + } + + readiness_probe { + exec { + command = [ "cat", "/app/tmp/sidekiq_process_has_started"] + } + + failure_threshold = 10 + initial_delay_seconds = 10 + period_seconds = 2 + success_threshold = 2 + timeout_seconds = 1 + } + } + restart_policy = "Always" + termination_grace_period_seconds = 60 + } + } + } +} + +resource "kubernetes_service" "service" { + metadata { + name = "${var.app-name}-service" + namespace = "${var.app-name}" + } + + spec { + port { + port = 80 + target_port = 3000 + } + + type = "NodePort" + + selector = { + app = "${var.app-name}" + } + } +} + +resource "kubernetes_ingress" "ingress" { + wait_for_load_balancer = true + metadata { + name = "${var.app-name}-ingress" + annotations = { + "kubernetes.io/ingress.class" = "nginx" + "nginx.ingress.kubernetes.io/proxy-body-size" = "16m" + } + namespace = "${var.app-name}" + } + spec { + rule { + # TODO: Figure out how we will use DNS/etc + host = "*.uat.library.ualberta.ca" + http { + path { + path = "/" + backend { + service_name = "${var.app-name}-service" + service_port = 80 + } + } + } + } + } +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 000000000..0d11662d1 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,159 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=2.90.0" + } + helm = { + source = "hashicorp/helm" + version = "2.4.1" + } + } +} + +provider "azurerm" { + subscription_id = var.azure-subscription-id + client_id = var.azure-client-id + client_secret = var.azure-client-secret + tenant_id = var.azure-tenant-id + + features {} +} + +resource "azurerm_resource_group" "rg" { + name = "${var.app-name}-rg" + location = "Canada Central" +} + +resource "azurerm_kubernetes_cluster" "cluster" { + name = "${var.app-name}-cluster" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + dns_prefix = "${var.app-name}Cluster" + + default_node_pool { + name = "nodes" + node_count = "3" + vm_size = "standard_d2_v2" + } + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_postgresql_server" "db" { + name = "${var.app-name}-psqlserver" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + administrator_login = var.postgresql-admin-login + administrator_login_password = var.postgresql-admin-password + + sku_name = "GP_Gen5_4" + version = "11" + + # storage_mb = 500000 + # backup_retention_days = 7 + # geo_redundant_backup_enabled = true + # auto_grow_enabled = true + + # TODO: Can we disabled public network access and use SSL? + public_network_access_enabled = true + ssl_enforcement_enabled = false + # ssl_minimal_tls_version_enforced = "TLS1_2" +} + +# Create a PostgreSQL Database +resource "azurerm_postgresql_database" "postgresql-db" { + name = "${var.app-name}_production" + resource_group_name = azurerm_resource_group.rg.name + server_name = azurerm_postgresql_server.db.name + charset = "utf8" + collation = "English_United States.1252" +} + +# TODO: Fix this, Currently we allow everything for now +resource "azurerm_postgresql_firewall_rule" "postgresql-fw-rules" { + name = "${var.app-name}-postgresql-fw-rules" + resource_group_name = azurerm_resource_group.rg.name + server_name = azurerm_postgresql_server.db.name + start_ip_address = "0.0.0.0" + end_ip_address = "255.255.255.255" +} + +resource "azurerm_redis_cache" "redis" { + name = "${var.app-name}-cache" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + capacity = 2 + family = "C" + sku_name = "Standard" + + # TODO: It possible to use SSL here? + enable_non_ssl_port = true + # minimum_tls_version = "1.2" + + redis_configuration { + # https://github.com/mperham/sidekiq/wiki/Using-Redis#memory + maxmemory_policy = "noeviction" + } +} + +# TODO: Fix this, Currently we allow everything for now +resource "azurerm_redis_firewall_rule" "redis-fw-rules" { + name = "${var.app-name}_redis_fw_rules" + redis_cache_name = azurerm_redis_cache.redis.name + resource_group_name = azurerm_resource_group.rg.name + start_ip = "0.0.0.0" + end_ip = "255.255.255.255" +} + + +# blob account name must be unique across Azure, so append random string to name +resource "random_string" "random" { + length = 10 + special = false + upper = false +} + +resource "azurerm_storage_account" "blob_account" { + name = "${var.app-name}account${random_string.random.result}" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + account_tier = "Standard" + account_replication_type = "LRS" + + # TODO: Probably not needed for Jupiter? + blob_properties { + cors_rule { + allowed_headers = ["Origin", "Content-Type", "Content-MD5", "x-ms-blob-content-disposition", "x-ms-blob-type"] + allowed_methods = ["PUT"] + allowed_origins = ["*"] + exposed_headers = ["Origin", "Content-Type", "Content-MD5", "x-ms-blob-content-disposition", "x-ms-blob-type"] + max_age_in_seconds = 3600 + } + } +} + +resource "azurerm_storage_container" "storage_container" { + name = "${var.app-name}-blob-container" + storage_account_name = azurerm_storage_account.blob_account.name + container_access_type = "private" +} + +provider "helm" { + kubernetes { + host = azurerm_kubernetes_cluster.cluster.kube_config.0.host + client_certificate = base64decode(azurerm_kubernetes_cluster.cluster.kube_config.0.client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.cluster.kube_config.0.client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.cluster.kube_config.0.cluster_ca_certificate) + } +} + +provider "kubernetes" { + host = azurerm_kubernetes_cluster.cluster.kube_config.0.host + client_certificate = base64decode(azurerm_kubernetes_cluster.cluster.kube_config.0.client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.cluster.kube_config.0.client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.cluster.kube_config.0.cluster_ca_certificate) +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 000000000..37e63fe96 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,5 @@ +resource "local_file" "kubeconfig" { + depends_on = [azurerm_kubernetes_cluster.cluster] + filename = "kubeconfig" + content = azurerm_kubernetes_cluster.cluster.kube_config_raw +} diff --git a/terraform/terraform.tfvars.sample b/terraform/terraform.tfvars.sample new file mode 100644 index 000000000..118295729 --- /dev/null +++ b/terraform/terraform.tfvars.sample @@ -0,0 +1,24 @@ +#################### +# Common Variables # +#################### +app-name = "jupiter" + +################## +# Authentication # +################## +azure-subscription-id = "" +azure-client-id = "" +azure-client-secret = "" +azure-tenant-id = "" + +##################### +# PostgreSQL Server # +##################### +postgresql-admin-login = "" +postgresql-admin-password = "" + + +######### +# Rails # +######### +rails-secret-key = "" diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 000000000..4735663f8 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,57 @@ +################################ +## Common - Variables ## +################################ + +variable "app-name" { + type = string + description = "Application name" +} + + +################################ +## Azure Provider - Variables ## +################################ + +variable "azure-subscription-id" { + type = string + description = "Azure Subscription ID" +} + +variable "azure-client-id" { + type = string + description = "Azure Client ID" +} + +variable "azure-client-secret" { + type = string + description = "Azure Client Secret" +} + +variable "azure-tenant-id" { + type = string + description = "Azure Tenant ID" +} + +############################################# +# Azure Database for PostgreSQL - Variables # +############################################# + +variable "postgresql-admin-login" { + type = string + description = "Login to authenticate to PostgreSQL Server" +} + +variable "postgresql-admin-password" { + type = string + description = "Password to authenticate to PostgreSQL Server" +} + + +##################### +# Rails - Variables # +##################### + +variable "rails-secret-key" { + type = string + description = "Rails secret key base" +}