DEV Community

Cover image for Securing the connectivity between a Scaleway Kubernetes Kapsule application and Scaleway RDB database
Chabane R. for Onepoint x Stack Labs

Posted on

Securing the connectivity between a Scaleway Kubernetes Kapsule application and Scaleway RDB database

It is very easy today to establish a connection between a container in Kubernetes and a relational database server, just create a SQL user and open a TCP connection. In cloud computing, in the case of Scaleway Elements, the equivalent is connecting a container in a Scaleway Kubernetes Kapsule cluster to a Scaleway Relational Database instance (RDB).

Important points should be taken into account in setting up this connectivity.

Which network topology to choose? How to authenticate and authorize the connection to the RDB instance? Can I publicly expose a RDB instance?

Which architecture could be the most efficient, maintainable and scalable?

Scenarios

RDB service supports the following scenarios for accessing the RDB instance:

  • A virtual instance in the same project
  • A virtual instance in a different project
  • A client application through the internet

  • The private network scenario is actually not possible. A feature request is opened and it's ongoing.

The scenarios that concern us are the first two:

  • A Kapsule cluster and a RDB instance in the same project
  • A Kapsule cluster and a RDB instance in a different project

Let's discover the possible architectures that could be used to implement each scenario.

Direct communication with public IP and authorized networks

Kapsule and RDB communication with public IP and authorized networks

In this architecture, our RDB instance is isolated on Scaleway network and accessible through public IP address to only Kapsule cluster that requires access to it. Pods have access to RDB database using Username/Password.

Public Gateway is not available yet for Kubernetes Kapsule and RDB instance, see featurer request, so you need to whitelist all the Kubernetes nodes IPs.

Direct communication in separate projects

Kapsule and RDB communication with public IP and authorized networks and in differents projects

In this architecture, our RDB instance is isolated on its own project accessible through public IP address to only Kapsule cluster that requires access to it. Pods have access to RDB database using Username/Password.

Each architecture has its own advantages and disadvantages but all apply project isolation best practices for securing sensitive data in RDB.

Let's implement the scenario 1.

Prerequisites

Architecture

The overall architecture that we will implement during this article is as follows:

Overral architecture

Objectives

During this section of the workshop:

With Terraform

  • We will create a Kapsule cluster
  • a higly available RDB instance
  • a virtual instance to act as a bastion to access RDB from outside
  • a security group for the virtual instance

With Kubectl

  • We will deploy a metabase application
  • A load balancer with Treafik 2
  • a SSL certificate

The article is divided into four sections:

  • Configuring the bastion
  • Creating a Kubernetes Kapsule cluster using Terraform
  • Securing sensitive data in Scaleway RDB
  • Securing the connectivity between a Kubernetes Kapsule application and a RDB database

Configuring the bastion

In this section we will deploy the following SCW resources:

  • a virtual instance to act as a bastion to access RDB from outside
  • a security group to allow only authorized users to access the virtual instance

Bastion

Security Groups

Security Groups allows us to restrict the inbound and outbound network traffic to and from a virtual instance. In our case, we implement the following rule:

  • A rule to restrict access to the virtual instance from the SSH port to only authorized networks.

Create a terraform file infra/plan/sg.tf

resource "scaleway_instance_security_group" "bastion" { name = "bastion" inbound_default_policy = "drop" outbound_default_policy = "accept" dynamic "inbound_rule" { for_each = var.authorized_source_ranges content { action = "accept" port = "22" ip = inbound_rule.value } } } 
Enter fullscreen mode Exit fullscreen mode

Bastion

In order to access the RDB instance from outside, we need to create a bastion host. We can achieve that by creating a virtual instance. During the initialization of the instance we install the PostgreSQL client.

We also create a SSH key to connect to the virtual instance. You can remove this resource if you want to manage the keys outside of terraform.

Create a terraform file infra/plan/bastion.tf

