Aller au contenu principal

5 articles tagués avec « kubernetes »

Voir tous les tags

· 11 minutes de lecture
TheBidouilleur

Bonne année, Bonne santé ! Que la réussite et la santé soient avec vous ! J'espère que cette année sera riche en découvertes techniques.

Mais avant d'être trop heureux, attaquons avec une mauvaise nouvelle :

K8S@HOME est mort

Qu'est-ce que K8S@HOME ?

K8S-at-home est le nom d'une communauté promouvant l'usage de Kubernetes comme Homelab. La communauté possédait un énorme dépôt Helm maintenu par quelques membres. Sur ce dépôt Helm, on pouvait avoir la plupart des applications selfhosts utilisées dans les communautés Reddit/Discord. (Plex, Firefly, Bitwarden etc…)

K8S@HOME permettait donc de déployer de nombreuses applications via Helm, sans s'embêter à écrire des charts.

La fin de K8S@HOME

Si toute bonne chose a une fin : voici celle de K8S@HOME. Suite au manque de contributeurs, le dépôt est archivé et les charts ne seront plus maintenus.

Pour l'instant, ça ne veut pas dire que les Helm déjà déployés à partir de K8S@HOME doivent être arrêtés : les images Docker sont choisies dans les fichiers values.yaml, et s'il y a faille → ce seront les images Docker qu'il faudra mettre à jour, pas le chart.

En revanche, avec le temps : nous auront de plus en plus d'instructions obsolètes et nous ne bénéficierons pas des nouvelles fonctionnalités prévues par Helm.

La Solution : Créer notre dépôt Helm

Mais oui ! Tout comme nous prenons l'habitude de créer un registre Docker avec nos images. Nous pouvons créer un dépôt Helm avec nos charts.

Fonctionnement d'un chart Helm

De base, un chart Helm se compose de différents fichiers YAML qui seront appliqués via kubectl après un traitement de "templating". Ce traitement permet de remplacer des valeurs dans les fichiers (Les utilisateurs de Jinja2 ne seront pas perdus) via le fichier values.yaml (qui contient les valeurs de remplacement) et les fichiers _helpers.tpl (qui contiennent des fonctions pour traiter les valeurs).

Une fois que les valeurs de remplacements sont appliquées sur la template, on envoie les modifications au cluster. (via kubectl ou en communiquant avec l'API).

Exemple rapide :

#service.yml
apiVersion: v1
kind: Service
metadata:
name: baikal
labels:
{{- include "baikal.labels" . | nindent 4 }}
spec:
type: ClusterIP
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "baikal.selectorLabels" . | nindent 4 }}
#values.yml
service:
type: ClusterIP
port: 80

Les fonctions sont appelées par le mot clé "include", et les valeurs du fichier values.yml sont appelées via le préfixe ".Values".

Vous pourrez apprendre à faire des charts Helm en suivant l'excellente documentation de Stéphane Robert : ici

Fonctionnement d'un dépôt Helm

Lorsque l'on ajoute un dépôt, on peut directement voir les charts disponibles dans ce dépôt :

helm repo add qjoly https://qjoly.github.io/helm-charts\
helm search repo qjoly

C'est grâce au dépôt qui contient un fichier index.yaml qui va répertorier les charts disponibles et les URLs permettant de les télécharger.

Schéma résumant le fonctionnement d'un depot

Ainsi, lorsque l'on veut ajouter le chart "Joplin" dépôt "qjoly", notre client va aller chercher dans notre fichier index.yaml l'url de téléchargement (en tar.gz) du chart. Notre client Helm va ensuite faire le remplacement des valeurs avant d'envoyer le manifest dans notre cluster.

Création d'un dépôt Helm

Pour créer un dépôt helm, voici les différentes solutions :

  • Utiliser ChartMuseum
  • Utiliser l'image GitHub Action Chart-Releaser
  • à la main (en créant le fichier index.yaml manuellement)

Nous, nous passerons par l'image GitHub Action (je réserve une page sur ChartMuseum).

Usage de GitHub Action pour générer les releases

       - name: Run chart-releaser
uses: helm/chart-releaser-action@v1.5.0
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
Ne pas créer de secret

Il n'est pas nécessaire de créer un secret. (celui-ci est automatiquement généré durant le CI)

Grâce à cette image, CR va chercher dans le dossier charts/ pour générer l'index.yaml et le stocker directement dans la branche gh-pages. (pensez à activer GitHub-pages pour que le site soit accessible à https://votre-username.github.io/votre-projet)

L'avantage de Chart-Releaser est qu'il va automatiquement créer des releases pour chaque chart présent dans notre dépôt GitHub. Ce sont d'ailleurs ces mêmes releases qui seront accessibles depuis le fichier index.yaml. En revanche, CR ne pourra pas mettre à jour une version déjà existante. Si nous voulons refaire la version 1.5 du chart "Baikal", il n'en fera rien. Il faudra manuellement supprimer la release/tag avant de relancer le CI.

curl https://qjoly.github.io/helm-charts/index.yaml

Nous avons bien un fichier renvoyant les charts disponibles, une description, ainsi que l'URL où l'archive du chart est accessible.

Il est très bien possible de se contenter de ça, mais puisque nous le pouvons : allons plus-loin !

Tester les charts avant de générer la release. (CI)

Pour être sûr de ne pas envoyer des charts non fonctionnels, j'ai voulu m'appuyer sur du CI pour vérifier le bon-fonctionnement de mon code.

La première chose simple que nous pouvons faire... c'est d'utiliser le linter de Helm.

Vérification de la syntaxe

Selon Wikipedia:

Un linter est un outil qui analyse le code source pour signaler les erreurs de programmation, les bogues, les erreurs stylistiques et les constructions suspectes.

L'objectif est donc de vérifier (avant d'exécuter un code) que sa syntaxe est correcte et qu'il n'y a pas d'erreur évidente. Nous pouvons directement taper la commande helm lint ..

Exemple :

➜  baikal git:(main) helm lint .
==> Linting .
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed
➜ baikal git:(main)

Helm lint est CI-Friendly, il renvoie un exit-code différent de 0 lorsque le lintage (c'est mon article, j'invente les mots que je veux) n'est pas correct.

Pour tester l'intégralité de mes charts, j'ai écrit un petit script helm_lint.sh qui va effectuer la commande helm lint . dans chaque sous-dossier de charts/.

cd ../../charts
for d in *
do
echo "Testing $d "
(cd "$d" && helm lint )
if [ $? -ne 0 ]; then
echo "Error"
exit 1
fi
done

Ainsi, à la moindre erreur dans le script (si le lintage est mauvais), celui-ci s'arrête et renvoie l'exitcode à 1. (Ce qui va stopper le CI et générer une erreur)


Pour lancer ce script via GitHub Action, j'ai installé Helm via l'action "azure/setup-helm". Ce qui nous donne ces instructions à rajouter devant notre chart-releaser :

       - uses: azure/setup-helm@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Helm Lint
run: |
cd .github/workflows/
./helm_lint.sh

Vérification du fonctionnement des charts

Si vérifier le lint se fait en quelques secondes, il est également possible de lancer le chart directement depuis le CI Github.

Pour cela, il faut au préalablement créer un cluster Kubernetes depuis Github Action. Et si cela est possible, c'est grâce à KIND (Kubernetes INside Docker) qui permet de faire un cluster virtuel dans des conteneurs Docker en seulement quelques commandes.

À rajouter dans notre CI:

       - name: Create k8s Kind Cluster
uses: helm/kind-action@v1.5.0

Maintenant que nos charts ont une syntaxe correcte, que nous avons un cluster fonctionnel, il faut y installer nos programmes un-par-un et les tester individuellement.

Vous rappelez-vous de helm_lint.sh ? Voici son grand-frère : helm_deploy.sh.

cd ../../charts
for d in *
do
echo "Deploying $d to kind"
(
set -x
cd "$d"
if [ -f ".no_ci" ]; then
echo "No CI for this chart."
else
helm install $d . --wait --timeout 120s
helm test $d
fi
)
if [ $? -ne 0 ]; then
echo "Error during deployment"
exit 1
else
echo "Success ! "
helm delete $d || true
fi
done

L'exécution de ce script va déployer chaque chart individuellement en lançant la commande helm test, permettant de lancer des tests (vérifier un port, vérifier le status d'une page web etc..). Si le test échoue, helm test renverra un exitcode à 1, et le script créera une erreur.

J'ai également la vérification de la présence d'un fichier .no_ci qui, comme son nom l'indique, permet de "skip" un chart. Cela permet de ne pas déployer certains charts dans le cluster de test. (Par exemple : mon chart plex-nfs qui ne peut pas fonctionner dans Github Action, ou un chart OpenLDAP).

Ressources ?

GitHub Action est limitée à 2000 minutes de CI mensuels. Avec une petite dizaine de charts, mes tests durent environ 5min. (soit 400 tests par mois)

Je suis conscient qu'à notre échelle : c'est suffisant. Mais à garder en tête si on commence à avoir un dépôt similaire à k8s-at-home.

Un README dynamique

Et pour rendre votre dépôt GitHub agréable pour vos utilisateurs, nous pouvons faire un README évoluant au fur et à mesure que vous créez vos charts.

L'idée est donc de créer un tableau comme celui-ci :

NameDescriptionChart VersionApp Version
baikalBaïkal is a lightweight CalDAV+CardDAV server0.1.60.9.2

Ces informations (nom, description, version) sont toutes accessibles depuis les fichiers Chart.yaml présents dans nos charts.

apiVersion: v2
name: baikal
description: Baïkal is a lightweight CalDAV+CardDAV server
type: application
version: 0.1.6
appVersion: "0.9.2"
keywords:
- baikal
home: https://sabre.io/baikal/
maintainers:
- email: github@thoughtless.eu
name: QJOLY
url: https://thebidouilleur.xyz
sources:
- https://github.com/sabre-io/Baikal
- https://github.com/QJoly/helm-charts

Du coup, vous rappelez-vous des scripts helm_lint.sh et helm_deploy.sh ? Eh bien voici le tonton : get_readme.py.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys, logging, os, yaml
from pathlib import Path
from glob import glob
from yaml.loader import SafeLoader
from jinja2 import Template

