Este trabajo obligatorio fue realizado en el marco de la materia Programación para DevOps de la carrera Analista en Infraestructura Informática (Universidad ORT Uruguay).
El objetivo principal es aplicar conocimientos de scripting y automatización tanto a nivel de sistema operativo Linux como en la interacción con servicios en la nube (AWS) mediante herramientas de línea de comandos y programación en Python.
A lo largo del desarrollo se integran distintas tecnologías:
- Scripting en Bash para administración de archivos y generación de backups
- Automatización con Python utilizando el SDK de AWS (boto3)
- Gestión de servicios cloud: S3, EC2, RDS
- Control de versiones con Git
El trabajo se divide en tres ejercicios, cada uno con un enfoque específico, pero todos alineados a los principios fundamentales de DevOps: automatización, repetibilidad, gestión segura de recursos y documentación.
El repositorio se organiza en subdirectorios por ejercicio, junto con carpetas auxiliares para manejar archivos sensibles o resultados generados durante la ejecución de los scripts.
.
├── ejercicio1Bash/
│ ├── ej1_encuentra_SetUID.sh # Script Bash para búsqueda y backup de archivos setuid
│ └── resultados/ # Carpeta donde se generan los backups .tar.gz
├── ejercicio2Python/
│ └── ej2python.py # Script Python para subir backups a S3
├── ejercicio3Python/
│ ├── ej3python.py # Script Python para crear una RDS y una EC2 que la consuma
│ └── script_data_base/
│ └── obli.sql # Script SQL de creación y carga de base de datos
│ └── secrets/ # (Generado en tiempo de ejecución) Contiene archivo con contraseña de RDS
│ └──password.txt # Generado automáticamente por ej3python.py
└── README.md # Documento principal de documentación del proyecto
Nota: La carpeta secrets/ y el archivo password.txt no están incluidos en el repositorio ya que se generan dinámicamente durante la ejecución del ejercicio 3 por razones de seguridad.
Realizar un respaldo de seguridad de archivos del sistema, específicamente, aquellos que son ejecutables, pertenecientes al usuario root, y tienen el permiso especial setuid activado.
Este script en Bash debe buscar de forma recursiva todos los archivos que cumplan las siguientes condiciones:
- Son archivos regulares ejecutables
- Pertenecen al usuario
root - Tienen activado el permiso especial
setuid - Poseen permiso de ejecución para root y otros usuarios
Además, debe permitir de forma opcional:
- Generar un log con los caminos absolutos de los archivos encontrados (
-c) - Filtrar solo scripts de Bash, es decir, que comiencen con
#!/bin/bash(-b) - Indicar el directorio de búsqueda como parámetro opcional (por defecto se utiliza el directorio actual)
./ej1_encuentra_SetUID.sh [-c] [-b] [Directorio_donde_buscar]
-c (opcional) Genera un archivo de log con las rutas absolutas encontradas
-b (opcional) Filtra únicamente los archivos que sean scripts de Bash
Directorio (opcional) Directorio donde se realiza la búsqueda. Si no se indica, se usa `.`
- Se debe estar ubicado dentro del subdirectorio
ejercicio1Bash/del repositorio para el correcto funcionamiento del script.
cd ejercicio1Bash/
./ej1_encuentra_SetUID.sh [-b] [-c] [ruta]- El script requiere permisos para acceder a archivos propiedad de
root. Se recomienda ejecutarlo comosudosi se desea un análisis completo del sistema.
El script realiza un backup de archivos críticos del sistema que cumplen condiciones específicas (detallada más arriba en la sección Objetivo). Su funcionamiento puede resumirse en los siguientes pasos:
-
Validación de parámetros recibidos:
- Controla que se ingresen como máximo 3 argumentos
- Acepta los parámetros
-cy-b, sin repeticiones - Verifica que, si se indica una ruta, esta exista, sea un directorio y tenga permisos de lectura y ejecución
-
Búsqueda de archivos:
- Utiliza
findpara buscar archivos tipo regular, propiedad deroot, con permisosetuidy ejecución habilitada - Obtiene sus rutas absolutas con
readlink -f
- Utiliza
-
Filtrado opcional (
-b):- Si se indicó
-b, se filtran solo los archivos cuya primera línea es#!/bin/bash
- Si se indicó
-
Log y Backup:
- Si se indicó
-c, se crea un archivo.repcon todas las rutas encontradas - Se copian los archivos encontrados a una carpeta temporal
- Se genera un archivo comprimido
.tar.gzen la carpetaresultados/del proyecto - Se elimina la carpeta temporal de trabajo
- Si se indicó
-
Nombres generados automáticamente:
- Todos los archivos (
log,tar.gz) incluyen fecha y hora para evitar sobreescrituras y facilitar auditorías
- Todos los archivos (
A continuación se detallan los códigos de error definidos en el script para diferentes situaciones:
Código | Descripción
-------|-----------------------------------------------
0 | Script finalizó como se esperaba
1 | Cantidad de parámetros recibidos incorrecto
2 | Parámetro repetido
3 | Parámetro inválido
4 | Se ingresaron dos rutas o parámetros no válidos
5 | La ruta no existe
6 | La ruta no es un directorio válido
7 | No se puede acceder al directorio. Permisos denegados
8 | No se encontraron archivos que cumplan los criterios
#!/bin/bash
hacerLog=0
esBash=0
rutaBuscar=""
contadorRuta=0
#Valido cantidad de parametros
if [ $# -gt 3 ]
then
echo "Error: Cantidad de parámetros recibidos incorrecto" >&2
echo "Uso: $0 [-c] [-b] [RUTA]" >&2
exit 1
fi
#Analizo parametros activos
for parametro in "$@"; do
case "$parametro" in
-c)
if [ $hacerLog -eq 1 ]
then
echo "Error: Parámetro "$parametro" repetido" >&2
echo "Uso: $0 [-c] [-b] [RUTA]" >&2
exit 2
fi
hacerLog=1
;;
-b)
if [ $esBash -eq 1 ]
then
echo "Error: Parámetro "$parametro" repetido" >&2
echo "Uso: $0 [-c] [-b] [RUTA]" >&2
exit 2
fi
esBash=1
;;
-*)
echo "Error: Parámetro inválido '$parametro'" >&2
echo "Uso: $0 [-c] [-b] [RUTA]" >&2
exit 3
;;
*)
if [ "$contadorRuta" -eq 1 ]
then
echo "Error: Se ingresaron dos rutas o parámetros no válidos" >&2
echo "Uso: $0 [-c] [-b] [RUTA]" >&2
exit 4
fi
rutaBuscar=$parametro
contadorRuta=$((contadorRuta + 1))
;;
esac
done
# Si no se especificó una ruta, se usa el directorio actual
if [ -z "$rutaBuscar" ]
then
rutaBuscar="."
else
# Se valida parámetro ruta
if [ ! -e "$rutaBuscar" ] #Valido si la ruta existe
then
echo "Error: La ruta "$rutaBuscar" no existe" >&2
exit 5
elif [ ! -d "$rutaBuscar" ] #Valido si la ruta es un directorio
then
echo "Error: La ruta "$rutaBuscar" no es un directorio válido." >&2
exit 6
elif [ ! -r "$rutaBuscar" ] || [ ! -x "$rutaBuscar" ] #Valido si tengo permisos de lectura y ejecución
then
echo "Error: No se puede acceder al directorio "$rutaBuscar". Permisos denegados" >&2
exit 7
fi
fi
echo "Ruta a buscar: $rutaBuscar"
#Fecha que va a tener archivo de logs y archivo tar.gz si -c
fecha=$(date '+%d-%m-%y_%H-%M-%S')
#Lista de las rutas absolutas de archivos regulares, con owner root, permiso setuid y x (ejecutables) para usuario, grupo y otros activados
lista_ejecutables=$(find ${rutaBuscar} -type f -user root -perm -4101 -exec readlink -f {} \; 2>/dev/null)
#Filtrado según modificador -b
if [ ! $esBash -eq 1 ]
then
lista_para_backup=${lista_ejecutables}
else
for elemento in ${lista_ejecutables}
do
if head -n 1 ${elemento} | grep -q "^#!/bin/bash"
then
lista_para_backup+="${elemento}"$'\n'
fi
done
fi
if [ -z "$lista_para_backup" ]; then
echo "Error: No se encontraron archivos que cumplan los criterios." >&2
exit 8
fi
#Carpeta temporal para guardar archivos, y posterior tar.gz
carpTemporal="backupSetUID_${fecha}"
mkdir "./$carpTemporal"
#Copio los archivos encontrados a la carpeta temporal
for archivo in $lista_para_backup
do
cp $archivo $carpTemporal
done
#Archivo de log en caso de ingresar modificador -c
if [ $hacerLog -eq 1 ]
then
echo "$lista_para_backup" > logcaminos_${fecha}.rep
mv "logcaminos_${fecha}.rep" "./$carpTemporal/"
echo "Mensaje: Se escribió el archivo de log."
fi
#Creo archivo .tar.gz con los archivos encontrados
tar -czf "./resultados/backupSetUID_${fecha}.tar.gz" "./$carpTemporal"
echo "Mensaje: Se creó el backup backupSetUID_${fecha}.tar.gz."
#Elimino carpeta temporal
rm -rf "./$carpTemporal"
exit 0Desarrollar un script en Python que genere un Bucket S3 y suba los backup generados por el script del ejercicio 1.
- Crear un bucket S3 cuyo nombre debe ser
el-maligno-nro_de_estudiante1-nro_de_estudiante2
(en nuestro caso: el-maligno-177294-321438). - Subir al bucket el archivo generado previamente por el script de Bash encargado de crear el backup (tar.gz).
- El archivo debe subirse con el nombre:
Log_dia-mes-año-hora-minuto-segundo (por ejemplo:Log_24-06-2025-13-57-22).
python3 ej2python.py- Se debe estar ubicado dentro del subdirectorio
ejercicio2Python/del repositorio para el correcto funcionamiento del script.
cd ejercicio2Python/
python3 ej2python.pyEl script de Python está diseñado para buscar y subir automáticamente el archivo de backup generado por el ejercicio 1 a un bucket S3.
-
Ubicación de los backups:
El script busca los archivos.tar.gzdentro de la carpetaresultados, ubicada en el directorioejercicio1Bash/. Por eso, es necesario ejecutar este script desde el subdirectorioejercicio2Python/para que las rutas relativas funcionen correctamente. -
Búsqueda de archivos:
Utiliza expresiones regulares para identificar el primer archivo cuyo nombre comience conbackupSetUIDdentro de la carpeta de backups. -
Verificación de backups:
Si no se encuentra ningún archivo de backup válido, el script termina con un mensaje de error y un código de salida. -
Creación del bucket S3:
El script verifica si el bucket S3 con el nombre correspondiente (el-maligno-177294-321438) ya existe. Si no existe, lo crea automáticamente. -
Carga del backup:
El archivo de backup encontrado se sube al bucket S3, asignándole un nombre en el formatoLog_dia-mes-año_hora-min-seg.tar.gz.
A continuación se detallan los códigos de error definidos en el script para diferentes situaciones:
Código | Descripción
-------|-----------------------------------------------
0 | Script finalizó como se esperaba
1 | No se detectaron archivos de backup para subir. Finaliza
2 | Error creando el bucket
3 | Error subiendo el backup al bucket
import boto3
import datetime
import re
import os
# Ruta donde se está ejecutando el script
ruta_script = os.path.dirname(os.path.abspath(__file__))
# Ruta donde se encuentran los archivos de backup
directorio_backup = os.path.abspath(os.path.join(ruta_script, "../ejercicio1Bash/resultados"))
#Expresión regular para buscar archivos de backup
patron_backup = re.compile(r"backupSetUID.*")
backup_encontrado = ""
# Se recorre el directorio buscando el primer archivo que cumpla con la expresión regular
for archivo in os.listdir(directorio_backup):
if patron_backup.match(archivo):
backup_encontrado = os.path.join(directorio_backup, archivo)
break
# Se verifica si se encontró un archivo válido
if backup_encontrado == "":
print("Error: No se encontró ningún archivo de backup para subir.")
exit (1)
s3_client = boto3.client('s3')
bucket_name = 'el-maligno-177294-321438'
ruta_backup = backup_encontrado
fecha_actual = datetime.datetime.now().strftime("%d-%m-%y_%H-%M-%S") # Se agrega hora para no repetir nombres de archivos
object_name = os.path.basename(backup_encontrado) # Me devuelve el nombre de archivo sin la ruta
nombre_subir = f"Log_{fecha_actual}.tar.gz" # Nombre a subir al bucket
# Se verifica que bucket exista
try:
s3_client.head_bucket(Bucket=bucket_name)
print("El bucket", bucket_name, "ya existe.")
print("Subiendo backup...")
except Exception as e:
try:
s3_client.create_bucket(Bucket=bucket_name) # Se crea el bucket
print("Bucket", bucket_name, "creado correctamente.")
print("Subiendo backup...")
except Exception as e:
print("Error al crear el bucket:", e)
exit (2)
# Subir archivo
try:
s3_client.upload_file(ruta_backup, bucket_name, nombre_subir)
print("Back up '" + object_name + "' subido correctamente como", nombre_subir + ".")
except Exception as e:
print("Error al subir el archivo:", e)
exit(3)El script crea una base de datos MySQL en RDS y una instancia EC2, y se encargan de cargar los datos iniciales en la base usando el archivo obli.sql
- La instancia RDS deberá llamarse Maligno-DB
- Se debe crear una instancia EC2 llamada Maligno-SRV
- La instancia EC2 deberá tener instalado el cliente de MySQL para poder conectarse a la base
- La base de datos deberá cargarse con los datos especificados en el archivo
obli.sqldesde la instancia EC2
python3 ej3python.py- Se debe estar ubicado dentro del subdirectorio
ejercicio3Python/para ejecutar el script.
cd ejercicio3Python/
python3 ej3python.pyEl script automatiza el aprovisionamiento completo de los recursos necesarios para inicializar una base de datos con contenido predefinido, y sigue este flujo:
-
Solicitud de contraseña:
- Solicita al usuario una contraseña para el usuario admin de la instancia RDS (no puede estar vacía).
- La contraseña se guarda automáticamente en
secrets/password.txt.
-
Creación de grupos de seguridad:
- Crea un grupo de seguridad para la base de datos RDS (
MalignoSQL). - Crea un grupo de seguridad para la instancia EC2 (
EC2MalignoSRV). - Configura la regla que permite a la EC2 acceder al puerto 3306 de la RDS.
- Crea un grupo de seguridad para la base de datos RDS (
-
Creación de la instancia RDS:
- Crea una instancia RDS con MySQL y espera a que esté disponible.
- Recupera y guarda el endpoint para que pueda ser usado desde la EC2.
-
Preparación del script SQL:
- Lee el contenido del archivo
obli.sql.
- Lee el contenido del archivo
-
Despliegue de la instancia EC2:
- Genera una instancia Amazon Linux.
- Usa
user-datapara:- Instalar cliente MySQL (
mariadb1011-client-utils) - Crear el archivo
obli.sqlen/tmp/ - Ejecutar el script sobre la base de datos remota
- Instalar cliente MySQL (
-
Mensajes informativos:
- Se imprimen mensajes de avance y confirmación por consola en cada paso.
La contraseña no se almacena en el código fuente. Se guarda exclusivamente en el archivo
secrets/password.txtcreado en la ejecución del script.
Código | Descripción
-------|---------------------------------------------------------
0 | Script finalizó como se esperaba
1 | Error al crear grupo de seguridad de RDS
2 | Error al crear grupo de seguridad de EC2 o al asociarlo
3 | Error al crear la instancia EC2
4 | Error al crear la base de datos RDS
import boto3
import os
ec2 = boto3.client('ec2')
security_group_name = "MalignoSQL"
try:
response = ec2.create_security_group(
GroupName=security_group_name,
Description="Permitir acceso al puerto 3306 para MySQL desde EC2 Maligno-SRV",
)
security_group_id_MySQL = response['GroupId']
print("Mensaje: Grupo de seguridad creado con ID:", security_group_id_MySQL)
except Exception as e:
print("Error: Error al crear el grupo de seguridad:", e)
exit(1)
security_group_name_ec2 = "EC2MalignoSRV"
try:
response = ec2.create_security_group(
GroupName=security_group_name_ec2,
Description="Grupo de seguridad para la instancia EC2 y Acceso a MySQL"
)
security_group_id_ec2 = response['GroupId']
print("Mensaje: Grupo de seguridad EC2 creado con ID:", security_group_id_ec2)
ec2.authorize_security_group_ingress(
GroupId=security_group_id_MySQL,
IpPermissions=[
{
'IpProtocol': 'tcp',
'FromPort': 3306,
'ToPort': 3306,
'UserIdGroupPairs': [
{
'GroupId': security_group_id_ec2
}
]
}
]
)
print("Mensaje: Regla de seguridad para MySQL agregada al grupo de seguridad EC2.")
except Exception as e:
print("Error: Error al crear el grupo de seguridad EC2:", e)
exit(2)
# Crear la instancia RDS
rds = boto3.client('rds')
db_instance_identifier = 'Maligno-DB'
# Pido contraseña para la base de datos, no debe ser vacía
adminPassword=input("Ingrese la contraseña para el usuario admin de la base de datos RDS: ").strip()
# Valido que no esté vacía
while adminPassword == "":
print("Error: La contraseña no puede estar vacía.")
adminPassword=input("Ingrese la contraseña para el usuario admin de la base de datos RDS: ").strip()
# Creo carpeta secrets si no existe
os.makedirs("secrets", exist_ok=True)
# Guardar en archivo
with open("secrets/password.txt", "w") as f:
f.write(adminPassword)
print("Mensaje: Contraseña guardada correctamente en 'secrets/password.txt'.")
#Creo la base de datos
try:
response = rds.create_db_instance(
DBInstanceIdentifier=db_instance_identifier,
AllocatedStorage=20,
DBInstanceClass='db.t3.micro',
Engine='mysql',
MasterUsername='admin',
MasterUserPassword=adminPassword,
VpcSecurityGroupIds=[security_group_id_MySQL]
)
print("Mensaje: Base de datos RDS creada con ID:" ,db_instance_identifier)
#Espero a que la base de datos esté disponible
print("Mensaje: Esperando a que la base de datos esté disponible...")
waiter = rds.get_waiter('db_instance_available')
waiter.wait(DBInstanceIdentifier=db_instance_identifier)
db_instance = rds.describe_db_instances(DBInstanceIdentifier=db_instance_identifier)
db_endpoint = db_instance['DBInstances'][0]['Endpoint']['Address']
print("Mensaje: Base de datos RDS disponible en:", db_endpoint)
except Exception as e:
print("Error: Error al crear la base de datos RDS:", e)
# Leo el contenido del archivo SQL
with open("script_data_base/obli.sql", "r", encoding="utf-8") as f:
obli_sql = f.read()
# Crear la instancia EC2
script_sql = f'''#!/bin/bash
sudo dnf update -y
sudo dnf install -y mariadb1011-client-utils
sudo echo "Conexión a la base de datos RDS: {db_endpoint}" > /home/ec2-user/db_connection.txt
cat << 'EOF' > /tmp/obli.sql
{obli_sql}
EOF
sudo mysql -h {db_endpoint} -u admin -p{adminPassword} < /tmp/obli.sql
'''
try:
response = ec2.run_instances(
ImageId='ami-09e6f87a47903347c', #Amazon Linux ami
MinCount=1,
MaxCount=1,
InstanceType='t2.micro',
SecurityGroupIds=[security_group_id_ec2],
UserData=script_sql,
IamInstanceProfile={
'Name': 'LabInstanceProfile'
}
)
instance_id = response['Instances'][0]['InstanceId']
print("Mensaje: Instancia EC2 creada con ID:", instance_id)
#Espero a que la EC2 esté disponible
print("Mensaje: Esperando a que la instancia EC2 esté disponible...")
waiter = ec2.get_waiter('instance_running')
waiter.wait(InstanceIds=[instance_id])
print("Mensaje: La instancia EC2 está en ejecución.")
except Exception as e:
print("Error: Error al crear la instancia EC2:", e)
exit(3)