resource "scaleway_instance_ip" "bastion" {} resource "scaleway_instance_server" "bastion" { depends_on = [scaleway_account_ssh_key.bastion] name = "bastion" type = var.bastion_instance_type image = "ubuntu_xenial" security_group_id = scaleway_instance_security_group.bastion.id ip_id = scaleway_instance_ip.bastion.id tags = ["bastion", var.env] user_data = { env = var.env cloud-init = file("${path.module}/cloud-init.sh") } } resource "scaleway_account_ssh_key" "bastion" { name = "bastion" public_key = var.bastion_public_ssh_key } 
Enter fullscreen mode Exit fullscreen mode

Create a script file cloud-init.sh

#!/bin/sh apt update apt install -y unzip # install awscli curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install # install psql wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list apt update apt install -y postgresql-client-13 
Enter fullscreen mode Exit fullscreen mode

Let's configure our terraform.

Create a terraform file infra/plan/variable.tf

variable "zone" { type = string } variable "region" { type = string } variable "env" { type = string } variable "authorized_source_ranges" { type = list(string) description = "Addresses or CIDR blocks which are allowed to connect to Virtual Instance." } variable "bastion_instance_type" { type = string } variable "bastion_public_ssh_key" { type = string } 
Enter fullscreen mode Exit fullscreen mode

Add a infra/plan/version.tf file

terraform { required_providers { scaleway = { source = "scaleway/scaleway" version = "2.1.0" } } required_version = ">= 0.13" } 
Enter fullscreen mode Exit fullscreen mode

Add a infra/plan/provider.tf file

provider "scaleway" { zone = var.zone region = var.region } 
Enter fullscreen mode Exit fullscreen mode

Add a infra/plan/backend.tf

terraform { backend "s3" { } } 
Enter fullscreen mode Exit fullscreen mode

And a ìnfra/plan/output.tf

output "bastion_ip" { value = scaleway_instance_ip.bastion.address } 
Enter fullscreen mode Exit fullscreen mode

Now, export the following variables and create a bucket to save your terraform states.

cat <<EOF >~/.aws/credentials [default] aws_access_key_id=<SCW_ACCESS_KEY> aws_secret_access_key=<SCW_SECRET_KEY> region=fr-par EOF export SCW_ACCESS_KEY=<SCW_ACCESS_KEY> export SCW_SECRET_KEY=<SCW_SECRET_KEY> export SCW_REGION=fr-par sed -i "s/<SCW_ACCESS_KEY>/$SCW_ACCESS_KEY/g; s/<SCW_SECRET_KEY>/${SCW_SECRET_KEY}/g;" ~/.aws/credentials export ENV=dev aws s3api create-bucket --bucket company-$ENV-terraform-backend --endpoint-url https://s3.$SCW_REGION.scw.cloud aws s3api put-bucket-versioning --bucket company-$ENV-terraform-backend --versioning-configuration Status=Enabled --endpoint-url https://s3.$SCW_REGION.scw.cloud 
Enter fullscreen mode Exit fullscreen mode

Create a infra/plan/config/dev/terraform.tfvars:

zone = "fr-par-1" region = "fr-par" env = "dev" authorized_source_ranges = ["<AUTHORIZED_NETWORK>"] bastion_instance_type = "STARDUST1-S" bastion_public_ssh_key = "<BASTION_PUBLIC_SSH_KEY>" 
Enter fullscreen mode Exit fullscreen mode

Create a infra/plan/config/dev/s3.backend and deploy the infrastructure:

bucket = "company-dev-terraform-backend" key = "terraform.tfstate" region = "fr-par" endpoint = "https://s3.fr-par.scw.cloud" skip_credentials_validation = true skip_region_validation = true 
Enter fullscreen mode Exit fullscreen mode
cd infra/plan sed -i "s,<AUTHORIZED_NETWORK>, $(curl -s http://checkip.amazonaws.com/),g; s,<BASTION_PUBLIC_SSH_KEY>,$(cat <PATH_TO_SSH_PUB>/id_rsa.pub),g;" terraform.tfvars terraform init --backend-config=config/$ENV/s3.backend -reconfigure terraform validate terraform apply -var-file=config/$ENV/terraform.tfvars 
Enter fullscreen mode Exit fullscreen mode

Let's check if all the resources have been created and are working correctly

