<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
    <title>Aperogeek.fr</title>
    <link>https://aperogeek.fr</link>
    <description>Des lignes de code et des pintes bien tirées. Bière et Linux.</description>
    <language>fr</language>
    
        <item>
            <title>Mise</title>
            <link>https://aperogeek.fr/mise.html</link>
            <guid>https://aperogeek.fr/mise.html</guid>
            <pubDate>Mon, 07 Sep 2026 00:00:00 +0000</pubDate>
            <description><![CDATA[# Mise

![Immich logo](../assets/img/mise-logo.png)

L'autre jour à l'apero, un pote m'a parlé d'un outil trop cool pour configurer son environnement de dev : 

[Mise en place](https://mise.jdx.dev)

Le concept est super simple : on créé un petit fichier `mise.toml` dans sont repertoire de travail, et mise va s'occuper d'installer, de configurer tous les outils necessaires pour vous !

C'est un peut comme du `ansible` avec un inventaire `localhost` sauf que c'est beaucoup plus rapide (normal c'est écrit Rust, pas en python) et que ça peut gérer plusieurs fichier mises.toml à la fois

## Hiérarchie

Le truc cool avec mise, c'est que l'on peut avoir plusieurs fichiers `mise.toml` qui vont être plus ou moins prioritaires par rapport à l'endoit où ils sont placés.

Par exemple on peut avoir :

- `/etc/mise/config.toml` : une config générale pour toute la machine
- `/home/user/mise.toml` : une config pour un user en particulier
- `/home/user/workspace/work1/mise.toml` : une conf spécifique pour le projet work1
- `/home/user/workspace/work2/mise.toml` : une conf spécifique pour le projet work2
- `/home/user/workspace/work2/backend/mise.toml` : une conf spécifique pour le repertoire backend du projet work2

Mise va "truster" les fichiers mise (`mise trust /path/to/mise.toml`) et ensuite les appliquer par ordre de prio.

## Dev Tools

La feature principale de mise, c'est de gérer les outils de développement pour chaque projet !

Fini les outils comme `sdkman` ou `nvm` pour gérer les version du JDK ou de nodejs sur sa machine.
Un fichier mise dans le bon répertorie et hop ! ça installe et utilise la bonne version pour le projet.

Concrètement, ça se présente comme ça :

```toml
[tools]
java = { version = "temurin-26.0.2+101" }
node = { version = "24.20.0" } #LTS
kubectl = "latest"
python = "latest"
uv = "latest"
```

Ensuite on a juste à lancer la commande `mise install` une première fois pour installer les outils.

Les fois d'après, le simple de fait d'aller dans le repertoire va charger les bon paths pour utiliser la version demandée.

Par exemple si dans mon `$HOME/mise.toml` j'ai `node = "latest"` mais que dans mon projet `$HOME/workspace/work1/mise.toml` j'ai `node = { version = "24.10.0" }`,
mise va utiliser la version 24.10 pour le projet work1 et latest pour les autres répertoires.

Il existe une multitude d'outils disponibles sur [mise-versions.jdx.dev](https://mise-versions.jdx.dev/)

On peut même installer des outils à partir de packages npm, de release github, gitlab, forgejo ou bien d'une url http.
Il y a plein de backends disponibles pour installer ce qu'on veut.

Bref, le catalogue est bien fourni !

## Bootstrap

Mise ce n'est pas que pour installer des outils dans les bonnes version ! C'est aussi un systeme de bootstrap qui va pouvoir installer et configurer des services sur la machine.

Par exemple, avec mise bootstrap il est possible de 

- créer des users ou des groupes
- créer des fichiers ou des répertoires
- lancer des docker compose
- installer des packages via le package manager de l'OS (brew, apt)
- démarrer des services

Pour cela il faut ajouter une section `[bootstrap]` dans son fichier `mise.toml` :

```toml
[bootstrap.packages]
"apt:libssl-dev" = "latest"
"apt:curl" = "latest"

[bootstrap.services]
docker = {state = "running", enabled = true}

[bootstrap.files."~/.gradle/gradle.properties"]
source = ".mise/files/gradle.properties"
template = true
```

## Tasks

Et c'est pas tout ! (j'ai l'impression que vous nous dites pas tout)

Mise est aussi capable de lancer des tâches !

Par exemple je peux ajouter dans mon `mise.toml` :

```toml
[tasks.my-script]
description = "Run my-script.sh"
run = "{{ confif_root }}/scripts/my-script.sh"
```

et ensuite lancer ma tache quand je veux avec : `mise run my-script`

## Env

Un autre truc cool avec mise, c'est la gestion des variables d'environnement et des secrets.
On peut :

- configurer des variables d'env statiques
- récupérer des variables d'en à partir d'un script
- lire des variables depuis un fichier secret sops
- configurer ces secrets dans des secrets mise pour les réutiliser ensuite dans un template

Par exemple :

```toml
[bootstrap.secrets]
secret_api_key = "SECRET_API_KEY"

[env]
_.file = { path = ".mise/.env.enc.yaml", redact = true } # fichier sops avec la clef SECRET_API_KEY
_.source = ".mise/env.sh"
_.path = ["{{config_root}}/bin"] 
FOO="foo"

[bootstrap.files."~/.local/share/work1/auth.json"]
content = "{\"secret_api_key\": \"{{ secret(name=\"secret_api_key\") }}\"}"
template = true
mode = "0644"
```

Perso j'utilise sops + age pour le chiffrement de mes secrets.
Comme ça je peux les garder dans mon repo git, et seul la clef privée age permet de déchiffrer les secrets.

## Divers

Mise peut aussi etre utilisé pour builder des images docker, pour installer des outils en fonction de l'OS (mac ou linux) comme ça on peut garder le même fichier mise pour toute l'équipe.
On peut l'utiliser dans la CI/CD pour lancer les builds et les test.

Y'a même une [chanson mise](https://mise.jdx.dev/mise-en-place.html) !!]]></description>
        </item>
        <item>
            <title>Immich</title>
            <link>https://aperogeek.fr/immich.html</link>
            <guid>https://aperogeek.fr/immich.html</guid>
            <pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate>
            <description><![CDATA[# Immich

![Immich logo](../assets/img/immich-logo.png)

Depuis quelque temps, j'ai remplacé Google photo par Immich.
C'est un gestionnaire de photo auto-hébergé qui permet de backuper et visualiser ses photos / videos super facilement.

## Backup des photos

Comme les photos c'est super important, j'ai choisi de les backuper automatiquement sur un bucket S3.
J'ai donc pris un bucket S3 chez Scaleway, et j'utilise l'application S3Drive pour backuper mes photos, mes vidéos et mes media whatsapp directement dans le bon dossier sur mon s3.

![S3Drive](../assets/img/S3Drive.png)

## Montage S3FS

Ensuite j'ai monté mon bucket s3 directement sur mon serveur linux via fstab :

```bash
s3fs#backup-aperogeek /data/s3 fuse _netdev,allow_other,use_path_request_style,url=https://s3.fr-par.scw.cloud,passwd_file=/home/seuf/.passwd-s3fs,default_acl=public-read,uid=1000,gid=1000 0 0
```

J'ai créé un petit fichier `.passwd-s3fs` dans mon HOME qui contient les credentials pour accéder au bucket s3

```bash
${S3_ACCESS_KEY}:${S3_SECRET_KEY}
```

## Immich

Après j'ai déployé le helm chart officiel immich dans mon cluster de raspberry-pi k3s.

La subtilité c'est de bien :

1. forcer les affinity / nodeSelector pour bien tourner sur le pi5 qui a le montage s3fs

```yaml
  defaultPodOptions:
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
          - matchExpressions:
            - key: raspi
              operator: In
              values:
              - pi5
```

2. monter les volumes au bon endroit sur le pod

```yaml
  server:
    persistence:
      external:
        enabled: true
        type: hostPath
        hostPath: /data/s3/Images
        globalMounts:
        - path: /s3/Images
          readOnly: true
```

Ensuite on ajoute une bibilothèque externe dans immich

![immich s3 library](../assets/img/immich-s3-library.png)

Et on lance un scan de la librairie via le menu jobs.

Le premier scan était hyper long car immich :
- créé des miniatures pour chaque photos
- détecte les visages
- fait de la reconnaissance faciale
- analyse les images avec de l'IA

et comme ça tourne sur un raspberry-pi 5, les perfs ne sont pas ouf.

Mais en tout cas ça marche et maintenant mes photos sont analysées au fil de l'eau et j'y ai accès n'importe ou grace à l'application mobile.

Bonus :

Une photo de mon cluster de raspberry-pi 4,5 + RDK-X5 :

![cluster Raspberry-pi 5](../assets/img/cluster-pi.jpg)]]></description>
        </item>
        <item>
            <title>K3Sup</title>
            <link>https://aperogeek.fr/k3sup.html</link>
            <guid>https://aperogeek.fr/k3sup.html</guid>
            <pubDate>Thu, 25 Jan 2024 00:00:00 +0000</pubDate>
            <description><![CDATA[# k3sup : Kubernetes cluster with k3s in a minute

![K3Sup Logo](../assets/img/k3sup.png)


Hey ! You want to deploy a kubernetes cluster easily ? You don’t want to use an expensive cloud managed solution ? You don’t want to read kubernetes the hard way to have a production ready cluster ?

Let’s deploy a k3s cluster with k3sup.


## K3SUP Installation


First download the latest release of k3sup from the release page on github. Then move it to your binary folder

```bash
curl -sLS https://get.k3sup.dev | sh
sudo install k3sup /usr/local/bin/
```

## K3S Cluster Deployment

Now you have k3Sup installed, you can install k3s nodes easily.

k3sup use ssh to install and configure k3s on you remote hosts, so you need to configure your ssh keys before. Then you can use k3sup !

- *install a « server » node :*

```bash
k3sup install --host <server hostname or IP> --k3s-extra-args '---disable traefik --node-label foo=bar'
```

- Join a « agent » node :

```bash
k3sup join --server-host <ip of the server> --host <ip of the agent> --k3s-extra-args '--node-label foo=baz'
```

Now you have a kubeconfig ready to use to deploy everything !

For information, this blog is deployed with k3sup on 2 raspberrypi nodes (pi4 and pi5) 😎]]></description>
        </item>
        <item>
            <title>Llama Index</title>
            <link>https://aperogeek.fr/llama-index.html</link>
            <guid>https://aperogeek.fr/llama-index.html</guid>
            <pubDate>Wed, 24 Jan 2024 00:00:00 +0000</pubDate>
            <description><![CDATA[# Llama-index : Build your RAG with Mistral AI

![K3Sup Logo](../assets/img/llama-index-mistral-logo.png)

Since last year and the release of ChatGPT, everyone talk about LLM (Large Langage Model) and how we can leverage it to help us in our daily work.

One of the coolest thing we can do with LLMs is building a RAG (Retriever Augmented Generation) to use your internal documentation / code as a context for llm to answer questions.

Also Mistral AI, a french startup launched an open source model with very good performances compared to proprietary models like ChatGPT or Google text-bison. They have launched la plateforme an api endpoint compatible with openai API so you can use it with openai tools. It is a pay as you go mode (no monthly pricing) and it depends on the model you choose.

## RAG

The RAG consist of 2 parts. The embedding part, and the prompt part.

The embedding part do the following tasks :

- Split your documents into chunks
- for each chunk compute the corresponding embedding (a big vector of numbers)
- store each embedding in a Vector Database

The prompt part do

- Ask for a question to the user
- calculate the embedding of the question
- fetch the nearest neighbors of your question embedding from the vector database
- Give the found chunks as a context for the question

## Llama-index

Llama-index is a python framework to help connecting your custom data to large langage models.

It provides tools to help you to ingest data, index data and query your data.

### Llama-index embedding

Here is an example of python script to index all your documents contained in a directory using

- Mistral AI embedding API
- Qdrant as a vector DB
- SimpleDirectoryReader as llamaindex document parser

The power of llama index is that you can found open source Reader directly on llamahub.ai. For example you can have loader to fetch documents from a sql database or a wordpress blog.

*Requirements :*

Before using this script, you need to install the following python dependencies

- llama-index
- qdrant-client
- mistralai

And also run a qdrant vector db instance locally with docker

```bash
docker run -d --name qdrant -p 6333:6333 qdrant/qdrant
```

llama-index-embedder.py
```py 
import logging
import sys
import os
import qdrant_client
from llama_index import VectorStoreIndex, ServiceContext, SimpleDirectoryReader, set_global_service_context
from llama_index.embeddings import MistralAIEmbedding
from llama_index.storage.storage_context import StorageContext
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.llms import MistralAI

MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
DIRECTORY_PATH = "/path/to/index"
QDRANT_URL = "http://localhost:6333"
QDRANT_COLLECTION = "llama-index-mistral"

logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

logging.info("Initilizing llm")
llm = MistralAI(api_key=MISTRAL_API_KEY, model="mistral-small")
embed_model = MistralAIEmbedding(model_name="mistral-embed", api_key=MISTRAL_API_KEY)
service_context = ServiceContext.from_defaults(llm=llm, embed_model=embed_model)
set_global_service_context(service_context)

logging.info("Initializing vector store...")
client = qdrant_client.QdrantClient(
    url=QDRANT_URL,
)
vector_store = QdrantVectorStore(client=client, collection_name=QDRANT_COLLECTION)
storage_context = StorageContext.from_defaults(vector_store=vector_store)

logging.info("Loading documents...")
documents = SimpleDirectoryReader(
    input_dir=DIRECTORY_PATH,
    recursive=True,
).load_data(show_progress=True)
logging.info(f"documents : {len(documents)}")

logging.info("Indexing...")
index = VectorStoreIndex.from_documents(
    documents, storage_context=storage_context, service_context=service_context, show_progress=True
)
```

Now you can execute the python script and browse you qdrant database. Go to http://localhost:6333/dashboard and you will see your new collection .

### llama-index prompt

Now we have indexed all our documents in qdrant, we can use this prompt script example to query it

```py
import os
import sys
import logging
import qdrant_client
from llama_index import (
    VectorStoreIndex,
    ServiceContext,
    get_response_synthesizer,
)
from llama_index.embeddings import MistralAIEmbedding
from llama_index.retrievers import VectorIndexRetriever
from llama_index.query_engine import RetrieverQueryEngine
from llama_index.postprocessor import SimilarityPostprocessor
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.llms import MistralAI

QDRANT_URL = "http://localhost:6333"
QDRANT_COLLECTION = "llama-index-mistral"
MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")

# llm
llm = MistralAI(api_key=MISTRAL_API_KEY, model="mistral-small")
embed_model = MistralAIEmbedding(model_name="mistral-embed", api_key=MISTRAL_API_KEY)
service_context = ServiceContext.from_defaults(llm=llm, embed_model=embed_model)

# vector db
client = qdrant_client.QdrantClient(url=QDRANT_URL)
vector_store = QdrantVectorStore(client=client, collection_name=QDRANT_COLLECTION)
index = VectorStoreIndex.from_vector_store(vector_store=vector_store, service_context=service_context)

# configure retriever
retriever = VectorIndexRetriever(index=index, similarity_top_k=10)

# configure response synthesizer
response_synthesizer = get_response_synthesizer(service_context=service_context, streaming=True, verbose=True)

# assemble query engine
query_engine = RetrieverQueryEngine(
    retriever=retriever,
    response_synthesizer=response_synthesizer,
    node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.7)],
)

# query
print("Welcome to malt-deltalake AI assistant.")
print("type 'exit' to quit this.")
while True:
    print("")
    print("")
    print("Enter your question :")
    query = input()
    if query == "exit":
        break
    query_engine.query(query).print_response_stream()
```


And finally you can run the prompter script to ask questions about your document.

### Demo:

For example I have put this article in the paths to index and if I ask a question about this article I have pretty nice responses:

**Question : What is a RAG ?**

> A RAG, in the context provided, refers to a Retriever Augmented Generation model. It is a system that combines a retriever, responsible for searching through a database, and a generator, which produces answers to questions based on the context provided by the retriever. This concept is often used in conjunction with Large Language Models (LLMs) to allow them to utilize internal documentation or code as context for answering questions.

**Question : What are the python dependencies required to use llama index with Mistral ?**

> The Python dependencies required to use Llama Index with Mistral are ‘llama-index’, ‘qdrant-client’, and ‘mistral’.

**How can i run qdrant locally ?**

> To run Qdrant locally, you can use Docker. The command to run Qdrant locally using Docker is provided in the context information:
> ```
> docker run -d --name qdrant -p 6333:6333 qdrant/qdrant
> ```
> This command will download the Qdrant Docker image, create a new container named « qdrant », and start the Qdrant vector database instance on port 6333.

And voila 🎉]]></description>
        </item>
        <item>
            <title>Strimzi Kafka K8S Operator</title>
            <link>https://aperogeek.fr/strimzi-kafka-k8s-operator.html</link>
            <guid>https://aperogeek.fr/strimzi-kafka-k8s-operator.html</guid>
            <pubDate>Tue, 16 May 2023 00:00:00 +0000</pubDate>
            <description><![CDATA[# Strimzi Kafka Kubernetes Operator

![Strimzi Logo](../assets/img/strimzi_logo.png)

Like I said in my last post about zalando postgresql operator, Kubernetes is a very powerful tool that can be extended with operators to handle new custom resources.

Custom resources can be anything ! A database cluster, a certificate, a prometheus rule or even a Kafka Cluster !

Strimzi operator goal is to deploy and manage kafka clusters inside Kubernetes, but it can also manage a lot of kafka related resources:

- kafka topics
- kafka users
- kafka connectors
- etc..

## Installation

First we need to install and configure the strimzi operator in your kubernetes cluster.

One way to install strimzi is to use the helm chart.

```bash
helm install strimzi
kubectl create namespace kafka
helm repo add strimzi https://strimzi.io/charts/
helm -n kafka upgrade --install strimzi strimzi/strimzi-kafka-operator
```

This will deploy the strimzi operator and all the kafka custom resources definitions.
Kafka Cluster

Once it is deployed inside our cluster, we can tell strimzi to create a kubernetes cluster for us.

Here is an example of kafka cluster with 3 nodes:

kafka.yml
```yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
  name: kafka-cluster
  namespace: kafka
spec:
  kafka:
    replicas: 3
    version: 3.3.1
    listeners:
    - name: plain
      port: 9092
      type: internal
      tls: false
    - name: tls
      port: 9093
      type: internal
      tls: true
      authentication:
        type: scram-sha-512
    storage:
      type: jbod
      volumes:
      - id: 0
        type: persistent-claim
        size: 10Gi
        deleteClaim: false
    jmxOptions: {}
    config:
      log.message.format.version: "3.3"
      inter.broker.protocol.version: "3.3"
      offsets.topic.replication.factor: 2
      transaction.state.log.replication.factor: 2
      transaction.state.log.min.isr: 2
      log.roll.hours: 24
      max.request.size: 536870912
      message.max.bytes: 500000000
      num.partitions: 6
      default.replication.factor: 1
      min.insync.replicas: 1
    metricsConfig:
      type: jmxPrometheusExporter
      valueFrom:
        configMapKeyRef:
          name: kafka-metrics
          key: kafka-metrics-config.yml
  zookeeper:
    replicas: 3
    storage:
      type: persistent-claim
      size: 10Gi
      deleteClaim: false
    resources:
      requests:
        cpu: 100m
        memory: 512Mi
  entityOperator:
    topicOperator: {}
    userOperator: {}
  kafkaExporter: {}
```

As you can see, I’ve configured a zookeeper and a kafka cluster with 3 replicas.

kafka will have 2 listeners. The first one will listen on port 9092 with no TLS or authentication and the second will listen on port 9093 with tls enabled and scram-sha256 authentification.

I’ve also configured persistent storage for each pod and some custom kafka default configuration (partitions, replication, etc..).

And finally I’ve enabled the kafka exporter to expose kafka metrics to prometheus.

It is possible to configure everything ! for example you can configure :

- pod affinities (to ensure kafka pods are on kafka node pool and different nodes)
- resources requests and limits
- jvm options
- custom tls certificates
- etc..

## kafka Users

Strimzi kubernetes operator is also capable of managing kafka users and permissions. To do this, you can delclare a KafkaUser resource in your kubernetes cluster and strimzi will automatically configure it in the corresponding cluster.

For example :

KafkaUser.yml
```yaml
apiVersion: kafka.strimzi.io/v1beta1
kind: KafkaUser
metadata:
  name: seuf
  labels:
    strimzi.io/cluster: kafka-cluster
spec:
  authentication:
    type: scram-sha-512
```

Once the user is created you will can found his password in the associated kubernetes secret generated by the operator.

## Kafka Connect

It is also possible to configure KafkaConnect and KafkaConnector directly with strimzi to replicate your postgresql database into kafka with debezium for example.

Strimzi is able to automatically build a KafkaConnect docker image for you with the plugin you want, but I prefer to build it myself with my CI/CD (gitlab-ci) and push it to my private registry.

Here is an example of Dockerfile to build a kafka connect docker image that can be used with strimzi:

Dockerfile kafka-connect debezium postgresql

```
FROM quay.io/strimzi/kafka:0.34.0-kafka-3.4.0

USER root:root

ENV DEBEZIUM_POSTGRESQL_VERSION=2.0.0.Final
ENV KAFKA_KUBERNETES_CONFIG_PROVIDER_VERSION=1.0.1

RUN mkdir -p /opt/kafka/plugins && \
    cd /tmp && \
    # debzium postgresql plugin
    curl -sL https://repo1.maven.org/maven2/io/debezium/debezium-connector-postgres/${DEBEZIUM_POSTGRESQL_VERSION}/debezium-connector-postgres-${DEBEZIUM_POSTGRESQL_VERSION}-plugin.tar.gz -o debezium-connector-postgres-${DEBEZIUM_POSTGRESQL_VERSION}-plugin.tar.gz && \
    tar -xzf debezium-connector-postgres-${DEBEZIUM_POSTGRESQL_VERSION}-plugin.tar.gz && \
    mv debezium-connector-postgres /opt/kafka/plugins && \
    rm -rf /tmp/debezium-connector-postgres* && \
    # kubernetes config plugin
    curl -sL https://github.com/strimzi/kafka-kubernetes-config-provider/releases/download/${KAFKA_KUBERNETES_CONFIG_PROVIDER_VERSION}/kafka-kubernetes-config-provider-${KAFKA_KUBERNETES_CONFIG_PROVIDER_VERSION}.tar.gz -o kafka-kubernetes-config-provider-${KAFKA_KUBERNETES_CONFIG_PROVIDER_VERSION}.tar.gz && \
    tar -xzf kafka-kubernetes-config-provider-${KAFKA_KUBERNETES_CONFIG_PROVIDER_VERSION}.tar.gz && \
    cp kafka-kubernetes-config-provider-${KAFKA_KUBERNETES_CONFIG_PROVIDER_VERSION}/libs/* /opt/kafka/libs/ && \
    rm -rf /tmp/kafka-kubernetes-config-provider*

USER 1001
```

As you can see, I have installed the debezium postgresql connector into the /opt/kafka/plugins folder and the kubernetes-config-provider lib in the /opt/kafka/libs folder.

The strimzi kubernetes config provider will allow me to use kubernetes secrets as source of credentials for my kafka connector configuration.

Once I have builded and pushed my docker image to my private registry, i can use it in a KafkaConnect.yml resource :

KafkaConnect.yml
```yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaConnect
metadata:
  name: debezium-connect-cluster
  namespace: kafka
  annotations:
    strimzi.io/use-connector-resources: "true"
spec:
  version: 2.6.0
  replicas: 1
  bootstrapServers: kafka-data.service.malt:9094
  image: my.private-docker-registry.io/strimzi-debezium-postgresql:0.34.0-kafka-3.4.0-postgres-2.0.0.Final
  logging:
    type: inline
    loggers:
      connect.root.logger.level: "INFO"
  config:
    config.providers: secrets
    config.providers.secrets.class: io.strimzi.kafka.KubernetesSecretConfigProvider
    group.id: strimzi-kafkaconnect
    offset.storage.topic: strimzi-kafkaconnect-offsets
    config.storage.topic: strimzi-kafkaconnect-configs
    status.storage.topic: strimzi-kafkaconnect-status
    config.storage.replication.factor: -1
    offset.storage.replication.factor: -1
    status.storage.replication.factor: -1
    max.request.size: 1048570
    producer.max.request.size: 1048570
  metricsConfig:
    type: jmxPrometheusExporter
    valueFrom:
      configMapKeyRef:
        name: kafka-metrics
        key: kafka-connect-metrics-config.yml
  template:
    pod:
      metadata:
        labels:
          app: kafka-connect-postgres
```

Here I can configure the kafka bootstrap url and the required kafka topics used for kafka connect configuration storage and offset tracking.

I also have added the metricsConfig to allow prometheus scrapping and monitor my connector with grafana dashboards.

The final step is to create a KafkaConnector resource. This will configure the KafkaConnect instance with the required informations to do the change data capture of your source database.

```yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaConnector
metadata:
  name: debezium-connector-postgresql
  namespace: kafka
  labels:
    strimzi.io/cluster: debezium-connect-cluster
spec:
  class: io.debezium.connector.postgresql.PostgresConnector
  tasksMax: 1
  config:
    plugin.name: pgoutput
    publication.name: strimzi
    slot.name: debezium_strimzi
    database.hostname: aperogeek.database.svc
    database.port: 5432
    database.user: kafkaconnect
    database.password: ${secrets:kafka/debezium-secret:postgresql_password}
    database.dbname: aperogeek
    database.server.name: aperogeek
    database.include.list: aperogeek
    snapshot.mode: never
    heartbeat.interval.ms: 1000

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: connector-configuration-role
  namespace: kafka
rules:
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["debezium-secret"]
  verbs: ["get"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: connector-configuration-role-binding
  namespace: kafka
subjects:
- kind: ServiceAccount
  name: debezium-connect-cluster-connect
  namespace: kafka
roleRef:
  kind: Role
  name: connector-configuration-role
  apiGroup: rbac.authorization.k8s.io
```

Here I have configured a connector in my kafka connect cluster debezium-connect-cluster .

It will connect to my postgresql database configured in the database namespace and output all the changes into the aperogeek.* kafka topics.

Note that I also have added some RBAC permissions to allow the auto generated service account to access my kubernetes secret containing the database password thanks to the kubernetes config provider previously installed installed in the docker image.

## Monitoring

Now we have kafka, kafka connect running in our cluster it’s time to monitor it with prometheus + grafana.

To do that you can declare PodMonitor (another custom resource managed by the prometheus operator). See the official documentation to help you with monitoring setup.

Strimzi provides some example grafana dashboards like this :
grafana dashboard kafka
grafana dashboard kafka exporter
Conclusion

To conclude, I will say that I really like this kubernetes operator. You can manage all the kafka resources with kubectl (useful to patch 200 topics in one bash command 😛 ). The project is really active and well maintained with all new kafka releases.

Strimzi have also introduced the concept of strimziPodSet instead of classic kubernetes StatefulSet. This allow strimzi to have better rolling restart strategy with better auto scaling method (no need for ordered pod restart).

The documentation is very (too) complete and sometimes it’s hard to find the thing you want.

Also it lack of example for kafka resource declaration (kafka connect for example).
Bonus

Here is a small bash script you can cron every 10 minutes with a kube cronjob to detect failed kafka connect tasks and restart it without restarting the whole kafka connect cluster. It happen from times to times, for example if you have a postgresql failover the kafka connect will not automatically reconnect to the new primary.

kafka connect check
```bash
#!/bin/sh

echo "Fetching tasks"
echo "curl -s \"http://${CONNECT_HOST:-localhost}:${CONNECT_PORT:-8083}/connectors?expand=status\""
tasks=$(curl -s "http://${CONNECT_HOST:-localhost}:${CONNECT_PORT:-8083}/connectors?expand=status")

echo "tasks : "
echo $tasks

tasks_tor_restart=$(echo $tasks | jq -rc -M 'map({name: .status.name } +  {tasks: .status.tasks}) | .[] | {task: ((.tasks[]) + {name: .name})}  | select(.task.state=="FAILED") | {name: .task.name, task_id: .task.id|tostring} | ("/connectors/"+ .name + "/tasks/" + .task_id + "/restart")')

for t in $tasks_tor_restart; do
  echo "Restarting task $t"
  echo "curl -X POST \"http://${CONNECT_HOST:-localhost}:${CONNECT_PORT:-8083}${t}\""
  curl -X POST "http://${CONNECT_HOST:-localhost}:${CONNECT_PORT:-8083}${t}"
done
```

Of course you need to install curl and jq in your docker image to make it work.

]]></description>
        </item>
        <item>
            <title>Zalando Postgreslq K8S Operator</title>
            <link>https://aperogeek.fr/zalando-postgreslq-k8s-operator.html</link>
            <guid>https://aperogeek.fr/zalando-postgreslq-k8s-operator.html</guid>
            <pubDate>Tue, 15 Nov 2022 00:00:00 +0000</pubDate>
            <description><![CDATA[# Zalando Postgresql operator

![postgres logo + kubernetess logo](../assets/img/pg-k8s.png)

Kubernetes is a very powerful tool to manage containerized applications. You can manage containers with « pod » resource, lifecycle with **deployment** resource, network with **service** and **ingress** resources, etc..

And if you want, you can extends the list of resources that kubernetes can handle with custom resources definitions. Then those CRDs will be managed by an operator which run inside your cluster.

For example, you can manage postgresql database resources with the zalando-postgresql-operator !

This is a very cool operator. It is powered by Patroni (also from zalando), a tool to high available Postgresql instances with auto failover and replication between instances.

## Installation

You can use kustomize to deploy this operator. Create a file postgres-operator/kustomize.yml with the content :

zalando-postgres-operator-kustomize.yml
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - https://raw.githubusercontent.com/zalando/postgres-operator/v1.8.2/manifests/configmap.yaml
  - https://raw.githubusercontent.com/zalando/postgres-operator/v1.8.2/manifests/operator-service-account-rbac.yaml
  - https://raw.githubusercontent.com/zalando/postgres-operator/v1.8.2/manifests/postgres-operator.yaml
  - https://raw.githubusercontent.com/zalando/postgres-operator/v1.8.2/manifests/api-service.yaml
```

Then apply it to your cluster wth kubectl

```bash
kubectl apply -k postgres-operator
```

## Configuration

Now we have a Postgresql operator deployed in our Kubernetes cluster, we can manage Postgresql instances.

The zalando team provide a full example of Postgresql manifests. Here is a simple example to have a postgresql primary + replica instance with a telegraf sidecar for the monitoring :

postgres-instance.yml
```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: aperogeek
  namespace: database
spec:
  teamId: "aperogeek"
  volume:
    size: 100Gi
  numberOfInstances: 2
  users:
    beer:  # database owner
    - superuser
    - createdb
    telegraf: []
    kafkaconnect:
    - superuser
    - replication
  databases:
    aperogeek: beer  # dbname: owner
  postgresql:
    version: "14"
    parameters:
      wal_level: logical
  additionalVolumes:
   - name: telegraf-config
     mountPath: /etc/telegraf
     targetContainers:
       - telegraf
     volumeSource:
       configMap:
         name: telegraf-config
  spiloFSGroup: 103
  sidecars:
    - name: "telegraf"
      image: "telegraf:latest"
      resources:
        limits:
          cpu: 500m
          memory: 500Mi
        requests:
          cpu: 10m
          memory: 64Mi
      ports:
        - containerPort: 9273
          protocol: TCP
      env:
      - name: DATABASE_NAME
        value: aperogeek
      - name: "TELEGRAF_PASSWORD"
        valueFrom:
          secretKeyRef:
            key: password
            name: telegraf.aperogeek.credentials.postgresql.acid.zalan.do
```

With this manifest, I tell the postgres operator to create a 2 instance cluster (1 primary and 1 standby). The operator will manage the replication between the 2 instances and the failover if the primary pod restart.

The operator will also manager postgres user and database for me. Each user will have a kubernetes secret with the name {username}.{clustername}.credentials.postgresql.acid.zalan.do.
So you can directly tell your application to use this secret to connect to postgresql without exporting it to external secret manager. (You can also create secrets in different namespaces).

Zalando operator can also prepare database for you with existing schema and pre-installed extensions.

One other cool stuff of the zalando postgres operator is the backup and restore feature.
You can tell Spilo (the postgresql + Patroni docker bundle) to continuously stream you wall file to a remote S3 compatible storage and periodically take a physical base backup of your database.
This way you can have a high available postgresql instance with Point In Time recovery 🎉
Monitoring

I’ve also added a sidecar container with telegraf image which is used to expose postgresql metrics on /metrics with the prometheus format.

I’ve also configured an additional volume to mount the telegraf config in the telegraf sidecar container. (thanks to my previous MR 😜)

Here is my telegraf config file :

```toml
[global_tags]

[agent]
  interval = "10s"
  round_interval = true
  metric_batch_size = 1000
  metric_buffer_limit = 10000
  collection_jitter = "0s"
  flush_interval = "10s"
  flush_jitter = "0s"
  logtarget = "stderr"
  omit_hostname = false

[[outputs.prometheus_client]]
  listen = ":9273"
  namedrop = ["tail"]

[[outputs.file]]
  namepass = ["tail"]
  files = ["stdout"]
  data_format = "json"

[[inputs.disk]]
  ignore_fs = ["tmpfs", "devtmpfs", "devfs", "iso9660", "overlay", "aufs", "squashfs"]
[[inputs.diskio]]

[[inputs.postgresql_extensible]]
  address = "host=127.0.0.1 user=telegraf dbname=$DATABASE_NAME sslmode=require password=$TELEGRAF_PASSWORD"
  databases = ["$DATABASE_NAME"]
  [[inputs.postgresql_extensible.query]]
    sqlquery="SELECT * FROM pg_stat_database where datname"
    version=110
    withdbname=true
    tagvalue=""
  [[inputs.postgresql_extensible.query]]
    sqlquery="SELECT * FROM pg_stat_bgwriter"
    version=1100
    withdbname=false
    tagvalue=""
  [[inputs.postgresql_extensible.query]]
    sqlquery="SELECT usename, count(*) as nb_connections FROM pg_stat_activity GROUP BY usename"
    version=1100
    withdbname=false
    tagvalue="usename"
  [[inputs.postgresql_extensible.query]]
    sqlquery="select setting as max_connections from pg_settings where name='max_connections'"
    version=1100
    withdbname=false
    tagvalue=""
  [[inputs.postgresql_extensible.query]]
    sqlquery="select setting as shared_buffers from pg_settings where name='shared_buffers'"
    version=1100
    withdbname=false
    tagvalue=""
  [[inputs.postgresql_extensible.query]]
    sqlquery="SELECT pg_database_size('$DATABASE_NAME') as db_size"
    version=1100
    withdbname=false
    tagvalue=""
  [[inputs.postgresql_extensible.query]]
    sqlquery="SELECT t.tablename, pg_total_relation_size('\"' || t.schemaname || '\".\"' || t.tablename || '\"') AS table_total_disc_size FROM pg_tables t WHERE t.schemaname = 'public' ORDER BY pg_total_relation_size('\"' || t.schemaname || '\".\"' || t.tablename || '\"') DESC LIMIT 10"
    version=1100
    withdbname=false
    tagvalue="tablename"
  [[inputs.postgresql_extensible.query]]
    sqlquery="SELECT CASE WHEN pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn() THEN 0 ELSE EXTRACT (EPOCH FROM now() - pg_last_xact_replay_timestamp()) END AS replication_lag"
    version=1100
    withdbname=false
    tagvalue=""

[[inputs.tail]]
  files = ["/home/postgres/pgdata/pgroot/pg_log/*.csv"]
  from_beginning = false
  data_format = "csv"
  csv_column_names = ["log_time", "user_name", "database_name", "process_id", "connection_from", "session_id", "session_line_num", "command_tag", "session_start_time", "virtual_transaction_id", "transaction_id", "error_severity", "sql_state_code", "message", "detail", "hint", "internal_query", "internal_query_pos", "context", "query", "query_pos", "location", "application_name"]
```

This config is stored in a telegraf-config configMap in the same namespace as my database.

Then I can tell prometheus to scrape this endpoint and visualize metrics in grafana.

## Conclusion :

I love this Kubernetes operator. It can handle all the features I need like :

- high availability (automatic failover and replication)
- pod anti-affinity
- node affinity
- backups and restore
- users and database management
- TLS
- monitoring with sidecar and Prometheus

Plus the dev team is really reactive. When I’ve submitted a merge request to add additional volume mount capabilities they responded very quickly and helped me a lot to make this feature work.

I didn’t tried other Postgresql operators for example

- CrunchyData postgres operator
- Percona postgres operator

Both looks cool, but I didn’t found them at the time I was looking for deploying Postgresql into Kubernetes.

The negative point I can make on this operator are :

- Everything is managed in only one manifest and it can be unreadable.
- Split configuration into separate manifest for database and roles can be a good idea.
- The monitoring is not included by default. You have to configure an external tool to monitor your database

## Bonus :

- The grafana postgresql dashboard
- Chaos testing for zalando postgresql operator
]]></description>
        </item>
        <item>
            <title>Vault Pki</title>
            <link>https://aperogeek.fr/vault-pki.html</link>
            <guid>https://aperogeek.fr/vault-pki.html</guid>
            <pubDate>Mon, 17 Jan 2022 00:00:00 +0000</pubDate>
            <description><![CDATA[# Hashicorp Vault PKI + Cert-manager

![Hashicorp Vault logo](../assets/img/vault.png)

Today, Kubernetes is the most popular container orchestration tool. It allow us to deploy all our applications without worry about networking, rolling update process, health checks, etc..

By default communications between application in the cluster are not encrypted, so we need to generate TLS certificate for each applications, and we need to automatize it !

That’s why here at Malt, we are using Hashicorp Vault and cert-manager to simplify the certificate generation process of each application.

## Hashicorp Vault

### Vault Installation

First we need to deploy our Hashicorp Vault cluster and configure our internal PKI.

To do this, we will use the official vault helm chart to install Vault in our kubernetes cluster.

vault helm install
```bash
kubectl create ns vault
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install -n vault vault hashicorp/vault \
  --set injector.enabled=false \
  --set server.ha.enabled=true \
  --set server.ha.raft.enabled=true
```

Once Vault is installed, it need to be initialized and unsealed. Here is a little script to init and unseal your vault :

vault init script
```bash
# vault init
vault_init=$(kubectl -n vault exec -it vault-0 -- vault operator init -format=json)
export VAULT_TOKEN=$(echo "$vault_init" | jq -r '.root_token')

# generate unseal script
key_1=$(echo "$vault_init" | jq '.unseal_keys_b64[0]')
key_2=$(echo "$vault_init" | jq '.unseal_keys_b64[1]')
key_3=$(echo "$vault_init" | jq '.unseal_keys_b64[2]')
echo "VAULT_ADDR=http://localhost:8200" > /tmp/vault_unseal.sh
echo "VAULT_TOKEN=$VAULT_TOKEN"     >> /tmp/vault_unseal.sh
echo "vault operator unseal $key_1" >> /tmp/vault_unseal.sh
echo "vault operator unseal $key_2" >> /tmp/vault_unseal.sh
echo "vault operator unseal $key_3" >> /tmp/vault_unseal.sh

# copy unseal script in each node
kubectl -n vault cp /tmp/vault_unseal.sh vault-0:/vault/file/vault_unseal.sh
kubectl -n vault cp /tmp/vault_unseal.sh vault-1:/vault/file/vault_unseal.sh
kubectl -n vault cp /tmp/vault_unseal.sh vault-2:/vault/file/vault_unseal.sh

# unseal the first node
kubectl -n vault exec -it vault-0 -- ash /vault/file/vault_unseal.sh
sleep 10

# join cluster
kubectl -n vault exec vault-1 -- vault operator raft join http://vault-0.vault-internal:8200
kubectl -n vault exec vault-2 -- vault operator raft join http://vault-0.vault-internal:8200

# unseal other nodes
kubectl -n vault exec -it vault-1 -- ash /vault/file/vault_unseal.sh
kubectl -n vault exec -it vault-2 -- ash /vault/file/vault_unseal.sh
```

Note that script generate and copy an unseal script directly inside vault containers. You should consider using vault auto unseal capabilities instead.

### Vault Configuration

We need to configure Hashicorp Vault to enable the Vault PKI. And what is the best way to configure a Hashicorp product ? An other Hashicorp product of course ! Let’s use Hashicorp Terraform to configure Hashicorp Vault.

![Yo Dawg Meme](assets/yoDawgHashicorp.jpg)

First, we need to configure terraform vault provider

terrafrom vault provider
```toml
terraform {
  required_providers {
    vault = {
      source = "hashicorp/vault"
      version = "3.1.1"
    }
  }
}

provider "vault" {}
```

We have to configure vault policies. Here is my policies/certs.hcl file :

policies/certs.hcl
```toml
path "auth/kubernetes/login" {
  capabilities = ["read"]
}
path "pki/issue/*" {
  capabilities = ["create", "read", "update"]
}
path "pki/certs" {
  capabilities = ["list"]
}
path "pki/cert/*" {
  capabilities = ["read"]
}
path "pki/sign/*" {
  capabilities = ["create", "read", "update"]
}
path "pki/ca/*" {
  capabilities = ["read"]
}
path "pki/ca_chain" {
  capabilities = ["read"]
}
path "pki/crl/*" {
  capabilities = ["read", "list"]
}
```

And add it to vault :

trerraform vault policy
```toml
resource "vault_policy" "certs" {
  name   = "certs"
  policy = file(format("%s/policies/certs.hcl", path.module))
}
```

Then, we need to enable the Kubernetes authentication method on vault.

terraform kubernetes auth
```toml
resource "vault_auth_backend" "kubernetes" {
  type                  = "kubernetes"
  path                  = "kubernetes"
  description           = "Kubernetes authentication backend mount"
}

resource "vault_kubernetes_auth_backend_config" "kubernetes" {
  backend            = vault_auth_backend.kubernetes.path
  kubernetes_host    = "https://kubernetes.default.svc"
  kubernetes_ca_cert = base64decode("<your k8s certificate>")
}

resource "vault_kubernetes_auth_backend_role" "kubernetes-certs" {
  role_name                        = "certs"
  backend                          = vault_auth_backend.kubernetes.path
  bound_service_account_names      = ["cert-manager"]
  bound_service_account_namespaces = ["cert-manager"]
  token_policies = [
    "certs",
  ]
}
```

With this role and policy, the kubernetes service account « cert-manager » in the « cert-manager » namespace will be able to login to vault directly with his service account token. Then once logged, it will be able to request certificate signatures from vault.

All have to do now is configure the PKI in vault :

terraform pki

```toml
# vault root pki mount point
resource "vault_mount" "pkirootca" {
  path                  = "pki-rootca"
  type                  = "pki"
  description           = "Self signed Vault root CA"
  max_lease_ttl_seconds = 20 * 365 * 24 * 3600
}

# vault root pki certificate
resource "vault_pki_secret_backend_root_cert" "pkirootca" {
  backend            = vault_mount.pkirootca.path
  type               = "internal"
  common_name        = "vault-active.vault.svc"
  ttl                = "630720000"
  format             = "pem"
  private_key_format = "der"
  key_type           = "rsa"
  key_bits           = 4096
}

# vault root pki urls
resource "vault_pki_secret_backend_config_urls" "pkirootca" {
  backend                 = vault_mount.pkirootca.path
  issuing_certificates    = ["http://vault-active.vault.svc:8200/v1/${vault_mount.pkirootca.path}/ca"]
  crl_distribution_points = ["http://vault-active.vault.svc:8200/v1/${vault_mount.pkirootca.path}/crl"]
}


# vault intermediate pki mount point /pki
resource "vault_mount" "pki" {
  path                  = "pki"
  type                  = "pki"
  description           = "Self signed Vault intermediate CA"
  max_lease_ttl_seconds = 3650 * 24 * 3600
}

# certificate request
resource "vault_pki_secret_backend_intermediate_cert_request" "pki" {
  backend            = vault_mount.pki.path
  type               = "exported"
  common_name        = "vault-active.vault.svc"
  alt_names          = ["vault.vault.svc", "vault-standby.vault.svc"]
  format             = "pem"
  private_key_format = "der"
  key_type           = "rsa"
  key_bits           = 2048
}

# sign certificate request by root ca
resource "vault_pki_secret_backend_root_sign_intermediate" "pkirootca-signs-pki" {
  backend        = vault_mount.pkirootca.path
  csr            = vault_pki_secret_backend_intermediate_cert_request.pki.csr
  use_csr_values = true
  common_name    = vault_pki_secret_backend_intermediate_cert_request.pki.common_name
  ttl            = vault_mount.pki.max_lease_ttl_seconds
}

# set signed certificate to vault intermediate pki
resource "vault_pki_secret_backend_intermediate_set_signed" "pki" {
  backend     = vault_mount.pki.path
  certificate = "${vault_pki_secret_backend_root_sign_intermediate.pkirootca-signs-pki.certificate}\n${vault_pki_secret_backend_root_sign_intermediate.pkirootca-signs-pki.issuing_ca}"
}

# vault pki urls
resource "vault_pki_secret_backend_config_urls" "pki" {
  backend                 = vault_mount.pki.path
  issuing_certificates    = ["http://vault-active.vault.svc:8200/v1/${vault_mount.pki.path}/ca"]
  crl_distribution_points = ["http://vault-active.vault.svc:8200/v1/${vault_mount.pki.path}/crl"]
}

# pki roles
resource "vault_pki_secret_backend_role" "pki-application" {
  backend            = vault_mount.pki.path
  name               = "application"
  ttl                = 35.5 * 24 * 3600
  max_ttl            = 36 * 24 * 3600
  generate_lease     = false
  allow_bare_domains = true
  allow_glob_domains = true
  allow_ip_sans      = true
  allow_localhost    = true
  allow_subdomains   = false
  allowed_domains = [
    "*.default.svc",
    "*.default.svc.cluster.local",
  ]
  key_bits  = 2048
  key_type  = "rsa"
  key_usage = ["DigitalSignature", "KeyAgreement", "KeyEncipherment"]
}
```

This block is more consistent but it does a lot of things.

- First it creates a root pki in vault with a self signed certificate.
- Then it create an intermediate pki (the one that will be used by cert-manager) with a certificate signed by the root pki.
- Finally it create a pki role « application » that allow certificates with domains « *.default.svc » to be signed by vault.

To apply this, you must export the vault required environment variables and create a tunnel to the vault port :

```bash
kubectl -n vault port-forward svc/vault-active 8200:8200 &
```

Then apply it

```bash
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=<the root token generated during initialization>
terraform init
terraform apply
```

## Cert-Manager

The cert-manager installation process is much more simpler :

```bash
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.6.1/cert-manager.yaml
```

That’s it ! cert-manager is installed, and can now be configured thanks to CRDs.

We can now configure a Vault Issuer for cert manager :

```yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: vault-issuer
  namespace: default
spec:
  vault:
    path: pki/sign/application
    server: http://vault-active.vault.svc:8200
    auth:
      kubernetes:
        role: my-app-1
        mountPath: /v1/auth/kubernetes
        secretRef:
          name: default-token-xkcd
          key: token
```

Note that you can also define Cert-manager « ClusterIssuer » instead of Namespaced Issuer. That kind will allow you to declare one issuer that will be available to all namespaces. The Vault pki role must be configured with the correct allowed domains to do this.

Also cert manager is not able yet to determine the secret name associated to a kubernetes service account. So you have to find the secret name of the service account you want to use for vault authentication.

We are now all good ! Let’s create a Certificate for an application in the default namespace :

```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: foo-certificate
  namespace: default
spec:
  secretName: foo-certificate
  secretTemplate:
    annotations:
      cert-manager-secret-transform: tls.der
  duration: 2160h0m0s # 90d
  renewBefore: 360h0m0s # 15d
  subject:
    organizations:
      - company
  commonName: foo.default.svc
  privateKey:
    algorithm: RSA
    encoding: PKCS8
    size: 2048
  usages:
    - server auth
    - client auth
  dnsNames:
    - foo.default.svc
    - foo.default.svc.cluster.local
  uris:
    - https://foo.default.svc
    - https://foo.default.svc.cluster.local
  ipAddresses:
    - 127.0.0.1
  # Issuer references are always required.
  issuerRef:
    name: vault-issuer
    kind: Issuer
    group: cert-manager.io
```

We should be able to see a new Kubernetes secret in the default namespace « foo-certificate ».

This secret can be mounted as a volume inside the application container. It contain a tls.crt and a tls.key with our certificate and private key.]]></description>
        </item>
        <item>
            <title>Argocd</title>
            <link>https://aperogeek.fr/argocd.html</link>
            <guid>https://aperogeek.fr/argocd.html</guid>
            <pubDate>Sun, 26 Jul 2020 00:00:00 +0000</pubDate>
            <description><![CDATA[# ArgoCD

![Argocd Logo](../assets/img/argocd-logo.png)

This article is about Argocd and how to deploy applications in a Kubernetes cluster with Kustomize and the GitOps method.

## Kubernetes

A lot of peoples thinks Kubernetes is very hard…

![kubernetes the hard way](../assets/img/mario-hard.gif)

But in fact you just have a few concepts to learn.
In a Kubernetes cluster, everything is Object. Declare your object (Kind) specification and Kubernetes will manage it for you.

All the Object declaration is made with YAML

![yaml everywhere](../assets/img/yaml-everywhere.jpg)

A **pod** is an object that describe how your application is deployed (docker image name, environment variables, volume mounts, etc..)

Kubernetes will manage relation between objects with labels. For example if you want to communicate between 2 applications, you will create services that will match each application labels and each application can use the service of the other app that will automatically point to the corresponding pod.

To communicate with your Kubernetes cluster, the only thing you need is kubectl and a kubeconfig.yaml file obtained at the cluster creation.

Now you can apply your resource with the kubectl command :

kubeconfig.yml
```bash
export KUBECONFIG=$HOME/.kube/config
kubectl apply -f deployment.yml
```

Here is some example of kubernetes objects

![kube schema](../assets/img/kubernetes-schema-1024x712.png)

All pods are managed with an object called « deployment » :

deployment.yml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 labels:
   app: vote
 name: vote
spec:
 replicas: 3
 selector:
   matchLabels:
     app: vote
 template:
   metadata:
     labels:
       app: vote
   spec:
     containers:
     - image: dockersamples/examplevotingapp_vote:before
       name: vote
       ports:
       - containerPort: 80
         name: vote
```

Then, to access the pod through the internal kubernetes network we can declare an object of type « service » that will redirect the traffic to all pods that match the label « app=vote »

service.yml
```yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: vote
  name: vote
spec:
  type: ClusterIP
  ports:
    - name: vote
      port: 80
      targetPort: 80
  selector:
    app: vote
```

And finally, to expose the service outside the kube, we can declare an object of type « ingress« .

Ingress rules can define rules with a host and a path that neeed to match, so the traffic can be routed to the corresponding service.

ingress.yml
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: vote
spec:
  rules:
   - host: vote.domain.com
     http:
       paths:
         - backend:
             serviceName: vote
             servicePort: 80
           path: /
```

## Kustomize

Now we have seen some Kubernetes basics, let’s go further with a tool called Kustomize.

Kustomize is a tools that allow you to declare all your kubernetes resources in a same folder and customize it before apply. It’s like a Helm chart but without complicated templating. With kustomize, you declare your Kube resources in a base folder and define overlays to patch your base resources, add other configMaps or define variables that will be replaced in base.

To explain how Kustomize and argocd works, I’ve created a public gitlab repo to deploy a demo « vote » application.

In the Vote app folder, I have a base folder containing all my application Kubernetes resources, and a kustomize.yml file :

base/kustomization.yml
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: vote
resources:
  - namespace.yml
  - db-deployment.yaml
  - db-service.yaml
  - redis-deployment.yaml
  - redis-service.yaml
  - result-deployment.yaml
  - result-service.yaml
  - result-ingress.yml
  - vote-deployment.yaml
  - vote-service.yaml
  - vote-ingress.yml
  - worker-deployment.yaml
```

There are lots of resources to be deployed :

- a namespace to isolate my application
- a database (postgres) deployment and service
- a redis to cache the vote result
- a vote web app written in python
- a worker to process votes from redis and store results in db
- a result app to show vote score

![voting app architecture](../assets/img/voting-app-architecture.png)

And there is also an overlay folder called « dev » which will include all the base resources and add some kustomizations.

dev/kustomize.yml
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: vote
bases:
  - ../base

configMapGenerator:
  - name: apps-config
    behavior: create
    literals:
      - DNS_NAME=demo

vars:
- name: DNS_NAME
  objref:
    apiVersion: v1
    kind: ConfigMap
    name: apps-config
  fieldref:
    fieldpath: data.DNS_NAME

patches:
  - target:
      group: apps
      version: v1
      kind: Deployment
      name: vote.*
    path: patch.deployment.yml
```

This kustomization overlay add a new configMap « apps-config » with a key DNS_NAME. Then it declare a kustomize variable which will read the configMap to fetch the value of this key and store it.

The variable DNS_NAME will be replaced in the base/ingress.yml file :

vote-app/base/vote-ingress.yml
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: vote
  namespace: vote
spec:
  rules:
  - host: vote.$(DNS_NAME)
    http:
      paths:
      - backend:
          serviceName: vote
          servicePort: 80
        path: /
```

Finally this overlay will patch the base vote deployment to change the numbers of replicas

vote-app/dev/patch.deployment.yml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: $(DEPLOYMENT)
spec:
  replicas: 2
```


## k3d

![k3s](../assets/img/k3s-logo.png)

To deploy all this stuff, we can use a local Kubernetes cluster.

Thanks to Rancher Labs who released an Open source tool called k3s, we can create Kubernetes cluster in a seconds. K3s is a single binary (<40Mo), written in Go, available for ARM kubernetes distribution.

As it’s a very simple binary, we can run it in docker ! Here comes k3d (k3s in docker)

In one command line, you can create a Kubernetes cluster with multiple worker nodes, a custom registry config, a load balancer already deployed !

```bash
k3d create cluster
k3d create cluster demo -w 3 --no-lb --wait
INFO[0000] Created network 'k3d-demo'                   
INFO[0000] Created volume 'k3d-demo-images'             
INFO[0001] Creating node 'k3d-demo-master-0'            
INFO[0001] Creating node 'k3d-demo-worker-0'            
INFO[0002] Creating node 'k3d-demo-worker-1'            
INFO[0002] Creating node 'k3d-demo-worker-2'            
INFO[0008] Cluster 'demo' created successfully!         
INFO[0008] You can now use it like this:                
export KUBECONFIG=$(k3d get kubeconfig demo)
kubectl cluster-info
```

Now I can connect to my local cluster :

```
k3d connect cluster
export KUBECONFIG=$(k3d get kubeconfig demo)
kubectl config use-context k3d-demo
```

In the demo repo, I skip the load balancer deployment because I need to set custom traefik configuration (insecureSkipVerify = true), So traefik is manually deployed (see the traefik base and dev overlay in the demo repo).

Let’s create the k3s cluster and deploy Traefik + ArgoCD with the script apply.sh

```bash
apply.sh
Listing clusters...
Creating kubernetes cluster...
INFO[0000] Created network 'k3d-demo'                   
INFO[0000] Created volume 'k3d-demo-images'             
INFO[0001] Creating node 'k3d-demo-master-0'            
INFO[0001] Creating node 'k3d-demo-worker-0'            
INFO[0002] Creating node 'k3d-demo-worker-1'            
INFO[0002] Creating node 'k3d-demo-worker-2'            
INFO[0008] Cluster 'demo' created successfully!         
INFO[0008] You can now use it like this:                
export KUBECONFIG=$(k3d get kubeconfig demo)
kubectl cluster-info
Exporting kubectl config...
Switched to context "k3d-demo".
--------------------------
         ARGOCD           
--------------------------
namespace/traefik created
customresourcedefinition.apiextensions.k8s.io/ingressroutes.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/ingressroutetcps.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/ingressrouteudps.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/middlewares.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/tlsoptions.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/tlsstores.traefik.containo.us created
customresourcedefinition.apiextensions.k8s.io/traefikservices.traefik.containo.us created
serviceaccount/traefik created
clusterrole.rbac.authorization.k8s.io/traefik created
clusterrolebinding.rbac.authorization.k8s.io/traefik created
configmap/traefik-config created
secret/traefik-default-cert created
service/traefik created
service/traefik-lb created
service/whoami created
deployment.apps/traefik created
deployment.apps/whoami created
ingress.extensions/whoami created
persistentvolumeclaim/traefik-acme created
namespace/argocd created
namespace/database created
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io created
serviceaccount/argocd-application-controller created
serviceaccount/argocd-dex-server created
serviceaccount/argocd-server created
role.rbac.authorization.k8s.io/argocd-application-controller created
role.rbac.authorization.k8s.io/argocd-dex-server created
role.rbac.authorization.k8s.io/argocd-server created
clusterrole.rbac.authorization.k8s.io/argocd-application-controller created
clusterrole.rbac.authorization.k8s.io/argocd-server created
rolebinding.rbac.authorization.k8s.io/argocd-application-controller created
rolebinding.rbac.authorization.k8s.io/argocd-dex-server created
rolebinding.rbac.authorization.k8s.io/argocd-server created
clusterrolebinding.rbac.authorization.k8s.io/argocd-application-controller created
clusterrolebinding.rbac.authorization.k8s.io/argocd-server created
configmap/argocd-cm created
configmap/argocd-rbac-cm created
configmap/argocd-ssh-known-hosts-cm created
configmap/argocd-tls-certs-cm created
configmap/argocd-8hgd69k9d2 created
secret/argocd-secret created
service/argocd-dex-server created
service/argocd-metrics created
service/argocd-redis created
service/argocd-repo-server created
service/argocd-server-metrics created
service/argocd-server created
deployment.apps/argocd-application-controller created
deployment.apps/argocd-dex-server created
deployment.apps/argocd-redis created
deployment.apps/argocd-repo-server created
deployment.apps/argocd-server created
ingress.extensions/argocd created
Waiting for traefik LoadBalancer IP...
....................................................
LB IP is 172.24.0.5
Add this line in your /etc/hosts :
172.24.0.5       argocd.demo traefik.demo vote.demo result.demo
Waiting for argocd-server to be ready...
....................................................
```

## Argocd

![ArgoCD](../assets/img/argocd-logo.png)

We are ready to deploy the vote application (Cat vs Dog) inside our kubernetes cluster with ArgoCD

Argoproj provides different Open source Kubernetes native tools to manage workflows (Argo), events (Argo-Events), CI and CD (ArgoCD) or rolling updates with A/B testing (Argo Rollout).

ArgoCD concept is that you declare your applications deployment configuration in a Git repo, then ArgoCD will detect modifications in the repo to apply the manifets automatically. Your application deployment configuration can be define as Helm Chart or Kustomize. (Tanka will probably be available soon)

If you push changes in your repo, ArgoCD, which fetch the repo periodically, will detect changes and deploy the application.

If you manually change a configMap or a pod spec in your Kubernetes cluster, ArgoCD will detect it, and re-apply to be conform to what’s in git repo. GitOps is the truth !

To deploy a new application with ArgoCD, a your repo in the « argo-cm » configMap and declare a CRD (Custom Resource Definition) of type « applications.argopoj.io » with the git repo url, the git branch and the path inside the repo.

argocd-cm
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/name: argocd-cm
    app.kubernetes.io/part-of: argocd
  name: argocd-cm
data:
  repositories: |
    - type: git
      url: https://gitlab.com/seuf/argocd-demo.git
```

vote.yml
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  finalizers:
  - resources-finalizer.argocd.argoproj.io
  name: demo-vote
  namespace: argocd
spec:
  destination:
    namespace: vote
    server: https://kubernetes.default.svc
  project: default
  source:
    path: vote-app/dev
    repoURL: git@gitlab.com:seuf/argocd-demo
    targetRevision: master
  syncPolicy:
    automated:
      prune: true
      selfHeal: false
```

You can also create new applications from the Argocd Web UI.

First, go to ArgoCD settings, in the left menu and select add repository (with https), and fill the form with your repo url and click « Connect » :

![argocd add repository](../assets/img/argocd-add-repo-1024x525.png)

Next add a new application (Applications in the left menu + Add new app)

![argocd add app](../assets/img/argocd-add-new-app.png)

Add a name for you app, select the previously created repo and then the path of your application deployment configuration inside the repo (vote/dev).

You can specify if you want autosync or not (apply automatically on git changes) and if autosync is enabled, you can enable prune (remove old resources not described in git repo) and auto-heal (re-apply automatically on infra changes)

Finally, Click on « Create », your app will be listed in the argocd managed apps. If you click on it, you will see all the app resources and deployment status.

![argocd demo app resources](../assets/img/argocd-demo-app-deployment-1024x398.png)

On the top right corner, you can select different views of the deployed resources (network view like the screenshot) or resources view (all the resources, including configMaps, secrets, etc..) or a simple list.

Now if you update the vote deployment configuration in the git repo, Argocd will automatically detect changes and deploy the new manifests. If you have selected « manual sync » during the app creation, you will be able to see the diff between git repo and live manifests and you can synchronize the resources you want !

Once all the resources are synchronized you can access the voting app directly through your browser at https://vote.demo and visualize the results at https://results.demo
![cat vs dog](../assets/img/cat-vs-dog.png)
![cat-vs-dog results](../assets/img/cat-vs-dog-results.png)

## Conclusion

This article only covered the basics of ArgoCD. There are a lots of features Like Sync Phases and Waves that allow you to order deployment resources. For example you can run a Job to upgrade your database schema before deploying the application.

ArgoCD also provides **Notifications** to send notifications on slack channels or mails, etc..

Since Argocd v1.6, Argoproj and Weave Flux have worked together to unify their GitOps Engine. You can declare your repo and applications with a simple cli if you want.

This is a CNCF Incubating Project with a lot of active contributors. I’m using it in production at Malt since a year now. I’m sure it will become a standard for many enterprises in a near future.]]></description>
        </item>
        <item>
            <title>Ansible Github Action</title>
            <link>https://aperogeek.fr/ansible-github-action.html</link>
            <guid>https://aperogeek.fr/ansible-github-action.html</guid>
            <pubDate>Thu, 09 Jan 2020 00:00:00 +0000</pubDate>
            <description><![CDATA[# Ansible Playbook from Github Action

![github](../assets/img/github-70.png)

I’ve recently started a side project with Ansible and I wanted to automatise the Ansible playbook deployment with Github Action. Here is a tiny tutorial on how to use Github actions to run ansible playbooks to deploy stuff on Digital Ocean Droplets.

## SSH Key

Before creating digital ocean droplets, you need to create a private ssh key.

```bash
ssh-keygen -f .ssh/id_digital_ocean
```

Then copy the content of the public key in your digital ocean account.

```bash
cat .ssh/id_digitalocean.pub
```

Go to your Digital Ocean account security page and add a new ssh-key.

You need to specify this key when you create a droplet. Here is the command to get the md5 checksum of the key used in droplet creation :

```bash
ssh-keygen -E md5 -l -f .ssh/id_digitalocean.pub
```

## Digital Ocean Droplet

Digital Ocean Droplets are created with the digital_ocean_droplet ansible module. You can use the digital ocean dynamic inventory script to retrieve droplet list : Just copy this file and the .ini in the inventory directory of your workspace.

The inventory script require to export the DO_API_KEY in environment variables.

To create a Digital Ocean API key, simply go to your console UI and click on API on the left menu, then generate a new token
Github Secrets

To use Ansible in Github Action, we need to create Github Secrets in the project. Go to your project settings, and in the secret menu add the following variables :

- `ANSIBLE_VAULT_PASSWORD` : The ansible vault password used to encrypt / decrypt secret variables
- `DO_API_KEY` : The Digital Ocean API Key
- `DO_SSH_KEY`: The private ssh key used by ansible to connect to droplets.

## GitHub Action

Now it time for Action !

![Action](../assets/img/source.gif)

Github Action is like gitlab-ci : create a .github/workflows/deploy.yml file in your git project ans specify the steps you want.

github-action
```yaml
name: Ansible deploy

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Set up Python 3.7
      uses: actions/setup-python@v1
      with:
        python-version: 3.7
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install ansible==2.9.2 requests
    - name: set ansible config secrets
      env:
        ANSIBLE_VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
        DO_SSH_KEY: ${{ secrets.DO_SSH_KEY }}
      run: |
        echo "$ANSIBLE_VAULT_PASSWORD" > .ansible-vault-password
        mkdir .ssh
        echo "$DO_SSH_KEY" > .ssh/id_digitalocean
        chmod 600 .ssh/id_digitalocean
    - name: run exporters playbook
      env:
        DO_API_KEY: ${{ secrets.DO_API_KEY }}
      run: |
        ansible-playbook -i inventory play.yml
```

Then, when I git push to my repo, I can see my github action running
github-action-running

Enjoy !
]]></description>
        </item>
        <item>
            <title>K3S</title>
            <link>https://aperogeek.fr/k3s.html</link>
            <guid>https://aperogeek.fr/k3s.html</guid>
            <pubDate>Mon, 04 Nov 2019 00:00:00 +0000</pubDate>
            <description><![CDATA[# K3S : Lightweight Kubernetes. 5 less than k8s.

![k3s](../assets/img/k3s-logo.png)

Today, I will talk about a tool I discovered recently that I use a lot to create local Kubernetes environments : k3s

k3s allow you to create local k8s cluster, and try your application deployments before pushing it to production.

You can use the k3s quick start, or use the k3d binary to deploy a Kubernetes cluster in docker.

Just download the latest k3d release from the github releases page of the project, chmod +x the binary and put it somewhere in your $PATH

```bash
curl -Lf https://github.com/rancher/k3d/releases/download/v1.3.4/k3d-linux-amd64 -o k3d
chmod +x k3d
sudo mv k3d /usr/local/bin/
```

Then, you can follow the following steps to play :

- **Create the k3s cluster :**

```
k3d create --workers 3
```

- **Fetch the credentials of the cluster :**

```bash
export KUBECONFIG="$(k3d get-kubeconfig --name='k3s-default')"
```

- **Install the local-path storage driver (if you want to use pvclaim)**

``` bash
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
```

Note : You must set the storage class to local-path in your pvclaims to use this driver.

- **Get the Traefik Loadbalancer IP (wait for traefik to be ready)**

```bash
kubectl -n kube-system get svc traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```

-> sudo vim /etc/hosts with your desired ingress name

```bash
/etc/hosts
172.18.0.2       foobar.aperogeek.fr
```

Now you can deploy an application within your cluster with your kubectl / kustomize config files !

You can even use the terraform kubernetes provider if you configure it like this :

```toml
provider "kubernetes" {
  config_path = "/home/seuf/.config/k3d/k3s-default/kubeconfig.yaml"
}
```

Enjoy 🙂]]></description>
        </item>
        <item>
            <title>Kustomize</title>
            <link>https://aperogeek.fr/kustomize.html</link>
            <guid>https://aperogeek.fr/kustomize.html</guid>
            <pubDate>Thu, 31 Oct 2019 00:00:00 +0000</pubDate>
            <description><![CDATA[# Kustomize

![kustomize logo](../assets/img/Kubernetes_container_engine.png)

Since we have migrated all our application deployment to Kubernetes, we needed to automatize the deployment of each application for different environments (integration and production).

By default kubectl files are not really customizable. That’s why we choose Kustomize to deploy our apps. And now Kustomize is part of kubectl binary. You can just put your kustomiszation file in a folder and apply it using the :

```bash
kubectl apply -k my-app
```

The Idea of Kustomize is to use a base to define your Kubernetes application deployment, service, ingress, etc.. and after you add overlay to override / patch your base for each environment.

Let’s start with and example. I want to deploy a simple docker image in my k8s cluster. So I have some kubectl files : namespace.yml, service.yml deployment.yml and ingress.yml all stored in a base directory.

Then I add a kustomization.yml in this directory which reference all this files :

base/kustomization.yml
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo
resources:
  - namespace.yml
  - service.yml
  - deployment.yml
  - ingress.yml
configMapGenerator:
  - name: demo-config
    behavior: create
    files:
      - config.json
```

Once I’ve create my base Kustomization, I can patch Kubernetes resources for different environment. For example for each environment, i want to create a Secret containing the db password.

I can use the Kustomize sops decoder plugin which is open sourced by Malt ^_^’

Here is the kustomization.yml stored in the staging folder :

staging/kustomization.yml
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo
resources:
  - ../base
generators:
  - sops-decoder.yml
vars:
  - name: INGRESS_PREFIX
    objref:
      apiVersion: v1
      kind: ConfigMap
      name: demo-config
    fieldref:
      fieldpath: data.INGRESS_PREFIX
configMapGenerator:
  - name: demo-config
    behavior: merge
    literals:
      - environment=staging
      - INGRESS_PREFIX="staging."
```

You can see that I’ve included the base Kustomization.yml, then I’ve overrided some variables with a sops decoder generator (see bellow) and a configMap which contain specific values for this environment.

Another trick here is to fetch the value of the INGRESS_PREFIX from the configMapGenerator into a Kustomize variable. This variable can be used after in my ingress to have a different ingress host for each environment :

`host: demo.$(INGRESS_PREFIX)aperogeek.fr`

Now, Let’s have a look to the sops-decoder.yml generator :

sops-decoder.yml
```yaml
apiVersion: gitlab.com/maltcommunity
kind: SopsDecoder
metadata:
  name: password
name: password
files:
  - path: password.enc
    key_name: password
    type: raw
```

The Kustomize sops decoder plugin will automatically decrypt the Mozzilla Sops encrypted file and generate a Kubernetes Secret with the content in the secrets data.

Here is a layout example of my Kustomize workspace :

directory layout
```
base/
  config.json
  deployment.yml
  ingress.yml
  kustomization.yml
  namespace.yml
  service.yml

production/
  kustomization.yml
  password.enc
  sops-decoder.yml

staging/
  kustomization.yml
  password.enc
  sops-decoder.yml
```

Now I can validate and apply my Kustomize files running the command

```bash
kustomize build
kustomize build staging --enable_alpha_plugins
```

The Kustomize build will return all the desired kubectl resources to deploy my kubernetes app :

kustomize build output
```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: v1
data:
  INGRESS_PREFIX: integration.
  environment: integration
  environment-type: staging
  config.json: |-
    {"port":8080}
kind: ConfigMap
metadata:
  annotations: {}
  labels: {}
  name: demo-config-bdbg5f6dd5
  namespace: demo
---
apiVersion: v1
data:
  password: Qydlc3QgYm9uIGxhIGJpw6hyZSAhCg==
kind: Secret
metadata:
  name: password-mcgc26kf8m
  namespace: demo
type: Opaque
---
apiVersion: v1
kind: Service
metadata:
  name: demo
  namespace: demo
spec:
  ports:
  - name: http
    port: 8080
    targetPort: 8080
  selector:
    app: demo
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: demo
  name: demo
  namespace: demo
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - env:
        - name: ENVIRONMENT
          valueFrom:
            configMapKeyRef:
              key: environment
              name: demo-config-bdbg5f6dd5
        - name: PASSWORD
          valueFrom:
            secretKeyRef:
              key: password
              name: password-mcgc26kf8m
              optional: false
        image: mon-image:latest
        imagePullPolicy: IfNotPresent
        name: demo
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        volumeMounts:
        - mountPath: /etc/my-app
          name: config
          readOnly: true
      volumes:
      - configMap:
          items:
          - key: config.json
            path: config.json
          name: demo-config-bdbg5f6dd5
        name: config
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip
  name: demo
  namespace: demo
spec:
  rules:
  - host: demo.staging.aperogeek.fr
    http:
      paths:
      - backend:
          serviceName: demo
          servicePort: 8080
        path: /
```

Finally I can apply this with a pipe to kubectl

```bash
kustomize build + apply
kustomize build staging --enable_alpha_plugins | kubectl apply -f -
```
]]></description>
        </item>
        <item>
            <title>Markus Biere</title>
            <link>https://aperogeek.fr/markus-biere.html</link>
            <guid>https://aperogeek.fr/markus-biere.html</guid>
            <pubDate>Wed, 20 Sep 2017 00:00:00 +0000</pubDate>
            <description><![CDATA[# Marcus Bière

Aperogeek c’est de l’actu Geek, mais aussi de l’actu Bières !

Du coup je vais vous présenter aujourd’hui une brasserie que j’aprécie particulièrement : Marcus Bière, la bière de la drôme !

Situé dans le petit village de Saou au coeur de la drôme, Marcus Bière est une brasserie artisanale qui fait de la super bonne bière !


Voici quelques petites photos histoire de vous donner envie d’y aller 🙂

![](../assets/img/IMG_20170826_183556-300x169.jpg)
![](../assets/img/IMG_20170826_183624-300x169.jpg)
![](../assets/img/IMG_20170826_183717-169x300.jpg)
![](../assets/img/IMG_20170826_190713.jpg)
![](../assets/img/IMG_20170826_190800-300x169.jpg)
![](../assets/img/IMG_20170826_193146-169x300.jpg)
![](../assets/img/IMG_20170827_103342-300x169.jpg)
![](../assets/img/IMG_20170827_110441-300x169.jpg)

Comme vous le voyez le coin est très agréable. En plus c’est juste a côté de chez beau papa, donc j’y vais régulièrement 😛]]></description>
        </item>
</channel>
</rss>