diff --git a/.github/workflows/publish_image.yml b/.github/workflows/publish_image.yml new file mode 100644 index 0000000..4634cef --- /dev/null +++ b/.github/workflows/publish_image.yml @@ -0,0 +1,35 @@ +name: publish_image + +on: + push: + branches: + - "master" + +jobs: + docker-image: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: downcase GITHUB_REPOSITORY + run: | + echo "GITHUB_REPOSITORY_LOWERCASE=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: ./ + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/${{ env.GITHUB_REPOSITORY_LOWERCASE }}:latest + ghcr.io/${{ env.GITHUB_REPOSITORY_LOWERCASE }}:1.0.1 diff --git a/.gitignore b/.gitignore index 0fcb15a..815fedb 100644 --- a/.gitignore +++ b/.gitignore @@ -390,3 +390,38 @@ Temporary Items docker-compose.yml .env dist + +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..cae686f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "hashicorp.terraform", + "pkief.material-icon-theme" + ] +} + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7c011cf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "files.eol": "\n", + "[terraform]": { + "editor.defaultFormatter": "hashicorp.terraform", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + }, + "[terraform-vars]": { + "editor.defaultFormatter": "hashicorp.terraform", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + }, + "workbench.iconTheme": "material-icon-theme" + } + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index be5bfe8..c285d14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,27 @@ -FROM node:12-alpine +# Build stage +FROM node:12-alpine AS builder WORKDIR /usr/src/app -COPY package.json . +COPY package*.json ./ RUN npm install --quiet COPY . . -CMD ["npm", "run", "start"] +RUN npm run build + +# Production stage +FROM node:12-alpine + +WORKDIR /usr/src/app + +COPY package*.json ./ + +# Install only production dependencies +RUN npm ci --only=production + +# Copy built app from the previous stage +COPY --from=builder /usr/src/app/dist ./dist + +CMD ["npm", "run", "start:prod"] \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..9a39098 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,31 @@ +version: "3.8" + +services: + web: + image: ghcr.io/juanmanuelgg/backvynils:latest + environment: + DB_HOST: "db" + ports: + - "3000:3000" + depends_on: + - db + links: + - db + networks: + - default + + db: + image: postgres + environment: + - POSTGRES_DB=vinyls + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - default + +volumes: + pgdata: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 5a80d0b..2d62d4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,9 +3,6 @@ version: "3.8" services: web: build: . - command: npm run start - volumes: - - /usr/src/app environment: DB_HOST: "db" ports: @@ -23,7 +20,12 @@ services: - POSTGRES_DB=vinyls - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres + volumes: + - pgdata:/var/lib/postgresql/data ports: - "5432:5432" networks: - default + +volumes: + pgdata: \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 2978258..057de14 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -42,7 +42,7 @@ import { AlbumMusicianModule } from './albummusician/albummusician.module'; password: 'postgres', database: 'vinyls', entities: [Album, CollectorAlbum, Band, Collector, Comment, Musician, Performer, PerformerPrize, Prize, Track,], - dropSchema: true, + dropSchema: false, synchronize: true, keepConnectionAlive: true, migrations: [__dirname + '/migration/**/*{.ts,.js}'], diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..658a911 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,26 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/digitalocean/digitalocean" { + version = "2.31.0" + constraints = "~> 2.0" + hashes = [ + "h1:Ra3eOZAAkwTzUBLdKRWubvJ2nnGiorEB100eSAXyIWU=", + "zh:1eaf43da5ba5cf0ad4579fc084866ac70d2f597250218db8e573ee6045b51879", + "zh:1fd974e38833443c7d2c4b6e56cbb83d5ac086858c6dcaa9be79a85cb4106984", + "zh:2143630f078f844e4006b546a6f43653308d00ef5a5afa51e5605d290d8f9270", + "zh:2ea11260f4aada058d978abf6371fb0016f1e5ad650583b1842733dde1a1c6ed", + "zh:2f77f4e385db98bfc26b7c43c52ab4bcda2221191404726d0b76939d96eeb254", + "zh:3262dbbbfa33f3e0e775f4e2a0d995f942a0606df3f92f3db58d3a2b828eb4c1", + "zh:39cc06925c2141b7fccd729e2d98d84194af4b6603849c8eeef9e16c6e473507", + "zh:4a401f612caef535905c793bad2c7d7e54ff385f0cd5fd352bad648468647a80", + "zh:50d59d672d7c9e7e7eb4228eb6bdbe8dae8e8dac106c8c5effced15bfa65300a", + "zh:7760d8ec7af57b95a115496698fed14676b584d85465459d50e21c37add4b3e1", + "zh:8bc9f5b3b74bc084724b7342fff44e93a4f552acf4cf5878e53bd4f327a6362a", + "zh:9abf8e64705d57e29129472ac937531154dc8eec83a3a4848fbaaee87a656276", + "zh:a3d7ac59fdeb704fcb0a8c354e0ae611b377637fb76d88f89e9dc9be0e3c3945", + "zh:c71fa05665304954f62d7e2632634f29e8ab7a0a228698c6605c951dfb843a51", + "zh:efd7b29d8f65a97cb65823948b878f0cd3e42703c2ed71e2fe39769a1a9b9faa", + "zh:fb04df7c68ee96b110b5e72da7a776f01007ec919ecff914fd84ae6cb8e3ddad", + ] +} diff --git a/terraform/main.sh b/terraform/main.sh new file mode 100644 index 0000000..8642bfb --- /dev/null +++ b/terraform/main.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# This script is used to run terraform commands +terraform init +terraform destroy +terraform apply diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..3fb5b76 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,24 @@ +module "foundation" { + source = "./modules/foundation" + do_token = var.do_token + region = var.region + domain = var.domain + do_ssh_pub_key_file = var.do_ssh_pub_key_file +} + +module "vm" { + source = "./modules/vm" + do_token = var.do_token + region = var.region + digitalocean_ssh_key_id = module.foundation.digitalocean_ssh_key_id + vpc_uuid = module.foundation.vpc_uuid + ip_address = module.foundation.ip_address + proyect_id = module.foundation.proyect_id +} + + +module "firewall" { + source = "./modules/firewall" + do_token = var.do_token + droplet_id = module.vm.droplet_id +} diff --git a/terraform/modules/firewall/main.tf b/terraform/modules/firewall/main.tf new file mode 100644 index 0000000..21d2439 --- /dev/null +++ b/terraform/modules/firewall/main.tf @@ -0,0 +1,57 @@ +resource "digitalocean_firewall" "web" { + name = "web-22-53-80-443" + + droplet_ids = [var.droplet_id] + + inbound_rule { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + } + /* La idea es quitar http cuando ya se tiene https. + inbound_rule { + protocol = "tcp" + port_range = "80" + source_addresses = ["0.0.0.0/0", "::/0"] + } + */ + inbound_rule { + protocol = "tcp" + port_range = "443" + source_addresses = ["0.0.0.0/0", "::/0"] + } + /* Solo para pruebas + inbound_rule { + protocol = "tcp" + port_range = "3000" + source_addresses = ["0.0.0.0/0", "::/0"] + } + */ + outbound_rule { + protocol = "tcp" + port_range = "53" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + outbound_rule { + protocol = "udp" + port_range = "53" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + outbound_rule { + protocol = "tcp" + port_range = "80" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + outbound_rule { + protocol = "tcp" + port_range = "443" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + /* Solo para pruebas + outbound_rule { + protocol = "tcp" + port_range = "3000" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + */ +} diff --git a/terraform/modules/firewall/provider.tf b/terraform/modules/firewall/provider.tf new file mode 100644 index 0000000..8f16273 --- /dev/null +++ b/terraform/modules/firewall/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + } +} + +provider "digitalocean" { + token = var.do_token +} diff --git a/terraform/modules/firewall/variables.tf b/terraform/modules/firewall/variables.tf new file mode 100644 index 0000000..b931a57 --- /dev/null +++ b/terraform/modules/firewall/variables.tf @@ -0,0 +1,9 @@ +variable "do_token" { + description = "DigitalOcean API token" + type = string +} + +variable "droplet_id" { + description = "The ID of the droplet to apply the firewall to" + type = number +} diff --git a/terraform/modules/foundation/main.tf b/terraform/modules/foundation/main.tf new file mode 100644 index 0000000..f5fe599 --- /dev/null +++ b/terraform/modules/foundation/main.tf @@ -0,0 +1,34 @@ +resource "digitalocean_ssh_key" "default" { + name = "SSH-DigitalOcean-key" + public_key = file(var.do_ssh_pub_key_file) +} + +resource "digitalocean_vpc" "default" { + name = "my-network" + region = var.region + ip_range = "10.10.11.0/24" +} + +resource "digitalocean_reserved_ip" "default" { + region = var.region +} + +resource "digitalocean_domain" "default" { + name = var.domain + ip_address = digitalocean_reserved_ip.default.ip_address +} + +resource "digitalocean_record" "www" { + domain = digitalocean_domain.default.id + type = "A" + name = "www" + value = digitalocean_reserved_ip.default.ip_address +} + +resource "digitalocean_project" "default" { + name = "AppBajoPruebas" + description = "Un proyecto web del curso: Ingenieria de software para aplicaciones moviles." + purpose = "Web Application" + environment = "Development" + resources = [digitalocean_domain.default.urn] +} diff --git a/terraform/modules/foundation/outputs.tf b/terraform/modules/foundation/outputs.tf new file mode 100644 index 0000000..ba25605 --- /dev/null +++ b/terraform/modules/foundation/outputs.tf @@ -0,0 +1,15 @@ +output "digitalocean_ssh_key_id" { + value = digitalocean_ssh_key.default.id +} + +output "vpc_uuid" { + value = digitalocean_vpc.default.id +} + +output "ip_address" { + value = digitalocean_reserved_ip.default.ip_address +} + +output "proyect_id" { + value = digitalocean_project.default.id +} diff --git a/terraform/modules/foundation/provider.tf b/terraform/modules/foundation/provider.tf new file mode 100644 index 0000000..8f16273 --- /dev/null +++ b/terraform/modules/foundation/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + } +} + +provider "digitalocean" { + token = var.do_token +} diff --git a/terraform/modules/foundation/variables.tf b/terraform/modules/foundation/variables.tf new file mode 100644 index 0000000..f5bfc54 --- /dev/null +++ b/terraform/modules/foundation/variables.tf @@ -0,0 +1,19 @@ +variable "do_token" { + description = "DigitalOcean API token" + type = string +} + +variable "do_ssh_pub_key_file" { + description = "Path to the public key file for DigitalOcean" + type = string +} + +variable "region" { + description = "The region to deploy to" + type = string +} + +variable "domain" { + description = "value for the domain name" + type = string +} diff --git a/terraform/modules/vm/main.tf b/terraform/modules/vm/main.tf new file mode 100644 index 0000000..99d59d5 --- /dev/null +++ b/terraform/modules/vm/main.tf @@ -0,0 +1,42 @@ +# Create a new tag +resource "digitalocean_tag" "default" { + name = "webapp" +} + +# Create a web server +resource "digitalocean_droplet" "default" { + name = "backvynils" + size = "s-1vcpu-2gb" + image = "ubuntu-22-04-x64" + region = var.region + monitoring = true + graceful_shutdown = true + ssh_keys = [var.digitalocean_ssh_key_id] + vpc_uuid = var.vpc_uuid + tags = [digitalocean_tag.default.id] +} + +resource "digitalocean_monitor_alert" "cpu_alert" { + alerts { + email = ["jm.gonzalez1844@uniandes.edu.co"] + } + window = "5m" + type = "v1/insights/droplet/cpu" + compare = "GreaterThan" + value = 90 + enabled = true + entities = [digitalocean_droplet.default.id] + description = "Alert about CPU usage" +} + +resource "digitalocean_reserved_ip_assignment" "default" { + ip_address = var.ip_address + droplet_id = digitalocean_droplet.default.id +} + +resource "digitalocean_project_resources" "default" { + project = var.proyect_id + resources = [ + digitalocean_droplet.default.urn + ] +} diff --git a/terraform/modules/vm/outputs.tf b/terraform/modules/vm/outputs.tf new file mode 100644 index 0000000..f585b03 --- /dev/null +++ b/terraform/modules/vm/outputs.tf @@ -0,0 +1,3 @@ +output "droplet_id" { + value = digitalocean_droplet.default.id +} diff --git a/terraform/modules/vm/provider.tf b/terraform/modules/vm/provider.tf new file mode 100644 index 0000000..8f16273 --- /dev/null +++ b/terraform/modules/vm/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + } +} + +provider "digitalocean" { + token = var.do_token +} diff --git a/terraform/modules/vm/variables.tf b/terraform/modules/vm/variables.tf new file mode 100644 index 0000000..89fa363 --- /dev/null +++ b/terraform/modules/vm/variables.tf @@ -0,0 +1,29 @@ +variable "do_token" { + description = "DigitalOcean API token" + type = string +} + +variable "region" { + description = "The region to deploy to" + type = string +} + +variable "digitalocean_ssh_key_id" { + description = "The ID of the SSH key to use for the droplet" + type = string +} + +variable "vpc_uuid" { + description = "The ID of the VPC to use for the droplet" + type = string +} + +variable "ip_address" { + description = "The IP address to assign to the droplet" + type = string +} + +variable "proyect_id" { + description = "The ID of the project to use for the droplet" + type = string +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..e4cfea2 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,4 @@ +# Output the reserved ip address. +output "reserved_ip_address" { + value = module.foundation.ip_address +} diff --git a/terraform/secure/.gitignore b/terraform/secure/.gitignore new file mode 100644 index 0000000..bc5ed1d --- /dev/null +++ b/terraform/secure/.gitignore @@ -0,0 +1,2 @@ +do_ecdsa +do_ecdsa.pub \ No newline at end of file diff --git a/terraform/secure/README.md b/terraform/secure/README.md new file mode 100644 index 0000000..0b57c5e --- /dev/null +++ b/terraform/secure/README.md @@ -0,0 +1,144 @@ +# Configuración de la infraestructura + +## Generate SSH key pair + +```bash +ssh-keygen -t ecdsa -b 521 -C 'Llave para digital ocean' -f do_ecdsa +``` + +## Configuracion del servidor + +```bash +ssh -i do_ecdsa root@appbajopruebas.com + +# Una vez dentro del servidor + +# 1. Actualizar el sistema e instalar nginx y docker +apt update +apt upgrade -y +apt install -y nginx certbot python3-certbot-nginx apt-transport-https ca-certificates curl software-properties-common apache2-utils +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +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" | tee /etc/apt/sources.list.d/docker.list > /dev/null +apt update +apt-cache policy docker-ce +apt -y install docker-ce docker-compose-plugin +systemctl status docker + +# 2. Configurar el usuario +useradd -ms /bin/bash appuser +# Asegurar que el nuevo ususario no pertenesca a ningun grupo mas que a el mismo. +groups appuser +# En caso de que pertenesca a alguno borrar con: gpasswd -d appuser grupo + +# Agregar el usuario al grupo docker +usermod -aG docker appuser +apt autoremove -y +su - appuser +mkdir -p ~/.ssh +chmod 700 ~/.ssh +touch ~/.ssh/authorized_keys +chmod 600 ~/.ssh/authorized_keys +vim ~/.ssh/authorized_keys # Copiar la llave publica del usuario que se usara para conectarse al servidor +``` + +## Levanatar el api de BackVynils + +```bash +# desde la maquina local +scp -i do_ecdsa ../../docker-compose.prod.yml appuser@appbajopruebas.com:~/docker-compose.yml +ssh -i do_ecdsa appuser@appbajopruebas.com + +# desde el servidor (como appuser) +docker-compose up -d --build +``` + +## Configurar nginx + +```bash +# desde el servidor (como root) +vim /etc/nginx/sites-available/appbajopruebas.com +``` + +```nginx +server { + listen 80; + server_name appbajopruebas.com www.appbajopruebas.com; + location / { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +``` + +```bash +# desde el servidor (como root) +ln -s /etc/nginx/sites-available/appbajopruebas.com /etc/nginx/sites-enabled/ +nginx -t +systemctl restart nginx +``` + +## Configurar certbot + +```bash +# desde el servidor (como root) +certbot --nginx -d appbajopruebas.com -d www.appbajopruebas.com +``` + +## Agregar un api-key a nginx para proteger el api + +```bash +# desde el servidor (como root) +htpasswd -c /etc/nginx/.htpasswd appuser +vim /etc/nginx/sites-available/appbajopruebas.com +``` + +```nginx +server { + listen 80; + server_name appbajopruebas.com www.appbajopruebas.com; + location / { + auth_basic "Restricted Content"; + auth_basic_user_file /etc/nginx/.htpasswd; + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +# ... El resto del archivo con la configuracion https que creo certbot +``` + +```bash +# desde el servidor (como root) +nginx -t +systemctl restart nginx +``` + +## Configurar el firewall de ubuntu (Como medida redundante al firewall de digital ocean) + +```bash +#!/bin/bash + +# Asegurar que el script esta siendo ejecutado como root. +if [[ "${UID}" -ne 0 ]]; then + echo 'Por favor correr con sudo o como root.' >&2 + exit 1 +fi + +ufw reset + +ufw default deny incoming +ufw default allow outgoing + +ufw allow in on lo +ufw allow from 127.0.0.1/8 + +ufw allow 22 +# ufw allow 80 # La idea es quitar http cuando ya se tiene https. +ufw allow 443 + +# Prender cuando ya tenga las reglas claras +ufw enable # disable + +ufw status numbered +``` \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..9fe1b16 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,16 @@ +# La primera esta vacia aqui pero tienen valor en terraform.tfvars (Archivo que debe meterse en el .gitignore) +# do_token = "" + +variable "do_token" {} + +variable "do_ssh_pub_key_file" { + default = "secure/do_ecdsa.pub" +} + +variable "region" { + default = "nyc3" +} + +variable "domain" { + default = "appbajopruebas.com" +}