Reserved IP

Reserved IP

Security Groups

Security group rules

Virtual instance

Virtual instance

Let's check the connection

ssh -i <PRIVATE_KEY_PATH> root@$(terraform output --raw bastion_ip) Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-159-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Thu Nov 4 21:13:14 UTC 2021 System load: 0.09 Processes: 83 Usage of /: 19.4% of 8.86GB Users logged in: 0 Memory usage: 15% IP address for ens2: 10.69.86.243 Swap usage: 0% 
Enter fullscreen mode Exit fullscreen mode

Creating a Kubernetes Kapsule cluster using Terraform

In the section we created our network stack. In this part we will create and configure the Kubernetes Kapsule cluster.

The following resources will be created:

  • Kubernetes Kapsule cluster
  • Kubernetes Kapsule pools
  • Traefik2 Load balancer

Kubernetes Kapsule Cluster and pools

Kubernetes Kapsule cluster

Our Kubernetes Kapsule cluster is hosted in the Scaleway network. Each node of a pool has its own public IP, there is no such a mecanism that permit to access the private IPs of the cluster from outside. So all of the communications between RDB and Kapsule will be throught the public internet.

Scaleway is working on a feature to attach a Kapsule cluster to a private network. See the feature request.

Create the terraform file infra/plan/kapsule.tf:

resource "scaleway_k8s_cluster" "kapsule" { name = "kapsule-${var.env}" description = "${var.env} cluster" version = var.kapsule_cluster_version cni = "calico" tags = [var.env] autoscaler_config { disable_scale_down = false scale_down_delay_after_add = "5m" estimator = "binpacking" expander = "random" ignore_daemonsets_utilization = true balance_similar_node_groups = true expendable_pods_priority_cutoff = -5 } auto_upgrade { enable = true maintenance_window_start_hour = 4 maintenance_window_day = "sunday" } } resource "scaleway_k8s_pool" "default" { cluster_id = scaleway_k8s_cluster.kapsule.id name = "default" node_type = var.kapsule_pool_node_type size = var.kapsule_pool_size autoscaling = true autohealing = true min_size = var.kapsule_pool_min_size max_size = var.kapsule_pool_max_size } 
Enter fullscreen mode Exit fullscreen mode

Complete the file infra/plan/variable.tf:

variable "kapsule_cluster_version" { type = string } variable "kapsule_pool_size" { type = number } variable "kapsule_pool_min_size" { type = number } variable "kapsule_pool_max_size" { type = number } variable "kapsule_pool_node_type" { type = string } 
Enter fullscreen mode Exit fullscreen mode

Complete the file infra/plan/config/$ENV/terraform.tfvars:

kapsule_cluster_version = "1.22" kapsule_pool_size = 2 kapsule_pool_min_size = 2 kapsule_pool_max_size = 4 kapsule_pool_node_type = "gp1-xs" 
Enter fullscreen mode Exit fullscreen mode

Let's deploy our cluster

cd infra/plan terraform apply -var-file=config/$ENV/terraform.tfvars 
Enter fullscreen mode Exit fullscreen mode

Let's check if the cluster and the pools have been created and are working correctly:

Kubernetes Kapsule Cluster

Kubernetes Kapsule Cluster

Kubernetes Kapsule Pool

Kubernetes Kapsule Pools

Kubernetes Kapsule NodePools

Kubernetes Kapsule NodePools

Enable the Traefik load balancer using the Scaleway CLI:

scw k8s cluster update $(scw k8s cluster list | grep kapsule-${ENV} | awk '{ print $1 }') ingress=traefik2 region=$SCW_REGION 
Enter fullscreen mode Exit fullscreen mode

Let's check if Traefik 2 has been enabled

Traefik2 enabled

Get the Kube config file and test the cluster access:

