commit 72c4d1644841e461af0afd8d365074335b8856f5 Author: root Date: Wed Feb 28 12:42:50 2024 +0200 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50cc0ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# dependencies +node_modules/ +.pnp +.pnp.js + +# testing +coverage/ + +# production +build/ + +# misc +.DS_Store +.env* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +requests.rest +.prettierrc +CSV_FILES/ +log/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..9828e71 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,266 @@ +--- +stages: + - preperations + - test + - build + - postbuild_test + - deploy_api + - deploy_web + +variables: + ANSIBLE_IMAGE_TAG: "${CI_REGISTRY_IMAGE}/ansible:latest" + API_IMAGE_TAG: "${CI_REGISTRY_IMAGE}/api:${CI_COMMIT_SHORT_SHA}" + CLIENT_IMAGE_TAG: "${CI_REGISTRY_IMAGE}/client:${CI_COMMIT_SHORT_SHA}" + SERVER_IMAGE_TAG: "${CI_REGISTRY_IMAGE}/server:${CI_COMMIT_SHORT_SHA}" + FRONTEND_PATH: ${CI_PROJECT_DIR}/client + DEPLOY_PROJECT_NAME: "selfservice" + WEB_DOMAIN: "selfservice.example.org" + TRAEFIK_VERSION: "2.11" + + +.ansible-template: + before_script: + - chmod 0640 ${CI_PROJECT_DIR} -R + - chmod 0600 ${PROJECT_SSH_KEY} + +build-debian-ansible: + image: docker:latest + stage: preperations + when: manual + services: + - docker:dind + + script: + - cd ansible/debian + - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - docker build -t ${CI_REGISTRY_IMAGE}/debian-ansible:${CI_COMMIT_SHORT_SHA} . + - docker tag ${CI_REGISTRY_IMAGE}/debian-ansible:${CI_COMMIT_SHORT_SHA} ${CI_REGISTRY_IMAGE}/debian-ansible:latest + - docker tag ${CI_REGISTRY_IMAGE}/debian-ansible:${CI_COMMIT_SHORT_SHA} ${ANSIBLE_IMAGE_TAG} + - docker push ${CI_REGISTRY_IMAGE}/debian-ansible:${CI_COMMIT_SHORT_SHA} + - docker push ${CI_REGISTRY_IMAGE}/debian-ansible:latest + - docker push ${ANSIBLE_IMAGE_TAG} + +build-alma-ansible: + image: docker:latest + stage: preperations + when: manual + services: + - docker:dind + + script: + - cd ansible/alma + - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - docker build -t ${CI_REGISTRY_IMAGE}/alma-ansible:${CI_COMMIT_SHORT_SHA} . + - docker tag ${CI_REGISTRY_IMAGE}/alma-ansible:${CI_COMMIT_SHORT_SHA} ${CI_REGISTRY_IMAGE}/alma-ansible:latest + - docker tag ${CI_REGISTRY_IMAGE}/alma-ansible:${CI_COMMIT_SHORT_SHA} ${ANSIBLE_IMAGE_TAG} + - docker push ${CI_REGISTRY_IMAGE}/alma-ansible:${CI_COMMIT_SHORT_SHA} + - docker push ${CI_REGISTRY_IMAGE}/alma-ansible:latest + - docker push ${ANSIBLE_IMAGE_TAG} + +build-python-ansible: + image: docker:latest + stage: preperations + when: manual + services: + - docker:dind + + script: + - cd ansible/python + - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - docker build -t ${CI_REGISTRY_IMAGE}/python-ansible:${CI_COMMIT_SHORT_SHA} . + - docker tag ${CI_REGISTRY_IMAGE}/python-ansible:${CI_COMMIT_SHORT_SHA} ${CI_REGISTRY_IMAGE}/python-ansible:latest + - docker tag ${CI_REGISTRY_IMAGE}/python-ansible:${CI_COMMIT_SHORT_SHA} ${ANSIBLE_IMAGE_TAG} + - docker push ${CI_REGISTRY_IMAGE}/python-ansible:${CI_COMMIT_SHORT_SHA} + - docker push ${CI_REGISTRY_IMAGE}/python-ansible:latest + - docker push ${ANSIBLE_IMAGE_TAG} + +build-api: + image: docker:latest + stage: build + when: manual + services: + - docker:dind + + script: + - cd api + - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - docker build -t ${API_IMAGE_TAG} . + - docker push ${API_IMAGE_TAG} + +build-server: + image: docker:latest + stage: build + when: manual + services: + - docker:dind + + script: + - cd server + - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - docker build -t ${SERVER_IMAGE_TAG} . + - docker push ${SERVER_IMAGE_TAG} + +build-client: + image: docker:latest + stage: build + when: manual + services: + - docker:dind + + script: + - cd client + - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - docker build -t ${CLIENT_IMAGE_TAG} . + - docker push ${CLIENT_IMAGE_TAG} + +test-client: + image: docker:latest + stage: test + allow_failure: true + when: manual + services: + - docker:dind + + script: + - cd client + - docker build -t ${CLIENT_IMAGE_TAG} -f Dockerfile.test . + +test-server: + image: docker:latest + stage: test + allow_failure: true + when: manual + services: + - docker:dind + + script: + - cd server + - docker build -t ${CLIENT_IMAGE_TAG} -f Dockerfile.test . + +.deploy-api: + extends: .ansible-template + image: ${ANSIBLE_IMAGE_TAG} + variables: + HOSTS_LIMIT: all + stage: deploy_api + when: manual + script: + - | + ansible-playbook --limit ${HOSTS_LIMIT} --inventory inventory deploy-api.yml --private-key ${PROJECT_SSH_KEY} \ + --extra-vars project_name=${DEPLOY_PROJECT_NAME}_api --extra-vars config_dir=/etc/app-${DEPLOY_PROJECT_NAME}-api \ + --extra-vars registry_url=${CI_REGISTRY} --extra-vars registry_username=${CI_REGISTRY_USER} \ + --extra-vars registry_password=${CI_REGISTRY_PASSWORD} --extra-vars api_image=${API_IMAGE_TAG} + +deploy-api-201: + extends: .deploy-api + variables: + HOSTS_LIMIT: 172.17.200.101 + +deploy-api-202: + extends: .deploy-api + variables: + HOSTS_LIMIT: 172.17.200.102 + +deploy-api-203: + extends: .deploy-api + variables: + HOSTS_LIMIT: 172.17.200.103 + +deploy-dev-api: + extends: .deploy-api + variables: + HOSTS_LIMIT: 172.17.200.104 + script: + - | + ansible-playbook --limit ${HOSTS_LIMIT} --inventory inventory deploy-api.yml --private-key ${PROJECT_SSH_KEY} \ + --extra-vars project_name=${DEPLOY_PROJECT_NAME}_api --extra-vars config_dir=/etc/app-${DEPLOY_PROJECT_NAME}-dev-api \ + --extra-vars registry_url=${CI_REGISTRY} --extra-vars registry_username=${CI_REGISTRY_USER} \ + --extra-vars registry_password=${CI_REGISTRY_PASSWORD} --extra-vars api_image=${API_IMAGE_TAG} + +.deploy-web: + extends: .ansible-template + image: ${ANSIBLE_IMAGE_TAG} + stage: deploy_web + when: manual + variables: + HOSTS_LIMIT: all + script: + - | + ansible-playbook --limit ${HOSTS_LIMIT} --inventory inventory deploy-web.yml --private-key ${PROJECT_SSH_KEY} \ + --extra-vars project_name=${DEPLOY_PROJECT_NAME}-web --extra-vars config_dir=/etc/app-${DEPLOY_PROJECT_NAME} \ + --extra-vars registry_url=${CI_REGISTRY} --extra-vars registry_username=${CI_REGISTRY_USER} \ + --extra-vars registry_password=${CI_REGISTRY_PASSWORD} --extra-vars client_image=${CLIENT_IMAGE_TAG} \ + --extra-vars server_image=${SERVER_IMAGE_TAG} \ + --extra-vars project_domain=${WEB_DOMAIN} + +deploy-web-231: + extends: .deploy-web + variables: + HOSTS_LIMIT: 172.17.200.111 + +deploy-web-232: + extends: .deploy-web + variables: + HOSTS_LIMIT: 172.17.200.112 + +deploy-web-233: + extends: .deploy-web + variables: + HOSTS_LIMIT: 172.17.200.113 + +deploy-dev-web: + extends: .deploy-web + variables: + HOSTS_LIMIT: 172.17.200.114 + script: + - | + ansible-playbook --limit ${HOSTS_LIMIT} --inventory inventory deploy-web.yml --private-key ${PROJECT_SSH_KEY} \ + --extra-vars project_name=${DEPLOY_PROJECT_NAME}-web --extra-vars config_dir=/etc/app-${DEPLOY_PROJECT_NAME}-dev \ + --extra-vars registry_url=${CI_REGISTRY} --extra-vars registry_username=${CI_REGISTRY_USER} \ + --extra-vars registry_password=${CI_REGISTRY_PASSWORD} --extra-vars client_image=${CLIENT_IMAGE_TAG} \ + --extra-vars server_image=${SERVER_IMAGE_TAG} \ + --extra-vars project_domain=$dev-{WEB_DOMAIN} + + + +deploy-traefik: + extends: .ansible-template + image: ${ANSIBLE_IMAGE_TAG} + stage: preperations + when: manual + script: + - | + ansible-playbook --inventory inventory deploy-traefik.yml --private-key ${PROJECT_SSH_KEY} \ + --extra-vars config_dir=/etc/app-traefik + +deploy-docker: + extends: .ansible-template + image: ${ANSIBLE_IMAGE_TAG} + stage: preperations + when: manual + script: + - | + ansible-playbook --inventory inventory deploy-docker-ce.yml --private-key ${PROJECT_SSH_KEY} + +deploy-portainer: + extends: .ansible-template + image: ${ANSIBLE_IMAGE_TAG} + stage: preperations + when: manual + script: + - | + ansible-playbook --inventory inventory deploy-portainer.yml --private-key ${PROJECT_SSH_KEY} \ + --extra-vars config_dir=/etc/app-portainer + + +deploy-node-exporter: + extends: .ansible-template + image: ${ANSIBLE_IMAGE_TAG} + variables: + HOSTS_LIMIT: all + stage: preperations + when: manual + script: + - | + ansible-playbook --limit ${HOSTS_LIMIT} --inventory inventory deploy-node-exporter.yml --private-key ${PROJECT_SSH_KEY} + diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..41d3950 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +host_key_checking = False + +#[inventory] +#enable_plugins = yaml + +[ssh_connection] +pipelining = True +control_path = /tmp/ansible-ssh-%%h-%%p-%%r diff --git a/ansible/alma/Dockerfile b/ansible/alma/Dockerfile new file mode 100644 index 0000000..0a38514 --- /dev/null +++ b/ansible/alma/Dockerfile @@ -0,0 +1,39 @@ +FROM almalinux:9 + +ENV container=docker + +# Install requirements. +RUN yum -y install rpm dnf-plugins-core \ + && yum -y config-manager --set-enabled crb \ + && yum -y update \ + && yum -y install --allowerasing \ + epel-release \ + sudo \ + which \ + hostname \ + libyaml-devel \ + python3 \ + python3-pip \ + python3-pyyaml \ + rsync \ + wget \ + curl \ + git \ + ruby \ + && yum clean all + +RUN python3 -m pip install --no-cache-dir --upgrade pip \ + && pip3 install --no-cache-dir ansible + +RUN ansible-galaxy collection install community.general && \ + ansible-galaxy collection install ansible.posix && \ + ansible-galaxy collection install community.docker && \ + ansible-galaxy collection list + +RUN mkdir -p /root/.ssh + +# Disable requiretty. +RUN sed -i -e 's/^\(Defaults\s*requiretty\)/#--- \1/' /etc/sudoers + +CMD ["ansible-playbook"] + diff --git a/ansible/alma/requirements.yml b/ansible/alma/requirements.yml new file mode 100644 index 0000000..c6075a3 --- /dev/null +++ b/ansible/alma/requirements.yml @@ -0,0 +1,5 @@ +--- +collections: + - community.general + - ansible.posix + - community.docker diff --git a/ansible/debian/Dockerfile b/ansible/debian/Dockerfile new file mode 100644 index 0000000..c7cdd77 --- /dev/null +++ b/ansible/debian/Dockerfile @@ -0,0 +1,19 @@ +FROM debian:bullseye + +RUN apt update && \ + apt install -y gnupg2 python3 python3-pip curl wget && \ + apt update && \ + apt install -y ansible && \ + pip3 install "ansible-lint[community,yamllint]" && \ + pip3 install --upgrade docker && \ + pip3 install --upgrade docker-compose && \ + rm -rf /var/cache/apk/* + +RUN ansible-galaxy collection install \ + community.docker + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT [ "/entrypoint.sh" ] +CMD ["ansible-playbook"] diff --git a/ansible/debian/entrypoint.sh b/ansible/debian/entrypoint.sh new file mode 100644 index 0000000..6d54b0e --- /dev/null +++ b/ansible/debian/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +## Do whatever you need with env vars here ... + +chmod 0600 ${PROJECT_SSH_KEY} + +# Hand off to the CMD +exec "$@" diff --git a/ansible/python/Dockerfile b/ansible/python/Dockerfile new file mode 100644 index 0000000..142755d --- /dev/null +++ b/ansible/python/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.10 + +RUN apt-get update && \ + apt-get install sshpass rsync curl wget ruby socat netcat git -y && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN pip install --upgrade pip --no-cache-dir && \ + pip install ansible --no-cache-dir + +RUN ansible-galaxy collection install community.general && \ + ansible-galaxy collection install ansible.posix && \ + ansible-galaxy collection install community.docker && \ + ansible-galaxy collection list + +RUN mkdir -p /root/.ssh + +CMD ["ansible-playbook"] diff --git a/ansible/python/requirements.yml b/ansible/python/requirements.yml new file mode 100644 index 0000000..c6075a3 --- /dev/null +++ b/ansible/python/requirements.yml @@ -0,0 +1,5 @@ +--- +collections: + - community.general + - ansible.posix + - community.docker diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..ff2819a --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,25 @@ +ARG NODE_IMAGE="node" +ARG NODE_VERSION="16.17-slim" + +FROM ${NODE_IMAGE}:${NODE_VERSION} + +ENV TZ=Asia/Jerusalem + +WORKDIR /project + +COPY package.json package-lock.json /project/ + +RUN npm ci --no-audit --no-fund + +COPY --chown=node:node . /project/ + +RUN chown node:node /project -R && \ + find /project -type d -exec chmod 0750 {} \; && \ + find /project -type f -exec chmod 0650 {} \; && \ + ls -la /project && \ + find /project ! -path '/project/node_modules/*' + +# Run as node (user id 1000) +USER node + +CMD ["node", "/project/app.js"] diff --git a/api/app.js b/api/app.js new file mode 100644 index 0000000..672ac2f --- /dev/null +++ b/api/app.js @@ -0,0 +1,15 @@ +const express = require('express'); +const path = require('path'); + +var app = express(); +app.use(express.json()); + + +require('dotenv').config(); + +const port = 3002; +app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); +}) + +module.exports = app; diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..cac1e8d --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,41 @@ +ARG NGINX_IMAGE="nginx" +ARG NGINX_VERSION="alpine" + +ARG NODE_IMAGE="node" +ARG NODE_VERSION="16.17.1-alpine" + + +## Builder image (builds the static content from JS sources) +FROM ${NODE_IMAGE}:${NODE_VERSION} AS appbuild + + +WORKDIR /app + +COPY package.json package-lock.json /app/ + +RUN npm ci --no-audit --no-fund \ + && ls -la /app \ + && find /app ! -path '/app/node_modules/*' + +COPY . /app/ + +RUN npm ci \ + && npm run build \ + && ls -la /app \ + && find /app ! -path '/app/node_modules/*' + + +## Client web server image +FROM ${NGINX_IMAGE}:${NGINX_VERSION} + + +COPY --from=appbuild app/build/ /usr/share/nginx/html/ + +# Copy the custom Nginx configuration +COPY default.conf /etc/nginx/conf.d/default.conf + +RUN ls -la /usr/share/nginx/html/ \ + && chown root:root -R /usr/share/nginx/html/ \ + && chmod 755 -R /usr/share/nginx/html/ \ + && ls -la /usr/share/nginx/html/ \ + && find /usr/share/nginx/html/ diff --git a/client/Dockerfile.test b/client/Dockerfile.test new file mode 100644 index 0000000..0601339 --- /dev/null +++ b/client/Dockerfile.test @@ -0,0 +1,11 @@ +ARG NODE_IMAGE="node" +ARG NODE_VERSION="16.17.1-alpine" + +FROM ${NODE_IMAGE}:${NODE_VERSION} AS appbuild + +WORKDIR /app + +COPY . /app/ + +RUN npm test + diff --git a/client/default.conf b/client/default.conf new file mode 100644 index 0000000..5e7592e --- /dev/null +++ b/client/default.conf @@ -0,0 +1,8 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } +} diff --git a/client/dockerignore b/client/dockerignore new file mode 100644 index 0000000..4771c3a --- /dev/null +++ b/client/dockerignore @@ -0,0 +1,4 @@ +Dokckerfile +Dokckerfile.* +node_modules +dockerignore \ No newline at end of file diff --git a/deploy-api.yml b/deploy-api.yml new file mode 100644 index 0000000..abeb354 --- /dev/null +++ b/deploy-api.yml @@ -0,0 +1,68 @@ +- name: Deploy Application + hosts: api + remote_user: root + pre_tasks: + - name: "Assert project_name is provided and not empty" + assert: + that: + - project_name is not undefined and project_name != "" + tasks: + +# debug section + - debug: var=inventory_hostname + + - name: check resolv.conf file exists + stat: + path: /etc/resolv.conf + register: resolv_conf + + - name: print a debug message when resolv.config file does not exists + debug: + msg: "The resolv.conf path doesn't exist." + when: resolv_conf.stat.exists == False + + - name: "read /etc/resolv.conf" + command: /bin/cat /etc/resolv.conf + register: resolv_content + + - debug: msg="{{ resolv_content.stdout_lines | quote }}" + + ## Login + - name: Login to registry + docker_login: + registry_url: "{{ registry_url }}" + username: "{{ registry_username }}" + password: "{{ registry_password }}" + reauthorize: true + + +# Deploy image + - name: Create config directory + file: + path: "{{ config_dir }}" # /etc/app-x-y + state: directory + recurse: yes + + - name: Create .env file + file: + state: touch + path: "{{ config_dir }}/.env" + mode: '0600' + + - name: Execute docker-compose + shell: cd "{{ config_dir }}" && docker-compose down + ignore_errors: true + + - name: "Upload docker-compose template" # fills the tempalte with variables from ansile + template: + src: "templates/docker-compose-api.yml" + dest: "{{ config_dir }}/docker-compose.yml" + + - name: Execute docker-compose + shell: cd "{{ config_dir }}" && docker-compose up -d + + - name: Create log file + file: + path: "/var/log/{{ project_name }}" + state: directory + recurse: yes diff --git a/deploy-docker-ce.yml b/deploy-docker-ce.yml new file mode 100644 index 0000000..395d060 --- /dev/null +++ b/deploy-docker-ce.yml @@ -0,0 +1,49 @@ +- name: Deploy Docker ce on Debian Based Systems + hosts: all + remote_user: root + + tasks: + + - meta: end_play + when: ansible_facts['os_family'] != 'Debian' + + - name: "install dependencies" + apt: + pkg: + - python3-pip + - apt-transport-https + + - name: "Get docker signing key" + block: + - name: "Fetch docker text signing key" + get_url: + url: "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg" + dest: "/usr/share/keyrings/docker.asc" + register: docker_gpg_key + - name: "De-armor docker gpg key" + shell: gpg --no-tty --yes -o /etc/apt/keyrings/docker.gpg --dearmor "/usr/share/keyrings/docker.asc" + when: docker_gpg_key.changed + + - name: "Add docker repo" + apt_repository: + repo: deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }} {{ ansible_distribution_release }} stable + filename: docker + update_cache: yes + + - name: "Install docker packages" + apt: + update_cache: yes + pkg: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-compose-plugin + + - name: "install docker-compose" + pip: + name: docker-compose + executable: pip3 + + - name: "Create traefik docker network" + shell: docker network create --driver=bridge --subnet=172.28.0.0/16 --ip-range=172.28.5.0/24 --gateway=172.28.5.254 traefik_http + ignore_errors: yes diff --git a/deploy-node-exporter.yml b/deploy-node-exporter.yml new file mode 100644 index 0000000..77a0af3 --- /dev/null +++ b/deploy-node-exporter.yml @@ -0,0 +1,55 @@ +--- +- hosts: all + tasks: + - name: "Ensure group \"prometheus\" exists with correct gid" + group: + name: prometheus + state: present + gid: 2998 + + - name: "Add the user prometheus" + user: + name: prometheus + groups: prometheus + state: present + uid: 2998 + + - name: "Ensure group \"node_exporter\" exists with correct gid" + group: + name: node_exporter + state: present + gid: 2997 + + - name: "Add the user node_exporter" + user: + name: node_exporter + groups: node_exporter + state: present + uid: 2997 + + - name: Creating /opt/src + shell: | + mkdir -p /opt/src + + - name: "Syncing dir to servers" + synchronize: + src: node_exportoer/node_exporter-1.7.0.linux-amd64.tar.gz + dest: /opt/src/node_exporter-1.7.0.linux-amd64.tar.gz + when: + + - name: "Extracting node_exporter-1.4.0" + shell: | + tar xvf /opt/src/node_exporter-1.7.0.linux-amd64.tar.gz -C /opt/src/ + cd /opt/src/node_exporter-*.linux-amd64 && cp -fv node_exporter /usr/local/bin/ + + - name: "Syncing service file to servers" + synchronize: + src: node_exporter/node-exporter.service + dest: /etc/systemd/system/node-exporter.service + + - name: "Restart and enable the service node-exporter" + systemd: + state: restarted + daemon_reload: yes + name: node-exporter + enabled: yes \ No newline at end of file diff --git a/deploy-portainer.yml b/deploy-portainer.yml new file mode 100644 index 0000000..2f07685 --- /dev/null +++ b/deploy-portainer.yml @@ -0,0 +1,41 @@ +- name: Deploy Portainer + hosts: all + remote_user: root + pre_tasks: + - name: "Assert config_dir is provided and not empty" + assert: + that: + - config_dir is not undefined and config_dir != "" + + tasks: + + ## + - name: "Create config directory" + file: + path: "{{ config_dir }}" + state: directory + recurse: yes + + - name: Execute docker-compose + shell: cd "{{ config_dir }}" && docker-compose down + ignore_errors: true + + - name: "Upload docker-compose template" + template: + src: templates/docker-compose-portainer.yml + dest: "{{ config_dir }}/docker-compose.yml" + + - name: "Upload gen-admin-passwd.sh" + copy: + src: portainer/gen-admin-passwd.sh + dest: "{{ config_dir }}/gen-admin-passwd.sh" + mode: a+x + + - name: "Upload reset-admin-passwd.sh" + copy: + src: portainer/reset-admin-passwd.sh + dest: "{{ config_dir }}/reset-admin-passwd.sh" + mode: a+x + + - name: Execute docker-compose + shell: cd "{{ config_dir }}" && docker-compose up -d diff --git a/deploy-traefik.yml b/deploy-traefik.yml new file mode 100644 index 0000000..fb6a050 --- /dev/null +++ b/deploy-traefik.yml @@ -0,0 +1,86 @@ +- name: Deploy Traefik Reverse Proxy + hosts: all + remote_user: root + pre_tasks: + - name: "Assert config_dir is provided and not empty" + assert: + that: + - config_dir is not undefined and config_dir != "" + + tasks: + + ## + - name: "Create config directory" + file: + path: "{{ config_dir }}" + state: directory + recurse: yes + + - name: "Create config sub-directory certs" + file: + path: "{{ config_dir }}/certs" + state: directory + recurse: yes + + - name: "Create config sub-directory traefik-config" + file: + path: "{{ config_dir }}/traefik-config" + state: directory + recurse: yes + ## + + - name: "Upload config file: {{ config_dir }}/traefik.toml" + copy: + src: "traefik/traefik.toml" + dest: "{{ config_dir }}/traefik.toml" + + - name: "Upload config file: {{ config_dir }}/traefik-config/auth-middleware.yml" + copy: + src: "traefik/auth-middleware.yml" + dest: "{{ config_dir }}/traefik-config/auth-middleware.yml" + + - name: "Upload config file: {{ config_dir }}/traefik-config/certificates.yml" + copy: + src: "traefik/certificates.yml" + dest: "{{ config_dir }}/traefik-config/certificates.yml" + + ## + - name: "Check if \"{{ config_dir }}/certs/cert.pem\" file exists" + stat: + path: "{{ config_dir }}/certs/cert.pem" + register: certfile + + - name: "copy file: \"{{ config_dir }}/certs/cert.pem\" if it doesn't exist" + copy: + src: traefik/cert.pem + dest: "{{ config_dir }}/certs/cert.pem" + when: not certfile.stat.exists + + + - name: "Check if \"{{ config_dir }}/certs/key.pem\" file exists" + stat: + path: "{{ config_dir }}/certs/key.pem" + register: keyfile + + - name: "copy file: \"{{ config_dir }}/certs/key.pem\" if it doesn't exist" + copy: + src: traefik/key.pem + dest: "{{ config_dir }}/certs/key.pem" + when: not keyfile.stat.exists + ## + + - name: Execute docker-compose + shell: cd "{{ config_dir }}" && docker-compose down + ignore_errors: true + + - name: "Upload docker-compose template" + template: + src: templates/docker-compose-traefik.yml + dest: "{{ config_dir }}/docker-compose.yml" + + - name: "Create traefik docker network" + shell: docker network create --driver=bridge --subnet=172.28.0.0/16 --ip-range=172.28.5.0/24 --gateway=172.28.5.254 traefik_http + ignore_errors: yes + + - name: Execute docker-compose + shell: cd "{{ config_dir }}" && docker-compose up -d diff --git a/deploy-web.yml b/deploy-web.yml new file mode 100644 index 0000000..7cb2248 --- /dev/null +++ b/deploy-web.yml @@ -0,0 +1,54 @@ +- name: Deploy Application + hosts: web + remote_user: root + pre_tasks: + - name: "Assert project_name is provided and not empty" + assert: + that: + - project_name is not undefined and project_name != "" + tasks: + + ## Login + - name: Login to registry + docker_login: + registry_url: "{{ registry_url }}" + username: "{{ registry_username }}" + password: "{{ registry_password }}" + reauthorize: true + +# Deploy image + - name: Create config directory + file: + path: "{{ config_dir }}" # /etc/app-x-y + state: directory + recurse: yes + + - name: Create .env file + file: + state: touch + path: "{{ config_dir }}/.env" + mode: '0600' + + - name: Create .env.local file + file: + state: touch + path: "{{ config_dir }}/.env.local" + mode: '0600' + + - name: Execute docker-compose + shell: cd "{{ config_dir }}" && docker-compose down + ignore_errors: true + + - name: "Upload docker-compose template" # fills the tempalte with variables from ansile + template: + src: "templates/docker-compose-web.yml" + dest: "{{ config_dir }}/docker-compose.yml" + + - name: Execute docker-compose + shell: cd "{{ config_dir }}" && docker-compose up -d + + - name: Create log file + file: + path: "/var/log/{{ project_name }}" + state: directory + recurse: yes diff --git a/inventory b/inventory new file mode 100644 index 0000000..4aa59d0 --- /dev/null +++ b/inventory @@ -0,0 +1,11 @@ +[api] +172.17.200.101 traefik_dashboard_hostname=traefik-service101.example.org api_domain=api201-service.example.org +172.17.200.102 traefik_dashboard_hostname=traefik-service102.example.org api_domain=api202-service.example.org +172.17.200.103 traefik_dashboard_hostname=traefik-service103.example.org api_domain=api203-service.example.org +172.17.200.104 traefik_dashboard_hostname=traefik-dev-service.example.org api_domain=dev-api-service.example.org + +[web] +172.17.200.111 traefik_dashboard_hostname=traefik-service111.example.org web_status_id=service231.example.org web_domain=service231.example.org +172.17.200.112 traefik_dashboard_hostname=traefik-service112.example.org web_status_id=service232.example.org web_domain=service232.example.org +172.17.200.113 traefik_dashboard_hostname=traefik-service113.example.org web_status_id=service233.example.org web_domain=service233.example.org +172.17.200.114 traefik_dashboard_hostname=traefik-dev-service.example.org web_status_id=dev-service.example.org web_domain=dev-service.example.org diff --git a/node_exportoer/node-exporter.service b/node_exportoer/node-exporter.service new file mode 100644 index 0000000..d870d79 --- /dev/null +++ b/node_exportoer/node-exporter.service @@ -0,0 +1,13 @@ +[Unit] +Description=Prometheus Node Exporter +Wants=network-online.target +After=network-online.target + +[Service] +User=node_exporter +Group=node_exporter +Type=simple +ExecStart=/usr/local/bin/node_exporter + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/node_exportoer/node_exporter-1.7.0.linux-amd64.tar.gz b/node_exportoer/node_exporter-1.7.0.linux-amd64.tar.gz new file mode 100644 index 0000000..1c76d6f Binary files /dev/null and b/node_exportoer/node_exporter-1.7.0.linux-amd64.tar.gz differ diff --git a/portainer/gen-admin-passwd.sh b/portainer/gen-admin-passwd.sh new file mode 100644 index 0000000..af01445 --- /dev/null +++ b/portainer/gen-admin-passwd.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +USERNAME="admin" +PASSWORD="$1" +if [ -z "\${PASSWORD}" ]; then + echo -e "\\nPlease call '\$0 ' to run this command!\\n" + exit 1 +fi + +which htpasswd +if [ "$?" -gt "0" ] +then + PASS=$(docker run --entrypoint htpasswd httpd:2 -bn -B ${USERNAME} ${PASSWORD} | cut -d ":" -f 2 ) +else + PASS=$(htpasswd -nb -B admin ${PASSWORD} | cut -d ":" -f 2) +fi + +echo "plaintext encryption: ${PASS}" +echo "docker-compse.yml escaped: ${PASS}"|sed -e "s@\\\$@\\\$\\\$@g" \ No newline at end of file diff --git a/portainer/reset-admin-passwd.sh b/portainer/reset-admin-passwd.sh new file mode 100644 index 0000000..d0148fa --- /dev/null +++ b/portainer/reset-admin-passwd.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +docker-compose down + +docker run --rm -v portainer_data:/data/ portainer/helper-reset-password + +docker-compose up -d \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..c87aaac --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,23 @@ +ARG NODE_IMAGE="node" +ARG NODE_VERSION="16.17.1-slim" + +FROM ${NODE_IMAGE}:${NODE_VERSION} + +WORKDIR /project + +COPY package.json package-lock.json /project/ + +RUN npm ci --no-audit --no-fund + +COPY --chown=node:node . /project/ + +RUN chown node:node /project -R && \ + find /project -type d -exec chmod 0750 {} \; && \ + find /project -type f -exec chmod 0650 {} \; && \ + ls -la /project && \ + find /project ! -path '/project/node_modules/*' + +# Run as node (user id 1000) +USER node + +CMD ["node", "/project/app.js"] diff --git a/server/Dockerfile.test b/server/Dockerfile.test new file mode 100644 index 0000000..cde6b68 --- /dev/null +++ b/server/Dockerfile.test @@ -0,0 +1,10 @@ +ARG NODE_IMAGE="node" +ARG NODE_VERSION="16.17.1-slim" + +FROM ${NODE_IMAGE}:${NODE_VERSION} + +WORKDIR /project + +COPY . /project/ + +RUN npm test \ No newline at end of file diff --git a/server/app.js b/server/app.js new file mode 100644 index 0000000..54cdc44 --- /dev/null +++ b/server/app.js @@ -0,0 +1,23 @@ +require('dotenv').config(); +const express = require('express'); +const path = require('path'); + +const app = express(); +const port = 3001; +const session = require('express-session'); + +app.use(session({ + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.SECURE === 'true', + } +})); + +app.use(express.json()); + +app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); +}); + +module.exports = app; diff --git a/server/dockerignore b/server/dockerignore new file mode 100644 index 0000000..4771c3a --- /dev/null +++ b/server/dockerignore @@ -0,0 +1,4 @@ +Dokckerfile +Dokckerfile.* +node_modules +dockerignore \ No newline at end of file diff --git a/templates/docker-compose-api.yml b/templates/docker-compose-api.yml new file mode 100644 index 0000000..135421b --- /dev/null +++ b/templates/docker-compose-api.yml @@ -0,0 +1,26 @@ +version: "3.9" + +services: + api: + image: "{{ api_image }}" + volumes: + - /var/log/{{ project_name }}:/var/log + env_file: + - "{{ config_dir }}/.env" + environment: + PORT: 3002 + expose: + - 3002 + labels: + traefik.enable: true + traefik.http.routers.{{ project_name }}-client.rule: 'Host(`{{ api_domain }}`)' + traefik.http.routers.{{ project_name }}-client.entryPoints: https + traefik.http.routers.{{ project_name }}-client.tls: true + networks: + - http + restart: always + +networks: + http: + external: true + name: traefik_http diff --git a/templates/docker-compose-portainer.yml b/templates/docker-compose-portainer.yml new file mode 100644 index 0000000..732aef5 --- /dev/null +++ b/templates/docker-compose-portainer.yml @@ -0,0 +1,19 @@ +version: "3.9" + +services: + portainer: + image: portainer/portainer-ce:latest + command: [ "--admin-password=$$2y$$05$$7pyfUY3vUdgTe/CMoV.jT.t01EU71OgqjIu.jd9pQGLEVCW.GFqbO" ] + ports: + - "9000:9000" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer_data:/data + labels: + traefik.enable: 'false' + restart: always + +volumes: + portainer_data: + driver: local + name: portainer_data \ No newline at end of file diff --git a/templates/docker-compose-traefik.yml b/templates/docker-compose-traefik.yml new file mode 100644 index 0000000..653cc7a --- /dev/null +++ b/templates/docker-compose-traefik.yml @@ -0,0 +1,29 @@ +version: "3.9" + +services: + traefik: + image: traefik:v2.11 + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - "{{ config_dir }}/traefik.toml:/etc/traefik/traefik.toml:ro" + - "{{ config_dir }}/traefik-config:/etc/traefik-config:ro" + - "{{ config_dir }}/certs:/certs:ro" + - "/var/run/docker.sock:/var/run/docker.sock:ro" + labels: + traefik.enable: 'true' + traefik.http.routers.dashboard-secure.rule: 'Host(`{{ traefik_dashboard_hostname }}`)' + traefik.http.routers.dashboard-secure.service: 'api@internal' + traefik.http.routers.dashboard-secure.middlewares: 'auth@file' + traefik.http.routers.dashboard-secure.entrypoints: 'https' + traefik.http.routers.dashboard-secure.tls: 'true' + restart: always + networks: + - http + +networks: + http: + external: true + name: traefik_http diff --git a/templates/docker-compose-web.yml b/templates/docker-compose-web.yml new file mode 100644 index 0000000..4571439 --- /dev/null +++ b/templates/docker-compose-web.yml @@ -0,0 +1,40 @@ +version: '3.9' + +services: + client: + image: "{{ client_image }}" + env_file: + - "{{ config_dir }}/.env.local" + labels: + traefik.enable: true + traefik.http.routers.{{ project_name }}-client.rule: '( Host(`{{ web_domain }}`) || Host(`{{ project_domain }}`) )' + traefik.http.routers.{{ project_name }}-client.entryPoints: https + traefik.http.routers.{{ project_name }}-client.tls: true + + networks: + - http + restart: always + + server: + image: "{{ server_image }}" + env_file: + - "{{ config_dir }}/.env" + environment: + PORT: 3001 + CHECK_STATUS_ID: "{{ web_status_id }}" + + expose: + - 3001 + labels: + traefik.enable: true + traefik.http.routers.{{ project_name }}-server.rule: '( Host(`{{ web_domain }}`) || Host(`{{ project_domain }}`) ) && ( PathPrefix(`/api`) || PathPrefix(`/app`) )' + traefik.http.routers.{{ project_name }}-server.entryPoints: https + traefik.http.routers.{{ project_name }}-server.tls: true + networks: + - http + restart: always + +networks: + http: + external: true + name: traefik_http diff --git a/traefik/auth-middleware.yml b/traefik/auth-middleware.yml new file mode 100644 index 0000000..d358e53 --- /dev/null +++ b/traefik/auth-middleware.yml @@ -0,0 +1,6 @@ +http: + middlewares: + auth: + basicauth: + users: + - 'admin:!!!' diff --git a/traefik/certificates.yml b/traefik/certificates.yml new file mode 100644 index 0000000..a084436 --- /dev/null +++ b/traefik/certificates.yml @@ -0,0 +1,26 @@ +tls: + stores: + default: + defaultCertificate: + certFile: /certs/star_example.org/fullchain.cer + keyFile: /certs/star_example.org/star_example.org.key + certificates: + - certFile: /certs/star_example.org/fullchain.cer + keyFile: /certs/star_example.org/star_example.org.key + + stores: + - default + options: + default: + minVersion: VersionTLS12 + alpnProtocols: + - http/1.1 + - h2 + preferServerCipherSuites: true + cipherSuites: + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + diff --git a/traefik/traefik.toml b/traefik/traefik.toml new file mode 100644 index 0000000..988c3e2 --- /dev/null +++ b/traefik/traefik.toml @@ -0,0 +1,23 @@ +[entryPoints.http] + address = ":80" + [entryPoints.http.http.redirections.entryPoint] + to = "https" + scheme = "https" + [entryPoints.https] + address = ":443" + +[http.middleware] + +[api] + dashboard = true + +[providers] +[providers.docker] + useBindPortIP = true + exposedByDefault = false +[providers.file] + directory = "/etc/traefik-config/" + watch = true + debugLogGeneratedTemplate = true + +[accessLog]