diff --git a/CHANGELOG.md b/CHANGELOG.md index 290aab63a8..2234a8bed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Infra** New `job-runner` crate responsible for managing the OCI bundle runtime & log shipping on the machine - **Infra** Jobs now log an explicit rate message when logs are rate limited & truncated - **Infra** `infra-artifacts` Terraform plan & S3 bucket used for automating building & uploading internal binaries, etc. +- **Infra** Aiven Redis provider - **Bolt** `bolt secret set ` command ### Changed diff --git a/infra/tf/redis_aiven/main.tf b/infra/tf/redis_aiven/main.tf new file mode 100644 index 0000000000..241e34ae58 --- /dev/null +++ b/infra/tf/redis_aiven/main.tf @@ -0,0 +1,79 @@ +locals { + db_configs = { + ephemeral = { + plan = var.redis_aiven.plan_ephemeral + maxmemory_policy = "allkeys-lru" + persistence = "off" + } + persistent = { + plan = var.redis_aiven.plan_persistent + maxmemory_policy = "noeviction" + persistence = "rdb" + } + } +} + +module "secrets" { + source = "../modules/secrets" + + keys = flatten([ + ["aiven/api_token"], + [ + for k, v in var.redis_dbs: + [ + "redis/${k}/username", + "redis/${k}/password", + ] + ], + ]) +} + +resource "aiven_redis" "main" { + for_each = var.redis_dbs + + project = var.redis_aiven.project + cloud_name = var.redis_aiven.cloud + plan = local.db_configs[each.key].plan + service_name = "rivet-${var.namespace}-${each.key}" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" + + tag { + key = "rivet:namespace" + value = var.namespace + } + + redis_user_config { + redis_ssl = true + redis_maxmemory_policy = local.db_configs[each.key].maxmemory_policy + redis_persistence = local.db_configs[each.key].persistence + + dynamic "ip_filter_object" { + for_each = sort(data.terraform_remote_state.k8s_cluster_aws.outputs.nat_public_ips) + + content { + network = "${ip_filter_object.value}/32" + description = "AWS NAT" + } + } + + public_access { + redis = true + } + } +} + +resource "aiven_redis_user" "main" { + for_each = var.redis_dbs + + project = var.redis_aiven.project + service_name = aiven_redis.main[each.key].service_name + username = module.secrets.values["redis/${each.key}/username"] + password = module.secrets.values["redis/${each.key}/password"] + + redis_acl_categories = ["+@all"] + redis_acl_commands = [] + redis_acl_channels = ["*"] + redis_acl_keys = ["*"] +} + diff --git a/infra/tf/redis_aiven/outputs.tf b/infra/tf/redis_aiven/outputs.tf new file mode 100644 index 0000000000..4008e1608b --- /dev/null +++ b/infra/tf/redis_aiven/outputs.tf @@ -0,0 +1,14 @@ +output "host" { + value = { + for k, v in var.redis_dbs: + k => aiven_redis.main[k].service_host + } +} + +output "port" { + value = { + for k, v in var.redis_dbs: + k => aiven_redis.main[k].service_port + } +} + diff --git a/infra/tf/redis_aiven/providers.tf b/infra/tf/redis_aiven/providers.tf new file mode 100644 index 0000000000..bc22d007d9 --- /dev/null +++ b/infra/tf/redis_aiven/providers.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + aiven = { + source = "aiven/aiven" + version = "4.12.1" + } + } +} + +provider "aiven" { + api_token = module.secrets.values["aiven/api_token"] +} + diff --git a/infra/tf/redis_aiven/vars.tf b/infra/tf/redis_aiven/vars.tf new file mode 100644 index 0000000000..33a73941ca --- /dev/null +++ b/infra/tf/redis_aiven/vars.tf @@ -0,0 +1,19 @@ +variable "namespace" { + type = string +} + +variable "redis_dbs" { + type = map(object({ + persistent = bool + })) +} + +variable "redis_aiven" { + type = object({ + project = string + cloud = string + plan_ephemeral = string + plan_persistent = string + }) +} + diff --git a/lib/bolt/Cargo.lock b/lib/bolt/Cargo.lock index 3910f0d407..eb01e36199 100644 --- a/lib/bolt/Cargo.lock +++ b/lib/bolt/Cargo.lock @@ -2418,9 +2418,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] diff --git a/lib/bolt/config/src/ns.rs b/lib/bolt/config/src/ns.rs index f028d42d17..9ba8d523ef 100644 --- a/lib/bolt/config/src/ns.rs +++ b/lib/bolt/config/src/ns.rs @@ -411,6 +411,13 @@ pub enum RedisProvider { Kubernetes {}, #[serde(rename = "aws")] Aws {}, + #[serde(rename = "aiven")] + Aiven { + project: String, + cloud: String, + plan_ephemeral: String, + plan_persistent: String, + }, } impl Default for RedisProvider { diff --git a/lib/bolt/core/src/context/service.rs b/lib/bolt/core/src/context/service.rs index 277739cd18..e010572ede 100644 --- a/lib/bolt/core/src/context/service.rs +++ b/lib/bolt/core/src/context/service.rs @@ -1115,7 +1115,8 @@ impl ServiceContextData { // Read auth secrets let (username, password) = match project_ctx.ns().redis.provider { - config::ns::RedisProvider::Kubernetes {} => ( + config::ns::RedisProvider::Kubernetes {} + | config::ns::RedisProvider::Aiven { .. } => ( project_ctx .read_secret(&["redis", &db_name, "username"]) .await?, diff --git a/lib/bolt/core/src/dep/k8s/gen.rs b/lib/bolt/core/src/dep/k8s/gen.rs index 1718849849..dbb85711de 100644 --- a/lib/bolt/core/src/dep/k8s/gen.rs +++ b/lib/bolt/core/src/dep/k8s/gen.rs @@ -696,7 +696,7 @@ async fn build_volumes( }) })); } - config::ns::RedisProvider::Aws { .. } => { + config::ns::RedisProvider::Aws { .. } | config::ns::RedisProvider::Aiven { .. } => { // Uses publicly signed cert } } diff --git a/lib/bolt/core/src/dep/terraform/gen.rs b/lib/bolt/core/src/dep/terraform/gen.rs index 65fb241a72..9e16250c10 100644 --- a/lib/bolt/core/src/dep/terraform/gen.rs +++ b/lib/bolt/core/src/dep/terraform/gen.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use indoc::{formatdoc, indoc}; use serde_json::json; use std::collections::HashMap; @@ -82,7 +82,9 @@ pub async fn gen_bolt_tf(ctx: &ProjectContext, plan_id: &str) -> Result<()> { format!("# This is generated by Bolt. Do not modify.\n\n{backend}\n\n{remote_states}"); let path = ctx.tf_path().join(plan_id).join("_bolt.tf"); - tokio::fs::write(&path, bolt_tf).await?; + tokio::fs::write(&path, bolt_tf) + .await + .context(format!("write _bolt.tf to {}", path.display()))?; Ok(()) } @@ -439,14 +441,31 @@ async fn vars(ctx: &ProjectContext) { } } + let redis_provider = match &config.redis.provider { + ns::RedisProvider::Kubernetes { .. } => "kubernetes", + ns::RedisProvider::Aws { .. } => "aws", + ns::RedisProvider::Aiven { + project, + cloud, + plan_ephemeral, + plan_persistent, + } => { + vars.insert( + "redis_aiven".into(), + json!({ + "project": project, + "cloud": cloud, + "plan_ephemeral": plan_ephemeral, + "plan_persistent": plan_persistent, + }), + ); + + "aiven" + } + }; + vars.insert("redis_replicas".into(), json!(config.redis.replicas)); - vars.insert( - "redis_provider".into(), - json!(match config.redis.provider { - ns::RedisProvider::Kubernetes { .. } => "kubernetes", - ns::RedisProvider::Aws { .. } => "aws", - }), - ); + vars.insert("redis_provider".into(), json!(redis_provider)); vars.insert("redis_dbs".into(), json!(redis_dbs)); } diff --git a/lib/bolt/core/src/dep/terraform/output.rs b/lib/bolt/core/src/dep/terraform/output.rs index c33e42902e..c98db29f3b 100644 --- a/lib/bolt/core/src/dep/terraform/output.rs +++ b/lib/bolt/core/src/dep/terraform/output.rs @@ -133,11 +133,11 @@ pub async fn read_clickhouse(ctx: &ProjectContext) -> ClickHouse { } pub async fn read_redis(ctx: &ProjectContext) -> Redis { - // match &ctx.ns().cluster.kind { - // config::ns::ClusterKind::SingleNode { .. } => read_plan::(ctx, "redis_k8s").await, - // config::ns::ClusterKind::Distributed { .. } => read_plan::(ctx, "redis_aws").await, - // } - read_plan::(ctx, "redis_k8s").await + match &ctx.ns().redis.provider { + config::ns::RedisProvider::Kubernetes { .. } => read_plan::(ctx, "redis_k8s").await, + config::ns::RedisProvider::Aws { .. } => read_plan::(ctx, "redis_aws").await, + config::ns::RedisProvider::Aiven { .. } => read_plan::(ctx, "redis_aiven").await, + } } /// Reads a Terraform plan's output and decodes in to type. diff --git a/lib/bolt/core/src/dep/terraform/remote_states.rs b/lib/bolt/core/src/dep/terraform/remote_states.rs index a5972b68d5..bc53c70362 100644 --- a/lib/bolt/core/src/dep/terraform/remote_states.rs +++ b/lib/bolt/core/src/dep/terraform/remote_states.rs @@ -11,6 +11,9 @@ use crate::context::ProjectContext; pub fn dependency_graph(_ctx: &ProjectContext) -> HashMap<&'static str, Vec> { hashmap! { "dns" => vec![RemoteStateBuilder::default().plan_id("pools").build().unwrap(), RemoteStateBuilder::default().plan_id("k8s_infra").build().unwrap()], + "redis_aiven" => vec![ + RemoteStateBuilder::default().plan_id("k8s_cluster_aws").build().unwrap() + ], "redis_aws" => vec![ RemoteStateBuilder::default().plan_id("k8s_cluster_aws").build().unwrap() ], diff --git a/lib/bolt/core/src/tasks/config/generate.rs b/lib/bolt/core/src/tasks/config/generate.rs index f85ce745b2..ee507154f7 100644 --- a/lib/bolt/core/src/tasks/config/generate.rs +++ b/lib/bolt/core/src/tasks/config/generate.rs @@ -482,7 +482,7 @@ pub async fn generate(project_path: &Path, ns_id: &str) -> Result<()> { "ephemeral" }; let (db_name, username) = match &ctx.ns().redis.provider { - config::ns::RedisProvider::Kubernetes {} => { + config::ns::RedisProvider::Kubernetes {} | config::ns::RedisProvider::Aiven { .. } => { (db_name.to_string(), "default".to_string()) } config::ns::RedisProvider::Aws {} => { diff --git a/lib/bolt/core/src/tasks/db.rs b/lib/bolt/core/src/tasks/db.rs index 1b27c93ad1..6aa443e61a 100644 --- a/lib/bolt/core/src/tasks/db.rs +++ b/lib/bolt/core/src/tasks/db.rs @@ -78,7 +78,7 @@ async fn redis_shell(shell_ctx: ShellContext<'_>) -> Result<()> { // Read auth secrets let (username, password) = match ctx.ns().redis.provider { - config::ns::RedisProvider::Kubernetes {} => ( + config::ns::RedisProvider::Kubernetes {} | config::ns::RedisProvider::Aiven { .. } => ( ctx.read_secret(&["redis", &db_name, "username"]).await?, ctx.read_secret_opt(&["redis", &db_name, "password"]) .await?, @@ -93,6 +93,10 @@ async fn redis_shell(shell_ctx: ShellContext<'_>) -> Result<()> { (username, password) } }; + let mount_ca = matches!( + ctx.ns().redis.provider, + config::ns::RedisProvider::Kubernetes {} + ); if let LogType::Default = log_type { rivet_term::status::progress("Connecting to Redis", &db_name); @@ -110,6 +114,7 @@ async fn redis_shell(shell_ctx: ShellContext<'_>) -> Result<()> { } else { Vec::new() }; + let cmd = formatdoc!( " sleep 1 && @@ -118,9 +123,15 @@ async fn redis_shell(shell_ctx: ShellContext<'_>) -> Result<()> { -p {port} \ --user {username} \ -c \ - --tls --cacert /local/redis-ca.crt - " + --tls {cacert} + ", + cacert = if mount_ca { + "--cacert /local/redis-ca.crt" + } else { + "" + } ); + let overrides = json!({ "apiVersion": "v1", "metadata": { @@ -146,28 +157,36 @@ async fn redis_shell(shell_ctx: ShellContext<'_>) -> Result<()> { "stdin": true, "stdinOnce": true, "tty": true, - "volumeMounts": [{ - "name": "redis-ca", - "mountPath": "/local/redis-ca.crt", - "subPath": "redis-ca.crt" - }] + "volumeMounts": if mount_ca { + json!([{ + "name": "redis-ca", + "mountPath": "/local/redis-ca.crt", + "subPath": "redis-ca.crt" + }]) + } else { + json!([]) + } } ], - "volumes": [{ - "name": "redis-ca", - "configMap": { - "name": format!("redis-{}-ca", db_name), - "defaultMode": 420, - // Distributed clusters don't need a CA for redis - "optional": true, - "items": [ - { - "key": "ca.crt", - "path": "redis-ca.crt" - } - ] - } - }] + "volumes": if mount_ca { + json!([{ + "name": "redis-ca", + "configMap": { + "name": format!("redis-{}-ca", db_name), + "defaultMode": 420, + // Distributed clusters don't need a CA for redis + "optional": true, + "items": [ + { + "key": "ca.crt", + "path": "redis-ca.crt" + } + ] + } + }]) + } else { + json!([]) + } } }); diff --git a/lib/bolt/core/src/tasks/infra/mod.rs b/lib/bolt/core/src/tasks/infra/mod.rs index 3d62f07240..98045d81d1 100644 --- a/lib/bolt/core/src/tasks/infra/mod.rs +++ b/lib/bolt/core/src/tasks/infra/mod.rs @@ -166,6 +166,15 @@ pub fn build_plan( }, }); } + ns::RedisProvider::Aiven { .. } => { + plan.push(PlanStep { + name_id: "redis-aiven", + kind: PlanStepKind::Terraform { + plan_id: "redis_aiven".into(), + needs_destroy: true, + }, + }); + } } // CockroachDB