def main():
files = glob('../../**/Chart.yaml', recursive=True)
charts = []
for chart in files:
with open(chart) as f:
data = yaml.load(f, Loader=SafeLoader)
print(f"nom : {data['name']} \ndescription: {data['description']}\nversion chart: {data['version']}\nversion app: {data['appVersion']}")
charts.append([data['name'],data['description'],data['version'], data['appVersion']])
print(f"Nombre de charts: {len(charts)}")
table_template=Path('table.j2').read_text()
tm = Template(table_template)
tableValue = tm.render({'charts':charts})
print("----")
readme_template=Path('./README.md.tmpl').read_text().replace("CHARTS_TABLE",tableValue).replace(""", '"')
print(readme_template)
Path("../../README.md").write_text(readme_template)

if __name__ == "__main__":
main()

Ce script Python va récupérer les différentes balises contenues dans les Charts.yaml, puis va générer un tableau Markdown à partir du fichier table.j2 (en jinja2, tout comme Helm), et va créer un README à partir du tableau ainsi que du fichier README.md.tmpl (contenant de la mise en page, et des informations supplémentaires).

Voici le résultat actuel :

Visuel actuel

à ajouter sur notre CI :

       - name: Modifying the readme on main
continue-on-error: true
run: |
git pull
git checkout main
cd .github/workflows/
python -m pip install -r requirements.txt
python3 get_readme.py
cd ../..
git add README.md
git commit -m ":lock: Auto-Update README with Charts versions"
git push

Créer une page d'accueil

Dans cet état, lorsque nous donnons l'URL du dépôt à ajouter aux clients helm de nos utilisateurs : ceux-ci accèderont sur une 404 (logique, le seul fichier créé est l'index.yaml). Mon idée est de reprendre les mêmes informations du README pour l'afficher sous forme de page web.

Si à la base, je voulais créer un système similaire au README (mais en HTML), j'ai opté pour la conversion du Markdown en HTML. Et un outil très utilisé est : pandoc

Nous allons donc utiliser Pandoc pour convertir notre readme en HTML, l'instruction est simple :

       - name: Setup Pandoc
uses: nikeee/setup-pandoc@v1

- name: Modifying index.html
continue-on-error: true
run: |
index=$(pandoc --from markdown_github README.md --to html5 --standalone --toc --citeproc --no-highlight)
git checkout gh-pages
echo $index > index.html
git add index.html
git commit -m "[AUTO] Update index.html of gh-pages"
git push

On ne peut pas dire que du grand art, mais le résultat est plutôt propre (dieu merci, Pandoc intègre du CSS).

Rendu du site

Dans mon cas, je rajoute même mon script JS de compteur de vues à cette étape.

Conclusion

Créer un dépôt Helm avec les petites modifications que je vous propose ne fera pas de vous un grand développeur (?) de charts. Mais ces outils vous permettrons de vous faciliter le travail, et de proposer une expérience agréable pour les personnes utilisant vos codes.

Il est toujours possible d'aller de plus en plus loin. Je pense notamment à RenovateBot qui peut vous proposer des modifications (ex: mettre à jour une image par défaut).

N'hésitez pas à me faire parvenir vos retours (mail/Twitter) ou vos propositions d'améliorations.

PS: Pour obtenir le CI complet (en reprenant chaque étape de cette page), vous pouvez visionner mon dépot ici.

· 42 minutes de lecture
TheBidouilleur

Introduction

On parle beaucoup de Terraform comme étant "la télécommande" du DevOps, celui-ci possède des chiffres assez conséquents : 2626 providers et 11397 modules.

Un providers ?

Un module est une intégration de Terraform avec un outil externe. On peut lancer un playbook, créer une instance sous AWS, ou même envoyer un message sur Slack.

J'utilise activement Terraform dans mes déploiements (création et/ou peuplement d'une VM sous Proxmox/LibVirt), mais depuis que mon infrastructure est basée sous Kubernetes, je me demande de la place que Terraform occupe dans mes déploiements. (Hors première installation du cluster)

Je me suis donc intéressé à Terraform et Kubernetes ensembles.

Les avantages de Kubernetes et Terraform ?

Kubernetes fonctionne bien sans Terraform, pourquoi commencer à rajouter des outils dans l'équation ?

Kubernetes souffre d'un grand mal : le YAML. Et même si j'adore le YAML (Vraiment, je veux pas retourner sur du JSON…) : celui-ci reste un simple format et non un réel langage de programmation.

C'est pourquoi le HCL peut potentiellement nous ouvrir des portes en proposant des intégrations à d'autres providers.

Un petit exemple en amuse-bouche

Une configmap en YAML

Si jamais je souhaite créer une configmap contenant un YAML pour une application. Voici le fichier que je souhaite stocker :

twitter=thebidouilleur
jobs=developper
favorite.meal=rougail
vehicule=electricunicycle

On peut créer notre fichier YAML avec la bonne entête, et indenter le contenu de notre fichier pour que YAML le reconnaisse comme un block de texte.

apiVersion: v1
kind: ConfigMap
metadata:
name: data-user
namespace: hcl
data:
data.ini: |
twitter=thebidouilleur
jobs=developper
favorite.meal=rougail
vehicule=electricunicycle

Facile, non?

Et maintenant on tente la même chose avec ce fichier ?
{
"pokemon": [{
"id": 1,
"num": "001",
"name": "Bulbasaur",
"img": "http://www.serebii.net/pokemongo/pokemon/001.png",
"type": [
"Grass",
"Poison"
],
"height": "0.71 m",
"weight": "6.9 kg",
"candy": "Bulbasaur Candy",
"candy_count": 25,
"egg": "2 km",
"spawn_chance": 0.69,
"avg_spawns": 69,
"spawn_time": "20:00",
"multipliers": [1.58],
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"next_evolution": [{
"num": "002",
"name": "Ivysaur"
}, {
"num": "003",
"name": "Venusaur"
}]
}, {
"id": 2,
"num": "002",
"name": "Ivysaur",
"img": "http://www.serebii.net/pokemongo/pokemon/002.png",
"type": [
"Grass",
"Poison"
],
"height": "0.99 m",
"weight": "13.0 kg",
"candy": "Bulbasaur Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.042,
"avg_spawns": 4.2,
"spawn_time": "07:00",
"multipliers": [
1.2,
1.6
],
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"prev_evolution": [{
"num": "001",
"name": "Bulbasaur"
}],
"next_evolution": [{
"num": "003",
"name": "Venusaur"
}]
}, {
"id": 3,
"num": "003",
"name": "Venusaur",
"img": "http://www.serebii.net/pokemongo/pokemon/003.png",
"type": [
"Grass",
"Poison"
],
"height": "2.01 m",
"weight": "100.0 kg",
"candy": "Bulbasaur Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.017,
"avg_spawns": 1.7,
"spawn_time": "11:30",
"multipliers": null,
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"prev_evolution": [{
"num": "001",
"name": "Bulbasaur"
}, {
"num": "002",
"name": "Ivysaur"
}]
}, {
"id": 4,
"num": "004",
"name": "Charmander",
"img": "http://www.serebii.net/pokemongo/pokemon/004.png",
"type": [
"Fire"
],
"height": "0.61 m",
"weight": "8.5 kg",
"candy": "Charmander Candy",
"candy_count": 25,
"egg": "2 km",
"spawn_chance": 0.253,
"avg_spawns": 25.3,
"spawn_time": "08:45",
"multipliers": [1.65],
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"next_evolution": [{
"num": "005",
"name": "Charmeleon"
}, {
"num": "006",
"name": "Charizard"
}]
}, {
"id": 5,
"num": "005",
"name": "Charmeleon",
"img": "http://www.serebii.net/pokemongo/pokemon/005.png",
"type": [
"Fire"
],
"height": "1.09 m",
"weight": "19.0 kg",
"candy": "Charmander Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.012,
"avg_spawns": 1.2,
"spawn_time": "19:00",
"multipliers": [1.79],
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"prev_evolution": [{
"num": "004",
"name": "Charmander"
}],
"next_evolution": [{
"num": "006",
"name": "Charizard"
}]
}, {
"id": 6,
"num": "006",
"name": "Charizard",
"img": "http://www.serebii.net/pokemongo/pokemon/006.png",
"type": [
"Fire",
"Flying"
],
"height": "1.70 m",
"weight": "90.5 kg",
"candy": "Charmander Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0031,
"avg_spawns": 0.31,
"spawn_time": "13:34",
"multipliers": null,
"weaknesses": [
"Water",
"Electric",
"Rock"
],
"prev_evolution": [{
"num": "004",
"name": "Charmander"
}, {
"num": "005",
"name": "Charmeleon"
}]
}, {
"id": 7,
"num": "007",
"name": "Squirtle",
"img": "http://www.serebii.net/pokemongo/pokemon/007.png",
"type": [
"Water"
],
"height": "0.51 m",
"weight": "9.0 kg",
"candy": "Squirtle Candy",
"candy_count": 25,
"egg": "2 km",
"spawn_chance": 0.58,
"avg_spawns": 58,
"spawn_time": "04:25",
"multipliers": [2.1],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "008",
"name": "Wartortle"
}, {
"num": "009",
"name": "Blastoise"
}]
}, {
"id": 8,
"num": "008",
"name": "Wartortle",
"img": "http://www.serebii.net/pokemongo/pokemon/008.png",
"type": [
"Water"
],
"height": "0.99 m",
"weight": "22.5 kg",
"candy": "Squirtle Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.034,
"avg_spawns": 3.4,
"spawn_time": "07:02",
"multipliers": [1.4],
"weaknesses": [
"Electric",
"Grass"
],
"prev_evolution": [{
"num": "007",
"name": "Squirtle"
}],
"next_evolution": [{
"num": "009",
"name": "Blastoise"
}]
}, {
"id": 9,
"num": "009",
"name": "Blastoise",
"img": "http://www.serebii.net/pokemongo/pokemon/009.png",
"type": [
"Water"
],
"height": "1.60 m",
"weight": "85.5 kg",
"candy": "Squirtle Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0067,
"avg_spawns": 0.67,
"spawn_time": "00:06",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass"
],
"prev_evolution": [{
"num": "007",
"name": "Squirtle"
}, {
"num": "008",
"name": "Wartortle"
}]
}, {
"id": 10,
"num": "010",
"name": "Caterpie",
"img": "http://www.serebii.net/pokemongo/pokemon/010.png",
"type": [
"Bug"
],
"height": "0.30 m",
"weight": "2.9 kg",
"candy": "Caterpie Candy",
"candy_count": 12,
"egg": "2 km",
"spawn_chance": 3.032,
"avg_spawns": 303.2,
"spawn_time": "16:35",
"multipliers": [1.05],
"weaknesses": [
"Fire",
"Flying",
"Rock"
],
"next_evolution": [{
"num": "011",
"name": "Metapod"
}, {
"num": "012",
"name": "Butterfree"
}]
}, {
"id": 11,
"num": "011",
"name": "Metapod",
"img": "http://www.serebii.net/pokemongo/pokemon/011.png",
"type": [
"Bug"
],
"height": "0.71 m",
"weight": "9.9 kg",
"candy": "Caterpie Candy",
"candy_count": 50,
"egg": "Not in Eggs",
"spawn_chance": 0.187,
"avg_spawns": 18.7,
"spawn_time": "02:11",
"multipliers": [
3.55,
3.79
],
"weaknesses": [
"Fire",
"Flying",
"Rock"
],
"prev_evolution": [{
"num": "010",
"name": "Caterpie"
}],
"next_evolution": [{
"num": "012",
"name": "Butterfree"
}]
}, {
"id": 12,
"num": "012",
"name": "Butterfree",
"img": "http://www.serebii.net/pokemongo/pokemon/012.png",
"type": [
"Bug",
"Flying"
],
"height": "1.09 m",
"weight": "32.0 kg",
"candy": "Caterpie Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.022,
"avg_spawns": 2.2,
"spawn_time": "05:23",
"multipliers": null,
"weaknesses": [
"Fire",
"Electric",
"Ice",
"Flying",
"Rock"
],
"prev_evolution": [{
"num": "010",
"name": "Caterpie"
}, {
"num": "011",
"name": "Metapod"
}]
}, {
"id": 13,
"num": "013",
"name": "Weedle",
"img": "http://www.serebii.net/pokemongo/pokemon/013.png",
"type": [
"Bug",
"Poison"
],
"height": "0.30 m",
"weight": "3.2 kg",
"candy": "Weedle Candy",
"candy_count": 12,
"egg": "2 km",
"spawn_chance": 7.12,
"avg_spawns": 712,
"spawn_time": "02:21",
"multipliers": [
1.01,
1.09
],
"weaknesses": [
"Fire",
"Flying",
"Psychic",
"Rock"
],
"next_evolution": [{
"num": "014",
"name": "Kakuna"
}, {
"num": "015",
"name": "Beedrill"
}]
}, {
"id": 14,
"num": "014",
"name": "Kakuna",
"img": "http://www.serebii.net/pokemongo/pokemon/014.png",
"type": [
"Bug",
"Poison"
],
"height": "0.61 m",
"weight": "10.0 kg",
"candy": "Weedle Candy",
"candy_count": 50,
"egg": "Not in Eggs",
"spawn_chance": 0.44,
"avg_spawns": 44,
"spawn_time": "02:30",
"multipliers": [
3.01,
3.41
],
"weaknesses": [
"Fire",
"Flying",
"Psychic",
"Rock"
],
"prev_evolution": [{
"num": "013",
"name": "Weedle"
}],
"next_evolution": [{
"num": "015",
"name": "Beedrill"
}]
}, {
"id": 15,
"num": "015",
"name": "Beedrill",
"img": "http://www.serebii.net/pokemongo/pokemon/015.png",
"type": [
"Bug",
"Poison"
],
"height": "0.99 m",
"weight": "29.5 kg",
"candy": "Weedle Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.051,
"avg_spawns": 5.1,
"spawn_time": "04:50",
"multipliers": null,
"weaknesses": [
"Fire",
"Flying",
"Psychic",
"Rock"
],
"prev_evolution": [{
"num": "013",
"name": "Weedle"
}, {
"num": "014",
"name": "Kakuna"
}]
}, {
"id": 16,
"num": "016",
"name": "Pidgey",
"img": "http://www.serebii.net/pokemongo/pokemon/016.png",
"type": [
"Normal",
"Flying"
],
"height": "0.30 m",
"weight": "1.8 kg",
"candy": "Pidgey Candy",
"candy_count": 12,
"egg": "2 km",
"spawn_chance": 15.98,
"avg_spawns": 1.598,
"spawn_time": "01:34",
"multipliers": [
1.71,
1.92
],
"weaknesses": [
"Electric",
"Rock"
],
"next_evolution": [{
"num": "017",
"name": "Pidgeotto"
}, {
"num": "018",
"name": "Pidgeot"
}]
}, {
"id": 17,
"num": "017",
"name": "Pidgeotto",
"img": "http://www.serebii.net/pokemongo/pokemon/017.png",
"type": [
"Normal",
"Flying"
],
"height": "1.09 m",
"weight": "30.0 kg",
"candy": "Pidgey Candy",
"candy_count": 50,
"egg": "Not in Eggs",
"spawn_chance": 1.02,
"avg_spawns": 102,
"spawn_time": "01:30",
"multipliers": [1.79],
"weaknesses": [
"Electric",
"Rock"
],
"prev_evolution": [{
"num": "016",
"name": "Pidgey"
}],
"next_evolution": [{
"num": "018",
"name": "Pidgeot"
}]
}, {
"id": 18,
"num": "018",
"name": "Pidgeot",
"img": "http://www.serebii.net/pokemongo/pokemon/018.png",
"type": [
"Normal",
"Flying"
],
"height": "1.50 m",
"weight": "39.5 kg",
"candy": "Pidgey Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.13,
"avg_spawns": 13,
"spawn_time": "01:50",
"multipliers": null,
"weaknesses": [
"Electric",
"Rock"
],
"prev_evolution": [{
"num": "016",
"name": "Pidgey"
}, {
"num": "017",
"name": "Pidgeotto"
}]
}, {
"id": 19,
"num": "019",
"name": "Rattata",
"img": "http://www.serebii.net/pokemongo/pokemon/019.png",
"type": [
"Normal"
],
"height": "0.30 m",
"weight": "3.5 kg",
"candy": "Rattata Candy",
"candy_count": 25,
"egg": "2 km",
"spawn_chance": 13.05,
"avg_spawns": 1.305,
"spawn_time": "01:55",
"multipliers": [
2.55,
2.73
],
"weaknesses": [
"Fighting"
],
"next_evolution": [{
"num": "020",
"name": "Raticate"
}]
}, {
"id": 20,
"num": "020",
"name": "Raticate",
"img": "http://www.serebii.net/pokemongo/pokemon/020.png",
"type": [
"Normal"
],
"height": "0.71 m",
"weight": "18.5 kg",
"candy": "Rattata Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.41,
"avg_spawns": 41,
"spawn_time": "01:56",
"multipliers": null,
"weaknesses": [
"Fighting"
],
"prev_evolution": [{
"num": "019",
"name": "Rattata"
}]
}, {
"id": 21,
"num": "021",
"name": "Spearow",
"img": "http://www.serebii.net/pokemongo/pokemon/021.png",
"type": [
"Normal",
"Flying"
],
"height": "0.30 m",
"weight": "2.0 kg",
"candy": "Spearow Candy",
"candy_count": 50,
"egg": "2 km",
"spawn_chance": 4.73,
"avg_spawns": 473,
"spawn_time": "12:25",
"multipliers": [
2.66,
2.68
],
"weaknesses": [
"Electric",
"Rock"
],
"next_evolution": [{
"num": "022",
"name": "Fearow"
}]
}, {
"id": 22,
"num": "022",
"name": "Fearow",
"img": "http://www.serebii.net/pokemongo/pokemon/022.png",
"type": [
"Normal",
"Flying"
],
"height": "1.19 m",
"weight": "38.0 kg",
"candy": "Spearow Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.15,
"avg_spawns": 15,
"spawn_time": "01:11",
"multipliers": null,
"weaknesses": [
"Electric",
"Rock"
],
"prev_evolution": [{
"num": "021",
"name": "Spearow"
}]
}, {
"id": 23,
"num": "023",
"name": "Ekans",
"img": "http://www.serebii.net/pokemongo/pokemon/023.png",
"type": [
"Poison"
],
"height": "2.01 m",
"weight": "6.9 kg",
"candy": "Ekans Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 2.27,
"avg_spawns": 227,
"spawn_time": "12:20",
"multipliers": [
2.21,
2.27
],
"weaknesses": [
"Ground",
"Psychic"
],
"next_evolution": [{
"num": "024",
"name": "Arbok"
}]
}, {
"id": 24,
"num": "024",
"name": "Arbok",
"img": "http://www.serebii.net/pokemongo/pokemon/024.png",
"type": [
"Poison"
],
"height": "3.51 m",
"weight": "65.0 kg",
"candy": "Ekans Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.072,
"avg_spawns": 7.2,
"spawn_time": "01:50",
"multipliers": null,
"weaknesses": [
"Ground",
"Psychic"
],
"prev_evolution": [{
"num": "023",
"name": "Ekans"
}]
}, {
"id": 25,
"num": "025",
"name": "Pikachu",
"img": "http://www.serebii.net/pokemongo/pokemon/025.png",
"type": [
"Electric"
],
"height": "0.41 m",
"weight": "6.0 kg",
"candy": "Pikachu Candy",
"candy_count": 50,
"egg": "2 km",
"spawn_chance": 0.21,
"avg_spawns": 21,
"spawn_time": "04:00",
"multipliers": [2.34],
"weaknesses": [
"Ground"
],
"next_evolution": [{
"num": "026",
"name": "Raichu"
}]
}, {
"id": 26,
"num": "026",
"name": "Raichu",
"img": "http://www.serebii.net/pokemongo/pokemon/026.png",
"type": [
"Electric"
],
"height": "0.79 m",
"weight": "30.0 kg",
"candy": "Pikachu Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0076,
"avg_spawns": 0.76,
"spawn_time": "23:58",
"multipliers": null,
"weaknesses": [
"Ground"
],
"prev_evolution": [{
"num": "025",
"name": "Pikachu"
}]
}, {
"id": 27,
"num": "027",
"name": "Sandshrew",
"img": "http://www.serebii.net/pokemongo/pokemon/027.png",
"type": [
"Ground"
],
"height": "0.61 m",
"weight": "12.0 kg",
"candy": "Sandshrew Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 1.11,
"avg_spawns": 111,
"spawn_time": "01:58",
"multipliers": [2.45],
"weaknesses": [
"Water",
"Grass",
"Ice"
],
"next_evolution": [{
"num": "028",
"name": "Sandslash"
}]
}, {
"id": 28,
"num": "028",
"name": "Sandslash",
"img": "http://www.serebii.net/pokemongo/pokemon/028.png",
"type": [
"Ground"
],
"height": "0.99 m",
"weight": "29.5 kg",
"candy": "Sandshrew Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.037,
"avg_spawns": 3.7,
"spawn_time": "12:34",
"multipliers": null,
"weaknesses": [
"Water",
"Grass",
"Ice"
],
"prev_evolution": [{
"num": "027",
"name": "Sandshrew"
}]
}, {
"id": 29,
"num": "029",
"name": "Nidoran ♀ (Female)",
"img": "http://www.serebii.net/pokemongo/pokemon/029.png",
"type": [
"Poison"
],
"height": "0.41 m",
"weight": "7.0 kg",
"candy": "Nidoran ♀ (Female) Candy",
"candy_count": 25,
"egg": "5 km",
"spawn_chance": 1.38,
"avg_spawns": 138,
"spawn_time": "01:51",
"multipliers": [
1.63,
2.48
],
"weaknesses": [
"Ground",
"Psychic"
],
"next_evolution": [{
"num": "030",
"name": "Nidorina"
}, {
"num": "031",
"name": "Nidoqueen"
}]
}, {
"id": 30,
"num": "030",
"name": "Nidorina",
"img": "http://www.serebii.net/pokemongo/pokemon/030.png",
"type": [
"Poison"
],
"height": "0.79 m",
"weight": "20.0 kg",
"candy": "Nidoran ♀ (Female) Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.088,
"avg_spawns": 8.8,
"spawn_time": "07:22",
"multipliers": [
1.83,
2.48
],
"weaknesses": [
"Ground",
"Psychic"
],
"prev_evolution": [{
"num": "029",
"name": "Nidoran(Female)"
}],
"next_evolution": [{
"num": "031",
"name": "Nidoqueen"
}]
}, {
"id": 31,
"num": "031",
"name": "Nidoqueen",
"img": "http://www.serebii.net/pokemongo/pokemon/031.png",
"type": [
"Poison",
"Ground"
],
"height": "1.30 m",
"weight": "60.0 kg",
"candy": "Nidoran ♀ (Female) Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.012,
"avg_spawns": 1.2,
"spawn_time": "12:35",
"multipliers": null,
"weaknesses": [
"Water",
"Ice",
"Ground",
"Psychic"
],
"prev_evolution": [{
"num": "029",
"name": "Nidoran(Female)"
}, {
"num": "030",
"name": "Nidorina"
}]
}, {
"id": 32,
"num": "032",
"name": "Nidoran ♂ (Male)",
"img": "http://www.serebii.net/pokemongo/pokemon/032.png",
"type": [
"Poison"
],
"height": "0.51 m",
"weight": "9.0 kg",
"candy": "Nidoran ♂ (Male) Candy",
"candy_count": 25,
"egg": "5 km",
"spawn_chance": 1.31,
"avg_spawns": 131,
"spawn_time": "01:12",
"multipliers": [
1.64,
1.7
],
"weaknesses": [
"Ground",
"Psychic"
],
"next_evolution": [{
"num": "033",
"name": "Nidorino"
}, {
"num": "034",
"name": "Nidoking"
}]
}, {
"id": 33,
"num": "033",
"name": "Nidorino",
"img": "http://www.serebii.net/pokemongo/pokemon/033.png",
"type": [
"Poison"
],
"height": "0.89 m",
"weight": "19.5 kg",
"candy": "Nidoran ♂ (Male) Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.083,
"avg_spawns": 8.3,
"spawn_time": "09:02",
"multipliers": [1.83],
"weaknesses": [
"Ground",
"Psychic"
],
"prev_evolution": [{
"num": "032",
"name": "Nidoran(Male)"
}],
"next_evolution": [{
"num": "034",
"name": "Nidoking"
}]
}, {
"id": 34,
"num": "034",
"name": "Nidoking",
"img": "http://www.serebii.net/pokemongo/pokemon/034.png",
"type": [
"Poison",
"Ground"
],
"height": "1.40 m",
"weight": "62.0 kg",
"candy": "Nidoran ♂ (Male) Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.017,
"avg_spawns": 1.7,
"spawn_time": "12:16",
"multipliers": null,
"weaknesses": [
"Water",
"Ice",
"Ground",
"Psychic"
],
"prev_evolution": [{
"num": "032",
"name": "Nidoran(Male)"
}, {
"num": "033",
"name": "Nidorino"
}]
}, {
"id": 35,
"num": "035",
"name": "Clefairy",
"img": "http://www.serebii.net/pokemongo/pokemon/035.png",
"type": [
"Normal"
],
"height": "0.61 m",
"weight": "7.5 kg",
"candy": "Clefairy Candy",
"candy_count": 50,
"egg": "2 km",
"spawn_chance": 0.92,
"avg_spawns": 92,
"spawn_time": "03:30",
"multipliers": [
2.03,
2.14
],
"weaknesses": [
"Fighting"
],
"next_evolution": [{
"num": "036",
"name": "Clefable"
}]
}, {
"id": 36,
"num": "036",
"name": "Clefable",
"img": "http://www.serebii.net/pokemongo/pokemon/036.png",
"type": [
"Normal"
],
"height": "1.30 m",
"weight": "40.0 kg",
"candy": "Clefairy Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.012,
"avg_spawns": 1.2,
"spawn_time": "03:29",
"multipliers": null,
"weaknesses": [
"Fighting"
],
"prev_evolution": [{
"num": "035",
"name": "Clefairy"
}]
}, {
"id": 37,
"num": "037",
"name": "Vulpix",
"img": "http://www.serebii.net/pokemongo/pokemon/037.png",
"type": [
"Fire"
],
"height": "0.61 m",
"weight": "9.9 kg",
"candy": "Vulpix Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.22,
"avg_spawns": 22,
"spawn_time": "13:43",
"multipliers": [
2.74,
2.81
],
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"next_evolution": [{
"num": "038",
"name": "Ninetales"
}]
}, {
"id": 38,
"num": "038",
"name": "Ninetales",
"img": "http://www.serebii.net/pokemongo/pokemon/038.png",
"type": [
"Fire"
],
"height": "1.09 m",
"weight": "19.9 kg",
"candy": "Vulpix Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0077,
"avg_spawns": 0.77,
"spawn_time": "01:32",
"multipliers": null,
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"prev_evolution": [{
"num": "037",
"name": "Vulpix"
}]
}, {
"id": 39,
"num": "039",
"name": "Jigglypuff",
"img": "http://www.serebii.net/pokemongo/pokemon/039.png",
"type": [
"Normal"
],
"height": "0.51 m",
"weight": "5.5 kg",
"candy": "Jigglypuff Candy",
"candy_count": 50,
"egg": "2 km",
"spawn_chance": 0.39,
"avg_spawns": 39,
"spawn_time": "08:46",
"multipliers": [1.85],
"weaknesses": [
"Fighting"
],
"next_evolution": [{
"num": "040",
"name": "Wigglytuff"
}]
}, {
"id": 40,
"num": "040",
"name": "Wigglytuff",
"img": "http://www.serebii.net/pokemongo/pokemon/040.png",
"type": [
"Normal"
],
"height": "0.99 m",
"weight": "12.0 kg",
"candy": "Jigglypuff Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.018,
"avg_spawns": 1.8,
"spawn_time": "12:28",
"multipliers": null,
"weaknesses": [
"Fighting"
],
"prev_evolution": [{
"num": "039",
"name": "Jigglypuff"
}]
}, {
"id": 41,
"num": "041",
"name": "Zubat",
"img": "http://www.serebii.net/pokemongo/pokemon/041.png",
"type": [
"Poison",
"Flying"
],
"height": "0.79 m",
"weight": "7.5 kg",
"candy": "Zubat Candy",
"candy_count": 50,
"egg": "2 km",
"spawn_chance": 6.52,
"avg_spawns": 652,
"spawn_time": "12:28",
"multipliers": [
2.6,
3.67
],
"weaknesses": [
"Electric",
"Ice",
"Psychic",
"Rock"
],
"next_evolution": [{
"num": "042",
"name": "Golbat"
}]
}, {
"id": 42,
"num": "042",
"name": "Golbat",
"img": "http://www.serebii.net/pokemongo/pokemon/042.png",
"type": [
"Poison",
"Flying"
],
"height": "1.60 m",
"weight": "55.0 kg",
"candy": "Zubat Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.42,
"avg_spawns": 42,
"spawn_time": "02:15",
"multipliers": null,
"weaknesses": [
"Electric",
"Ice",
"Psychic",
"Rock"
],
"prev_evolution": [{
"num": "041",
"name": "Zubat"
}]
}, {
"id": 43,
"num": "043",
"name": "Oddish",
"img": "http://www.serebii.net/pokemongo/pokemon/043.png",
"type": [
"Grass",
"Poison"
],
"height": "0.51 m",
"weight": "5.4 kg",
"candy": "Oddish Candy",
"candy_count": 25,
"egg": "5 km",
"spawn_chance": 1.02,
"avg_spawns": 102,
"spawn_time": "03:58",
"multipliers": [1.5],
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"next_evolution": [{
"num": "044",
"name": "Gloom"
}, {
"num": "045",
"name": "Vileplume"
}]
}, {
"id": 44,
"num": "044",
"name": "Gloom",
"img": "http://www.serebii.net/pokemongo/pokemon/044.png",
"type": [
"Grass",
"Poison"
],
"height": "0.79 m",
"weight": "8.6 kg",
"candy": "Oddish Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.064,
"avg_spawns": 6.4,
"spawn_time": "11:33",
"multipliers": [1.49],
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"prev_evolution": [{
"num": "043",
"name": "Oddish"
}],
"next_evolution": [{
"num": "045",
"name": "Vileplume"
}]
}, {
"id": 45,
"num": "045",
"name": "Vileplume",
"img": "http://www.serebii.net/pokemongo/pokemon/045.png",
"type": [
"Grass",
"Poison"
],
"height": "1.19 m",
"weight": "18.6 kg",
"candy": "Oddish Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0097,
"avg_spawns": 0.97,
"spawn_time": "23:58",
"multipliers": null,
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"prev_evolution": [{
"num": "043",
"name": "Oddish"
}, {
"num": "044",
"name": "Gloom"
}]
}, {
"id": 46,
"num": "046",
"name": "Paras",
"img": "http://www.serebii.net/pokemongo/pokemon/046.png",
"type": [
"Bug",
"Grass"
],
"height": "0.30 m",
"weight": "5.4 kg",
"candy": "Paras Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 2.36,
"avg_spawns": 236,
"spawn_time": "01:42",
"multipliers": [2.02],
"weaknesses": [
"Fire",
"Ice",
"Poison",
"Flying",
"Bug",
"Rock"
],
"next_evolution": [{
"num": "047",
"name": "Parasect"
}]
}, {
"id": 47,
"num": "047",
"name": "Parasect",
"img": "http://www.serebii.net/pokemongo/pokemon/047.png",
"type": [
"Bug",
"Grass"
],
"height": "0.99 m",
"weight": "29.5 kg",
"candy": "Paras Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.074,
"avg_spawns": 7.4,
"spawn_time": "01:22",
"multipliers": null,
"weaknesses": [
"Fire",
"Ice",
"Poison",
"Flying",
"Bug",
"Rock"
],
"prev_evolution": [{
"num": "046",
"name": "Paras"
}]
}, {
"id": 48,
"num": "048",
"name": "Venonat",
"img": "http://www.serebii.net/pokemongo/pokemon/048.png",
"type": [
"Bug",
"Poison"
],
"height": "0.99 m",
"weight": "30.0 kg",
"candy": "Venonat Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 2.28,
"avg_spawns": 228,
"spawn_time": "02:31",
"multipliers": [
1.86,
1.9
],
"weaknesses": [
"Fire",
"Flying",
"Psychic",
"Rock"
],
"next_evolution": [{
"num": "049",
"name": "Venomoth"
}]
}, {
"id": 49,
"num": "049",
"name": "Venomoth",
"img": "http://www.serebii.net/pokemongo/pokemon/049.png",
"type": [
"Bug",
"Poison"
],
"height": "1.50 m",
"weight": "12.5 kg",
"candy": "Venonat Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.072,
"avg_spawns": 7.2,
"spawn_time": "23:40",
"multipliers": null,
"weaknesses": [
"Fire",
"Flying",
"Psychic",
"Rock"
],
"prev_evolution": [{
"num": "048",
"name": "Venonat"
}]
}, {
"id": 50,
"num": "050",
"name": "Diglett",
"img": "http://www.serebii.net/pokemongo/pokemon/050.png",
"type": [
"Ground"
],
"height": "0.20 m",
"weight": "0.8 kg",
"candy": "Diglett Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.40,
"avg_spawns": 40,
"spawn_time": "02:22",
"multipliers": [2.69],
"weaknesses": [
"Water",
"Grass",
"Ice"
],
"next_evolution": [{
"num": "051",
"name": "Dugtrio"
}]
}, {
"id": 51,
"num": "051",
"name": "Dugtrio",
"img": "http://www.serebii.net/pokemongo/pokemon/051.png",
"type": [
"Ground"
],
"height": "0.71 m",
"weight": "33.3 kg",
"candy": "Dugtrio",
"egg": "Not in Eggs",
"spawn_chance": 0.014,
"avg_spawns": 1.4,
"spawn_time": "12:37",
"multipliers": null,
"weaknesses": [
"Water",
"Grass",
"Ice"
],
"prev_evolution": [{
"num": "050",
"name": "Diglett"
}]
}, {
"id": 52,
"num": "052",
"name": "Meowth",
"img": "http://www.serebii.net/pokemongo/pokemon/052.png",
"type": [
"Normal"
],
"height": "0.41 m",
"weight": "4.2 kg",
"candy": "Meowth Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.86,
"avg_spawns": 86,
"spawn_time": "02:54",
"multipliers": [1.98],
"weaknesses": [
"Fighting"
],
"next_evolution": [{
"num": "053",
"name": "Persian"
}]
}, {
"id": 53,
"num": "053",
"name": "Persian",
"img": "http://www.serebii.net/pokemongo/pokemon/053.png",
"type": [
"Normal"
],
"height": "0.99 m",
"weight": "32.0 kg",
"candy": "Meowth Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.022,
"avg_spawns": 2.2,
"spawn_time": "02:44",
"multipliers": null,
"weaknesses": [
"Fighting"
],
"prev_evolution": [{
"num": "052",
"name": "Meowth"
}]
}, {
"id": 54,
"num": "054",
"name": "Psyduck",
"img": "http://www.serebii.net/pokemongo/pokemon/054.png",
"type": [
"Water"
],
"height": "0.79 m",
"weight": "19.6 kg",
"candy": "Psyduck Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 2.54,
"avg_spawns": 254,
"spawn_time": "03:41",
"multipliers": [2.27],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "055",
"name": "Golduck"
}]
}, {
"id": 55,
"num": "055",
"name": "Golduck",
"img": "http://www.serebii.net/pokemongo/pokemon/055.png",
"type": [
"Water"
],
"height": "1.70 m",
"weight": "76.6 kg",
"candy": "Psyduck Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.087,
"avg_spawns": 8.7,
"spawn_time": "23:06",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass"
],
"prev_evolution": [{
"num": "054",
"name": "Psyduck"
}]
}, {
"id": 56,
"num": "056",
"name": "Mankey",
"img": "http://www.serebii.net/pokemongo/pokemon/056.png",
"type": [
"Fighting"
],
"height": "0.51 m",
"weight": "28.0 kg",
"candy": "Mankey Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.92,
"avg_spawns": 92,
"spawn_time": "12:52",
"multipliers": [
2.17,
2.28
],
"weaknesses": [
"Flying",
"Psychic",
"Fairy"
],
"next_evolution": [{
"num": "057",
"name": "Primeape"
}]
}, {
"id": 57,
"num": "057",
"name": "Primeape",
"img": "http://www.serebii.net/pokemongo/pokemon/057.png",
"type": [
"Fighting"
],
"height": "0.99 m",
"weight": "32.0 kg",
"candy": "Mankey Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.031,
"avg_spawns": 3.1,
"spawn_time": "12:33",
"multipliers": null,
"weaknesses": [
"Flying",
"Psychic",
"Fairy"
],
"prev_evolution": [{
"num": "056",
"name": "Mankey"
}]
}, {
"id": 58,
"num": "058",
"name": "Growlithe",
"img": "http://www.serebii.net/pokemongo/pokemon/058.png",
"type": [
"Fire"
],
"height": "0.71 m",
"weight": "19.0 kg",
"candy": "Growlithe Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.92,
"avg_spawns": 92,
"spawn_time": "03:57",
"multipliers": [
2.31,
2.36
],
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"next_evolution": [{
"num": "059",
"name": "Arcanine"
}]
}, {
"id": 59,
"num": "059",
"name": "Arcanine",
"img": "http://www.serebii.net/pokemongo/pokemon/059.png",
"type": [
"Fire"
],
"height": "1.91 m",
"weight": "155.0 kg",
"candy": "Growlithe Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.017,
"avg_spawns": 1.7,
"spawn_time": "03:11",
"multipliers": null,
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"prev_evolution": [{
"num": "058",
"name": "Growlithe"
}]
}, {
"id": 60,
"num": "060",
"name": "Poliwag",
"img": "http://www.serebii.net/pokemongo/pokemon/060.png",
"type": [
"Water"
],
"height": "0.61 m",
"weight": "12.4 kg",
"candy": "Poliwag Candy",
"candy_count": 25,
"egg": "5 km",
"spawn_chance": 2.19,
"avg_spawns": 219,
"spawn_time": "03:40",
"multipliers": [
1.72,
1.73
],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "061",
"name": "Poliwhirl"
}, {
"num": "062",
"name": "Poliwrath"
}]
}, {
"id": 61,
"num": "061",
"name": "Poliwhirl",
"img": "http://www.serebii.net/pokemongo/pokemon/061.png",
"type": [
"Water"
],
"height": "0.99 m",
"weight": "20.0 kg",
"candy": "Poliwag Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.13,
"avg_spawns": 13,
"spawn_time": "09:14",
"multipliers": [1.95],
"weaknesses": [
"Electric",
"Grass"
],
"prev_evolution": [{
"num": "060",
"name": "Poliwag"
}],
"next_evolution": [{
"num": "062",
"name": "Poliwrath"
}]
}, {
"id": 62,
"num": "062",
"name": "Poliwrath",
"img": "http://www.serebii.net/pokemongo/pokemon/062.png",
"type": [
"Water",
"Fighting"
],
"height": "1.30 m",
"weight": "54.0 kg",
"candy": "Poliwag Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.011,
"avg_spawns": 1.1,
"spawn_time": "01:32",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass",
"Flying",
"Psychic",
"Fairy"
],
"prev_evolution": [{
"num": "060",
"name": "Poliwag"
}, {
"num": "061",
"name": "Poliwhirl"
}]
}, {
"id": 63,
"num": "063",
"name": "Abra",
"img": "http://www.serebii.net/pokemongo/pokemon/063.png",
"type": [
"Psychic"
],
"height": "0.89 m",
"weight": "19.5 kg",
"candy": "Abra Candy",
"candy_count": 25,
"egg": "5 km",
"spawn_chance": 0.42,
"avg_spawns": 42,
"spawn_time": "04:30",
"multipliers": [
1.36,
1.95
],
"weaknesses": [
"Bug",
"Ghost",
"Dark"
],
"next_evolution": [{
"num": "064",
"name": "Kadabra"
}, {
"num": "065",
"name": "Alakazam"
}]
}, {
"id": 64,
"num": "064",
"name": "Kadabra",
"img": "http://www.serebii.net/pokemongo/pokemon/064.png",
"type": [
"Psychic"
],
"height": "1.30 m",
"weight": "56.5 kg",
"candy": "Abra Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.027,
"avg_spawns": 2.7,
"spawn_time": "11:25",
"multipliers": [1.4],
"weaknesses": [
"Bug",
"Ghost",
"Dark"
],
"prev_evolution": [{
"num": "063",
"name": "Abra"
}],
"next_evolution": [{
"num": "065",
"name": "Alakazam"
}]
}, {
"id": 65,
"num": "065",
"name": "Alakazam",
"img": "http://www.serebii.net/pokemongo/pokemon/065.png",
"type": [
"Psychic"
],
"height": "1.50 m",
"weight": "48.0 kg",
"candy": "Abra Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0073,
"avg_spawns": 0.73,
"spawn_time": "12:33",
"multipliers": null,
"weaknesses": [
"Bug",
"Ghost",
"Dark"
],
"prev_evolution": [{
"num": "063",
"name": "Abra"
}, {
"num": "064",
"name": "Kadabra"
}]
}, {
"id": 66,
"num": "066",
"name": "Machop",
"img": "http://www.serebii.net/pokemongo/pokemon/066.png",
"type": [
"Fighting"
],
"height": "0.79 m",
"weight": "19.5 kg",
"candy": "Machop Candy",
"candy_count": 25,
"egg": "5 km",
"spawn_chance": 0.49,
"avg_spawns": 49,
"spawn_time": "01:55",
"multipliers": [
1.64,
1.65
],
"weaknesses": [
"Flying",
"Psychic",
"Fairy"
],
"next_evolution": [{
"num": "067",
"name": "Machoke"
}, {
"num": "068",
"name": "Machamp"
}]
}, {
"id": 67,
"num": "067",
"name": "Machoke",
"img": "http://www.serebii.net/pokemongo/pokemon/067.png",
"type": [
"Fighting"
],
"height": "1.50 m",
"weight": "70.5 kg",
"candy": "Machop Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.034,
"avg_spawns": 3.4,
"spawn_time": "10:32",
"multipliers": [1.7],
"weaknesses": [
"Flying",
"Psychic",
"Fairy"
],
"prev_evolution": [{
"num": "066",
"name": "Machop"
}],
"next_evolution": [{
"num": "068",
"name": "Machamp"
}]
}, {
"id": 68,
"num": "068",
"name": "Machamp",
"img": "http://www.serebii.net/pokemongo/pokemon/068.png",
"type": [
"Fighting"
],
"height": "1.60 m",
"weight": "130.0 kg",
"candy": "Machop Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0068,
"avg_spawns": 0.68,
"spawn_time": "02:55",
"multipliers": null,
"weaknesses": [
"Flying",
"Psychic",
"Fairy"
],
"prev_evolution": [{
"num": "066",
"name": "Machop"
}, {
"num": "067",
"name": "Machoke"
}]
}, {
"id": 69,
"num": "069",
"name": "Bellsprout",
"img": "http://www.serebii.net/pokemongo/pokemon/069.png",
"type": [
"Grass",
"Poison"
],
"height": "0.71 m",
"weight": "4.0 kg",
"candy": "Bellsprout Candy",
"candy_count": 25,
"egg": "5 km",
"spawn_chance": 1.15,
"avg_spawns": 115,
"spawn_time": "04:10",
"multipliers": [1.57],
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"next_evolution": [{
"num": "070",
"name": "Weepinbell"
}, {
"num": "071",
"name": "Victreebel"
}]
}, {
"id": 70,
"num": "070",
"name": "Weepinbell",
"img": "http://www.serebii.net/pokemongo/pokemon/070.png",
"type": [
"Grass",
"Poison"
],
"height": "0.99 m",
"weight": "6.4 kg",
"candy": "Bellsprout Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.072,
"avg_spawns": 7.2,
"spawn_time": "09:45",
"multipliers": [1.59],
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"prev_evolution": [{
"num": "069",
"name": "Bellsprout"
}],
"next_evolution": [{
"num": "071",
"name": "Victreebel"
}]
}, {
"id": 71,
"num": "071",
"name": "Victreebel",
"img": "http://www.serebii.net/pokemongo/pokemon/071.png",
"type": [
"Grass",
"Poison"
],
"height": "1.70 m",
"weight": "15.5 kg",
"candy": "Bellsprout Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0059,
"avg_spawns": 0.59,
"spawn_time": "12:19",
"multipliers": null,
"weaknesses": [
"Fire",
"Ice",
"Flying",
"Psychic"
],
"prev_evolution": [{
"num": "069",
"name": "Bellsprout"
}, {
"num": "070",
"name": "Weepinbell"
}]
}, {
"id": 72,
"num": "072",
"name": "Tentacool",
"img": "http://www.serebii.net/pokemongo/pokemon/072.png",
"type": [
"Water",
"Poison"
],
"height": "0.89 m",
"weight": "45.5 kg",
"candy": "Tentacool Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.81,
"avg_spawns": 81,
"spawn_time": "03:20",
"multipliers": [2.52],
"weaknesses": [
"Electric",
"Ground",
"Psychic"
],
"next_evolution": [{
"num": "073",
"name": "Tentacruel"
}]
}, {
"id": 73,
"num": "073",
"name": "Tentacruel",
"img": "http://www.serebii.net/pokemongo/pokemon/073.png",
"type": [
"Water",
"Poison"
],
"height": "1.60 m",
"weight": "55.0 kg",
"candy": "Tentacool Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.082,
"avg_spawns": 8.2,
"spawn_time": "23:36",
"multipliers": null,
"weaknesses": [
"Electric",
"Ground",
"Psychic"
],
"prev_evolution": [{
"num": "072",
"name": "Tentacool"
}]
}, {
"id": 74,
"num": "074",
"name": "Geodude",
"img": "http://www.serebii.net/pokemongo/pokemon/074.png",
"type": [
"Rock",
"Ground"
],
"height": "0.41 m",
"weight": "20.0 kg",
"candy": "Geodude Candy",
"candy_count": 25,
"egg": "2 km",
"spawn_chance": 1.19,
"avg_spawns": 119,
"spawn_time": "12:40",
"multipliers": [
1.75,
1.76
],
"weaknesses": [
"Water",
"Grass",
"Ice",
"Fighting",
"Ground",
"Steel"
],
"next_evolution": [{
"num": "075",
"name": "Graveler"
}, {
"num": "076",
"name": "Golem"
}]
}, {
"id": 75,
"num": "075",
"name": "Graveler",
"img": "http://www.serebii.net/pokemongo/pokemon/075.png",
"type": [
"Rock",
"Ground"
],
"height": "0.99 m",
"weight": "105.0 kg",
"candy": "Geodude Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.071,
"avg_spawns": 7.1,
"spawn_time": "04:53",
"multipliers": [
1.64,
1.72
],
"weaknesses": [
"Water",
"Grass",
"Ice",
"Fighting",
"Ground",
"Steel"
],
"prev_evolution": [{
"num": "074",
"name": "Geodude"
}],
"next_evolution": [{
"num": "076",
"name": "Golem"
}]
}, {
"id": 76,
"num": "076",
"name": "Golem",
"img": "http://www.serebii.net/pokemongo/pokemon/076.png",
"type": [
"Rock",
"Ground"
],
"height": "1.40 m",
"weight": "300.0 kg",
"candy": "Geodude Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0047,
"avg_spawns": 0.47,
"spawn_time": "12:16",
"multipliers": null,
"weaknesses": [
"Water",
"Grass",
"Ice",
"Fighting",
"Ground",
"Steel"
],
"prev_evolution": [{
"num": "074",
"name": "Geodude"
}, {
"num": "075",
"name": "Graveler"
}]
}, {
"id": 77,
"num": "077",
"name": "Ponyta",
"img": "http://www.serebii.net/pokemongo/pokemon/077.png",
"type": [
"Fire"
],
"height": "0.99 m",
"weight": "30.0 kg",
"candy": "Ponyta Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.51,
"avg_spawns": 51,
"spawn_time": "02:50",
"multipliers": [
1.48,
1.5
],
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"next_evolution": [{
"num": "078",
"name": "Rapidash"
}]
}, {
"id": 78,
"num": "078",
"name": "Rapidash",
"img": "http://www.serebii.net/pokemongo/pokemon/078.png",
"type": [
"Fire"
],
"height": "1.70 m",
"weight": "95.0 kg",
"candy": "Ponyta Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.011,
"avg_spawns": 1.1,
"spawn_time": "04:00",
"multipliers": null,
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"prev_evolution": [{
"num": "077",
"name": "Ponyta"
}]
}, {
"id": 79,
"num": "079",
"name": "Slowpoke",
"img": "http://www.serebii.net/pokemongo/pokemon/079.png",
"type": [
"Water",
"Psychic"
],
"height": "1.19 m",
"weight": "36.0 kg",
"candy": "Slowpoke Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 1.05,
"avg_spawns": 105,
"spawn_time": "07:12",
"multipliers": [2.21],
"weaknesses": [
"Electric",
"Grass",
"Bug",
"Ghost",
"Dark"
],
"next_evolution": [{
"num": "080",
"name": "Slowbro"
}]
}, {
"id": 80,
"num": "080",
"name": "Slowbro",
"img": "http://www.serebii.net/pokemongo/pokemon/080.png",
"type": [
"Water",
"Psychic"
],
"height": "1.60 m",
"weight": "78.5 kg",
"candy": "Slowpoke Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.036,
"avg_spawns": 3.6,
"spawn_time": "02:56",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass",
"Bug",
"Ghost",
"Dark"
],
"prev_evolution": [{
"num": "079",
"name": "Slowpoke"
}]
}, {
"id": 81,
"num": "081",
"name": "Magnemite",
"img": "http://www.serebii.net/pokemongo/pokemon/081.png",
"type": [
"Electric"
],
"height": "0.30 m",
"weight": "6.0 kg",
"candy": "Magnemite Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.71,
"avg_spawns": 71,
"spawn_time": "04:04",
"multipliers": [
2.16,
2.17
],
"weaknesses": [
"Fire",
"Water",
"Ground"
],
"next_evolution": [{
"num": "082",
"name": "Magneton"
}]
}, {
"id": 82,
"num": "082",
"name": "Magneton",
"img": "http://www.serebii.net/pokemongo/pokemon/082.png",
"type": [
"Electric"
],
"height": "0.99 m",
"weight": "60.0 kg",
"candy": "Magnemite Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.023,
"avg_spawns": 2.3,
"spawn_time": "15:25",
"multipliers": null,
"weaknesses": [
"Fire",
"Water",
"Ground"
],
"prev_evolution": [{
"num": "081",
"name": "Magnemite"
}]
}, {
"id": 83,
"num": "083",
"name": "Farfetch'd",
"img": "http://www.serebii.net/pokemongo/pokemon/083.png",
"type": [
"Normal",
"Flying"
],
"height": "0.79 m",
"weight": "15.0 kg",
"candy": "None",
"egg": "5 km",
"spawn_chance": 0.0212,
"avg_spawns": 2.12,
"spawn_time": "01:09",
"multipliers": null,
"weaknesses": [
"Electric",
"Rock"
]
}, {
"id": 84,
"num": "084",
"name": "Doduo",
"img": "http://www.serebii.net/pokemongo/pokemon/084.png",
"type": [
"Normal",
"Flying"
],
"height": "1.40 m",
"weight": "39.2 kg",
"candy": "Doduo Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.52,
"avg_spawns": 52,
"spawn_time": "05:10",
"multipliers": [
2.19,
2.24
],
"weaknesses": [
"Electric",
"Rock"
],
"next_evolution": [{
"num": "085",
"name": "Dodrio"
}]
}, {
"id": 85,
"num": "085",
"name": "Dodrio",
"img": "http://www.serebii.net/pokemongo/pokemon/085.png",
"type": [
"Normal",
"Flying"
],
"height": "1.80 m",
"weight": "85.2 kg",
"candy": "Doduo Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.22,
"avg_spawns": 22,
"spawn_time": "02:12",
"multipliers": null,
"weaknesses": [
"Electric",
"Rock"
],
"prev_evolution": [{
"num": "084",
"name": "Doduo"
}]
}, {
"id": 86,
"num": "086",
"name": "Seel",
"img": "http://www.serebii.net/pokemongo/pokemon/086.png",
"type": [
"Water"
],
"height": "1.09 m",
"weight": "90.0 kg",
"candy": "Seel Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.28,
"avg_spawns": 28,
"spawn_time": "06:46",
"multipliers": [
1.04,
1.96
],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "087",
"name": "Dewgong"
}]
}, {
"id": 87,
"num": "087",
"name": "Dewgong",
"img": "http://www.serebii.net/pokemongo/pokemon/087.png",
"type": [
"Water",
"Ice"
],
"height": "1.70 m",
"weight": "120.0 kg",
"candy": "Seel Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.013,
"avg_spawns": 1.3,
"spawn_time": "06:04",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass",
"Fighting",
"Rock"
],
"prev_evolution": [{
"num": "086",
"name": "Seel"
}]
}, {
"id": 88,
"num": "088",
"name": "Grimer",
"img": "http://www.serebii.net/pokemongo/pokemon/088.png",
"type": [
"Poison"
],
"height": "0.89 m",
"weight": "30.0 kg",
"candy": "Grimer Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.052,
"avg_spawns": 5.2,
"spawn_time": "15:11",
"multipliers": [2.44],
"weaknesses": [
"Ground",
"Psychic"
],
"next_evolution": [{
"num": "089",
"name": "Muk"
}]
}, {
"id": 89,
"num": "089",
"name": "Muk",
"img": "http://www.serebii.net/pokemongo/pokemon/089.png",
"type": [
"Poison"
],
"height": "1.19 m",
"weight": "30.0 kg",
"candy": "Grimer Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0031,
"avg_spawns": 0.31,
"spawn_time": "01:28",
"multipliers": null,
"weaknesses": [
"Ground",
"Psychic"
],
"prev_evolution": [{
"num": "088",
"name": "Grimer"
}]
}, {
"id": 90,
"num": "090",
"name": "Shellder",
"img": "http://www.serebii.net/pokemongo/pokemon/090.png",
"type": [
"Water"
],
"height": "0.30 m",
"weight": "4.0 kg",
"candy": "Shellder Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.52,
"avg_spawns": 52,
"spawn_time": "07:39",
"multipliers": [2.65],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "091",
"name": "Cloyster"
}]
}, {
"id": 91,
"num": "091",
"name": "Cloyster",
"img": "http://www.serebii.net/pokemongo/pokemon/091.png",
"type": [
"Water",
"Ice"
],
"height": "1.50 m",
"weight": "132.5 kg",
"candy": "Shellder Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.015,
"avg_spawns": 1.5,
"spawn_time": "02:33",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass",
"Fighting",
"Rock"
],
"prev_evolution": [{
"num": "090",
"name": "Shellder"
}]
}, {
"id": 92,
"num": "092",
"name": "Gastly",
"img": "http://www.serebii.net/pokemongo/pokemon/092.png",
"type": [
"Ghost",
"Poison"
],
"height": "1.30 m",
"weight": "0.1 kg",
"candy": "Gastly Candy",
"candy_count": 25,
"egg": "5 km",
"spawn_chance": 0.79,
"avg_spawns": 79,
"spawn_time": "04:21",
"multipliers": [1.78],
"weaknesses": [
"Ground",
"Psychic",
"Ghost",
"Dark"
],
"next_evolution": [{
"num": "093",
"name": "Haunter"
}, {
"num": "094",
"name": "Gengar"
}]
}, {
"id": 93,
"num": "093",
"name": "Haunter",
"img": "http://www.serebii.net/pokemongo/pokemon/093.png",
"type": [
"Ghost",
"Poison"
],
"height": "1.60 m",
"weight": "0.1 kg",
"candy": "Gastly Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.052,
"avg_spawns": 5.2,
"spawn_time": "00:10",
"multipliers": [
1.56,
1.8
],
"weaknesses": [
"Ground",
"Psychic",
"Ghost",
"Dark"
],
"prev_evolution": [{
"num": "092",
"name": "Gastly"
}],
"next_evolution": [{
"num": "094",
"name": "Gengar"
}]
}, {
"id": 94,
"num": "094",
"name": "Gengar",
"img": "http://www.serebii.net/pokemongo/pokemon/094.png",
"type": [
"Ghost",
"Poison"
],
"height": "1.50 m",
"weight": "40.5 kg",
"candy": "Gastly Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0067,
"avg_spawns": 0.67,
"spawn_time": "03:55",
"multipliers": null,
"weaknesses": [
"Ground",
"Psychic",
"Ghost",
"Dark"
],
"prev_evolution": [{
"num": "092",
"name": "Gastly"
}, {
"num": "093",
"name": "Haunter"
}]
}, {
"id": 95,
"num": "095",
"name": "Onix",
"img": "http://www.serebii.net/pokemongo/pokemon/095.png",
"type": [
"Rock",
"Ground"
],
"height": "8.79 m",
"weight": "210.0 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.10,
"avg_spawns": 10,
"spawn_time": "01:18",
"multipliers": null,
"weaknesses": [
"Water",
"Grass",
"Ice",
"Fighting",
"Ground",
"Steel"
]
}, {
"id": 96,
"num": "096",
"name": "Drowzee",
"img": "http://www.serebii.net/pokemongo/pokemon/096.png",
"type": [
"Psychic"
],
"height": "0.99 m",
"weight": "32.4 kg",
"candy": "Drowzee Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 3.21,
"avg_spawns": 321,
"spawn_time": "01:51",
"multipliers": [
2.08,
2.09
],
"weaknesses": [
"Bug",
"Ghost",
"Dark"
],
"next_evolution": [{
"num": "097",
"name": "Hypno"
}]
}, {
"id": 97,
"num": "097",
"name": "Hypno",
"img": "http://www.serebii.net/pokemongo/pokemon/097.png",
"type": [
"Psychic"
],
"height": "1.60 m",
"weight": "75.6 kg",
"candy": "Drowzee Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.10,
"avg_spawns": 10,
"spawn_time": "02:17",
"multipliers": null,
"weaknesses": [
"Bug",
"Ghost",
"Dark"
],
"prev_evolution": [{
"num": "096",
"name": "Drowzee"
}]
}, {
"id": 98,
"num": "098",
"name": "Krabby",
"img": "http://www.serebii.net/pokemongo/pokemon/098.png",
"type": [
"Water"
],
"height": "0.41 m",
"weight": "6.5 kg",
"candy": "Krabby Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 2.12,
"avg_spawns": 212,
"spawn_time": "03:33",
"multipliers": [
2.36,
2.4
],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "099",
"name": "Kingler"
}]
}, {
"id": 99,
"num": "099",
"name": "Kingler",
"img": "http://www.serebii.net/pokemongo/pokemon/099.png",
"type": [
"Water"
],
"height": "1.30 m",
"weight": "60.0 kg",
"candy": "Krabby Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.062,
"avg_spawns": 6.2,
"spawn_time": "03:44",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass"
],
"prev_evolution": [{
"num": "098",
"name": "Krabby"
}]
}, {
"id": 100,
"num": "100",
"name": "Voltorb",
"img": "http://www.serebii.net/pokemongo/pokemon/100.png",
"type": [
"Electric"
],
"height": "0.51 m",
"weight": "10.4 kg",
"candy": "Voltorb Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.65,
"avg_spawns": 65,
"spawn_time": "04:36",
"multipliers": [
2.01,
2.02
],
"weaknesses": [
"Ground"
],
"next_evolution": [{
"num": "101",
"name": "Electrode"
}]
}, {
"id": 101,
"num": "101",
"name": "Electrode",
"img": "http://www.serebii.net/pokemongo/pokemon/101.png",
"type": [
"Electric"
],
"height": "1.19 m",
"weight": "66.6 kg",
"candy": "Voltorb Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.02,
"avg_spawns": 2,
"spawn_time": "04:10",
"multipliers": null,
"weaknesses": [
"Ground"
],
"prev_evolution": [{
"num": "100",
"name": "Voltorb"
}]
}, {
"id": 102,
"num": "102",
"name": "Exeggcute",
"img": "http://www.serebii.net/pokemongo/pokemon/102.png",
"type": [
"Grass",
"Psychic"
],
"height": "0.41 m",
"weight": "2.5 kg",
"candy": "Exeggcute Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.78,
"avg_spawns": 78,
"spawn_time": "09:09",
"multipliers": [
2.7,
3.18
],
"weaknesses": [
"Fire",
"Ice",
"Poison",
"Flying",
"Bug",
"Ghost",
"Dark"
],
"next_evolution": [{
"num": "103",
"name": "Exeggutor"
}]
}, {
"id": 103,
"num": "103",
"name": "Exeggutor",
"img": "http://www.serebii.net/pokemongo/pokemon/103.png",
"type": [
"Grass",
"Psychic"
],
"height": "2.01 m",
"weight": "120.0 kg",
"candy": "Exeggcute Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.014,
"avg_spawns": 1.4,
"spawn_time": "12:34",
"multipliers": null,
"weaknesses": [
"Fire",
"Ice",
"Poison",
"Flying",
"Bug",
"Ghost",
"Dark"
],
"prev_evolution": [{
"num": "102",
"name": "Exeggcute"
}]
}, {
"id": 104,
"num": "104",
"name": "Cubone",
"img": "http://www.serebii.net/pokemongo/pokemon/104.png",
"type": [
"Ground"
],
"height": "0.41 m",
"weight": "6.5 kg",
"candy": "Cubone Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.61,
"avg_spawns": 61,
"spawn_time": "01:51",
"multipliers": [1.67],
"weaknesses": [
"Water",
"Grass",
"Ice"
],
"next_evolution": [{
"num": "105",
"name": "Marowak"
}]
}, {
"id": 105,
"num": "105",
"name": "Marowak",
"img": "http://www.serebii.net/pokemongo/pokemon/105.png",
"type": [
"Ground"
],
"height": "0.99 m",
"weight": "45.0 kg",
"candy": "Cubone Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.02,
"avg_spawns": 2,
"spawn_time": "03:59",
"multipliers": null,
"weaknesses": [
"Water",
"Grass",
"Ice"
],
"prev_evolution": [{
"num": "104",
"name": "Cubone"
}]
}, {
"id": 106,
"num": "106",
"name": "Hitmonlee",
"img": "http://www.serebii.net/pokemongo/pokemon/106.png",
"type": [
"Fighting"
],
"height": "1.50 m",
"weight": "49.8 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.02,
"avg_spawns": 2,
"spawn_time": "03:59",
"multipliers": null,
"weaknesses": [
"Flying",
"Psychic",
"Fairy"
]
}, {
"id": 107,
"num": "107",
"name": "Hitmonchan",
"img": "http://www.serebii.net/pokemongo/pokemon/107.png",
"type": [
"Fighting"
],
"height": "1.40 m",
"weight": "50.2 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.022,
"avg_spawns": 2.2,
"spawn_time": "05:58",
"multipliers": null,
"weaknesses": [
"Flying",
"Psychic",
"Fairy"
]
}, {
"id": 108,
"num": "108",
"name": "Lickitung",
"img": "http://www.serebii.net/pokemongo/pokemon/108.png",
"type": [
"Normal"
],
"height": "1.19 m",
"weight": "65.5 kg",
"candy": "None",
"egg": "5 km",
"spawn_chance": 0.011,
"avg_spawns": 1.1,
"spawn_time": "02:46",
"multipliers": null,
"weaknesses": [
"Fighting"
]
}, {
"id": 109,
"num": "109",
"name": "Koffing",
"img": "http://www.serebii.net/pokemongo/pokemon/109.png",
"type": [
"Poison"
],
"height": "0.61 m",
"weight": "1.0 kg",
"candy": "Koffing Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.20,
"avg_spawns": 20,
"spawn_time": "08:16",
"multipliers": [1.11],
"weaknesses": [
"Ground",
"Psychic"
],
"next_evolution": [{
"num": "110",
"name": "Weezing"
}]
}, {
"id": 110,
"num": "110",
"name": "Weezing",
"img": "http://www.serebii.net/pokemongo/pokemon/110.png",
"type": [
"Poison"
],
"height": "1.19 m",
"weight": "9.5 kg",
"candy": "Koffing Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.016,
"avg_spawns": 1.6,
"spawn_time": "12:17",
"multipliers": null,
"weaknesses": [
"Ground",
"Psychic"
],
"prev_evolution": [{
"num": "109",
"name": "Koffing"
}]
}, {
"id": 111,
"num": "111",
"name": "Rhyhorn",
"img": "http://www.serebii.net/pokemongo/pokemon/111.png",
"type": [
"Ground",
"Rock"
],
"height": "0.99 m",
"weight": "115.0 kg",
"candy": "Rhyhorn Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 0.63,
"avg_spawns": 63,
"spawn_time": "03:21",
"multipliers": [1.91],
"weaknesses": [
"Water",
"Grass",
"Ice",
"Fighting",
"Ground",
"Steel"
],
"next_evolution": [{
"num": "112",
"name": "Rhydon"
}]
}, {
"id": 112,
"num": "112",
"name": "Rhydon",
"img": "http://www.serebii.net/pokemongo/pokemon/112.png",
"type": [
"Ground",
"Rock"
],
"height": "1.91 m",
"weight": "120.0 kg",
"candy": "Rhyhorn Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.022,
"avg_spawns": 2.2,
"spawn_time": "05:50",
"multipliers": null,
"weaknesses": [
"Water",
"Grass",
"Ice",
"Fighting",
"Ground",
"Steel"
],
"prev_evolution": [{
"num": "111",
"name": "Rhyhorn"
}]
}, {
"id": 113,
"num": "113",
"name": "Chansey",
"img": "http://www.serebii.net/pokemongo/pokemon/113.png",
"type": [
"Normal"
],
"height": "1.09 m",
"weight": "34.6 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.013,
"avg_spawns": 1.3,
"spawn_time": "04:46",
"multipliers": null,
"weaknesses": [
"Fighting"
]
}, {
"id": 114,
"num": "114",
"name": "Tangela",
"img": "http://www.serebii.net/pokemongo/pokemon/114.png",
"type": [
"Grass"
],
"height": "0.99 m",
"weight": "35.0 kg",
"candy": "None",
"egg": "5 km",
"spawn_chance": 0.228,
"avg_spawns": 22.8,
"spawn_time": "23:13",
"multipliers": null,
"weaknesses": [
"Fire",
"Ice",
"Poison",
"Flying",
"Bug"
]
}, {
"id": 115,
"num": "115",
"name": "Kangaskhan",
"img": "http://www.serebii.net/pokemongo/pokemon/115.png",
"type": [
"Normal"
],
"height": "2.21 m",
"weight": "80.0 kg",
"candy": "None",
"egg": "5 km",
"spawn_chance": 0.0086,
"avg_spawns": 0.86,
"spawn_time": "02:40",
"multipliers": null,
"weaknesses": [
"Fighting"
]
}, {
"id": 116,
"num": "116",
"name": "Horsea",
"img": "http://www.serebii.net/pokemongo/pokemon/116.png",
"type": [
"Water"
],
"height": "0.41 m",
"weight": "8.0 kg",
"candy": "Horsea Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 1.13,
"avg_spawns": 113,
"spawn_time": "02:53",
"multipliers": [2.23],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "117",
"name": "Seadra"
}]
}, {
"id": 117,
"num": "117",
"name": "Seadra",
"img": "http://www.serebii.net/pokemongo/pokemon/117.png",
"type": [
"Water"
],
"height": "1.19 m",
"weight": "25.0 kg",
"candy": "Horsea Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.034,
"avg_spawns": 3.4,
"spawn_time": "03:18",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass"
],
"prev_evolution": [{
"num": "116",
"name": "Horsea"
}]
}, {
"id": 118,
"num": "118",
"name": "Goldeen",
"img": "http://www.serebii.net/pokemongo/pokemon/118.png",
"type": [
"Water"
],
"height": "0.61 m",
"weight": "15.0 kg",
"candy": "Goldeen Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 2.18,
"avg_spawns": 218,
"spawn_time": "03:14",
"multipliers": [
2.15,
2.2
],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "119",
"name": "Seaking"
}]
}, {
"id": 119,
"num": "119",
"name": "Seaking",
"img": "http://www.serebii.net/pokemongo/pokemon/119.png",
"type": [
"Water"
],
"height": "1.30 m",
"weight": "39.0 kg",
"candy": "Goldeen Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.08,
"avg_spawns": 8,
"spawn_time": "05:21",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass"
],
"prev_evolution": [{
"num": "118",
"name": "Goldeen"
}]
}, {
"id": 120,
"num": "120",
"name": "Staryu",
"img": "http://www.serebii.net/pokemongo/pokemon/120.png",
"type": [
"Water"
],
"height": "0.79 m",
"weight": "34.5 kg",
"candy": "Staryu Candy",
"candy_count": 50,
"egg": "5 km",
"spawn_chance": 1.95,
"avg_spawns": 195,
"spawn_time": "22:59",
"multipliers": [
2.38,
2.41
],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "121",
"name": "Starmie"
}]
}, {
"id": 121,
"num": "121",
"name": "Starmie",
"img": "http://www.serebii.net/pokemongo/pokemon/121.png",
"type": [
"Water",
"Psychic"
],
"height": "1.09 m",
"weight": "80.0 kg",
"candy": "Staryu Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.034,
"avg_spawns": 3.4,
"spawn_time": "06:57",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass",
"Bug",
"Ghost",
"Dark"
],
"prev_evolution": [{
"num": "120",
"name": "Staryu"
}]
}, {
"id": 122,
"num": "122",
"name": "Mr. Mime",
"img": "http://www.serebii.net/pokemongo/pokemon/122.png",
"type": [
"Psychic"
],
"height": "1.30 m",
"weight": "54.5 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.0031,
"avg_spawns": 0.31,
"spawn_time": "01:51",
"multipliers": null,
"weaknesses": [
"Bug",
"Ghost",
"Dark"
]
}, {
"id": 123,
"num": "123",
"name": "Scyther",
"img": "http://www.serebii.net/pokemongo/pokemon/123.png",
"type": [
"Bug",
"Flying"
],
"height": "1.50 m",
"weight": "56.0 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.14,
"avg_spawns": 14,
"spawn_time": "05:43",
"multipliers": null,
"weaknesses": [
"Fire",
"Electric",
"Ice",
"Flying",
"Rock"
]
}, {
"id": 124,
"num": "124",
"name": "Jynx",
"img": "http://www.serebii.net/pokemongo/pokemon/124.png",
"type": [
"Ice",
"Psychic"
],
"height": "1.40 m",
"weight": "40.6 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.35,
"avg_spawns": 35,
"spawn_time": "05:41",
"multipliers": null,
"weaknesses": [
"Fire",
"Bug",
"Rock",
"Ghost",
"Dark",
"Steel"
]
}, {
"id": 125,
"num": "125",
"name": "Electabuzz",
"img": "http://www.serebii.net/pokemongo/pokemon/125.png",
"type": [
"Electric"
],
"height": "1.09 m",
"weight": "30.0 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.074,
"avg_spawns": 7.4,
"spawn_time": "04:28",
"multipliers": null,
"weaknesses": [
"Ground"
]
}, {
"id": 126,
"num": "126",
"name": "Magmar",
"img": "http://www.serebii.net/pokemongo/pokemon/126.png",
"type": [
"Fire"
],
"height": "1.30 m",
"weight": "44.5 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.10,
"avg_spawns": 10,
"spawn_time": "20:36",
"multipliers": null,
"weaknesses": [
"Water",
"Ground",
"Rock"
]
}, {
"id": 127,
"num": "127",
"name": "Pinsir",
"img": "http://www.serebii.net/pokemongo/pokemon/127.png",
"type": [
"Bug"
],
"height": "1.50 m",
"weight": "55.0 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.99,
"avg_spawns": 99,
"spawn_time": "03:25",
"multipliers": null,
"weaknesses": [
"Fire",
"Flying",
"Rock"
]
}, {
"id": 128,
"num": "128",
"name": "Tauros",
"img": "http://www.serebii.net/pokemongo/pokemon/128.png",
"type": [
"Normal"
],
"height": "1.40 m",
"weight": "88.4 kg",
"candy": "None",
"egg": "5 km",
"spawn_chance": 0.12,
"avg_spawns": 12,
"spawn_time": "00:37",
"multipliers": null,
"weaknesses": [
"Fighting"
]
}, {
"id": 129,
"num": "129",
"name": "Magikarp",
"img": "http://www.serebii.net/pokemongo/pokemon/129.png",
"type": [
"Water"
],
"height": "0.89 m",
"weight": "10.0 kg",
"candy": "Magikarp Candy",
"candy_count": 400,
"egg": "2 km",
"spawn_chance": 4.78,
"avg_spawns": 478,
"spawn_time": "14:26",
"multipliers": [
10.1,
11.8
],
"weaknesses": [
"Electric",
"Grass"
],
"next_evolution": [{
"num": "130",
"name": "Gyarados"
}]
}, {
"id": 130,
"num": "130",
"name": "Gyarados",
"img": "http://www.serebii.net/pokemongo/pokemon/130.png",
"type": [
"Water",
"Flying"
],
"height": "6.50 m",
"weight": "235.0 kg",
"candy": "Magikarp Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0032,
"avg_spawns": 0.32,
"spawn_time": "02:15",
"multipliers": null,
"weaknesses": [
"Electric",
"Rock"
],
"prev_evolution": [{
"num": "129",
"name": "Magikarp"
}]
}, {
"id": 131,
"num": "131",
"name": "Lapras",
"img": "http://www.serebii.net/pokemongo/pokemon/131.png",
"type": [
"Water",
"Ice"
],
"height": "2.49 m",
"weight": "220.0 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.006,
"avg_spawns": 0.6,
"spawn_time": "08:59",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass",
"Fighting",
"Rock"
]
}, {
"id": 132,
"num": "132",
"name": "Ditto",
"img": "http://www.serebii.net/pokemongo/pokemon/132.png",
"type": [
"Normal"
],
"height": "0.30 m",
"weight": "4.0 kg",
"candy": "None",
"egg": "Not in Eggs",
"spawn_chance": 0,
"avg_spawns": 0,
"spawn_time": "N/A",
"multipliers": null,
"weaknesses": [
"Fighting"
]
}, {
"id": 133,
"num": "133",
"name": "Eevee",
"img": "http://www.serebii.net/pokemongo/pokemon/133.png",
"type": [
"Normal"
],
"height": "0.30 m",
"weight": "6.5 kg",
"candy": "Eevee Candy",
"candy_count": 25,
"egg": "10 km",
"spawn_chance": 2.75,
"avg_spawns": 275,
"spawn_time": "05:32",
"multipliers": [
2.02,
2.64
],
"weaknesses": [
"Fighting"
],
"next_evolution": [{
"num": "134",
"name": "Vaporeon"
}, {
"num": "135",
"name": "Jolteon"
}, {
"num": "136",
"name": "Flareon"
}]
}, {
"id": 134,
"num": "134",
"name": "Vaporeon",
"img": "http://www.serebii.net/pokemongo/pokemon/134.png",
"type": [
"Water"
],
"height": "0.99 m",
"weight": "29.0 kg",
"candy": "Eevee Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.014,
"avg_spawns": 1.4,
"spawn_time": "10:54",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass"
],
"prev_evolution": [{
"num": "133",
"name": "Eevee"
}]
}, {
"id": 135,
"num": "135",
"name": "Jolteon",
"img": "http://www.serebii.net/pokemongo/pokemon/135.png",
"type": [
"Electric"
],
"height": "0.79 m",
"weight": "24.5 kg",
"candy": "None",
"egg": "Not in Eggs",
"spawn_chance": 0.012,
"avg_spawns": 1.2,
"spawn_time": "02:30",
"multipliers": null,
"weaknesses": [
"Ground"
],
"prev_evolution": [{
"num": "133",
"name": "Eevee"
}]
}, {
"id": 136,
"num": "136",
"name": "Flareon",
"img": "http://www.serebii.net/pokemongo/pokemon/136.png",
"type": [
"Fire"
],
"height": "0.89 m",
"weight": "25.0 kg",
"candy": "Eevee Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.017,
"avg_spawns": 1.7,
"spawn_time": "07:02",
"multipliers": null,
"weaknesses": [
"Water",
"Ground",
"Rock"
],
"prev_evolution": [{
"num": "133",
"name": "Eevee"
}]
}, {
"id": 137,
"num": "137",
"name": "Porygon",
"img": "http://www.serebii.net/pokemongo/pokemon/137.png",
"type": [
"Normal"
],
"height": "0.79 m",
"weight": "36.5 kg",
"candy": "None",
"egg": "5 km",
"spawn_chance": 0.012,
"avg_spawns": 1.2,
"spawn_time": "02:49",
"multipliers": null,
"weaknesses": [
"Fighting"
]
}, {
"id": 138,
"num": "138",
"name": "Omanyte",
"img": "http://www.serebii.net/pokemongo/pokemon/138.png",
"type": [
"Rock",
"Water"
],
"height": "0.41 m",
"weight": "7.5 kg",
"candy": "Omanyte Candy",
"candy_count": 50,
"egg": "10 km",
"spawn_chance": 0.14,
"avg_spawns": 14,
"spawn_time": "10:23",
"multipliers": [2.12],
"weaknesses": [
"Electric",
"Grass",
"Fighting",
"Ground"
],
"next_evolution": [{
"num": "139",
"name": "Omastar"
}]
}, {
"id": 139,
"num": "139",
"name": "Omastar",
"img": "http://www.serebii.net/pokemongo/pokemon/139.png",
"type": [
"Rock",
"Water"
],
"height": "0.99 m",
"weight": "35.0 kg",
"candy": "None",
"egg": "Omanyte Candy",
"spawn_chance": 0.0061,
"avg_spawns": 0.61,
"spawn_time": "05:04",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass",
"Fighting",
"Ground"
],
"prev_evolution": [{
"num": "138",
"name": "Omanyte"
}]
}, {
"id": 140,
"num": "140",
"name": "Kabuto",
"img": "http://www.serebii.net/pokemongo/pokemon/140.png",
"type": [
"Rock",
"Water"
],
"height": "0.51 m",
"weight": "11.5 kg",
"candy": "Kabuto Candy",
"candy_count": 50,
"egg": "10 km",
"spawn_chance": 0.10,
"avg_spawns": 10,
"spawn_time": "00:05",
"multipliers": [
1.97,
2.37
],
"weaknesses": [
"Electric",
"Grass",
"Fighting",
"Ground"
],
"next_evolution": [{
"num": "141",
"name": "Kabutops"
}]
}, {
"id": 141,
"num": "141",
"name": "Kabutops",
"img": "http://www.serebii.net/pokemongo/pokemon/141.png",
"type": [
"Rock",
"Water"
],
"height": "1.30 m",
"weight": "40.5 kg",
"candy": "Kabuto Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0032,
"avg_spawns": 0.32,
"spawn_time": "23:40",
"multipliers": null,
"weaknesses": [
"Electric",
"Grass",
"Fighting",
"Ground"
],
"prev_evolution": [{
"num": "140",
"name": "Kabuto"
}]
}, {
"id": 142,
"num": "142",
"name": "Aerodactyl",
"img": "http://www.serebii.net/pokemongo/pokemon/142.png",
"type": [
"Rock",
"Flying"
],
"height": "1.80 m",
"weight": "59.0 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.018,
"avg_spawns": 1.8,
"spawn_time": "23:40",
"multipliers": null,
"weaknesses": [
"Water",
"Electric",
"Ice",
"Rock",
"Steel"
]
}, {
"id": 143,
"num": "143",
"name": "Snorlax",
"img": "http://www.serebii.net/pokemongo/pokemon/143.png",
"type": [
"Normal"
],
"height": "2.11 m",
"weight": "460.0 kg",
"candy": "None",
"egg": "10 km",
"spawn_chance": 0.016,
"avg_spawns": 1.6,
"spawn_time": "23:40",
"multipliers": null,
"weaknesses": [
"Fighting"
]
}, {
"id": 144,
"num": "144",
"name": "Articuno",
"img": "http://www.serebii.net/pokemongo/pokemon/144.png",
"type": [
"Ice",
"Flying"
],
"height": "1.70 m",
"weight": "55.4 kg",
"candy": "None",
"egg": "Not in Eggs",
"spawn_chance": 0,
"avg_spawns": 0,
"spawn_time": "N/A",
"multipliers": null,
"weaknesses": [
"Fire",
"Electric",
"Rock",
"Steel"
]
}, {
"id": 145,
"num": "145",
"name": "Zapdos",
"img": "http://www.serebii.net/pokemongo/pokemon/145.png",
"type": [
"Electric",
"Flying"
],
"height": "1.60 m",
"weight": "52.6 kg",
"candy": "None",
"egg": "Not in Eggs",
"spawn_chance": 0,
"avg_spawns": 0,
"spawn_time": "N/A",
"multipliers": null,
"weaknesses": [
"Ice",
"Rock"
]
}, {
"id": 146,
"num": "146",
"name": "Moltres",
"img": "http://www.serebii.net/pokemongo/pokemon/146.png",
"type": [
"Fire",
"Flying"
],
"height": "2.01 m",
"weight": "60.0 kg",
"candy": "None",
"egg": "Not in Eggs",
"spawn_chance": 0,
"avg_spawns": 0,
"spawn_time": "N/A",
"multipliers": null,
"weaknesses": [
"Water",
"Electric",
"Rock"
]
}, {
"id": 147,
"num": "147",
"name": "Dratini",
"img": "http://www.serebii.net/pokemongo/pokemon/147.png",
"type": [
"Dragon"
],
"height": "1.80 m",
"weight": "3.3 kg",
"candy": "Dratini Candy",
"candy_count": 25,
"egg": "10 km",
"spawn_chance": 0.30,
"avg_spawns": 30,
"spawn_time": "06:41",
"multipliers": [
1.83,
1.84
],
"weaknesses": [
"Ice",
"Dragon",
"Fairy"
],
"next_evolution": [{
"num": "148",
"name": "Dragonair"
}, {
"num": "149",
"name": "Dragonite"
}]
}, {
"id": 148,
"num": "148",
"name": "Dragonair",
"img": "http://www.serebii.net/pokemongo/pokemon/148.png",
"type": [
"Dragon"
],
"height": "3.99 m",
"weight": "16.5 kg",
"candy": "Dratini Candy",
"candy_count": 100,
"egg": "Not in Eggs",
"spawn_chance": 0.02,
"avg_spawns": 2,
"spawn_time": "11:57",
"multipliers": [2.05],
"weaknesses": [
"Ice",
"Dragon",
"Fairy"
],
"prev_evolution": [{
"num": "147",
"name": "Dratini"
}],
"next_evolution": [{
"num": "149",
"name": "Dragonite"
}]
}, {
"id": 149,
"num": "149",
"name": "Dragonite",
"img": "http://www.serebii.net/pokemongo/pokemon/149.png",
"type": [
"Dragon",
"Flying"
],
"height": "2.21 m",
"weight": "210.0 kg",
"candy": "Dratini Candy",
"egg": "Not in Eggs",
"spawn_chance": 0.0011,
"avg_spawns": 0.11,
"spawn_time": "23:38",
"multipliers": null,
"weaknesses": [
"Ice",
"Rock",
"Dragon",
"Fairy"
],
"prev_evolution": [{
"num": "147",
"name": "Dratini"
}, {
"num": "148",
"name": "Dragonair"
}]
}, {
"id": 150,
"num": "150",
"name": "Mewtwo",
"img": "http://www.serebii.net/pokemongo/pokemon/150.png",
"type": [
"Psychic"
],
"height": "2.01 m",
"weight": "122.0 kg",
"candy": "None",
"egg": "Not in Eggs",
"spawn_chance": 0,
"avg_spawns": 0,
"spawn_time": "N/A",
"multipliers": null,
"weaknesses": [
"Bug",
"Ghost",
"Dark"
]
}, {
"id": 151,
"num": "151",
"name": "Mew",
"img": "http://www.serebii.net/pokemongo/pokemon/151.png",
"type": [
"Psychic"
],
"height": "0.41 m",
"weight": "4.0 kg",
"candy": "None",
"egg": "Not in Eggs",
"spawn_chance": 0,
"avg_spawns": 0,
"spawn_time": "N/A",
"multipliers": null,
"weaknesses": [
"Bug",
"Ghost",
"Dark"
]
}]
}

Ce n'est évidemment pas impossible, mais cela donne un fichier assez ilisible et complexe à maintenir.

Une configmap en HCL

Maintenant.. l'avantage de Terraform est de pouvoir séparer le manifest des données.

resource "kubernetes_config_map" "data_user" {
metadata {
name = "data-user"
namespace = "hcl"
}

data = {
"data.ini" = "${file("${path.module}/data.ini")}"
}
}

Le HCL n'a rien à envier du YAML de Kubernetes.

Et concernant un déploiement ?

Dans un bon écosystème, nous déployons via Helm, voyons comment Hashicorp nous présente ça.

Qu'est ce que Helm ?

Helm est un outil de templating YAML similaire à Jinja2, on l'utilise dans les déploiements nécéssitant de nombreux fichiers YAML (service, deploy, pvc, scaler…)

Hashicorp propose également un module Helm :

variable "if_clusterenabled" {
type = string
default = "true"
}


resource "helm_release" "redisexample" {
name = "my-redis-release"
repository = "https://charts.bitnami.com/bitnami"
chart = "redis"
version = "17.3.9"
namespace = "hcl"

values = [
"${file("values.yaml")}"
]

set {
name = "cluster.enabled"
value = var.if_clusterenabled
}

set {
name = "metrics.enabled"
value = "true"
}

}

asciicast

Dans ce cas là, je peux injecter les variables:

  • provenants du fichier values.yaml
  • présentes dans le fichier terraform

Comme dit dans l'introduction de cet article : la force de terraform vient de ses providers, il est donc possible de récupérer des variables depuis un serveur Vault, un bitwarden, ou même un KeePass.

k2tf - Migrer ses yaml vers Terraform

Histoire de convaincre les flemmards, voici un projet Github permettant de convertir ses fichiers YAML vers des fichiers Terraform. Les fichiers générés n'ont besoin que d'un provider.tf avant de pouvoir être déployés.

asciicast

Conclusion

Le HCL apporte de nombreux avantages dans l'administration d'un cluster Kubernetes. En intégrant les modules Terraform,on se soustrait d'une configuration statique sans alourdir le cluster. (par exemple,l'usage des initPods qui peuvent être utilisés pour récupérer du contenu stocké ailleurs alors que Terraform peut le faire durant le déploiement). J'ai hâte de voir comment Terraform et Kubernetes fonctionneront à l'avenir, et quelles intégrations seront possibles.

Pulumi?

Mais pour vous ouvrir d'autres possibilités et vous débarasser d'un DSL, il existe un outil bien pratique nommé Pulumi qui permet de faire les mêmes choses que Terraform depuis un réel langage de programmation comme le Python, le Go, le Java etc…

Merci de votre lecture!

· 6 minutes de lecture
TheBidouilleur

Introduction

Depuis que le mouvement DevOps a commencé (ou plutôt Platform engineering), on met le thème de la haute-disponibilité sur le devant de la scène. Et une des solutions les plus polyvalentes pour faire de la haute disponibilité est de créer des clusters d'applications. (et de facto : des conteneurs)

J'ai donc administré un cluster Swarm pendant quelques années et je suis récemment passé sous Kubernetes (k3s pour être précis). Et à force d'avoir des clusters contenant plusieurs centaines de conteneurs, on en oublie la maintenance et la mise à jour.

Et dans cet article, on va parler des mises à jour.

Solutions de mise à jour de conteneur hors cluster

WatchTower

Je pense que la solution la plus connue est Watchtower

Watchtower est facile d'utilisation et se base (comme beaucoup d'autre) sur les labels. Un label permet de définir quelques paramètres et d'activer (ou de désactiver) la surveillance des mises à jour.

Mettre à jour, ce n'est pas toujours bien…

Attention à ne pas mettre à jour automatiquement des programmes sensibles ! Nous ne pouvons pas vérifier ce que contient une mise à jour si elle ne va pas casser quelque chose. Il ne tient qu'à vous de choisir les applications à surveiller, et à déclencher une mise à jour ou non.

WatchTower vous notifiera de plusieurs manières :

  • email
  • slack
  • msteams
  • gotify
  • shoutrrr

Et parmi ces méthodes, vous n'avez pas que des solutions propriétaires, libre à vous d'héberger un shoutrrr, un gotify ou d'utiliser votre smtp pour que ces informations ne sortent pas de votre SI ! (Je reproche beaucoup l'usage de msteams, slack, discord pour recevoir ses notifications)

WatchTower scannera les mises à jour de manière régulière (configurable).

container-updater (de @PAPAMICA)

La solution la plus fournie/complexe n'est pas toujours la meilleure. Papamica a mis en place un script bash répondant à ses besoins précis (que beaucoup d'autres personnes doivent avoir) : un système de mise à jour le notifiant par Discord et Zabbix.

Celui-ci se base aussi par les labels et prend également en charge le cas où l'on veut mettre à jour par docker-compose. (au lieu de faire un docker pull, docker restart comme Watchtower)

labels:
- "autoupdate=true"
- "autoupdate.docker-compose=/link/to/docker-compose.yml"

Même si je ne m'en sers pas, j'ai eu une époque où j'utilisais Zabbix et où j'avais le besoin d'être notifié sur mon Zabbix. (qui lui me notifiait par Mail/Gotify)

Papamica précise qu'il compte ajouter le support de registre privé (pour le moment que le github registry ou dockerhub) ainsi que d'autres méthodes de notification.

Solutions pour Swarm

Swarm est surement l'orchestrateur de conteneur sur lequel j'ai pris le plus de plaisir : c'est **simple** ! On apprend vite, on découvre vite et on a vite des résultats. Mais j'ai déjà écrit des éloges à Swarm dans un autre article...

Sheperd

Ce que j'aime dans le programme de Papamica (et qui va avec Sheperd) c'est qu'on garde le bash comme langage central. Un langage que l'on connait tous dans les grandes lignes grâce à Linux, et que l'on peut lire et modifier pour peu qu'on y prenne le temps.

Le code de Sheperd ne fait que ~200 lignes et fonctionne très bien comme ça.

version: "3"
services:
...
shepherd:
build: .
image: mazzolino/shepherd
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
placement:
constraints:
- node.role == manager

Celui-ci acceptera plusieurs registres privés, ce qui donne un bel avantage comparé aux autres solutions présentées. Exemple :

    deploy:
labels:
- shepherd.enable=true
- shepherd.auth.config=blog

Sheperd n'inclut pas (par défaut) de système de notification. C'est pourquoi son créateur a décidé de proposer un sidecar Apprise en alternative. Qui peut rediriger vers beaucoup de choses comme Telegram, SMS, Gotify, Mail, Slack, msteams etc....

Je pense que c'est la solution la plus simple et la plus polyvalente. J'espère qu'on la retrouvera dans d'autres contextes. (mais je ne m'étale pas trop sur le sujet, j'aimerais bien écrire un article sur ça).

J'ai utilisé Sheperd pendant une bonne période et je n'ai eu aucun soucis.

Solutions pour Kubernetes

Pour Kubernetes, on commence à perdre en simplicité. D'autant plus qu'avec l'option imagePullPolicy: Always, il suffit juste de redémarrer un pod pour récupérer la dernière image avec le même tag. Pendant un long moment, j'ai utilisé ArgoCD pour mettre à jour mes configurations et re-déployer mes images à chaque mise à jour sur Git.

Mais ArgoCD ne sert qu'à mettre à jour la configuration et non pas l'image. La méthodologie est incorrecte et il est nécessaire de trouver un outil adapté pour ça.

Keel.sh

Keel est un outil répondant au même besoin : Mettre à jour les images des pods. Mais il incorpore plusieurs fonctionnalités qu'on ne retrouve pas ailleurs.

Keel

Si on souhaite garder le même fonctionnement que les alternatives (c.-à-d régulièrement vérifier les mises à jour), c'est possible :

metadata:
annotations:
keel.sh/policy: force
keel.sh/trigger: poll
keel.sh/pollSchedule: "@every 3m"

Mais là où Keel brille, c'est qu'il propose des triggers et des approvals.

Un trigger, c'est un événement qui va déclencher la mise à jour de Keel. On peut imaginer un webhook provenant de Github, Dockerhub, Gitea qui va déclencher la mise à jour du serveur. (On évite donc une crontab régulière et on économise des ressources, du trafic et du temps) Comme l'usage de webhook s'est beaucoup répandu dans les systèmes de CICD, on peut coupler ça à de nombreux cas d'usages.

Les approvals, c'est la petite perle qui manquait aux autres outils. En effet, j'ai précisé que mettre à jour des images : c'est dangereux et il faut ne pas cibler des applications sensibles dans les mises à jour automatiques. Et c'est juste en réponse à ça que Keel a développé les approvals.

Système d'approval de Keel

L'idée est de donner l'autorisation à Keel de mettre à jour le pod. On peut de ce fait choisir le moment et vérifier manuellement.

Je trouve quand même dommage qu'on ait Slack ou MSTeams d'imposé pour les approvals, c'est alors une fonctionnalité que je n'utiliserai pas.

Une UI

Alors pour le moment, j'utilise Keel sans son interface web, il se peut qu'elle apporte de nouvelles fonctionnalités, mais j'aimerais éviter une énième interface à gérer.

Conclusion

Mettre à jour un conteneur, ce n'est pas si simple que ça quand on cherche l'automatisation et la sécurité. Si aujourd'hui, je trouve que Keel correspond à mes besoins, j'ai l'impression que les outils se ressemblent sans proposer de réelles innovations. (j'envisage d'aborder le principe de canary un jour) J'espère découvrir de nouvelles solutions prochainement en espérant qu'elles collent plus à mes besoins.

· 4 minutes de lecture
TheBidouilleur

Je suis en plein apprentissage de Kubernetes et des solutions pour gérer un cluster, je pratique sur un cluster de test sur lequel se trouve des petits conteneurs comme celui gérant thebidouilleur.xyz.

Longhorn est un incontournable dans l'univers Kubernetes (et notamment k3s), je ne pouvais pas continuer à apprendre sans m'attarder sur Longhorn. Mais avant tout..

Qu'est ce que Longhorn ?

Longhorn se présente sous cette simple phrase :

Longhorn is a lightweight, reliable and easy-to-use distributed block storage system for Kubernetes.

Mais on peut aller un peu plus loin que cette simple phrase... Longhorn est système de centralisation de stockage entre les noeuds du cluster. Cela veut dire qu'au lieu d'utiliser un stockage externe comme un NFS (ou autre, voici la liste des possibilités on va pouvoir utiliser garder les données en internes en utiliser les disques de nos machines présentes dans le cluster.

Et si vous vous posez la même question que moi avant de connaitre : Longhorn va faire l'équivalent d'un RAID 0 en réplicant les données sur plusieurs noeuds pour éviter que la perte d'une machine entraine la perte de donnée.

Valeurs concrètes

Par exemple, en comptant les disques de mes noeuds j'ai 4x32Gio et 1x16Gio, soit 144Gio ( ou 132Go parce que Rancher utilise cette valeur ). Sur ces 132Go, j'en occupe 36 actuellement, je peux en utiliser 56 sur Longhorn, et j'en ai 40 réservés aux réplicas. (par défaut, Rancher génère 3 replicas)

panel longhorn

Comment déployer Longhorn ?

lien de la documentation officielle

On peut déployer Longhorn via Helm, le catalogue Rancher ou juste via Kubectl (l'option que j'ai choisi)

kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.3.0/deploy/longhorn.yaml

Version !

Attention, cette commande va uniquement déployer la version 1.3.0 de longhorn, pensez à récupérer le dernier lien dans la documentation (ou éditer le lien que j'ai mis)

Par mesure de sécurité, il faut toujours vérifier ce que contient le yaml appliqué. Pensez à jeter un coup d'oeil !

Il faudra attendre que les pods se déploient pour commencer à utiliser Longhorn. Pour vérifier l'état en temps réel, la documentation vous propose la commande suivante:

kubectl get pods \
--namespace longhorn-system \
--watch

Mais vous pouvez aussi bien utiliser k9s.

Une fois OK, nous pourrons déployer notre premier pod lié à longhorn.

Mise en pratique Longhorn

Voici le manifest que l'on va déployer pour utiliser un volume dans longhorn:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: longhorn-nginx-thebidouilleur-demo
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: longhorn-thebidouilleur-demo
namespace: default
spec:
containers:
- name: block-volume-test
image: nginx:stable-alpine
imagePullPolicy: IfNotPresent
volumeMounts:
- name: volume-longhorn
mountPath: "/usr/share/nginx/html"
ports:
- containerPort: 80
volumes:
- name: volume-longhorn
persistentVolumeClaim:
claimName: longhorn-nginx-thebidouilleur-demo

Volume fonctionnel

On demande bien 1Gio à allouer à ce volume (ça influera sur le stockage alloué pour les replicas) et on déploie un nginx classique. Une fois déployé, on va ouvrir un tunnel vers ce pod:

kubectl port-forward longhorn-thebidouilleur-demo 8080:80
astuce

En sachant que le tunnel doit s'ouvrir sur votre poste local (et non sur un des noeuds du cluster). Je vous invite à consulter cette page pour mettre kubectl sur votre machine.

et si on interroge le nginx, on tombe évidemment sur une erreur 403 puisque le dossier longhorn est vide. On va donc créer notre fichier index.html directement depuis le pod.

kubectl exec longhorn-thebidouilleur-demo -i -t -- /bin/sh
echo "Hello World" > /usr/share/nginx/html/index.html

Et en réinterrogeant notre pod : on tombe bien sur notre Hello World

[thebidouilleur@bertha ~]$ curl localhost:8080
Hello World

Maintenant.. c'est bien mignon mais est-ce que nous gardons bien notre page en cas de suppression du pod ?

kubectl delete pod longhorn-thebidouilleur-demo

On voit bien que sur le dashboard longhorn : le volume est passé en deattach. (ce qui veut dire que les données sont toujours présentes mais pas utilisées sur un pod)

On va ré-appliquer le même manifest pour recréer notre pod et refaire le même tunnel pour accéder au nginx

[thebidouilleur@bertha ~]$ curl localhost:8080
Hello World

Nous retrouvons bel et bien notre page "Hello World"!

Conclusion

Longhorn est un outil extremement simple d'utilisation et permettant d'éviter de créer une solution externe au cluster qui serait moins pratique à gérer. Je ne suis pas non-plus aller très loin dans ses fonctionnalitées et je vous laisse vous faire votre propre avis pour longhorn en production (et pour ça, allez voir l'article du site easyadmin.tech) Longhorn est le bienvenue dans mon Homelab de test et sera au centre de celui-ci !

· 4 minutes de lecture
TheBidouilleur

Introduction

Mon premier cluster Kubernetes est actuellement en ligne. C'est encore un banc de test mais je l'ai pris prématurement en prod pour me forcer à l'administrer de manière sérieuse. Aujourd'hui (et en espérant que ça ait déjà évolué lorsque vous lirez l'article), mes pods utilisent un backend storage en NFS (accessible sur mon NAS).

Je veux que Kubernetes devienne mon manager de conteneur principal, je pense donc essentiel de découvrir les particularités générales de Kubernetes avant de commencer à y déployer des applications un peu plus complexes. Le stockage S3 est souvent référencé comme pratique et utile avec Kubernetes.

remarque

J'ai déjà utilisé Minio dans un autre contexte. Mais je ne compte pas utiliser Minio pour débuter S3, je veux une solution déjà prête et générique (apprendre la normalité avant de se spécialiser). Inutile de préciser qu'à l'avenir : Minio sera ma solution principale en Object Storage.

Je me suis donc orienté vers AWS Contabo qui propose une solution bien moins chère que notre amis américain. je paye 2,39€ mensuels pour 250Go à la place des 5,75€ demandés par Amazon.

Qu'est ce que le S3 ?

Pas besoin de faire une définition bancale, voici directement l'explication d'Amazon :

Amazon Simple Storage Service (Amazon S3) est un service de stockage d'objets qui offre une capacité de mise à l'échelle, une disponibilité des données, une sécurité et des performances de pointe. Les clients de toutes les tailles et de tous les secteurs peuvent stocker et protéger n'importe quelle quantité de données pour la quasi-totalité des cas d'utilisation, par exemple les lacs de données ainsi que les applications natives cloud et mobiles. Grâce à des classes de stockage économiques et à des fonctions de gestion faciles à utiliser, vous pouvez optimiser les coûts, organiser les données et configurer des contrôles d'accès affinés pour répondre à des exigences opérationnelles, organisationnelles et de conformité spécifiques.

Traduction : C'est une méthode performante et rapide de transférer des masses de données.

Et comment utiliser un stockage S3 ?

Il convient avant tout de rappeler une notion importante dans l'utilisation d'un Cloud : Un cloud n'est que l'ordinateur de quelqu'un d'autre

Si vous ne stockez pas chez vous : considerez que vos données peuvent être visionnées sans votre concentement. (Gouv, NSA, Mamie, Hacker etc...) Alors il convient de chiffrer vos données. Nous parlerons de Minio dans sur une autre page, une solution libre et open-source à héberger à la maison.

On peut dialoguer avec un serveur S3 via de nombreux outils :

Pour chiffrer mes données, je peux très bien passer par un simple script Bash chiffrant via GPG, puis envoyant les objets vers mon s3. Mais je n'apprécie pas cette solution bancale, et autant utiliser une solution all-in-one comme rclone ou restic. Et c'est effectivement avec restic que l'on va chiffrer et push les données.

Chiffrer puis envoyer ses objets

Comme dit précédemment : restic va être notre outil principal. Celui-ci fonctionne avec un système de "dépot"

Création du dépot restic

Restic permet de créer un dépot (qui peut être distant ou local), ce dépot chiffré sera le lieu où nous enverrons nos objets. Pour une première utilisation, on doit initialiser le dépot avec un restic init qui va créer la structure de fichier, et décider de la clé de chiffrement. Une fois le dépot créer, nous pourrons envoyer nos snapshots.

Restic autorise l'utilisation de variables d'environnement. On peut les définir avant d'utiliser restic.

export AWS_ACCESS_KEY_ID=ab5u8coxxpvjxwq4zu74jifmvfvxfu2y
export AWS_SECRET_ACCESS_KEY=3hs9sopqqto9sf8hhet8i92di987qcs6
bucketName="thebidouilleur" # variable séparée pour pouvoir la réutiliser ailleurs
export RESTIC_PASSWORD=Smudge9476 # Mot de passe de chiffrement
export RESTIC_REPOSITORY="s3:https://eu2.contabostorage.com/${bucketName}"

Ce ne sont pas mes vrais tokens, ne tentez pas d'utiliser les mêmes variables.

On peut enfin laisser restic créer notre dépot :

restic init

Si aucune erreur n'apparait ... félicitation ! On peut faire un restic backup pour créer notre première snapshot !

L'usage d'un S3 me permettra également de sauvegarder mes conteneurs utilisant des volumes sur Longhorn. Je pourrai ainsi sauvegarder mes données et les restaurer sur un autre cluster.