Securing the connectivity between a Scaleway Kubernetes Kapsule application and Scaleway RDB database
Tags: scaleway, kubernetes, terraform, postgres
Published: 2021-11-05 08:22:46

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][feat-req-1] 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g39g8zkgbaqp50z82s86.png)

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][feat-req-2], 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j4ve6zudp5ob98138vjp.png)

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

* Download, install, and configure the [Scaleway CLI][install-1].
* [Terraform][install-2]
* [Kubectl][install-3]
* [Kustomize][install-4]

# Architecture

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

![Overral architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2n6oqnutc9saj3bqj32.png)

# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vsj4q3oh4atpcft880c4.png)

## 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`

```terraform
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
    }
  }
}
```

## 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`

```terraform
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
}
```

Create a script file `cloud-init.sh`

```yml
#!/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
```

Let's configure our terraform.

Create a terraform file `infra/plan/variable.tf`

```terraform
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
}
```

Add a `infra/plan/version.tf` file

```terraform
terraform {
  required_providers {
    scaleway = {
      source  = "scaleway/scaleway"
      version = "2.1.0"
    }
  }
  required_version = ">= 0.13"
}
```

Add a `infra/plan/provider.tf` file

```terraform
provider "scaleway" {
  zone            = var.zone
  region          = var.region
}
```

Add a `infra/plan/backend.tf`

```terraform
terraform {
  backend "s3" {
  }
}
```

And a `ìnfra/plan/output.tf`

```terraform
output "bastion_ip" {
  value = scaleway_instance_ip.bastion.address
}
```

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

```shell
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
```

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

```terraform
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>"
```

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

```terraform
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
```

```shell
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
```

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

Reserved IP

![Reserved IP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qb4yxlwaft6plv3gmoj5.png)

Security Groups

![Security group rules](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kmsl4b3xut6woo8ln2l3.png)

Virtual instance

![Virtual instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/djk0eepyp1j4zricgnl5.png)

Let's check the connection

```shell
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%
```

# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zl55vk8c5amelshosyqe.png)

## 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][feat-req-3].

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

```terraform
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
}
```

Complete the file `infra/plan/variable.tf`:

```terraform
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 
}
```

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

```terraform
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"
```

Let's deploy our cluster

```shell
cd infra/plan

terraform apply -var-file=config/$ENV/terraform.tfvars
```

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

Kubernetes Kapsule Cluster

![Kubernetes Kapsule Cluster](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nll6cjxa1jhtbav19x7x.png)

Kubernetes Kapsule Pool

![Kubernetes Kapsule Pools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m86nno7ai6nx2p845b9z.png)

Kubernetes Kapsule NodePools

![Kubernetes Kapsule NodePools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gojpk0b7ges11ggaa855.png)


Enable the Traefik load balancer using the Scaleway CLI:

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

Let's check if Traefik 2 has been enabled

![Traefik2 enabled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/97b10q6y6wdx0vodnv3v.png)

Get the Kube config file and test the cluster access:

```shell
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
```

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

```yaml
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
```

Deploy the configuration:

```shell
kubectl create -f infra/k8s/traefik-loadbalancer.yml
```

Verify that the LoadBalancer has been deployed correctly:

```shell
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
```

![Kubernetes Kapsule Load balancer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q5xnmg72ghroec7s6d7u.png)


> See [Exposing services in Scaleway Kubernetes][scw-1] for more details.

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

```shell
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\"}}"
```

![Kubernetes Kapsule Load balancer Reserved IP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zc54go8k6t9fucrxhwhk.png)


# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xun61ef43pdr3v1t12qo.png)

# 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`

```terraform
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
}
```

Complete the file `infra/plan/variable.tf`:

```terraform
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
}
```

## 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:

```terraform
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"]
    }
  }
}
```

Complete the `ìnfra/plan/output.tf`

```terraform
output "rdb_endpoint_ip" {
  value = scaleway_rdb_instance.rdb.endpoint_ip
}

output "rdb_endpoint_port" {
  value = scaleway_rdb_instance.rdb.endpoint_port
}
```

Complete the file `infra/plan/variable.tf`:

