{"id":51507954,"url":"https://github.com/explicit-logic/ansible-module-15.1","last_synced_at":"2026-07-08T02:30:43.164Z","repository":{"id":362768756,"uuid":"1259698282","full_name":"explicit-logic/ansible-module-15.1","owner":"explicit-logic","description":"Automate Node.js application Deployment","archived":false,"fork":false,"pushed_at":"2026-06-05T19:39:17.000Z","size":1141,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T21:16:48.774Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","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/explicit-logic.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":"2026-06-04T19:10:45.000Z","updated_at":"2026-06-05T19:39:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/explicit-logic/ansible-module-15.1","commit_stats":null,"previous_names":["explicit-logic/ansible-module-15.1"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/explicit-logic/ansible-module-15.1","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fansible-module-15.1","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fansible-module-15.1/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fansible-module-15.1/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fansible-module-15.1/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/explicit-logic","download_url":"https://codeload.github.com/explicit-logic/ansible-module-15.1/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/explicit-logic%2Fansible-module-15.1/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35249883,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-08T02:00:06.796Z","response_time":61,"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":[],"created_at":"2026-07-08T02:30:42.426Z","updated_at":"2026-07-08T02:30:43.158Z","avatar_url":"https://github.com/explicit-logic.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Module 15 - Configuration Management with Ansible\n\nThis repository contains a demo project created as part of my **DevOps studies** in the [TechWorld with Nana – DevOps Bootcamp](https://www.techworld-with-nana.com/devops-bootcamp).\n\n**Demo Project:** Automate Node.js application Deployment\n\n**Technologies used:** Ansible, Node.js, DigitalOcean, Linux\n\n**Project Description:**\n\n- Create Server on DigitalOcean\n- Write Ansible Playbook that installs necessary technologies, creates Linux user for an application and deploys a NodeJS application with that user\n\n---\n\n## Prerequisites\n\n- **Ansible** installed on your local control machine. Ansible runs *from here* — it connects to the server over SSH; nothing is installed on the target ahead of time.\n\nmacOS (Homebrew):\n```sh\nbrew install ansible\n```\n\nWith pip (any OS with Python ≥ 3.9):\n```sh\npip install ansible\n```\n\nVerify the install and see which Python interpreter Ansible uses:\n```sh\nansible --version\n```\n\n- **Node.js \u0026 npm** on your local machine, used to build the application package with `npm pack`.\n- An **SSH key pair** (e.g. `~/.ssh/id_rsa`) whose public key is added to the droplet.\n- The **`community.general`** collection, which provides the `npm` module used by this playbook:\n```sh\nansible-galaxy collection install community.general\n```\n\n\u003e ℹ️ `apt`, `user`, `unarchive`, `command`, and `shell` ship with `ansible-core`. The `npm` module lives in the separate `community.general` collection, so it must be installed before running the playbook.\n\n---\n\n### Overview\n\n![](./images/overview.png)\n\n\n### Create Server on DigitalOcean\n\nDroplet configuration:\n\n| Setting     | Value   |\n| ----------- | ------- |\n| Image       | Ubuntu  |\n| CPU Option  | Regular |\n| vCPU        | 1       |\n| RAM         | 1 GB    |\n| Disk        | 25 GB   |\n\n![](./images/create-droplet.png)\n\n\u003e Ansible is **agentless** — it manages the server purely over SSH. The only requirement on the target is a Python interpreter, which Ubuntu ships with by default. Confirm the path (e.g. `/usr/bin/python3.12`) and set it as `ansible_python_interpreter` in your inventory.\n\n- Copy the public IP address of the new droplet.\n\n\n### Write the Ansible Playbook\n\nThe playbook installs the required technologies, creates a dedicated Linux user for the application, and deploys the Node.js app as that user. See [deploy-node.yaml](deploy-node.yaml) for the full playbook.\n\n#### 1. Create the inventory (`hosts`) file\n\n```sh\ncp hosts.example hosts\n```\n\n```conf\nwebserver ansible_host=\u003cDROPLET-IP\u003e ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_rsa ansible_python_interpreter=/usr/bin/python3.12\n```\n\n| Variable                       | Purpose                                            |\n| ------------------------------ | -------------------------------------------------- |\n| `ansible_host`                 | Public IP of the droplet                           |\n| `ansible_user`                 | SSH user Ansible connects as (`root`)              |\n| `ansible_ssh_private_key_file` | Private key matching the key added to the droplet  |\n| `ansible_python_interpreter`   | Python on the target used to run modules           |\n\n\u003e Host key checking is disabled in [ansible.cfg](ansible.cfg) (`host_key_checking = False`) so the first connection to a fresh droplet doesn't prompt for fingerprint confirmation.\n\n#### 2. Externalize values in `project-vars`\n\nShared values live in a [project-vars](project-vars) file and are pulled into each play with `vars_files`, keeping the playbook DRY:\n\n```yaml\nlocation: nodejs-app\nversion: 1.0.0\nuser_name: app-user\ndestination: \"/home/{{user_name}}\"\n```\n\n#### 3. Install Node.js and npm — `apt` module\n\n```yaml\n- name: Install node and npm\n  hosts: webserver\n  tasks:\n    - name: Update apt repo and cache\n      apt: update_cache=yes force_apt_get=yes cache_valid_time=3600\n    - name: Install nodejs and npm\n      apt:\n        pkg:\n          - nodejs\n          - npm\n```\n\n\u003e 📘 [`apt` module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_module.html)\n\u003e - `update_cache=yes` runs `apt-get update` before installing (refreshes the package lists).\n\u003e - `cache_valid_time=3600` skips that refresh if the cache is younger than 3600 s, avoiding redundant updates on repeated runs.\n\u003e - `force_apt_get=yes` forces `apt-get` instead of `aptitude`.\n\u003e - Passing a **list** to `pkg` installs all packages in a single transaction — much more efficient than looping the module once per package.\n\n#### 4. Build \u0026 copy the app — `npm pack` + `unarchive` module\n\nBuild the deployable tarball from the `nodejs-app` directory:\n\n```sh\ncd nodejs-app\nnpm i\nnpm pack\n```\n\n`npm pack` produces `nodejs-app-1.0.0.tgz` — the exact artifact npm would publish.\n\n```yaml\n- name: Unpack the nodejs tar file\n  unarchive:\n    src: \"{{location}}/nodejs-app-{{version}}.tgz\"\n    dest: \"{{destination}}\"\n```\n\n\u003e 📘 [`unarchive` module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/unarchive_module.html)\n\u003e - With the default `remote_src: no`, the archive is taken from the **control machine** and copied to the target before being unpacked. Use `remote_src: yes` when the archive already exists on the server (or is a URL).\n\u003e - `dest` must already exist on the target.\n\u003e - Requires `tar`/`unzip` on the target host.\n\u003e - Add `creates:` to make it idempotent — the unpack is skipped if the given path already exists.\n\nRun the playbook:\n```sh\nansible-playbook -i hosts deploy-node.yaml\n```\n\n![](./images/deploy-node-1.png)\n\nCheck the result on the server:\n\n![](./images/check-server.png)\n\n\n#### 5. Install dependencies \u0026 start the app — `npm` + `command` modules\n\n```yaml\n- name: Install dependencies\n  npm:\n    path: \"{{destination}}/package\"\n\n- name: Start the application\n  command:\n    chdir: \"{{destination}}/package/app\"\n    cmd: node server\n  async: 1000\n  poll: 0\n```\n\n\u003e 📘 [`npm` module](https://docs.ansible.com/ansible/latest/collections/community/general/npm_module.html) (in `community.general`)\n\u003e - With `path` set and no `name`, it installs the dependencies declared in that directory's `package.json` (equivalent to running `npm install` there).\n\n\u003e 📘 [`command` module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/command_module.html)\n\u003e - `chdir` changes into the directory before executing; `cmd` is the command to run.\n\u003e - `command` does **not** run through a shell, so `|`, `\u003e`, `\u003c`, `;`, `\u0026`, and `*` are **not** interpreted. Prefer it over `shell` when you don't need those features — it's more secure.\n\u003e - It is **not idempotent**: it runs every time unless guarded with `creates:`/`removes:`.\n\n\u003e ⚡ **Why `async` + `poll: 0`?** `node server` is a long-running process that never returns. Without async, the task would block forever and the playbook would hang. [Asynchronous \"fire-and-forget\"](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_async.html) starts the process and moves on:\n\u003e - `async: 1000` — allow up to 1000 s of runtime before Ansible would terminate it.\n\u003e - `poll: 0` — don't wait for the result; start it and continue to the next task.\n\nRun again:\n```sh\nansible-playbook -i hosts deploy-node.yaml\n```\n\n#### 6. Verify the app is running — `shell` module\n\n```yaml\n- name: Ensure app is running\n  shell: ps aux | grep node\n  register: app_status\n- debug: msg={{app_status.stdout_lines}}\n```\n\n\u003e The `shell` module is used here (not `command`) precisely because the task uses a **pipe** (`|`). `register` captures the task's output into `app_status`, and `debug` prints it — a handy pattern for inspecting results during a run.\n\nCheck Node status on the server directly:\n```sh\nps aux | grep node\n```\n![](./images/ps-node-server.png)\n\n![](./images/app-status.png)\n\n\n#### 7. Create a dedicated Linux user — `user` module + privilege escalation\n\nRunning the app under its own unprivileged user (`app-user`) instead of `root` is a security best practice.\n\n```yaml\n- name: Create new linux user for node app\n  hosts: webserver\n  vars_files:\n    project-vars\n  tasks:\n    - name: Create linux user\n      user:\n        name: \"{{user_name}}\"\n        comment: App User\n        group: admin\n      register: user_creation_result\n    - debug: msg={{user_creation_result}}\n```\n\n\u003e 📘 [`user` module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/user_module.html)\n\u003e - `group` sets the user's **primary** group and must reference a group that already exists; `groups` (with `append: yes`) would set **supplementary** groups instead.\n\u003e - On Ubuntu the group that grants sudo rights is typically `sudo` — adjust `group`/`groups` to match the access the app user actually needs.\n\nDeploy the app **as that user** by adding `become` parameters to the deploy play:\n\n```yaml\n- name: Deploy nodejs app\n  hosts: webserver\n  become: True\n  become_user: \"{{user_name}}\"\n  vars_files:\n    project-vars\n  tasks:\n    ...\n```\n\n\u003e 📘 [Privilege escalation (`become`)](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_privilege_escalation.html)\n\u003e - `become: true` activates privilege escalation; `become_user` selects the target user (defaults to `root`). Note that setting `become_user` alone does **not** imply `become: true`.\n\u003e - Here we connect as `root` and then *drop down* to `app-user`. This is the recommended pattern — escalating from `root` to an unprivileged user avoids the temp-file permission issues Ansible runs into when **both** the connection user and `become_user` are unprivileged.\n\nRun the full playbook:\n```sh\nansible-playbook -i hosts deploy-node.yaml\n```\n\n![](./images/become-user.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexplicit-logic%2Fansible-module-15.1","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexplicit-logic%2Fansible-module-15.1","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexplicit-logic%2Fansible-module-15.1/lists"}