mkdir -p ~/.kube/$ENV scw k8s kubeconfig get $(scw k8s cluster list | grep kapsule-${ENV} | awk '{ print $1 }') region=$SCW_REGION > ~/.kube/$ENV/config export KUBECONFIG=~/.kube/$ENV/config kubectl get nodes NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME scw-kapsule-dev-default-ad56eb1786504a99957ea8 Ready <none> 100m v1.22.3 10.66.32.141 212.47.252.20 Ubuntu 20.04.1 LTS fc08d0ff0a 5.4.0-80-generic containerd://1.5.5 scw-kapsule-dev-default-c4056d33bede4f3883fade Ready <none> 100m v1.22.3 10.66.242.89 51.15.205.158 Ubuntu 20.04.1 LTS fc08d0ff0a 5.4.0-80-generic containerd://1.5.5 
Enter fullscreen mode Exit fullscreen mode

To expose Traefik 2 with a Scaleway LoadBalancer, create the file infra/k8s/traefik-loadbalancer.yml:

apiVersion: v1 kind: Service metadata: name: traefik-ingress namespace: kube-system labels: k8s.scw.cloud/ingress: traefik2 spec: type: LoadBalancer ports: - port: 80 name: http targetPort: 8000 - port: 443 name: https targetPort: 8443 selector: app.kubernetes.io/name: traefik 
Enter fullscreen mode Exit fullscreen mode

Deploy the configuration:

kubectl create -f infra/k8s/traefik-loadbalancer.yml 
Enter fullscreen mode Exit fullscreen mode

Verify that the LoadBalancer has been deployed correctly:

kubectl -n kube-system get svc traefik-ingress NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE traefik-ingress LoadBalancer 10.43.99.132 51.159.74.228 80:31912/TCP,443:30860/TCP 16s 
Enter fullscreen mode Exit fullscreen mode

Kubernetes Kapsule Load balancer

See Exposing services in Scaleway Kubernetes for more details.

To avoid losing the IP, let's reserve this one:

export TRAEFIK_EXTERNAL_IP=$(kubectl get svc traefik-ingress -n kube-system -o json | jq -r .status.loadBalancer.ingress[0].ip) kubectl patch svc traefik-ingress -n kube-system --type merge --patch "{\"spec\":{\"loadBalancerIP\": \"$TRAEFIK_EXTERNAL_IP\"}}" 
Enter fullscreen mode Exit fullscreen mode

Kubernetes Kapsule Load balancer Reserved IP

Securing sensitive data in Scaleway RDB

Our Kapsule Kubernetes cluster is now active. In this section we will configure the RDB Instance.

The following resources will be created:

  • A highly available RDB Instance
  • A database and a user for metabase
  • ACLs to restrict the traffic to only Kubernetes Kapsule pools nodes and Bastion public IP

RDB

RDB instance

  • The RDB Instance used is a PostgreSQL database server
  • The Multiples zones option is enabled to ensure high-availability
  • Automated backup is enabled
  • We create a database and a user for later

Create a terraform file infra/plan/rdb.tf

resource "random_string" "db_name_suffix" { length = 4 special = false upper = false } resource "scaleway_rdb_instance" "rdb" { name = "postgresql-${var.env}" node_type = var.rdb_instance_node_type volume_type = var.rdb_instance_volume_type engine = var.rdb_instance_engine is_ha_cluster = var.rdb_is_ha_cluster disable_backup = var.rdb_disable_backup volume_size_in_gb = var.rdb_instance_volume_size_in_gb user_name = "root" password = var.rdb_user_root_password } resource "scaleway_rdb_database" "metabase" { instance_id = scaleway_rdb_instance.rdb.id name = "metabase" } resource "scaleway_rdb_user" "metabase" { instance_id = scaleway_rdb_instance.rdb.id name = "metabase" password = var.rdb_user_metabase_password is_admin = false } 
Enter fullscreen mode Exit fullscreen mode

Complete the file infra/plan/variable.tf:

variable "rdb_is_ha_cluster" { type = bool } variable "rdb_disable_backup" { type = bool } variable "rdb_instance_node_type" { type = string } variable "rdb_instance_engine" { type = string } variable "rdb_instance_volume_size_in_gb" { type = string } variable "rdb_user_root_password" { type = string } variable "rdb_user_metabase_password" { type = string } variable "rdb_instance_volume_type" { type = string } 
Enter fullscreen mode Exit fullscreen mode