```terraform
variable "rdb_acl_rules" {
  type = list(object({
      ip          = string
      description = string
  }))
}
```

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

```terraform
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"
  }]
```

Let's deploy our RDB instance

```shell
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
```

> 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:

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

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

RDB HA instance
![RDB instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xyrb5fr1ioa83ukrinbf.png)

RDB backup enabled
![RDB instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/feln43dr9nzkd5p6ifd2.png)

RDB databases
![RDB Databases](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d6d2yol5j3vbj9oqzv99.png)

RDB Users
![RDB Users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e3bxfc4bkq1cw7rtts7g.png)

RDB ACLs
![RDB ACLs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7m7t2x20kwbwc14soi3d.png)

Let's check the connection

```shell
ssh -i <PRIVATE_KEY_PATH> root@$(terraform output --raw bastion_ip)
```

Run the `psql` command:

```shell
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=>
```

# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2n6oqnutc9saj3bqj32.png)

## Kustomize files

Let's create the Kustomize `base` files.

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

```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
```

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

```yaml
apiVersion: v1
kind: Service
metadata:
  name: metabase
  labels:
    app: metabase
spec:
  ports:
    - port: 80
      targetPort: 3000
      protocol: TCP
  selector:
    app: metabase
```

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

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: metabase
type: Opaque
data:
  password: metabase
```

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

```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
```

> Replace <DOMAIN_NAME> by your domain name.

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

```yaml
namespace: metabase

resources:
- deployment.yaml
- service.yaml
- database-secret.yaml
- ingress.yaml
```

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

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

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: metabase
type: Opaque
data:
  password: <MB_DB_PASS>
```

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

```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
```

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

```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>
```

> Replace <DOMAIN_NAME> by your domain name.

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

```yaml
namespace: metabase

resources:
- ../../base

patchesStrategicMerge:
- database-secret.patch.yaml
- deployment.patch.yaml
- ingress.patch.yaml
```

Let's prepare the kubernetes files. 

```shell
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
```

## 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
][scw-2].

Create the DNS zone:

```shell
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>
```

Create the DNS record:

```shell
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>
```

## Create the SSL certificates

To make metabase more secure, we need to deploy Cert-manager to create [Let's Encrypt](https://www.letsencrypt.org/) TLS certificates:

> More information in the documentation: [Deploying Cert Manager][scw-3].

Use the command below to install cert-manager and its needed CRD ([Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)):

```
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
```

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`:

```yml
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
```

```shell
kubectl create -f infra/k8s/cluster-issuer.yaml

kubectl get ClusterIssuer letsencrypt-prod

NAME               READY   AGE
letsencrypt-prod   True    32s
```

Now we can deploy metabase:

```shell
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
```

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

```shell
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
```

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

![metabase page 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8f18xhvj206eol7gwi2x.png)![metabase page 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vo8eyaxypmi2t9gutsus.png)

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:

```shell
terraform destroy -var-file=config/$ENV/terraform.tfvars

scw dns record delete dev.<DOMAIN_NAME> name=metabase data=$TRAEFIK_EXTERNAL_IP type=A
```

# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hg7sv0px4pphsviia4el.png)

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!

[feat-req-1]: https://feature-request.scaleway.com/posts/72/private-networks-for-managed-database
[feat-req-2]: https://feature-request.scaleway.com/posts/79/nat-gateway
[feat-req-3]: https://feature-request.scaleway.com/posts/8/private-networks-for-kapsule
[install-1]: https://github.com/scaleway/scaleway-cli
[install-2]: https://www.terraform.io/downloads.html
[install-3]: https://kubernetes.io/docs/tasks/tools/
[install-4]: https://kubectl.docs.kubernetes.io/installation/kustomize/
[scw-1]: https://www.scaleway.com/en/docs/compute/kubernetes/api-cli/exposing-services/#exposing-traefik2
[scw-2]: https://www.scaleway.com/en/docs/network/dns-cloud/how-to/add-external-domain/
[scw-3]: https://www.scaleway.com/en/docs/compute/kubernetes/api-cli/exposing-services/#deploying-cert-manager