diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index 2fb5fed..7ee70f4 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -22,4 +22,4 @@ runs: shell: bash run: | sudo apt-get update - sudo apt-get install -y python3-pip + sudo apt-get install -y python3-pip rsync diff --git a/.github/workflows/ansible-apply-playbook.yml b/.github/workflows/ansible-apply-playbook.yml index b4f461b..1032c23 100644 --- a/.github/workflows/ansible-apply-playbook.yml +++ b/.github/workflows/ansible-apply-playbook.yml @@ -5,6 +5,7 @@ on: - main paths: - ansible/playbook_*.yml + - '!ansible/playbook_websites.yml' jobs: ansible-apply: runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-websites.yaml b/.github/workflows/deploy-websites.yaml new file mode 100644 index 0000000..c1b3644 --- /dev/null +++ b/.github/workflows/deploy-websites.yaml @@ -0,0 +1,54 @@ +name: Build and Release Hugo Website + +on: + push: + branches: + - main + paths: + - 'ansible/playbook_websites.yml' + - 'websites/**' + +jobs: + build: + name: Build Hugo Website + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 + + - name: Setup Hugo + uses: peaceiris/actions-hugo@v2 + with: + hugo-version: 'latest' + + - name: Setup environment + id: setup_env + uses: ./.github/actions/setup-env + + - name: Setup Ansible environment + uses: ./.github/actions/setup-ansible-env + with: + project-id: ${{ secrets.BW_PROJECT_ID }} + organization-id: ${{ secrets.BW_ORGANIZATION_ID }} + access-token: ${{ secrets.BW_ACCESS_TOKEN }} + ansible-user: ${{ secrets.ANSIBLE_USER }} + trusted-hosts: ${{ secrets.TRUSTED_HOSTS }} + + - name: Run hugo_build command + run: bin/hugo_build.sh + + - name: Run the playbook to deploy websites + env: + BW_PROJECT_ID: ${{ secrets.BW_PROJECT_ID }} + BW_ORGANIZATION_ID: ${{ secrets.BW_ORGANIZATION_ID }} + BW_ACCESS_TOKEN: ${{ secrets.BW_ACCESS_TOKEN }} + ANSIBLE_USER: ${{ secrets.ANSIBLE_USER }} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + CHANGED_FILES: ${{ steps.setup_env.outputs.CHANGED_FILES }} + run: | + cd ansible + sectool -f ../sectool.json exec ansible-playbook -u $ANSIBLE_USER playbook_websites.yml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..87cbe9e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "websites/themes/hugo-profile"] + path = websites/themes/hugo-profile + url = https://github.com/gurusabarish/hugo-profile.git diff --git a/ansible/host_vars/prod-01.alexpires.me.yml b/ansible/host_vars/prod-01.alexpires.me.yml index d480c34..96f6a98 100644 --- a/ansible/host_vars/prod-01.alexpires.me.yml +++ b/ansible/host_vars/prod-01.alexpires.me.yml @@ -163,3 +163,7 @@ duo_api_hostname: api-7cda1654.duosecurity.com # Apps persistent_data: "/mnt/microk8s_persistent" database_path: "/var/lib" + +# Websites +websites: + - "alexpires.me" diff --git a/ansible/playbook_websites.yml b/ansible/playbook_websites.yml new file mode 100644 index 0000000..b81cbd0 --- /dev/null +++ b/ansible/playbook_websites.yml @@ -0,0 +1,50 @@ +--- +- name: Websites (Static Files) Playbook (K8s) + hosts: web + gather_facts: true + +# Specific tasks for this playbook + tasks: + - name: Set required facts + ansible.builtin.set_fact: + web_home: "{{ persistent_data | default('/var/lib') }}/web" + web_user_id: 1000 + web_group_id: 1000 + tags: always + + - name: Create required web data folder + become: true + ansible.builtin.file: + state: directory + path: "{{ web_home }}" + mode: "0750" + owner: "{{ web_user_id }}" + group: "{{ web_group_id }}" + tags: + - tasks + - tasks::folders + + - name: Create required web data folder + become: true + ansible.builtin.file: + state: directory + path: "{{ web_home }}/{{ item }}" + mode: "0750" + owner: "{{ web_user_id }}" + group: "{{ web_group_id }}" + with_items: + - "{{ websites | default([]) }}" + tags: + - tasks + - tasks::folders + + - name: Synchronize websites + become: true + ansible.posix.synchronize: + mode: push + src: "{{ lookup('env', 'GITHUB_WORKSPACE') + '/websites/releases/'+ item }}/" + dest: "{{ web_home }}/{{ item }}" + delete: yes + recursive: yes + with_items: + - "{{ websites | default([]) }}" diff --git a/bin/hugo_build b/bin/hugo_build new file mode 100755 index 0000000..27c45c1 --- /dev/null +++ b/bin/hugo_build @@ -0,0 +1,34 @@ +#!/bin/bash +# This script is used to build a Hugo site and prepare it for deployment. +GITHUB_WORKSPACE=${GITHUB_WORKSPACE:-$(pwd)} + +command -v hugo >/dev/null 2>&1 || { echo >&2 "Hugo is not installed. Please install Hugo to use this script."; exit 1; } + +mkdir -p "$GITHUB_WORKSPACE/websites/releases" + +for dir in $(find "$GITHUB_WORKSPACE/websites" -maxdepth 1 -type d | grep -vE '^\.$'); do + if [ "$dir" == "$GITHUB_WORKSPACE/websites" ]; then + echo "Skipping $dir" + continue + fi + if [ "$dir" == "$GITHUB_WORKSPACE/websites/themes" ]; then + echo "Skipping $dir" + continue + fi + if [ "$dir" == "$GITHUB_WORKSPACE/websites/releases" ]; then + echo "Skipping $dir" + continue + fi + if [ -d "$dir" ]; then + echo "Building Hugo site in $dir" + cd "$dir" || exit 1 + hugo --minify --themesDir $GITHUB_WORKSPACE/websites/themes --destination $GITHUB_WORKSPACE/websites/releases/$(basename "$dir") --baseURL "https://$(basename "$dir").example.com" + if [ $? -ne 0 ]; then + echo "Hugo build failed in $dir" + exit 1 + fi + echo "Hugo site built successfully in $dir" + else + echo "$dir is not a directory" + fi +done \ No newline at end of file diff --git a/inventory/hosts b/inventory/hosts index 238322d..7fe6924 100644 --- a/inventory/hosts +++ b/inventory/hosts @@ -10,9 +10,6 @@ prod-01.alexpires.me [gitea] prod-01.alexpires.me -[wordpress] -prod-01.alexpires.me - [nextcloud] prod-01.alexpires.me @@ -33,3 +30,6 @@ prod-01.alexpires.me [mail] prod-01.alexpires.me + +[web] +prod-01.alexpires.me diff --git a/terraform/k8sapps/config.tf b/terraform/k8sapps/config.tf index 302a089..b4ce05e 100644 --- a/terraform/k8sapps/config.tf +++ b/terraform/k8sapps/config.tf @@ -1,25 +1,26 @@ locals { # general - domain_name = "alexpires.me" - # storage persistent_folder = "/mnt/microk8s_persistent" database_folder = "/var/lib" - # apps - wordpress_version = "6.7.2" - wordpress_fqdns = ["blog.${local.domain_name}", "www.${local.domain_name}", "${local.domain_name}"] - wordpress_sql_backup_cron_schedule = "0 2 * * *" - wordpress_files_backup_cron_schedule = "10 2 * * *" + # domains + primary_domain = "alexpires.me" + websites = { + "alexpires.me" = { + domain_name = "alexpires.me" + fqdn = ["blog.alexpires.me", "www.alexpires.me", "alexpires.me"] + } + } gitea_version = "1.23.4" - gitea_fqdn = "code.${local.domain_name}" + gitea_fqdn = "code.${local.primary_domain}" gitea_sql_backup_cron_schedule = "20 2 * * *" gitea_files_backup_cron_schedule = "30 2 * * *" nextcloud_version = "30.0.0-apache" - nextcloud_fqdn = "cloud.${local.domain_name}" + nextcloud_fqdn = "cloud.${local.primary_domain}" nextcloud_sql_backup_cron_schedule = "40 2 * * *" nextcloud_files_backup_cron_schedule = "50 2 * * *" @@ -34,7 +35,7 @@ locals { m3uproxy_version = "latest" wireguard_version = "1.0.20210914-r4-ls54" squid_version = "6.1-23.10_edge" - m3uproxy_fqdn = "tv.${local.domain_name}" + m3uproxy_fqdn = "tv.${local.primary_domain}" geoip_image = "maxmindinc/geoipupdate" geoip_version = "latest" @@ -47,7 +48,7 @@ locals { # rustdesk rustdesk_version = "latest" - rustdesk_fqdn = "rustdesk.${local.domain_name}" + rustdesk_fqdn = "rustdesk.${local.primary_domain}" # backup local_backup_retention_days = 2 diff --git a/terraform/k8sapps/mail.tf b/terraform/k8sapps/mail.tf index dc8e7fe..fb051ef 100644 --- a/terraform/k8sapps/mail.tf +++ b/terraform/k8sapps/mail.tf @@ -25,10 +25,10 @@ locals { # Email configuration SMTP_HOST = local.scaleway_smtp_host SMTP_PORT = 465 - MY_PRIMARY_DOMAIN = local.domain_name + MY_PRIMARY_DOMAIN = local.primary_domain MY_DESTINATION = join(",", [for account in local.email_accounts : account.domain]) MY_NETWORKS = "127.0.0.0/8" - MAILNAME = "smtp.${local.domain_name}" + MAILNAME = "smtp.${local.primary_domain}" DOVECOT_AUTH_ADDRESS = "dovecot-auth.mail.svc.cluster.local" DOVECOT_AUTH_PORT = 31000 DOVECOT_LMTP_ADDRESS = "dovecot-lmtp.mail.svc.cluster.local" @@ -201,8 +201,8 @@ resource "kubernetes_manifest" "mail_certificate" { name = "letsencrypt-prod" kind = "ClusterIssuer" } - commonName = "imap.${local.domain_name}" - dnsNames = ["imap.${local.domain_name}", "smtp.${local.domain_name}"] + commonName = "imap.${local.primary_domain}" + dnsNames = flatten([for account in local.email_accounts : ["imap.${account.domain}", "smtp.${account.domain}"]]) } } } diff --git a/terraform/k8sapps/resources/nginx/conf.d/default.conf b/terraform/k8sapps/resources/nginx/conf.d/default.conf new file mode 100644 index 0000000..d352c7b --- /dev/null +++ b/terraform/k8sapps/resources/nginx/conf.d/default.conf @@ -0,0 +1,35 @@ +server { + listen 8080; + server_name localhost; + + gzip on; + gzip_comp_level 9; + gzip_types + text/html + text/plain + text/css + text/js + text/xml + text/javascript + application/javascript + application/json + application/xml + application/rss+xml + image/svg+xml; + + root /tmp/site; + + location / { + index index.html index.htm; + } + + location ~* ^/([^/]+) { + index index.html index.htm; + error_page 404 = @error; + } + + error_page 404 /404.html; + location @error { + try_files /$1/404.html /404.html =404; + } +} diff --git a/terraform/k8sapps/resources/nginx/nginx.conf b/terraform/k8sapps/resources/nginx/nginx.conf new file mode 100644 index 0000000..12c02fa --- /dev/null +++ b/terraform/k8sapps/resources/nginx/nginx.conf @@ -0,0 +1,34 @@ +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /tmp/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + proxy_temp_path /tmp/proxy_temp; + client_body_temp_path /tmp/client_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + + keepalive_timeout 65; + + + include /etc/nginx/conf.d/*.conf; +} \ No newline at end of file diff --git a/terraform/k8sapps/website.tf b/terraform/k8sapps/website.tf new file mode 100644 index 0000000..f43c532 --- /dev/null +++ b/terraform/k8sapps/website.tf @@ -0,0 +1,201 @@ +locals { + websites_path = "${local.persistent_folder}/web" +} + +resource "kubernetes_namespace" "website" { + metadata { + annotations = { + name = "website" + } + + labels = { + name = "website" + } + + name = "website" + } +} + +resource "kubernetes_service_account" "website_service_account" { + + metadata { + name = "website-app-sa" + namespace = kubernetes_namespace.website.metadata[0].name + } + + automount_service_account_token = false +} + + +resource "kubernetes_service" "website" { + for_each = local.websites + metadata { + name = format("website-%s", replace(each.key, ".", "-")) + namespace = kubernetes_namespace.website.metadata[0].name + } + spec { + port { + port = 80 + target_port = "http" + } + selector = { + app = format("website-%s", replace(each.key, ".", "-")) + } + cluster_ip = "None" + } +} + +resource "kubernetes_config_map" "nginx_config" { + metadata { + name = "nginx-config" + namespace = kubernetes_namespace.website.metadata[0].name + } + + data = { + "nginx.conf" = file("${path.module}/resources/nginx/nginx.conf") + "default.conf" = file("${path.module}/resources/nginx/conf.d/default.conf") + } +} + +resource "kubernetes_ingress_v1" "website_ingress" { + for_each = local.websites + metadata { + name = "website-ingress" + namespace = kubernetes_namespace.website.metadata[0].name + + annotations = { + "kubernetes.io/ingress.class" = "public" + "cert-manager.io/cluster-issuer" = "letsencrypt-prod" + } + } + spec { + tls { + hosts = each.value.fqdn + secret_name = format("website-%s-tls", replace(each.key, ".", "-")) + } + dynamic "rule" { + for_each = each.value.fqdn + content { + host = rule.value + http { + path { + backend { + service { + name = format("website-%s", replace(each.key, ".", "-")) + port { + number = 80 + } + } + } + } + } + } + } + } +} + +resource "kubernetes_deployment" "website" { + for_each = local.websites + + metadata { + name = "website-app" + namespace = kubernetes_namespace.website.metadata[0].name + } + spec { + selector { + match_labels = { + app = format("website-%s", replace(each.key, ".", "-")) + } + } + strategy { + type = "RollingUpdate" + } + template { + metadata { + labels = { + app = format("website-%s", replace(each.key, ".", "-")) + } + } + spec { + service_account_name = kubernetes_service_account.website_service_account.metadata[0].name + + security_context { + fs_group = 1000 + run_as_user = 1000 + } + + container { + name = "website" + image = "nginx:stable-alpine" + image_pull_policy = "Always" + + resources { + requests = { + memory = "100Mi" + } + limits = { + memory = "500Mi" + } + } + + readiness_probe { + http_get { + path = "/" + port = 8080 + } + initial_delay_seconds = 5 + period_seconds = 10 + } + + liveness_probe { + http_get { + path = "/" + port = 8080 + } + initial_delay_seconds = 5 + period_seconds = 10 + } + + security_context { + allow_privilege_escalation = false + } + + port { + container_port = 8080 + name = "http" + } + volume_mount { + name = "nginx-config" + mount_path = "/etc/nginx/nginx.conf" + sub_path = "nginx.conf" + read_only = true + } + volume_mount { + name = "nginx-config" + mount_path = "/etc/nginx/conf.d/default.conf" + sub_path = "default.conf" + read_only = true + } + volume_mount { + name = "website" + mount_path = "/tmp/site" + read_only = true + } + } + volume { + name = "nginx-config" + config_map { + name = kubernetes_config_map.nginx_config.metadata[0].name + } + } + volume { + name = "website" + host_path { + path = format("%s/%s", local.websites_path, each.key) + } + } + } + } + } +} + diff --git a/terraform/k8sapps/wordpress.tf b/terraform/k8sapps/wordpress.tf deleted file mode 100644 index 83768f3..0000000 --- a/terraform/k8sapps/wordpress.tf +++ /dev/null @@ -1,239 +0,0 @@ -locals { - wordpress_path = "${local.persistent_folder}/wordpress" - wordpress_db_path = "${local.database_folder}/mysql.wordpress" - wordpress_backup_path = "${local.persistent_folder}/backup.wordpress" -} - -resource "kubernetes_namespace" "wordpress" { - metadata { - annotations = { - name = "wordpress" - } - - labels = { - name = "wordpress" - } - - name = "wordpress" - } -} - - -# -# Setup MariaDB database -# -module "wordpress_database" { - source = "./modules/k8s_mariadb" - - namespace = kubernetes_namespace.wordpress.metadata[0].name - app_name = "wordpress" - - app_password = var.wordpress_mysql_password - root_password = var.mysql_root_password - - retention_days = local.local_backup_retention_days - cron_schedule = local.wordpress_sql_backup_cron_schedule - - database_folder = local.wordpress_db_path - backup_folder = local.wordpress_backup_path -} - - -# -# Setup rsync backup job to scaleway bucket -# -module "wordpress_backup" { - source = "./modules/scw_backup_job" - - namespace = kubernetes_namespace.wordpress.metadata[0].name - app_name = "wordpress" - - scaleway_organization_id = var.scaleway_organization_id - scaleway_project_id = var.scaleway_organization_id - access_key = scaleway_iam_api_key.backup_app_api.access_key - secret_key = scaleway_iam_api_key.backup_app_api.secret_key - bucket_name = scaleway_object_bucket.backup.name - - retention_days = local.local_backup_retention_days - cron_schedule = local.wordpress_files_backup_cron_schedule - - source_folder = local.wordpress_path - target_folder = local.wordpress_backup_path -} - -# -# Gitea service, ingress, deployment and file backup job -# -resource "kubernetes_service_account" "wordpress_service_account" { - - metadata { - name = "wordpress-app-sa" - namespace = kubernetes_namespace.wordpress.metadata[0].name - } - - automount_service_account_token = false -} - - -resource "kubernetes_service" "wordpress" { - metadata { - name = "http" - namespace = kubernetes_namespace.wordpress.metadata[0].name - } - spec { - port { - port = 80 - target_port = "http" - } - selector = { - app = "wordpress" - } - cluster_ip = "None" - } -} - -resource "kubernetes_ingress_v1" "wordpress_ingress" { - metadata { - name = "wordpress-ingress" - namespace = kubernetes_namespace.wordpress.metadata[0].name - - annotations = { - "kubernetes.io/ingress.class" = "public" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - spec { - tls { - hosts = local.wordpress_fqdns - secret_name = "wordpress-le-tls" - } - dynamic "rule" { - for_each = local.wordpress_fqdns - content { - host = rule.value - http { - path { - backend { - service { - name = "http" - port { - number = 80 - } - } - } - } - } - } - } - } -} - -resource "kubernetes_deployment" "wordpress" { - - depends_on = [module.wordpress_database] - - metadata { - name = "wordpress-app" - namespace = kubernetes_namespace.wordpress.metadata[0].name - } - spec { - selector { - match_labels = { - app = "wordpress" - } - } - strategy { - type = "RollingUpdate" - } - template { - metadata { - labels = { - app = "wordpress" - } - } - spec { - service_account_name = kubernetes_service_account.wordpress_service_account.metadata[0].name - - container { - name = "wordpress" - image = "wordpress:${local.wordpress_version}" - image_pull_policy = "IfNotPresent" - - resources { - requests = { - memory = "100Mi" - } - limits = { - memory = "500Mi" - } - } - - # readiness_probe { - # http_get { - # path = "/?nocache" - # port = "http" - # } - # initial_delay_seconds = 20 - # period_seconds = 10 - # success_threshold = 1 - # failure_threshold = 5 - # } - - # liveness_probe { - # tcp_socket { - # port = "http" - # } - # initial_delay_seconds = 30 - # period_seconds = 60 - # success_threshold = 1 - # failure_threshold = 10 - # } - security_context { - allow_privilege_escalation = false - } - - env { - name = "WORDPRESS_DB_NAME" - value = "wordpress" - } - env { - name = "WORDPRESS_DB_HOST" - value = "${module.wordpress_database.host}:${module.wordpress_database.port}" - } - env { - name = "WORDPRESS_DB_USER" - value_from { - secret_key_ref { - key = "username" - name = module.wordpress_database.app_crendentials_name - } - } - } - env { - name = "WORDPRESS_DB_PASSWORD" - value_from { - secret_key_ref { - key = "password" - name = module.wordpress_database.app_crendentials_name - } - } - } - port { - container_port = 80 - name = "http" - } - volume_mount { - mount_path = "/var/www/html" - name = "persistent-storage" - } - } - volume { - name = "persistent-storage" - host_path { - path = local.wordpress_path - } - } - } - } - } -} diff --git a/websites/.gitignore b/websites/.gitignore new file mode 100644 index 0000000..ed7fb70 --- /dev/null +++ b/websites/.gitignore @@ -0,0 +1 @@ +releases \ No newline at end of file diff --git a/websites/alexpires.me/.gitignore b/websites/alexpires.me/.gitignore new file mode 100644 index 0000000..89f032d --- /dev/null +++ b/websites/alexpires.me/.gitignore @@ -0,0 +1,39 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,hugo +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,hugo + +### Hugo ### +# Generated files by hugo +/public/ +/resources/_gen/ +/assets/jsconfig.json +hugo_stats.json + +# Executable may be added to repository +hugo.exe +hugo.darwin +hugo.linux + +# Temporary lock file while building +/.hugo_build.lock + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide +.~lock.*.odt# + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,hugo diff --git a/websites/alexpires.me/archetypes/default.md b/websites/alexpires.me/archetypes/default.md new file mode 100644 index 0000000..0d5eebd --- /dev/null +++ b/websites/alexpires.me/archetypes/default.md @@ -0,0 +1,5 @@ +--- +date: '{{ .Date }}' +draft: true +title: '{{ replace .File.ContentBaseName "-" " " | title }}' +--- diff --git a/websites/alexpires.me/hugo.yaml b/websites/alexpires.me/hugo.yaml new file mode 100644 index 0000000..a9de038 --- /dev/null +++ b/websites/alexpires.me/hugo.yaml @@ -0,0 +1,368 @@ +baseURL: https://alexpires.me/ +languageCode: en-us +title: Alexandre Pires +theme: hugo-profile + +outputs: + home: + - "HTML" + - "RSS" + - "JSON" + page: + - "HTML" + - "RSS" + +enableRobotsTXT: true + +#services: +#googleAnalytics: +#id: G-MEASUREMENT_ID +#disqus: +#shortname: your-disqus-shortname + +pagination: + pagerSize: 3 + +markup: + goldmark: + renderer: + unsafe: true + +Menus: + # main: + # - identifier: blog + # name: Blog + # title: Blog posts + # url: /blogs + # weight: 1 + # - identifier: gallery + # name: Gallery + # title: Blog posts + # url: /gallery + # weight: 2 + #Dropdown menu + # - identifier: dropdown + # title: Example dropdown menu + # name: Dropdown + # weight: 3 + # - identifier: dropdown1 + # title: example dropdown 1 + # name: example 1 + # url: /# + # parent: dropdown + # weight: 1 + # - identifier: dropdown2 + # title: example dropdown 2 + # name: example 2 + # url: /# + # parent: dropdown + # weight: 2 + +params: + title: "Alexandre Pires - Software/Systems Engineer" + description: Text about my cool site + # staticPath: "" # The path to serve the static files from + favicon: "/fav.png" + + # Whether to serve bootstrap css and js files from CDN or not. Can be set to true, "css" or "js" to choose between + # serving both, only the css, or only the js files through the CDN. Any other value will make so that CDN is not used. + # Note the lack of "" in true, it should be of boolean type. + useBootstrapCDN: false + + # If you want to load dynamically responsive images from Cloudinary + # This requires your images to be uploaded + hosted on Cloudinary + # Uncomment and change YOUR_CLOUD_NAME to the Cloud Name in your Cloudinary console + # cloudinary_cloud_name: "YOUR_CLOUD_NAME" + + # Whether to add mathjax support on all pages. Alternatively, you can opt-in per page by adding `mathjax: true` in the frontmatter. + mathjax: false + + # Whether the fade animations on the home page will be enabled + animate: true + + theme: + disableThemeToggle: true + defaultTheme: "dark" + + font: + fontSize: 1rem # default: 1rem + fontWeight: 400 # default: 400 + lineHeight: 1.5 # default: 1.5 + textAlign: left # default: left + + # color preference + # When using hex codes for colors, quotations must be used along with the # sign + # color: + # textColor: "#343a40" + # secondaryTextColor: "#6c757d" + # textLinkColor: "#007bff" + # backgroundColor: "#eaedf0" + # secondaryBackgroundColor: "#64ffda1a" + # primaryColor: "#007bff" + # secondaryColor: "#f8f9fa" + + # darkmode: + # textColor: "#e4e6eb" + # secondaryTextColor: "#b0b3b8" + # textLinkColor: "#ffffff" + # backgroundColor: "#18191a" + # secondaryBackgroundColor: "#212529" + # primaryColor: "#ffffff" + # secondaryColor: "#212529" + + # If you want to customize the menu, you can change it here + navbar: + align: ms-auto # Left: ms-auto | center: mx-auto | right: me-auto | Default: ms-auto + # brandLogo: "/logo.png" # Logo for the brand | default is the favicon variable + # showBrandLogo: false # Show brand logo in nav bar | default is true + brandName: "Alexandre Pires" # Brand name for the brand | default is the title variable + disableSearch: true + # searchPlaceholder: "Search" + stickyNavBar: + enable: true + showOnScrollUp: true + enableSeparator: false + menus: + disableAbout: false + disableExperience: false + disableEducation: false + disableProjects: false + disableAchievements: false + disableContact: false + + # Hero + hero: + enable: true + intro: "Welcome, my name is" + title: "Alexandre Pires." + subtitle: "Software/Systems Engineer" + content: "A passionate IT professional with 10+ years of experience building tools on open-source tech, delivering solutions that drive business success." + image: /images/hero.png + bottomImage: + enable: true + # roundImage: true # Make hero image circular | default false + button: + enable: true + name: "Resume" + url: "cv.pdf" + download: true + newPage: false + socialLinks: + fontAwesomeIcons: + - icon: fab fa-github + url: https://github.com/a13labs/ + - icon: fab fa-linkedin-in + url: https://www.linkedin.com/in/carlos-pires-b2597886/ + + # About + about: + enable: true + title: "About Me" + image: "/images/me.png" + content: |- + Over 10 years of experience in developing software and tools on top of open-source technologies, managing server infrastructures across architectures for high availability and performance, delivering business-driven solutions, and leading systems, projects, and teams across diverse IT environments. + skills: + enable: true + title: "Here are a few skills I've been improving over the years:" + items: + - "Software development" + - "Cloud platforms" + - "Automation" + - "Infrastructure as Code" + - "Virtualization" + - "Project management" + - "Team leading and mentoring" + + # Experience + experience: + enable: true + # title: "Custom Name" + items: + - job: "Cloud Native/Platform Engineer" + company: "KBC" + companyUrl: "https://kbc.be" + date: "Jul 2021 - present" + info: + enable: false + content: "AWS engineer focused on building a Kubernetes-based platform. Part of a delivery-to-business team, responsible for designing, developing, and driving adoption of an EKS platform for business teams, with a strong focus on architectural design and building CI/CD solutions using GitHub Workflows." + + - job: "DevOps/Linux System Engineer" + company: "Siemens" + companyUrl: "https://siemens.com" + date: "Jan 2021 - Jun 2021" + info: + enable: false + content: | + Hired as a Linux System Engineer to develop and serve as a CI/CD Engineer for a new Azure DevOps-based Linux solution supporting the company's internal clients. Main responsibilities included using technologies like Docker, Ansible, Molecule, and Podman to design and build the core platform for clients to build, test, and deploy their software. + + - job: "DevOps/Backend Lead Developer" + company: "MovAI" + companyUrl: "https://mov.ai" + date: "Set 2019 - Dec 2020" + info: + enable: false + content: | + As the Backend/DevOps Lead, I was responsible for designing, implementing, and maintaining the CI/CD pipeline infrastructure. I worked closely with the Project Manager and R&D Manager to establish proper procedures and methodologies for a software development company, participated in the QA process, and led the design and implementation of the company's product deployment strategy. As the most senior developer, I played a key role in mentoring new team members, introducing best practices and design patterns, and helping to elevate the technical skills of the existing development team. + + - job: "Linux System Engineer / DevOps Engineer" + company: "ALPEGA Group" + companyUrl: "https://www.alpegagroup.com/" + date: "Set 2018 - Aug 2019" + info: + enable: false + content: | + Hired full-time as a Systems and DevOps Engineer to operate and enhance infrastructure, implement a new monitoring solution and deployment pipeline in AWS, enabling the adoption of DevOps practices and CI/CD. Played a key technical role in the company's 'lift-and-shift' migration project to AWS." + + - job: "Linux System Engineer / Senior IT Manager" + company: "HBD" + companyUrl: "https:///hbd.com" + date: "Mar 2013 - Aug 2018" + info: + enable: false + content: | + HBD is a private South African investment company focused on the sustainable and responsible development of Príncipe Island. With around 500 employees, HBD invests in social, agricultural, and infrastructure development, aiming to establish eco-resorts, including 5-star properties. + Initially hired as a consultant, then brought on full-time to build the IT department from the ground up and restructure the company’s network and IT technology portfolio. + + # Education + education: + enable: true + # title: "Custom Name" + index: false + items: + - title: "Bachelor of Science Robotics Engineering" + school: + name: "International University of Applied Sciences, Germany" + url: "https://iu.org" + date: "2021 - present" + GPA: "" + content: + - title: "Bachelor of Science Computer Science Engineering" + school: + name: "Universidade Aberta, Portugal" + url: "https://portal.uab.pt/" + date: "2016 - 2023" + GPA: "4 out of 5.0" + content: |- + I Publiced one papers in the University Journal on parallel computing. + - [Proposal for an optimized variant of A* Algorithm for multi-core systems ](https://journals.uab.pt/index.php/rcc/article/view/297) + - title: "High School" + school: + name: "Escola secundária de Anadia" + url: "https://aeanadia.pt/" + GPA: "3 out of 5.0" + + # Achievements + achievements: + enable: false + # title: "Custom Name" + items: + - title: Google kickstart runner + content: I solved all problems with optimal solution. + url: https://example.com + image: /images/achievement.jpg + + # projects + projects: + enable: false + # title: "Custom Name" + items: + - title: Hugo Profile + content: A highly customizable and mobile first Hugo template for personal portfolio and blog. + image: /images/projects/profile.png + featured: + name: Demo + link: https://hugo-profile.netlify.app + badges: + - "Hugo" + - "Bootstrap" + - "Javascript" + links: + - icon: fa fa-envelope + url: mailto:?subject=Hugo%20Profile%20Template&body=Check%20it%20out:%20https%3a%2f%2fhugo-profile.netlify.app%2fblog%2fmarkdown-syntax%2f + - icon: fab fa-github + url: https://github.com/gurusabarish/hugo-profile + - icon: fab fa-twitter + url: https://twitter.com/intent/tweet?text=Check+it+out:&url=https%3A%2F%2Fgithub.com%2Fgurusabarish%2Fhugo-profile + + - title: Image Converter + content: A web app to convert image to pdf, png to jpg, png to jpg and png to webp without database using django. + image: /images/projects/converter.jpg + featured: + name: Demo + link: https://django-converter.herokuapp.com + badges: + - "Django" + - "Bootstrap" + links: + - icon: fab fa-github + url: https://github.com/gurusabarish/converter + + - title: Hugo Profile V2 + content: A clean and simple Hugo template for personal portfolio and blog. + image: /images/projects/profile2.jpg + featured: + name: Demo V2 + link: https://hugo-profile-2.netlify.app + badges: + - "Hugo" + - "Bootstrap" + - "Javascript" + links: + - icon: fab fa-github + url: https://github.com/gurusabarish/HugoProfileV2 + + #Contact + contact: + enable: true + # title: "Custom Name" + content: My inbox is always open. Whether you have a question or just want to say hi, I’ll try my best to get back to you! + btnName: Mail me + btnLink: mailto:c.alexandre.pires@alexpires.me + # formspree: + # enable: true # `contact.email` value will be ignored + # formId: abcdefgh # Take it from your form's endpoint, like 'https://formspree.io/f/abcdefgh' + # emailCaption: "Enter your email address" + # messageCaption: "Enter your message here" + # messageRows: 5 + + footer: + recentPosts: + path: "blogs" + count: 3 + title: Recent Posts + enable: false + disableFeaturedImage: false + socialNetworks: + github: https://github.com/a13labs/ + linkedin: https://www.linkedin.com/in/carlos-pires-b2597886/ + + # List pages like blogs and posts + listPages: + disableFeaturedImage: false + + # Single pages like blog and post + singlePages: + socialShare: true + readTime: + enable: true + content: "min read" + scrollprogress: + enable: true + tags: + openInNewTab: true + + # For translations + terms: + read: "Read" + toc: "Table Of Contents" + copyright: "All rights reserved" + pageNotFound: "Page not found" + emailText: "Check out this site" + + datesFormat: + article: "Jan 2, 2006" + articleList: "Jan 2, 2006" + articleRecent: "Jan 2, 2006" + + #customScripts: -| # You can add custom scripts which will be added before tag + # diff --git a/websites/alexpires.me/static/cv.odt b/websites/alexpires.me/static/cv.odt new file mode 100644 index 0000000..91b3e35 Binary files /dev/null and b/websites/alexpires.me/static/cv.odt differ diff --git a/websites/alexpires.me/static/cv.pdf b/websites/alexpires.me/static/cv.pdf new file mode 100644 index 0000000..83fcfeb Binary files /dev/null and b/websites/alexpires.me/static/cv.pdf differ diff --git a/websites/alexpires.me/static/fav.png b/websites/alexpires.me/static/fav.png new file mode 100644 index 0000000..b25373c Binary files /dev/null and b/websites/alexpires.me/static/fav.png differ diff --git a/websites/alexpires.me/static/images/hero.png b/websites/alexpires.me/static/images/hero.png new file mode 100644 index 0000000..f060efd Binary files /dev/null and b/websites/alexpires.me/static/images/hero.png differ diff --git a/websites/alexpires.me/static/images/me.png b/websites/alexpires.me/static/images/me.png new file mode 100644 index 0000000..859373c Binary files /dev/null and b/websites/alexpires.me/static/images/me.png differ diff --git a/websites/themes/hugo-profile b/websites/themes/hugo-profile new file mode 160000 index 0000000..b8c83af --- /dev/null +++ b/websites/themes/hugo-profile @@ -0,0 +1 @@ +Subproject commit b8c83aff4c4b3eb715e6f2ba65a51072b912cfc6