RDB ACLs

Each node in the Kubernetes Kapsule has an attached ephemeral IP. For now, we cannot reserve the node IP or use Public Gateway with Kubernetes Kapsule. If we put the actual node IPs manually, the IP can change if an autoscaling occurs. A temporary solution is to create a Kubernetes cronjob that refresh the RDB ACLs each minute with the current node IPs.

Complete the file infra/plan/rdb.tf with the following resources:

resource "scaleway_rdb_acl" "rdb" { instance_id = scaleway_rdb_instance.rdb.id dynamic "acl_rules" { for_each = concat(var.rdb_acl_rules, [{ ip = "${scaleway_instance_ip.bastion.address}/32" description = "Bastion IP" }]) content { ip = acl_rules.value["ip"] description = acl_rules.value["description"] } } } 
Enter fullscreen mode Exit fullscreen mode

Complete the ìnfra/plan/output.tf

output "rdb_endpoint_ip" { value = scaleway_rdb_instance.rdb.endpoint_ip } output "rdb_endpoint_port" { value = scaleway_rdb_instance.rdb.endpoint_port } 
Enter fullscreen mode Exit fullscreen mode

Complete the file infra/plan/variable.tf:

variable "rdb_acl_rules" { type = list(object({ ip = string description = string })) } 
Enter fullscreen mode Exit fullscreen mode

Complete the file infra/plan/config/$ENV/terraform.tfvars:

rdb_instance_node_type = "db-gp-xs" rdb_instance_engine = "PostgreSQL-13" rdb_is_ha_cluster = true rdb_disable_backup = false rdb_instance_volume_type = "bssd" rdb_instance_volume_size_in_gb = "50" rdb_user_root_password = "<RDB_ROOT_USER_PWD>" rdb_user_metabase_password = "<RDB_METABASE_USER_PWD>" rdb_acl_rules = [{ ip = "<KAPSULE_NODEPOOL_IP_1>" description = "Kapsule dev node 1" }, { ip = "<KAPSULE_NODEPOOL_IP_2>" description = "Kapsule dev node 2" }] 
Enter fullscreen mode Exit fullscreen mode

Let's deploy our RDB instance

cd infra/plan KAPSULE_NODEPOOL_IP_1=$(scw k8s node list cluster-id=$(scw k8s cluster list | grep kapsule-${ENV} | awk '{ print $1 }') | awk '{ print $4 }' | sed -n '2 p') KAPSULE_NODEPOOL_IP_2=$(scw k8s node list cluster-id=$(scw k8s cluster list | grep kapsule-${ENV} | awk '{ print $1 }') | awk '{ print $4 }' | sed -n '3 p') sed -i "s/<RDB_ROOT_USER_PWD>/$RDB_ROOT_USER_PWD/g; s/<RDB_METABASE_USER_PWD>/${RDB_METABASE_USER_PWD}/g; s,<KAPSULE_NODEPOOL_IP_1>,${KAPSULE_NODEPOOL_IP_1}/32,g; s,<KAPSULE_NODEPOOL_IP_2>,${KAPSULE_NODEPOOL_IP_2}/32,g; " config/$ENV/terraform.tfvars terraform apply -var-file=config/$ENV/terraform.tfvars 
Enter fullscreen mode Exit fullscreen mode

The secret should be stored in a secret storage like Vault. You can deploy a vault in your Kapsule or in a virtual instance and retrieve the secret in terraform using the vault provider. This method avoid to store the password in the terraform state.

Add the connect privilege to the metabase RDB user:

scw rdb privilege set instance-id=$(scw rdb instance list | grep postgresql-${ENV} | awk '{ print $1 }') database-name=metabase user-name=metabase permission=all 
Enter fullscreen mode Exit fullscreen mode

Let's check if all the resources have been created and are working correctly:

RDB HA instance
RDB instance

RDB backup enabled
RDB instance

RDB databases
RDB Databases

RDB Users
RDB Users

RDB ACLs
RDB ACLs

Let's check the connection

