{"id":32730747,"url":"https://github.com/markokroselj/devops-hw1","last_synced_at":"2026-05-12T12:43:56.293Z","repository":{"id":321492887,"uuid":"1085996744","full_name":"markokroselj/devops-hw1","owner":"markokroselj","description":"First homework for DevOps class","archived":false,"fork":false,"pushed_at":"2025-10-29T22:26:08.000Z","size":8,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-30T00:22:46.090Z","etag":null,"topics":["devops","vagrant","webapp"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/markokroselj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-29T19:54:13.000Z","updated_at":"2025-10-29T22:29:54.000Z","dependencies_parsed_at":"2025-10-30T00:22:48.431Z","dependency_job_id":"4c66aea3-aa83-48b7-b6e9-0ef785f4b6d7","html_url":"https://github.com/markokroselj/devops-hw1","commit_stats":null,"previous_names":["markokroselj/devops-hw1"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/markokroselj/devops-hw1","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markokroselj%2Fdevops-hw1","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markokroselj%2Fdevops-hw1/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markokroselj%2Fdevops-hw1/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markokroselj%2Fdevops-hw1/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/markokroselj","download_url":"https://codeload.github.com/markokroselj/devops-hw1/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markokroselj%2Fdevops-hw1/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":282403735,"owners_count":26663510,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-03T02:00:05.676Z","response_time":108,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["devops","vagrant","webapp"],"created_at":"2025-11-03T05:00:45.144Z","updated_at":"2026-05-12T12:43:56.285Z","avatar_url":"https://github.com/markokroselj.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Automatic web application deployment on virtual machine using [Vagrant](https://developer.hashicorp.com/vagrant) and [Cloud-init](https://cloud-init.io/) + [Multipass](https://canonical.com/multipass) \n\nFor first homework for DevOps class I set up [Vagrant file](https://developer.hashicorp.com/vagrant/docs/vagrantfile) to automatically provision a virtual machine and deploy a simple web application on it. \n\n## Web application\n\nApp is Vehicle inventory manager. It lets you manage information about SpaceX Starship rockets. For each rocket vehicles, you can view name, type and its current location. New vehicle can be added by pressing \"+\" button. You can enter its name and select type and location. You can select to show both types or just Ship or Booster. You can search vehicle by its name, location or both. Vehicle can be deleted by double clicking on its name or selecting it with the tab and pressing delete key. All vehicles can be deleted at once by clicking Remove all vehicles button.\n\n![Web app](app-screenshots/vehicle-inventory-manager.png \"Web app\")\n![Web app new vehicle](app-screenshots/new-vehicle.png \"New vehicle\")\n\nApp consist of 4 components. Frontend, REST API, MySQL database and [Caddy](https://caddyserver.com/) as HTTP server and reverse proxy. Frontend is made using vanilla JavaScript, HTML and CSS. REST API is made in Python web framework [Flask](https://flask.palletsprojects.com/en/stable/). \n\n### API\nAPI is reversed proxied to /api/. It has three GET end points that return all vehicles ``/get-vehicles``, locations ``/get-locations`` and types ``/get-types`` in JSON format. To get a specific vehicle by its id, use ``/get/vehicle/id``. To add a new vehicle use POST endpoint ``/add-vehicle``. For deleting it has two DELETE end points. For deleting all ``/delete-all-vehicles`` and a specific one by its id ``/delete-vehicle/id``\n\n# Host configuration\n\nVirtual machine with the application was deployed on virtual machine server running Ubuntu 24.04.3. It has 4 core 2.8GHz CPU and 16GB of RAM.\n\nFor hypervisor I used VirtualBox. \n\nOn Host machine I installed Git, VirtualBox and Vagrant.\n1. Git\n    ``sudo apt install git``\n2. VirtualBox\n3. Vagrant\n [Linux install commands](https://developer.hashicorp.com/vagrant/install#linux)\n\n\nAnd then configured firewall\n- allowed port 22 for ssh\n    \n    ``sudo ufw allow 22``\n- allowed ports 80 and 443 for http and https\n\n    ``sudo ufw allow 80``\n\n    ``sudo ufw allow 443``\n- enabled firewall\n    \n    ``sudo ufw enable``\n\n  \n# Vagrantfile\n \n Initial Vagrantfile was created using ``vagrant init hashicorp-education/ubuntu-24-04 --box-version 0.1.0``\n\nVagrant box or base image I set to ``hashicorp-education/ubuntu-24-0`` with the box version of 0.1.0.\n\n## Hardware provisioning\n\nHardware is provisioned to 4GB of RAM and 2 CPUs, using \n```ruby  \nconfig.vm.provider \"virtualbox\" do |v|\n    v.memory = 4096\n    v.cpus = 2\n  end\n```\n\n## Port forwarding \nFor web application to be accessible over http and https, ports 80 and 443 need to be forwarded from guest to host.\n```ruby\n  config.vm.network \"forwarded_port\", guest: 80, host: 80\n  config.vm.network \"forwarded_port\", guest: 443, host: 443\n``` \n\n## Application provisioning \nApplication stack is deployed inside virtual machine using shell script inside Vagrantfile.\n``config.vm.provision \"shell\", inline: $script``\n\n Environment variables are passed into shell script using #{VARIABLE}. Variables are declared at the top of the file. \n\n ### Script\n First it updates the list of packages using ``sudo apt-get update``.\n We are using apt-get instead of apt, because it's better for running inside automated script\n 1. Caddy\n    \n    Caddy is installed using ``sudo apt-get install -y caddy``. \n    y flag is used so installer script doesn't ask us any questions, for which we would have to provide manual keyboard input and just installs it using the defaults. \n\n    Caddy configuration is defined inside Caddyfile ``/etc/caddy/Caddyfile``. Static server root is set to ``/var/www/app`` and reverse proxy is set to rederect all request starting with /api/ to Flask API. \n    \n    Caddy automatically configures HTTPS and SSL certificate. \n2. MySQL server\n\n    MySQL server is installed using ``sudo apt-get install -y mysql-server``. Then it creates database user with its username and password from variables. Creates database with the name from variable. All privileges are granted to user on database. Initial database is imported from dbsetup.sql SQL file. ``-e`` flag in mysql commands are so SQL statment can be executed directly and not in the interactive environment. \n\n3. Environment variables\n\n    Then are environment variables set up. To allow dynamic environment configuration. Example for DB_HOST: ``echo \"export DB_HOST=#{DB_HOST}\" \u003e\u003e /home/vagrant/.bashrc``. Other variables  are: DB_USERNAME,DB_PASSWORD,DB_NAME,APP_PORT,DOMAIN. APP_PORT defines the port api is running internaliy. DOMAIN is the domain name app would be accesible from. \n\n4. API\n\n    Then Python REST API is set up. First pip is installed using ``sudo apt-get  install -y pip``. Because we can't install Python packages directly, Python virtual environment needs to be set up. Using ``sudo apt-get install -y python3.12-venv python3 -m venv /home/vagrant/api/.venv source /home/vagrant/api/.venv/bin/activate``. Required dependencies are specified inside pyproject.toml file and are installed using ``pip install .``. To run API automaticly on startup, systemd service file is created and installed. Because service is running under root user, environment variables need to be again defined inside the service file.\n\n5. Frontend \n    \n    Frontend files are copied to server root. `` /var/www/app/``\n\n\nVirtual machine is started using ``sudo vagrant up``. Sudo is used so privileged ports 80 and 443 work. \n\nDuring development I constantly committed and pushed Vagrant file and other changes to GitHub. To get the changes on virtual server I used command ``git fetch origin \u0026\u0026 git reset --hard origin/main \u0026\u0026 git clean -fd``. This fetched changes even though Vagrant file was changed on the server. \n\n# Domain\nFor app domain I created new subdomain. Inside my domain DNS managed I created new A record and pointed it to the host IP address.\n\n# Usage\nMake sure you have Git, Vagrant and VirtualBox installed.\n- point domain to the IP of your machine\n- clone this repository ``git clone https://github.com/markokroselj/devops-hw1.git``\n- cd into it ``cd devops-hw1``\n- edit Vagrantfile and add variables. Set DB_NAME to vehicles_db. DB_HOST needs to be localhost. DB_USERNAME and DB_PASSWORD can be anything.\n- start virtual machine using ``sudo vagrant up``\n\n## Vagrant usage demo\n[YouTube video of app deploy using Vagrantfile](https://www.youtube.com/watch?v=2hj3_0td8bE)\n\n\n\n# Cloud-init\n\n# Host configuration\nVirtual machine with the application was deployed on regular PC running Windows 10. It has 8 cores and 16GB of RAM.\n\nFor Hypervisor, we used Windows Hyper-V.\n\nOn host machine, we installed the latest verison of Multipass only. No further configuration was required.\n\n# Cloud-init file\nWe created cloud-config.yaml file that is passed on as an argument to --cloud-init switch when launching a virtual machine with Multipass.\n\n# Hardware provisioning\nHardware was provisioned by default, to 1 core and ~1GB of RAM.\n\n# Port forwarding\nTo make the web application accessible from outside the local network, and for Caddy to issue a valid certificate, we launched the virtual machine in bridged network mode and then forwarded ports 80 and 443 directly to the guest. Due to a mix of Caddy certificate and router constraints, the web application was only accessible from outside the local network. \n\n# Application provisioning\nApplication stack was deployed inside virtual machine using cloud-config.yaml file, which pre-configures the virtual machine during boot according to our needs.\n\nEnvironment variables were defined in plain text inside ```/root/secrets.env```, which was then moved to Ubuntu user's home directory and claimed by Ubuntu user. The variables are supposed to be added to the cloud-config.yaml right before launching a virtual machine, therefore reducing the risk of being compromised by someone who gets access to the file. The security could be improved by utilizing a secrets Vault, like for example, Bitwarden Secrets Manager, but this approach might not be much better, since we would still have to pass the access token to the virtual machine/config file right before launch and we also might have to store the secrets in a file regardless, because the secrets are required by many components that make our application.\n\n### Script\nThe config file first installs the necessary packages.\n\n1. Git\n    Git will be used to clone the GitHub repository, which includes files that our application will serve.\n2. Caddy\n    Like mentioned before, Caddy will serve files from ```/var/www/app``` directory, redirect API calls to Flask API and also configure HTTPS and SSL certificates automatically.\n3. MySQL server\n    MySQL server will create a user and their password, create a database that will be used by the web application, and grant privileges for this database to the user. Then, the service will run all SQL statements from ```/home/ubuntu/db/dbsetup.sql```, which define the tables and rows in the database.\n4. Python, pip, venv\n    Python, pip, and the virtual environment are used to set up a self-contained environment for our API. This ensures that the service has all the packages it needs and can start automatically, without requiring manual setup or interfering with other Python projects on the system.\n\nIn the ```runcmd```, we first set a static IP to the bridged network interface, due to the port forwarding that we set up on the router earlier. We then clone the GitHub repository and organize the files into their respected directories. We then initialize the Python virtual environment, move the secrets file to Ubuntu user's home directory, wait for MySQL to start up and run the SQL statements. Finally, we configure the ```/etc/caddy/Caddyfile``` to serve files from ```/var/www/app``` and forward API calls to the backend service running locally. Then we simply reload Caddy and start our API service. After these steps, the web application should be online and running.\n\n# Domain\nThe same subdomain as before was used, but this time pointing to the public IP of the regular PC.\n\n# Usage\nMultipass and Hypervisor is required on the host machine.\n- point the domain to your public IP\n- move into the folder, where cloud-config.yaml file is present\n- fill in the missing secret credentials in the file\n- run a Command Prompt from inside this folder\n- find out the main network interface that the host uses and run ```multipass set local.bridged-network=\u003cname_of_interface\u003e```\n- finally, run ```multipass launch --name \u003cname\u003e --cloud-init cloud-config.yam```\n\n[Usage demo video](https://drive.google.com/file/d/1gZkffDfWlcnnYtKCqZ0_SedRc7nTyYAk/view)\n[Web app deployed using cloud-init](https://drive.google.com/file/d/1J4sv55dBQdwBHHq9ndLKOZ2k2WT2MuXc/view)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkokroselj%2Fdevops-hw1","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarkokroselj%2Fdevops-hw1","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkokroselj%2Fdevops-hw1/lists"}