
Devops From Scratch (Automated Basics)
Description of Devops From Scratch with Automated Basics
Recap
What we did
Created a Django TODO application
Provisionned a VM on AWS
Installed the required Python dependencies on VM
Uploaded Application Code to VM
Ran Dabase Migrtions on VM
Setup Supervisord for Running, Monitoring, and Keeping Alive the Application
Pain Points
Complicated hand-written commands
Not easily repeatable accross machines
Keeping the process alive
Machine Dependencies
Better Tools and Processes
Use code as it is familiar
Use configuration files for reuseability
Use code and configuration to provision at scale
Portable infrastructure through containers
Container runtime for building and shipping applications
Use container scheduler for bin packing
Use container scheduler for scale
Automation
Infrastructure (VMs / Fabric)
*.pem *.tar
[tool.poetry] name = "devops-from-scratch-automated-basics" version = "0.1.0" description = "DevOps from Scratch (Automated Basics)" authors = ["user"] license = "MIT" readme = "README.md" #packages = [{include = "devops_from_scratch_automated_basics"}] [tool.poetry.dependencies] #python = "^3.10.6" python = "^3.11" sortedcontainers = "^2.4.0" httpx = "^0.23.0" invoke = "2.0.0" fabric = "^3.0.1" paramiko = "^3.1.0" typer = { version = "^0.9.0", extras = ["all"] } #[tool.poetry.group.dev.dependencies] #black = "^23.3.0" #pytest = "7.3.1" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api"
#+name config.ini
[ubuntu.main] host = ubuntu@15.222.44.100 app_dir = /home/ubuntu pem_file = ubuntu-main.pem
#+name fabfile.py
import configparser from fabric import ( Connection, Config, task, ) # Read the configuration file config = configparser.ConfigParser() config.read('config.ini') def get_server_connection(server, config=config): # Define the hosts where the tasks will be executed host = config.get(server, 'host') # Set the path to the .pem file pem_file = config.get(server, 'pem_file') # Create a configuration for the connection fab_config = Config( overrides={ 'connect_kwargs': { 'key_filename': pem_file } }) # Create a connection conn = Connection(host, config=fab_config) return conn # Ubuntu Main Configuration Section Name #server = 'ubuntu.main' # Define the directory of the application #app_dir = config.get(server, 'app_dir') @task def machine_info(ctx, server): # Define the server connection with get_server_connection(server) as conn: conn.run('uname') conn.run('uname -n') conn.run('uname -v') conn.run('uname -m') conn.run('lsb_release -a') conn.run('whoami') conn.run('date')
Testing (CI / PyTest)
Another time...
Deployment (CD / Docker)
We will take the following steps to Prepare our initial Virtual Machine:
Acquire VM
Install Docker
Setup Docker Swarm
Mark it as Master Node
Install Docker
# Update your existing list of packages sudo apt update # Install a few prerequisite packages which let `apt` use packages over HTTPS sudo apt install apt-transport-https ca-certificates curl software-properties-common # Add the GPG key for the official Docker repository to your system curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # Add the Docker repository to APT sources echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Update your existing list of packages again for the addition to be recognized sudo apt update # Make sure you are about to install from the Docker repo instead of the default Ubuntu repo apt-cache policy docker-ce # Install Docker sudo apt install docker-ce # Check that it’s running sudo systemctl status docker
#+name fabfile.py
@task def install_docker(ctx, server): # Define the server connection with get_server_connection(server) as conn: # Update your existing list of packages conn.sudo('apt update') # Install a few prerequisite packages which let `apt` use packages over HTTPS #with settings(prompts={'Do you want to continue [Y/n]?':'Y'}): conn.sudo('apt install apt-transport-https ca-certificates curl software-properties-common') # Add the GPG key for the official Docker repository to your system conn.run('curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg') # Add the Docker repository to APT sources conn.run('echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null') # Update your existing list of packages again for the addition to be recognized conn.sudo('apt update') # Make sure you are about to install from the Docker repo instead of the default Ubuntu repo conn.run('apt-cache policy docker-ce') # Install Docker conn.sudo('apt install docker-ce') # Check that it’s running conn.sudo('systemctl status docker')
Note that the `install_docker` function is not idenpotent and may fail on certain steps during execution.
Configure Docker
Docker without `sudo`
# Add your username to the docker group sudo usermod -aG docker ${USER}
Apply the new group membership, log out of the server and back in (Optional?)
su - ${USER} groups
#+name fabfile.py
@task def configure_docker(ctx, server): # Define the server connection with get_server_connection(server) as conn: # Add your username to the docker group conn.sudo('usermod -aG docker ${USER}') #conn.run('su - ${USER}') conn.run('groups')
Check Docker's Installation
Check whether Docker Hub's images are accessible and can be downloaded
docker run hello-world
#+name fabfile.py
@task def check_docker(ctx, server): # Define the server connection with get_server_connection(server) as conn: conn.run('docker run hello-world')
Containers
What? (Definition)
English
A container is a lightweight, standalone, executable software package that includes everything needed to run a piece of software, including the code, a runtime, libraries, environment variables, and config files. Containers are designed to provide a consistent and reproducible environment, which can be easily ported across different host systems. They isolate the application process from the host, providing an added layer of security, and allow for resource control, providing efficiency.
Français
Un conteneur est un package logiciel léger, autonome et exécutable qui comprend tout ce qui est nécessaire pour exécuter un logiciel, y compris le code, un environnement d'exécution, des bibliothèques, des variables d'environnement et des fichiers de configuration. Les conteneurs sont conçus pour fournir un environnement cohérent et reproductible, qui peut être facilement porté sur différents systèmes hôtes. Ils isolent le processus d'application de l'hôte, offrant une couche supplémentaire de sécurité, et permettent un contrôle des ressources, offrant une efficacité.
Why? (Benefit)
English
Containers are used because they offer several benefits. Firstly, they ensure that applications run the same regardless of where and when they are run, increasing deployment speed and reliability. Secondly, they provide process isolation by keeping applications and their dependencies separate from each other, thus improving security. Thirdly, they utilize system resources efficiently, which reduces infrastructure costs.
Français
Les conteneurs sont utilisés parce qu'ils offrent plusieurs avantages. Premièrement, ils garantissent que les applications fonctionnent de la même manière, quel que soit le lieu et le moment où elles sont exécutées, ce qui augmente la vitesse et la fiabilité du déploiement. Deuxièmement, ils fournissent une isolation des processus en gardant les applications et leurs dépendances séparées les unes des autres, améliorant ainsi la sécurité. Troisièmement, ils utilisent efficacement les ressources du système, ce qui réduit les coûts d'infrastructure.
When? (LXC vs Docker)
English
LXC (LinuX Containers) is an operating system-level virtualization method that allows running multiple isolated Linux systems (containers) on a single control host. LXC uses the Linux kernel cgroups and namespace functionality to isolate software applications and their environment from the underlying system. LXC was first released in 2008 and was the first, most complete implementation of Linux container manager. It's considered "lighter" than a VM because it runs at the OS level, not the hardware level.
Docker is an open-source platform used to automate the deployment, scaling, and management of applications by packaging them into containers. Docker, introduced in 2013, expanded on LXC's approach to providing a standardized interface for developers and sysadmins to develop, package, and run applications agnostic to the operating environment. Docker has since replaced LXC as the predominant containerization technology.
Français
LXC (LinuX Containers) est une méthode de virtualisation au niveau du système d'exploitation qui permet d'exécuter plusieurs systèmes Linux isolés (conteneurs) sur un seul hôte de contrôle. LXC utilise les fonctionnalités cgroups et namespace du noyau Linux pour isoler les applications logicielles et leur environnement du système sous-jacent. LXC a été lancé pour la première fois en 2008 et a été la première implémentation la plus complète du gestionnaire de conteneurs Linux. Il est considéré comme "plus léger" qu'une VM car il fonctionne au niveau du système d'exploitation, et non au niveau du matériel.
Docker est une plateforme open source utilisée pour automatiser le déploiement, la mise à l'échelle et la gestion des applications en les emballant dans des conteneurs. Docker, introduit en 2013, a développé l'approche de LXC en fournissant une interface standardisée pour les développeurs et les administrateurs système afin de développer, empaqueter et exécuter des applications indépendamment de l'environnement d'exploitation. Docker a depuis remplacé LXC comme la principale technologie de conteneurisation.
Docker Ecosystem
English
This diagram covers a few basic concepts in Docker:
graph TB DockerEngine(Docker Engine) DockerEngine -- Runs --> DockerContainer DockerEngine -- Builds --> DockerImage DockerFile(Dockerfile: Recipe for Images) -- Defines --> DockerImage DockerHub(Docker Hub: Public Repository) -- Stores and Shares --> DockerImage DockerContainer(Docker Container: Tiny, stand-alone, executable package) DockerImage(Docker Image: Blueprints for Containers) -- Creates --> DockerContainer subgraph "Analogy: Construction" DockerFile -- "Architect's Plan" --> DockerImage DockerImage -- "Pre-fab house parts" --> DockerContainer end
Docker Engine: The runtime that builds and runs containers. This is like a construction company that builds and maintains houses.
Dockerfile: A text document that contains all the commands a user could call on the command line to assemble an image. It's like an architect's plan for a house.
Docker Image: The blueprint of Docker that represents a specific instance of a container. It's like the pre-fabricated parts of a house, ready to be constructed.
Docker Container: An executable package that includes everything needed to run an application. This is like the constructed house, based on the pre-fab parts (Image).
Docker Hub: A cloud-based registry service which allows you to link to code repositories, build your images, and test them. This is like a public display or storage of various pre-fab house parts, which can be used by any construction company (Docker Engine).
Français
graph TB DockerEngine(Moteur Docker) DockerEngine -- Exécute --> DockerContainer DockerEngine -- Construit --> DockerImage DockerFile(Fichier Docker: Recette pour les Images) -- Définit --> DockerImage DockerHub(Hub Docker: Dépôt Public) -- Stocke et Partage --> DockerImage DockerContainer(Conteneur Docker: Petit paquet exécutable autonome) DockerImage(Image Docker: Plans pour les Conteneurs) -- Crée --> DockerContainer subgraph "Analogie: Construction" DockerFile -- "Plan d'architecte" --> DockerImage DockerImage -- "Pièces préfabriquées de maison" --> DockerContainer end
Ce diagramme couvre quelques concepts de base de Docker :
Docker Engine (Moteur Docker): L'environnement d'exécution qui construit et exécute des conteneurs. C'est comme une entreprise de construction qui construit et entretient des maisons.
Dockerfile (Fichier Docker): Un document texte qui contient toutes les commandes qu'un utilisateur pourrait appeler sur la ligne de commande pour assembler une image. C'est comme le plan d'un architecte pour une maison.
Docker Image (Image Docker): Le plan de Docker qui représente une instance spécifique d'un conteneur. C'est comme les pièces préfabriquées d'une maison, prêtes à être construites.
Docker Container (Conteneur Docker): Un paquet exécutable qui comprend tout ce qui est nécessaire pour exécuter une application. C'est comme la maison construite, basée sur les pièces préfabriquées (Image).
Docker Hub (Hub Docker): Un service de registre basé sur le cloud qui vous permet de lier à des dépôts de code, de construire vos images et de les tester. C'est comme une présentation publique ou un stockage de diverses pièces de maison préfabriquées, qui peuvent être utilisées par n'importe quelle entreprise de construction (Moteur Docker).
Docker Swarm
English
Docker Swarm is a container orchestration tool built into Docker. It allows you to manage multiple Docker hosts (nodes) and deploy services to them in a way that makes them act like a single, large host. The nodes in a Swarm communicate with each other to ensure they maintain the desired state of the services you deploy.
Let's break it down with an analogy. Think of Docker Swarm as a conductor of an orchestra, where each musician (Docker host/node) has a specific part to play (task), and the conductor (Docker Swarm) ensures everyone plays in harmony to create beautiful music (smoothly running application).
Here's a Mermaid diagram that illustrates how Docker Swarm works:
graph LR DockerSwarm(Docker Swarm: Orchestra Conductor) DockerHost1(Docker Host 1: Musician 1) DockerHost2(Docker Host 2: Musician 2) DockerHost3(Docker Host 3: Musician 3) DockerSwarm --> DockerHost1 DockerSwarm --> DockerHost2 DockerSwarm --> DockerHost3 subgraph ServicesOnHost1 Service1a(Service 1: Musical Score 1) Service1b(Service 2: Musical Score 2) end subgraph ServicesOnHost2 Service2a(Service 1: Musical Score 1) Service2b(Service 2: Musical Score 2) end subgraph ServicesOnHost3 Service3a(Service 1: Musical Score 1) Service3b(Service 2: Musical Score 2) end DockerHost1 --> ServicesOnHost1 DockerHost2 --> ServicesOnHost2 DockerHost3 --> ServicesOnHost3
In this diagram:
Docker Swarm: This is the conductor of the orchestra, managing how each Docker host (or node) runs its services.
Docker Hosts (1, 2, 3): The musicians in the orchestra, each one playing their part according to the musical scores (services).
Services (1, 2): The musical scores, representing the different tasks that each Docker host needs to perform. Each Docker host might run multiple services, just like a musician might play different parts in the same piece of music.
The arrows represent Docker Swarm managing the Docker hosts, like a conductor directing the musicians in an orchestra, and each Docker host managing its services, like a musician following their musical scores.
Français
Docker Swarm est un outil d'orchestration de conteneurs intégré à Docker. Il permet de gérer plusieurs hôtes Docker (nœuds) et de déployer des services sur ceux-ci de manière à ce qu'ils agissent comme un seul et grand hôte. Les nœuds d'un Swarm communiquent entre eux pour garantir qu'ils maintiennent l'état souhaité des services que vous déployez.
Prenons une analogie. Pensez à Docker Swarm comme à un chef d'orchestre, où chaque musicien (hôte Docker/nœud) a une partie spécifique à jouer (tâche), et le chef d'orchestre (Docker Swarm) veille à ce que tout le monde joue en harmonie pour créer une belle musique (application fonctionnant sans accroc).
Voici un diagramme Mermaid qui illustre comment fonctionne Docker Swarm:
graph LR DockerSwarm(Docker Swarm: Chef d'Orchestre) DockerHost1(Hôte Docker 1: Musicien 1) DockerHost2(Hôte Docker 2: Musicien 2) DockerHost3(Hôte Docker 3: Musicien 3) DockerSwarm --> DockerHost1 DockerSwarm --> DockerHost2 DockerSwarm --> DockerHost3 subgraph ServicesOnHost1 Service1a(Service 1: Partition Musicale 1) Service1b(Service 2: Partition Musicale 2) end subgraph ServicesOnHost2 Service2a(Service 1: Partition Musicale 1) Service2b(Service 2: Partition Musicale 2) end subgraph ServicesOnHost3 Service3a(Service 1: Partition Musicale 1) Service3b(Service 2: Partition Musicale 2) end DockerHost1 --> ServicesOnHost1 DockerHost2 --> ServicesOnHost2 DockerHost3 --> ServicesOnHost3
Dans ce diagramme :
Docker Swarm: Il s'agit du chef d'orchestre, qui gère la façon dont chaque hôte Docker (ou nœud) exécute ses services.
Hôtes Docker (1, 2, 3): Ce sont les musiciens de l'orchestre, chacun jouant son rôle en fonction des partitions musicales (services).
Services (1, 2): Ce sont les partitions musicales, représentant les différentes tâches que chaque hôte Docker doit accomplir. Chaque hôte Docker peut exécuter plusieurs services, tout comme un musicien peut jouer différentes parties dans la même pièce musicale.
Les flèches représentent Docker Swarm qui gère les hôtes Docker, comme un chef d'orchestre dirigeant les musiciens dans un orchestre, et chaque hôte Docker gère ses services, comme un musicien suivant ses partitions musicales.
Django Application Stack on Docker
English
Here's a high-level overview:
First, create a Dockerfile for your Django application, which will be the blueprint for building a Docker image of your application.
Then, define a Docker Compose file (docker-compose.yml), which will describe the multi-container Docker environment for your application. This file will include the configuration for your Django application container and Postgres database container.
Finally, use Docker Compose commands to build and start your application.
graph TB DockerComposeFile[Docker Compose File] DockerComposeCommand{Docker Compose Commands} DockerComposeFile --> DjangoAppDockerFile[Django App Dockerfile] DjangoAppDockerFile --> DjangoAppImage[Django App Docker Image] DjangoAppImage --> DjangoAppContainer[Django App Container] DockerComposeFile --> PostgresImage[Postgres Docker Image] PostgresImage --> PostgresContainer[Postgres Container] DockerComposeCommand --> DockerComposeFile DjangoAppContainer -- Connects To --> PostgresContainer subgraph "Application Stack" DjangoAppContainer -- "Django Application" --> PostgresContainer end
This diagram represents the process:
Docker Compose File: This file contains the multi-container Docker configuration for your application.
Docker Compose Commands: These are the commands you run to start your Docker Compose environment.
Django App Dockerfile: This is the Dockerfile for your Django application. It's like a blueprint for creating a Docker image of your Django application.
Django App Docker Image: This is the Docker image created from the Dockerfile for your Django application.
Django App Container: This is the Docker container running your Django application. It is created from the Docker image of your Django application.
Postgres Docker Image: This is the Docker image for the Postgres database. It's usually downloaded from Docker Hub.
Postgres Container: This is the Docker container running your Postgres database. It is created from the Postgres Docker image.
The arrow labeled "Connects To" between the Django App Container and Postgres Container nodes indicates that the Django application connects to the Postgres database.
The subgraph titled "Application Stack" represents your complete application stack including the Django application and the Postgres database.
Français
Voici un aperçu de haut niveau :
Premièrement, créez un Dockerfile pour votre application Django, qui sera le plan pour construire une image Docker de votre application.
Ensuite, définissez un fichier Docker Compose (docker-compose.yml), qui décrira l'environnement Docker multi-conteneur pour votre application. Ce fichier inclura la configuration pour votre conteneur d'application Django et le conteneur de base de données Postgres.
Enfin, utilisez les commandes Docker Compose pour construire et démarrer votre application.
graph TB DockerComposeFile[Fichier Docker Compose] DockerComposeCommand{Commandes Docker Compose} DockerComposeFile --> DjangoAppDockerFile[Dockerfile App Django] DjangoAppDockerFile --> DjangoAppImage[Image Docker App Django] DjangoAppImage --> DjangoAppContainer[Conteneur App Django] DockerComposeFile --> PostgresImage[Image Docker Postgres] PostgresImage --> PostgresContainer[Conteneur Postgres] DockerComposeCommand --> DockerComposeFile DjangoAppContainer -- Se Connecte à --> PostgresContainer subgraph "Pile d'Applications" DjangoAppContainer -- "Application Django" --> PostgresContainer end
Ce diagramme représente le processus :
Fichier Docker Compose : Ce fichier contient la configuration Docker multi-conteneur pour votre application.
Commandes Docker Compose : Ce sont les commandes que vous exécutez pour démarrer votre environnement Docker Compose.
Dockerfile App Django : C'est le Dockerfile pour votre application Django. C'est comme un plan pour créer une image Docker de votre application Django.
Image Docker App Django : C'est l'image Docker créée à partir du Dockerfile pour votre application Django.
Conteneur App Django : C'est le conteneur Docker exécutant votre application Django. Il est créé à partir de l'image Docker de votre application Django.
Image Docker Postgres : C'est l'image Docker pour la base de données Postgres. Elle est généralement téléchargée depuis Docker Hub.
Conteneur Postgres : C'est le conteneur Docker exécutant votre base de données Postgres. Il est créé à partir de l'image Docker Postgres.
La flèche étiquetée "Se Connecte à" entre les nœuds Conteneur App Django et Conteneur Postgres indique que l'application Django se connecte à la base de données Postgres.
Le sous-graphique intitulé "Pile d'Applications" représente votre pile d'applications complète, incluant l'application Django et la base de données Postgres.
Demo
Build Local Container
Add the Dockerfile to the project:
#+name Dockerfile
FROM python:3.10.6-bullseye ENV PIP_NO_CACHE_DIR=True ENV POETRY_VIRTUAL_ENVS_CREATE=False RUN pip install -U \ pip \ setuptools \ wheel \ poetry WORKDIR /app RUN useradd -m -r user && \ chown user /app COPY pyproject.toml poetry.lock ./ #RUN poetry install --no-dev #RUN poetry install --no-dev && \ # rm -rf ~/.cache/pypoetry/{cache,artifacts} RUN poetry export --without-hashes -f requirements.txt -o requirements.txt RUN pip3 install -r requirements.txt COPY . . RUN python3 manage.py migrate USER user EXPOSE 8000/tcp CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]
Build the container:
docker build -t mytodos:v1 ./
Export Docker Image Archive (.tar file)
docker save mytodos:v1 > mytodos_app_container.tar
Upload Docker Image Archive and Install
#+name fabfile.py
@task def upload_and_install_docker_archive(ctx, server, docker_archive_src, docker_archive_dest): # Define the server connection with get_server_connection(server) as conn: # Upload Docker Image Archive conn.put(docker_archive_src, docker_archive_dest) # Install Docker Archive conn.run(f'docker load -i {docker_archive_dest}')
Run Docker Image
#+name fabfile.py
@task def remote_exec(ctx, server, cmd): # Define the server connection with get_server_connection(server) as conn: # Execute Command conn.run(cmd)
Fix Any Imcompatibilities
The Fix is:
docker buildx build --platform linux/amd64 -t mytodos:v1 ./
docker save mytodos:v2 > mytodos_app_container.tar
Docker Swarm
fab remote-exec ubuntu.main "docker swarm init"
#+name config.ini
[ubuntu.worker1] host = ubuntu@35.183.61.2 app_dir = /home/ubuntu pem_file = ubuntu-worker1.pem token = SWMTKN-1-24z5u12objl9qho41g77nhkmds8qpjoea9ta8mpvfvwxojjs3y-2k5buiwingdxa97zalxu2076b 172.26.7.85:2377
#+name config.ini
[ubuntu.worker2] host = ubuntu@15.156.205.168 app_dir = /home/ubuntu pem_file = ubuntu-worker2.pem token = SWMTKN-1-24z5u12objl9qho41g77nhkmds8qpjoea9ta8mpvfvwxojjs3y-2k5buiwingdxa97zalxu2076b 172.26.7.85:2377
docker swarm join --token SWMTKN-1-24z5u12objl9qho41g77nhkmds8qpjoea9ta8mpvfvwxojjs3y-2k5buiwingdxa97zalxu2076b 172.26.7.85:2377 docker node ls # Check Swarm Cluster docker stack deploy -c docker-compose.yaml mytodos # Deploy Stack with service name mytodos
My Django TODOs App Stack
DATABASES = { "default": { "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"), "NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"), "USER": os.environ.get("SQL_USER", "user"), "PASSWORD": os.environ.get("SQL_PASSWORD", "password"), "HOST": os.environ.get("SQL_HOST", "localhost"), "PORT": os.environ.get("SQL_PORT", "5432"), } }
#+name docker-compose.yaml
services: db: image: postgres volumes: - ./data/db:/var/lib/postgresql/data environment: - POSTGRES_DB=postgres - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres web: image: mytodos:v3 #build: . command: python manage.py runserver 0.0.0.0:8000 #volumes: # - .:/code ports: - "8000:8000" environment: - POSTGRES_NAME=postgres - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres depends_on: - db