ssh -i <PRIVATE_KEY_PATH> root@$(terraform output --raw bastion_ip) 
Enter fullscreen mode Exit fullscreen mode

Run the psql command:

psql -h <RDB_ENDPOINT_IP> -p <RDB_ENDPOINT_PORT> -U metabase -d metabase psql (13.3 (Ubuntu 13.3-1.pgdg16.04+1)) SSL connection (protocol: TLSv1.2, cipher: <CIPHER>, bits: 256, compression: off) Type "help" for help. metabase=> 
Enter fullscreen mode Exit fullscreen mode

Securing the connectivity between a Kubernetes Kapsule application and a RDB database

Our RDB instance is now available. In this section, we put them all together and deploy Metabase to Kubernetes and connect it to the RDB database. Our objectives are to:

  • Deploy the metabase application.
  • Create a DNS zone and a DNS record for the metabase application.
  • Create the SSL certificates

Overral architecture

Kustomize files

Let's create the Kustomize base files.

Create the metabase deployment infra/k8s/base/deployment.yaml:

apiVersion: apps/v1 kind: Deployment metadata: name: metabase labels: app: metabase spec: selector: matchLabels: app: metabase replicas: 1 template: metadata: labels: app: metabase spec: containers: - name: metabase image: metabase/metabase imagePullPolicy: IfNotPresent 
Enter fullscreen mode Exit fullscreen mode

Create the metabase service infra/k8s/base/service.yaml:

apiVersion: v1 kind: Service metadata: name: metabase labels: app: metabase spec: ports: - port: 80 targetPort: 3000 protocol: TCP selector: app: metabase 
Enter fullscreen mode Exit fullscreen mode

Create a secret to save the RDB user password infra/k8s/base/database-secret.yaml:

apiVersion: v1 kind: Secret metadata: name: metabase type: Opaque data: password: metabase 
Enter fullscreen mode Exit fullscreen mode

Create the ingress file infra/k8s/base/ingress.yaml:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: metabase spec: rules: - host: metabase.<DOMAIN_NAME> http: paths: - pathType: Prefix path: / backend: service: name: metabase port: number: 8000 
Enter fullscreen mode Exit fullscreen mode

Replace by your domain name.

Create the kustomization file infra/k8s/base/kustomization.yaml:

namespace: metabase resources: - deployment.yaml - service.yaml - database-secret.yaml - ingress.yaml 
Enter fullscreen mode Exit fullscreen mode

Now let's create the files for the dev environment:

infra/k8s/envs/dev/database-secret.patch.yaml:

apiVersion: v1 kind: Secret metadata: name: metabase type: Opaque data: password: <MB_DB_PASS> 
Enter fullscreen mode Exit fullscreen mode

infra/k8s/envs/dev/deployment.patch.yaml:

apiVersion: apps/v1 kind: Deployment metadata: name: metabase labels: app: metabase spec: selector: matchLabels: app: metabase replicas: 1 template: metadata: labels: app: metabase spec: containers: - name: metabase image: metabase/metabase imagePullPolicy: IfNotPresent env: - name: MB_DB_TYPE value: postgres - name: MB_DB_HOST value: "<MB_DB_HOST>" - name: MB_DB_PORT value: "<MB_DB_PORT>" - name: MB_DB_DBNAME value: metabase - name: MB_DB_USER value: metabase - name: MB_DB_PASS valueFrom: secretKeyRef: name: metabase key: password 
Enter fullscreen mode Exit fullscreen mode

infra/k8s/envs/dev/ingress.patch.yaml:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: metabase annotations: kubernetes.io/tls-acme: "true" cert-manager.io/cluster-issuer: letsencrypt-prod traefik.ingress.kubernetes.io/router.tls: "true" spec: rules: - host: metabase.dev.<DOMAIN_NAME> http: paths: - pathType: Prefix path: / backend: service: name: metabase port: number: 80 tls: - secretName: metabase-tls hosts: - metabase.dev.<DOMAIN_NAME> 
Enter fullscreen mode Exit fullscreen mode

Replace by your domain name.

infra/k8s/envs/dev/kustomization.yaml:

