Update elastic stack test
This commit is contained in:
parent
89959d29ee
commit
4756d756ac
191
catch-all/05_infra_test/04_elastic_stack/README.md
Normal file
191
catch-all/05_infra_test/04_elastic_stack/README.md
Normal file
@ -0,0 +1,191 @@
|
||||
|
||||
# Cómo Usar Elasticsearch en Python con Docker
|
||||
|
||||
|
||||
|
||||
**Índice**
|
||||
- [Cómo Usar Elasticsearch en Python con Docker](#cómo-usar-elasticsearch-en-python-con-docker)
|
||||
- [¿Qué es Elasticsearch?](#qué-es-elasticsearch)
|
||||
- [Requisitos Previos](#requisitos-previos)
|
||||
- [Crear un Clúster Local del Elastic Stack con Docker Compose](#crear-un-clúster-local-del-elastic-stack-con-docker-compose)
|
||||
- [Desplegar el Elastic Stack](#desplegar-el-elastic-stack)
|
||||
- [Dockerizar el Programa Python](#dockerizar-el-programa-python)
|
||||
- [Programa Python](#programa-python)
|
||||
- [Conectar a Tu Clúster desde el Contenedor Python](#conectar-a-tu-clúster-desde-el-contenedor-python)
|
||||
- [Ejecutar los Scripts Python en Docker](#ejecutar-los-scripts-python-en-docker)
|
||||
- [Construir y Ejecutar el Contenedor Python](#construir-y-ejecutar-el-contenedor-python)
|
||||
- [Visualización de Datos con Kibana](#visualización-de-datos-con-kibana)
|
||||
- [Usar Logstash para Ingestión de Datos](#usar-logstash-para-ingestión-de-datos)
|
||||
- [Recopilar Métricas con Metricbeat](#recopilar-métricas-con-metricbeat)
|
||||
- [Eliminar Documentos del Índice](#eliminar-documentos-del-índice)
|
||||
- [Eliminar un Índice](#eliminar-un-índice)
|
||||
- [Conclusión](#conclusión)
|
||||
|
||||
[Elasticsearch](https://www.elastic.co/elasticsearch/) (ES) es una tecnología utilizada por muchas empresas, incluyendo GitHub, Uber y Facebook. Aunque no se enseña con frecuencia en cursos de Ciencia de Datos, es probable que te encuentres con ella en tu carrera profesional.
|
||||
|
||||
Muchos científicos de datos enfrentan dificultades al configurar un entorno local o al intentar interactuar con Elasticsearch en Python. Además, no hay muchos recursos actualizados disponibles.
|
||||
|
||||
Por eso, decidí crear este tutorial. Te enseñará lo básico y podrás configurar un clúster de Elasticsearch en tu máquina para el desarrollo local rápidamente. También aprenderás a crear un índice, almacenar datos en él y usarlo para buscar información.
|
||||
|
||||
¡Vamos a empezar!
|
||||
|
||||
|
||||
## ¿Qué es Elasticsearch?
|
||||
|
||||
Elasticsearch es un motor de búsqueda distribuido, rápido y fácil de escalar, capaz de manejar datos textuales, numéricos, geoespaciales, estructurados y no estructurados. Es un motor de búsqueda popular para aplicaciones, sitios web y análisis de registros. También es un componente clave del Elastic Stack (también conocido como ELK Stack), que incluye Logstash y Kibana, junto con Beats para la recolección de datos.
|
||||
|
||||
Para entender el funcionamiento interno de Elasticsearch, piensa en él como dos procesos distintos. Uno es la ingestión, que normaliza y enriquece los datos brutos antes de indexarlos usando un [índice invertido](https://www.elastic.co/guide/en/elasticsearch/reference/current/documents-indices.html). El segundo es la recuperación, que permite a los usuarios recuperar datos escribiendo consultas que se ejecutan contra el índice.
|
||||
|
||||
Eso es todo lo que necesitas saber por ahora. A continuación, prepararás tu entorno local para ejecutar un clúster del Elastic Stack.
|
||||
|
||||
## Requisitos Previos
|
||||
|
||||
Debes configurar algunas cosas antes de comenzar. Asegúrate de tener lo siguiente listo:
|
||||
|
||||
1. Instala Docker y Docker Compose.
|
||||
2. Descarga los datos necesarios.
|
||||
|
||||
## Crear un Clúster Local del Elastic Stack con Docker Compose
|
||||
|
||||
Para desplegar el Elastic Stack (Elasticsearch, Kibana, Logstash y Beats) en tu máquina local, utilizaremos Docker Compose. Este enfoque simplifica el despliegue y la administración de múltiples servicios.
|
||||
|
||||
Mediante el fichero [docker-compose.yaml](docker-compose.yaml) vamos a configurar los servicios para Elasticsearch, Kibana, Logstash, Metricbeat, y la aplicación Python, todos conectados a una red llamada `elastic`.
|
||||
|
||||
|
||||
### Desplegar el Elastic Stack
|
||||
|
||||
Para iniciar los servicios, abre una terminal en el directorio donde se encuentra `docker-compose.yml` y ejecuta el siguiente comando:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Este comando iniciará Elasticsearch, Kibana, Logstash, Metricbeat y la aplicación Python en contenedores separados. Aquí tienes un desglose de los servicios:
|
||||
|
||||
- **Elasticsearch**: El motor de búsqueda que almacena y analiza los datos.
|
||||
- **Kibana**: Una interfaz de usuario para visualizar y explorar datos en Elasticsearch.
|
||||
- **Logstash**: Una herramienta de procesamiento de datos que ingiere datos desde múltiples fuentes, los transforma y los envía a un destino como Elasticsearch.
|
||||
- **Metricbeat**: Un agente de monitoreo que recopila métricas del sistema y las envía a Elasticsearch.
|
||||
- **Python App**: Un contenedor que ejecutará tus scripts de Python para interactuar con Elasticsearch. (Hasta que no construyamos el contenedor, este servicio fallará).
|
||||
|
||||
|
||||
## Dockerizar el Programa Python
|
||||
|
||||
Para dockerizar la aplicación Python, utilizaremos el archivo [Dockerfile](./app/Dockerfile) del directorio `app`. El directorio `app` también debe contener el programa Python y un archivo [requirements.txt](./app/requirements.txt) para manejar las dependencias.
|
||||
|
||||
### Programa Python
|
||||
|
||||
El archivo [main.py](./app/main.py) en el directorio `app` manejará la conexión a Elasticsearch, la creación de índices y la carga de datos.
|
||||
|
||||
Este programa realiza las siguientes acciones:
|
||||
|
||||
1. **Crea un índice** en Elasticsearch con las configuraciones de mapeo necesarias.
|
||||
2. **Carga datos** desde un archivo CSV (`wiki_movie_plots_deduped.csv`) al índice creado.
|
||||
|
||||
|
||||
## Conectar a Tu Clúster desde el Contenedor Python
|
||||
|
||||
Tu aplicación Python se conectará al clúster de Elasticsearch usando el hostname `elasticsearch`, que es el nombre del servicio definido en `docker-compose.yml`.
|
||||
|
||||
|
||||
## Ejecutar los Scripts Python en Docker
|
||||
|
||||
Una vez que hayas creado los archivos `Dockerfile`, `requirements.txt` y `main.py`, puedes construir la imagen de Docker para tu aplicación Python y ejecutarla usando Docker Compose.
|
||||
|
||||
|
||||
### Construir y Ejecutar el Contenedor Python
|
||||
|
||||
1. Construye la imagen de Docker para tu aplicación Python:
|
||||
|
||||
```bash
|
||||
docker compose build python-app
|
||||
```
|
||||
|
||||
2. Ejecuta el contenedor:
|
||||
|
||||
```bash
|
||||
docker compose up python-app
|
||||
```
|
||||
|
||||
La aplicación Python se ejecutará y cargará los datos en Elasticsearch. Puedes verificar que los datos se hayan indexado correctamente ejecutando una consulta en Elasticsearch o usando Kibana para explorar los datos:
|
||||
|
||||
```python
|
||||
es.search(index="movies", body={"query": {"match_all": {}}})
|
||||
```
|
||||
|
||||
|
||||
## Visualización de Datos con Kibana
|
||||
|
||||
Kibana es una herramienta de visualización que se conecta a Elasticsearch y te permite explorar y visualizar tus datos.
|
||||
|
||||
Para acceder a Kibana, abre un navegador web y navega a `http://localhost:5601`. Deberías ver la interfaz de Kibana, donde puedes crear visualizaciones y dashboards.
|
||||
|
||||
1. **Crea un índice en Kibana**: Ve a *Management > Index Patterns* y crea un nuevo patrón de índice para el índice `movies`.
|
||||
2. **Explora tus datos**: Usa la herramienta *Discover* para buscar y explorar los datos que has indexado.
|
||||
3. **Crea visualizaciones**: En la sección *Visualize*, crea gráficos y tablas que te permitan entender mejor tus datos.
|
||||
|
||||
|
||||
## Usar Logstash para Ingestión de Datos
|
||||
|
||||
Logstash es una herramienta para procesar y transformar datos antes de enviarlos a Elasticsearch. Aquí tienes un ejemplo básico de cómo configurar Logstash para que procese y envíe datos a Elasticsearch.
|
||||
|
||||
Mediante el archivo de configuración en la carpeta `logstash-config/` llamado [pipeline.conf](./logstash-config/pipeline.conf) realizaremos los siguientes pasos:
|
||||
|
||||
- Lee un archivo CSV de entrada.
|
||||
- Usa el filtro `csv` para descomponer cada línea en campos separados.
|
||||
- Usa `mutate` para convertir el año de lanzamiento en un número entero.
|
||||
- Envía los datos procesados a Elasticsearch.
|
||||
|
||||
Para usar Logstash con esta configuración, asegúrate de que el archivo `wiki_movie_plots_deduped.csv` esté accesible en tu sistema y modifica la ruta en el archivo de configuración según sea necesario. Luego, reinicia el contenedor de Logstash para aplicar los cambios.
|
||||
|
||||
```bash
|
||||
docker compose restart logstash
|
||||
```
|
||||
|
||||
|
||||
## Recopilar Métricas con Metricbeat
|
||||
|
||||
Metricbeat es un agente ligero que recopila métricas del sistema y las envía a Elasticsearch. Está configurado en el archivo `docker-compose.yml` que has creado anteriormente.
|
||||
|
||||
Para ver las métricas en Kibana:
|
||||
|
||||
1. **Configura Metricbeat**: Edita el archivo de configuración de Metricbeat si necesitas recopilar métricas específicas.
|
||||
2. **Importa dashboards preconfigurados**: En Kibana, navega a *Add Data > Metricbeat* y sigue las instrucciones para importar los dashboards preconfigurados.
|
||||
3. **Explora las métricas**: Usa los dashboards importados para explorar las métricas de tu sistema.
|
||||
|
||||
|
||||
## Eliminar Documentos del Índice
|
||||
|
||||
Puedes usar el siguiente código para eliminar documentos del índice:
|
||||
|
||||
```python
|
||||
es.delete(index="movies", id="2500")
|
||||
```
|
||||
|
||||
El código anterior eliminará el documento con ID 2500 del índice `movies`.
|
||||
|
||||
|
||||
## Eliminar un Índice
|
||||
|
||||
Finalmente, si por alguna razón deseas eliminar un índice (y todos sus documentos), aquí te mostramos cómo hacerlo:
|
||||
|
||||
```python
|
||||
es.indices.delete(index='movies')
|
||||
```
|
||||
|
||||
|
||||
## Conclusión
|
||||
|
||||
Este tutorial te enseñó los conceptos básicos de Elasticsearch y cómo usarlo junto con el Elastic Stack y Docker. Esto será útil en tu carrera, ya que seguramente te encontrarás con Elasticsearch en algún momento.
|
||||
|
||||
En este tutorial, has aprendido:
|
||||
|
||||
- Cómo configurar un clúster del Elastic Stack en tu máquina usando Docker Compose.
|
||||
- Cómo dockerizar una aplicación Python para interactuar con Elasticsearch.
|
||||
- Cómo crear un índice y almacenar datos en él.
|
||||
- Cómo buscar tus datos usando Elasticsearch.
|
||||
- Cómo visualizar datos con Kibana.
|
||||
- Cómo procesar datos con Logstash.
|
||||
- Cómo recopilar métricas con Metricbeat.
|
||||
|
||||
Explora más funcionalidades de Elasticsearch y el Elastic Stack para sacar el máximo provecho de tus datos.
|
15
catch-all/05_infra_test/04_elastic_stack/app/Dockerfile
Normal file
15
catch-all/05_infra_test/04_elastic_stack/app/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
# Usa la imagen base de Python
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Establece el directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copia el archivo requirements.txt e instala las dependencias
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copia el código fuente a /app
|
||||
COPY . /app/
|
||||
|
||||
# Comando para ejecutar la aplicación
|
||||
CMD ["python", "main.py"]
|
114
catch-all/05_infra_test/04_elastic_stack/app/main.py
Normal file
114
catch-all/05_infra_test/04_elastic_stack/app/main.py
Normal file
@ -0,0 +1,114 @@
|
||||
import pandas as pd
|
||||
from elasticsearch import Elasticsearch
|
||||
from elasticsearch.helpers import bulk
|
||||
|
||||
# Configura la conexión a Elasticsearch
|
||||
es = Elasticsearch("http://elasticsearch:9200")
|
||||
|
||||
|
||||
def create_index():
|
||||
"""
|
||||
Crea un índice en Elasticsearch con el nombre 'movies' si no existe.
|
||||
Define el mapeo del índice para los campos de los documentos.
|
||||
"""
|
||||
|
||||
# Define el mapeo del índice 'movies'
|
||||
mappings = {
|
||||
"properties": {
|
||||
# Campo para el título de la película
|
||||
"title": {"type": "text", "analyzer": "english"},
|
||||
# Campo para la etnicidad
|
||||
"ethnicity": {"type": "text", "analyzer": "standard"},
|
||||
# Campo para el director
|
||||
"director": {"type": "text", "analyzer": "standard"},
|
||||
# Campo para el elenco
|
||||
"cast": {"type": "text", "analyzer": "standard"},
|
||||
# Campo para el género
|
||||
"genre": {"type": "text", "analyzer": "standard"},
|
||||
# Campo para el argumento de la película
|
||||
"plot": {"type": "text", "analyzer": "english"},
|
||||
# Campo para el año de lanzamiento
|
||||
"year": {"type": "integer"},
|
||||
# Campo para la página de Wikipedia
|
||||
"wiki_page": {"type": "keyword"}
|
||||
}
|
||||
}
|
||||
|
||||
# Verifica si el índice 'movies' ya existe
|
||||
if not es.indices.exists(index="movies"):
|
||||
|
||||
# Crea el índice 'movies' si no existe
|
||||
es.indices.create(index="movies", mappings=mappings)
|
||||
print("\n[+] Índice 'movies' creado.")
|
||||
|
||||
else:
|
||||
|
||||
print("\n[!] El índice 'movies' ya existe.")
|
||||
|
||||
|
||||
def load_data():
|
||||
"""
|
||||
Carga datos desde un archivo CSV a Elasticsearch.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
# Lee el archivo CSV
|
||||
df = pd.read_csv("/app/wiki_movie_plots_deduped.csv", quoting=1)
|
||||
|
||||
# Verifica el número de filas en el DataFrame
|
||||
num_rows = len(df)
|
||||
sample_size = min(5000, num_rows)
|
||||
|
||||
# Elimina filas con valores nulos y toma una muestra
|
||||
df = df.dropna().sample(sample_size, random_state=42).reset_index(drop=True)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
print(f"\n[!] Error al leer el archivo CSV: {e}")
|
||||
|
||||
return
|
||||
|
||||
# Prepara los datos para la carga en Elasticsearch
|
||||
bulk_data = [
|
||||
{
|
||||
"_index": "movies", # Nombre del índice en Elasticsearch
|
||||
"_id": i, # ID del documento en Elasticsearch
|
||||
"_source": {
|
||||
"title": row["Title"], # Título de la película
|
||||
"ethnicity": row["Origin/Ethnicity"], # Etnicidad
|
||||
"director": row["Director"], # Director
|
||||
"cast": row["Cast"], # Elenco
|
||||
"genre": row["Genre"], # Género
|
||||
"plot": row["Plot"], # Argumento
|
||||
"year": row["Release Year"], # Año de lanzamiento
|
||||
"wiki_page": row["Wiki Page"], # Página de Wikipedia
|
||||
}
|
||||
}
|
||||
for i, row in df.iterrows() # Itera sobre cada fila del DataFrame
|
||||
]
|
||||
|
||||
try:
|
||||
|
||||
# Carga los datos en Elasticsearch en bloques
|
||||
bulk(es, bulk_data)
|
||||
print("\n[+] Datos cargados en Elasticsearch.")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
print(f"\n[!] Error al cargar datos en Elasticsearch: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Función principal que crea el índice y carga los datos.
|
||||
"""
|
||||
|
||||
create_index() # Crea el índice en Elasticsearch
|
||||
load_data() # Carga los datos en Elasticsearch
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Ejecuta la función principal si el script se ejecuta directamente
|
||||
|
||||
main()
|
@ -0,0 +1,3 @@
|
||||
pandas==2.0.1
|
||||
numpy==1.24.2
|
||||
elasticsearch==8.8.0
|
@ -0,0 +1,7 @@
|
||||
Title,Origin/Ethnicity,Director,Cast,Genre,Plot,Release Year,Wiki Page
|
||||
The Shawshank Redemption,American,Frank Darabont,"Tim Robbins, Morgan Freeman",Drama,"Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.",1994,https://en.wikipedia.org/wiki/The_Shawshank_Redemption
|
||||
The Godfather,American,Francis Ford Coppola,"Marlon Brando, Al Pacino",Crime,"The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",1972,https://en.wikipedia.org/wiki/The_Godfather
|
||||
The Dark Knight,American,Christopher Nolan,"Christian Bale, Heath Ledger",Action,"When the menace known as the Joker emerges from his mysterious past, he wreaks havoc and chaos on the people of Gotham.",2008,https://en.wikipedia.org/wiki/The_Dark_Knight
|
||||
Pulp Fiction,American,Quentin Tarantino,"John Travolta, Uma Thurman",Crime,"The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.",1994,https://en.wikipedia.org/wiki/Pulp_Fiction
|
||||
The Lord of the Rings: The Return of the King,American,Peter Jackson,"Elijah Wood, Viggo Mortensen",Fantasy,"The final battle for Middle-earth begins. The forces of good and evil are drawn into a confrontation and the outcome will determine the fate of the world.",2003,https://en.wikipedia.org/wiki/The_Lord_of_the_Rings:_The_Return_of_the_King
|
||||
Inception,American,Christopher Nolan,"Leonardo DiCaprio, Joseph Gordon-Levitt",Sci-Fi,"A thief who enters the dreams of others to steal secrets from their subconscious is given the inverse task of planting an idea into the mind of a CEO.",2010,https://en.wikipedia.org/wiki/Inception
|
|
59
catch-all/05_infra_test/04_elastic_stack/docker-compose.yaml
Normal file
59
catch-all/05_infra_test/04_elastic_stack/docker-compose.yaml
Normal file
@ -0,0 +1,59 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.15.2
|
||||
container_name: elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
ports:
|
||||
- "9200:9200"
|
||||
networks:
|
||||
- elastic
|
||||
|
||||
kibana:
|
||||
image: docker.elastic.co/kibana/kibana:7.15.2
|
||||
container_name: kibana
|
||||
environment:
|
||||
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
|
||||
ports:
|
||||
- "5601:5601"
|
||||
networks:
|
||||
- elastic
|
||||
|
||||
logstash:
|
||||
image: docker.elastic.co/logstash/logstash:7.15.2
|
||||
container_name: logstash
|
||||
volumes:
|
||||
- ./logstash-config/:/usr/share/logstash/pipeline/
|
||||
environment:
|
||||
LS_JAVA_OPTS: "-Xmx256m -Xms256m"
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "9600:9600"
|
||||
networks:
|
||||
- elastic
|
||||
|
||||
metricbeat:
|
||||
image: docker.elastic.co/beats/metricbeat:7.15.2
|
||||
container_name: metricbeat
|
||||
command: metricbeat -e -E output.elasticsearch.hosts=["http://elasticsearch:9200"]
|
||||
depends_on:
|
||||
- elasticsearch
|
||||
networks:
|
||||
- elastic
|
||||
|
||||
python-app:
|
||||
build:
|
||||
context: ./app
|
||||
dockerfile: Dockerfile
|
||||
container_name: python-app
|
||||
volumes:
|
||||
- ./app:/app
|
||||
networks:
|
||||
- elastic
|
||||
|
||||
networks:
|
||||
elastic:
|
||||
driver: bridge
|
@ -0,0 +1,26 @@
|
||||
input {
|
||||
file {
|
||||
path => "/path/to/your/data/wiki_movie_plots_deduped.csv"
|
||||
start_position => "beginning"
|
||||
sincedb_path => "/dev/null"
|
||||
}
|
||||
}
|
||||
|
||||
filter {
|
||||
csv {
|
||||
separator => ","
|
||||
columns => ["Release Year", "Title", "Origin/Ethnicity", "Director", "Cast", "Genre", "Wiki Page", "Plot"]
|
||||
}
|
||||
|
||||
mutate {
|
||||
convert => { "Release Year" => "integer" }
|
||||
}
|
||||
}
|
||||
|
||||
output {
|
||||
elasticsearch {
|
||||
hosts => ["http://elasticsearch:9200"]
|
||||
index => "movies"
|
||||
}
|
||||
stdout { codec => rubydebug }
|
||||
}
|
@ -6,10 +6,11 @@
|
||||
|
||||
</div>
|
||||
|
||||
| Nombre | Descripción | Nivel |
|
||||
| --------------------------------- | --------------------------------------------------- | ---------- |
|
||||
| [redis](./01_redis_flask_docker/) | Despliegue de un contenedor de redis y flask | Básico |
|
||||
| [rabbit](./02_rabbitmq/README.md) | Despliegue de distintas arquitecturas para rabbitmq | Intermedio |
|
||||
| Apache Kafka (proximamente) | Despliegue de un contenedor de Apache Kafka | Básico |
|
||||
| Prometheus Grafana (proximamente) | Despliegue de un contenedor de Prometheus Grafana | Básico |
|
||||
| SonarQube (proximamente) | Despliegue de un contenedor de SonarQube | Básico |
|
||||
| Nombre | Descripción | Nivel |
|
||||
| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------- |
|
||||
| [redis](./01_redis_flask_docker/) | Despliegue de un contenedor de redis y flask | Básico |
|
||||
| [rabbit](./02_rabbitmq/README.md) | Despliegue de distintas arquitecturas para rabbitmq | Intermedio |
|
||||
| Apache Kafka (próximamente) <!-- [Apache Kafka](./03_kafka/README.md) --> | Despliegue de Apache Kafka con productor y consumidor | Básico |
|
||||
| [Elastic stack](./04_elastic_stack/README.md) | Despliegue de Elastic Stack | Básico |
|
||||
| Prometheus Grafana (proximamente) <!-- [Prometheus Grafana](./05_prometheus_grafana/README.md) --> | Despliegue de un contenedor de Prometheus Grafana | Básico |
|
||||
| SonarQube (proximamente) <!-- [SonarQube](./06_sonarqube/README.md) --> | Despliegue de un contenedor de SonarQube | Básico |
|
||||
|
Loading…
Reference in New Issue
Block a user