namespace: metabase resources: - ../../base patchesStrategicMerge: - database-secret.patch.yaml - deployment.patch.yaml - ingress.patch.yaml 
Enter fullscreen mode Exit fullscreen mode

Let's prepare the kubernetes files.

cd infra/k8s/envs/dev RDB_METABASE_USER_PWD=$(echo -n $RDB_METABASE_USER_PWD | base64 -w 0 ) sed -i "s/<MB_DB_PASS>/$RDB_METABASE_USER_PWD/g" database-secret.patch.yaml sed -i "s/<MB_DB_HOST>/$(terraform output --raw rdb_endpoint_ip)/g; s/<MB_DB_PORT>/$(terraform output --raw rdb_endpoint_port)/g;" deployment.patch.yaml 
Enter fullscreen mode Exit fullscreen mode

Create the DNS record

To add an external domain name to Scaleway, please follow the official documentation: How to add an external domain to DNS
.

Create the DNS zone:

scw dns zone create domain=<DOMAIN_NAME> subdomain=dev Domain <DOMAIN_NAME> Subdomain dev Ns.0 ns0.dom.scw.cloud Ns.1 ns1.dom.scw.cloud NsDefault.0 ns0.dom.scw.cloud NsDefault.1 ns1.dom.scw.cloud Status active UpdatedAt now ProjectID <PROJECT_ID> 
Enter fullscreen mode Exit fullscreen mode

Create the DNS record:

scw dns record add dev.<DOMAIN_NAME> name=metabase data=$TRAEFIK_EXTERNAL_IP type=A DATA NAME PRIORITY TTL TYPE COMMENT ID 51.159.74.228 metabase 0 300 A - <DNS_RECORD_ID> 
Enter fullscreen mode Exit fullscreen mode

Create the SSL certificates

To make metabase more secure, we need to deploy Cert-manager to create Let's Encrypt TLS certificates:

More information in the documentation: Deploying Cert Manager.

Use the command below to install cert-manager and its needed CRD (Custom Resource Definitions):

kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.6.0/cert-manager.yaml customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created namespace/cert-manager created serviceaccount/cert-manager-cainjector created serviceaccount/cert-manager created serviceaccount/cert-manager-webhook created clusterrole.rbac.authorization.k8s.io/cert-manager-cainjector created clusterrole.rbac.authorization.k8s.io/cert-manager-controller-issuers created clusterrole.rbac.authorization.k8s.io/cert-manager-controller-clusterissuers created clusterrole.rbac.authorization.k8s.io/cert-manager-controller-certificates created clusterrole.rbac.authorization.k8s.io/cert-manager-controller-orders created clusterrole.rbac.authorization.k8s.io/cert-manager-controller-challenges created clusterrole.rbac.authorization.k8s.io/cert-manager-controller-ingress-shim created clusterrole.rbac.authorization.k8s.io/cert-manager-view created clusterrole.rbac.authorization.k8s.io/cert-manager-edit created clusterrole.rbac.authorization.k8s.io/cert-manager-controller-approve:cert-manager-io created clusterrole.rbac.authorization.k8s.io/cert-manager-controller-certificatesigningrequests created clusterrole.rbac.authorization.k8s.io/cert-manager-webhook:subjectaccessreviews created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-cainjector created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-issuers created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-clusterissuers created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-certificates created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-orders created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-challenges created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-ingress-shim created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-approve:cert-manager-io created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-controller-certificatesigningrequests created clusterrolebinding.rbac.authorization.k8s.io/cert-manager-webhook:subjectaccessreviews created role.rbac.authorization.k8s.io/cert-manager-cainjector:leaderelection created role.rbac.authorization.k8s.io/cert-manager:leaderelection created role.rbac.authorization.k8s.io/cert-manager-webhook:dynamic-serving created rolebinding.rbac.authorization.k8s.io/cert-manager-cainjector:leaderelection created rolebinding.rbac.authorization.k8s.io/cert-manager:leaderelection created rolebinding.rbac.authorization.k8s.io/cert-manager-webhook:dynamic-serving created service/cert-manager created service/cert-manager-webhook created deployment.apps/cert-manager-cainjector created deployment.apps/cert-manager created deployment.apps/cert-manager-webhook created mutatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook created validatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook created 
Enter fullscreen mode Exit fullscreen mode

Create a cluster issuer that allow you to specify:

  • the Let’s Encrypt server, if you want to replace the production environment with the staging one.
  • the mail used by Let’s Encrypt to warn you about certificate expiration.

Create the file infra/k8s/cluster-issuer.yaml:

apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: # You must replace this email address with your own. # Let's Encrypt will use this to contact you about expiring # certificates, and issues related to your account. email: <MAILING_LIST> server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: # Secret resource used to store the account's private key. name: issuer-account-key # Add a single challenge solver, HTTP01 solvers: - http01: ingress: class: traefik 
Enter fullscreen mode Exit fullscreen mode
kubectl create -f infra/k8s/cluster-issuer.yaml kubectl get ClusterIssuer letsencrypt-prod NAME READY AGE letsencrypt-prod True 32s 
Enter fullscreen mode Exit fullscreen mode

Now we can deploy metabase:

cd infra/k8s/envs/dev kubectl create namespace metabase kubectl config set-context --current --namespace=metabase kustomize build . | kubectl apply -f - kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE cm-acme-http-solver-n8x5s traefik metabase.dev.<DOMAIN_NAME> 80 1s metabase traefik metabase.dev.<DOMAIN_NAME> 80, 443 5s kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE metabase traefik metabase.dev.<DOMAIN_NAME> 80, 443 2m16s kubectl get deploy metabase NAME READY UP-TO-DATE AVAILABLE AGE metabase 1/1 1 1 2m kubectl get svc metabase NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE metabase ClusterIP 10.39.118.166 <none> 80/TCP 2m40s kubectl get secret metabase NAME TYPE DATA AGE metabase Opaque 1 7m30s 
Enter fullscreen mode Exit fullscreen mode

Let's check if the connection to the database has been completed:

kubectl logs deploy/metabase [..] 2021-11-04 19:30:15,524 INFO db.setup :: Verifying postgres Database Connection ... 2021-11-04 19:30:16,029 INFO db.setup :: Successfully verified PostgreSQL 13.3 (Debian 13.3-1.pgdg100+1) application database connection. ✅ 2021-11-04 19:30:16,030 INFO db.setup :: Running Database Migrations... [..] 2021-11-04 19:30:56,208 INFO metabase.core :: Metabase Initialization COMPLETE 
Enter fullscreen mode Exit fullscreen mode

After a few minutes of waiting, you will see that the metabase is accessible via HTTPS!

metabase page 1metabase page 2

Ok then!

Conclusion

Congratulations! You have completed this long workshop. In this series we have:

  • Created a highly-available Scaleway RDB instance
  • Configured a Scaleway Kubernetes Kapsule cluster with fine-grained access control to RDB instance
  • Tested the connectivity between a Kubernetes container and a RDB database.
  • Secured the access to the metabase application

That's it!

Clean

To clean the resoures, run the following commands:

terraform destroy -var-file=config/$ENV/terraform.tfvars scw dns record delete dev.<DOMAIN_NAME> name=metabase data=$TRAEFIK_EXTERNAL_IP type=A 
Enter fullscreen mode Exit fullscreen mode

Final Words

I will update the article as soon as private network and/or public gateway are available for Kapsule and RDB.

Scaleway Elements Private networks Web page

If you have any questions or feedback, please feel free to leave a comment.

Otherwise, I hope I have helped you answer some of the hard questions about connecting Kapsule cluster to RDB instance.

By the way, do not hesitate to share with peers 😊

Thanks for reading!

Top comments (3)

Collapse
 
ahmedouyahya profile image
ahmedou-yahya

Great article. I just wonder what tool did you use to draw the above architecture?

Collapse
 
chabane profile image
Chabane R.

Hello Ahmedou

Thank you for your message !

draw.io !

Collapse
 
ahmedouyahya profile image
ahmedou-yahya

So beautiful. Thanks Chabane!