{"id":997,"date":"2026-05-29T11:49:31","date_gmt":"2026-05-29T03:49:31","guid":{"rendered":"https:\/\/connectword.dpdns.org\/?p=997"},"modified":"2026-05-29T11:49:31","modified_gmt":"2026-05-29T03:49:31","slug":"how-to-design-an-end-to-end-ansible-automation-lab-with-playbooks-inventories-roles-vault-dynamic-inventory-and-custom-modules","status":"publish","type":"post","link":"https:\/\/connectword.dpdns.org\/?p=997","title":{"rendered":"How to Design an End-to-End Ansible Automation Lab with Playbooks, Inventories, Roles, Vault, Dynamic Inventory, and Custom Modules"},"content":{"rendered":"<p class=\"wp-block-paragraph\">In this tutorial, we build a complete<a href=\"https:\/\/github.com\/ansible\/ansible\"> <strong>Ansible<\/strong><\/a> lab that runs end-to-end in Google Colab or any Linux environment. We start by installing ansible-core, setting up a local workspace, creating an Ansible configuration file, and defining both static and dynamic inventories. We then explore key Ansible concepts, including group variables, host variables, variable precedence, ad hoc commands, playbooks, loops, conditionals, registered outputs, facts, templates, custom filters, custom modules, roles, handlers, tags, dry runs, idempotency, and Ansible Vault. Since every host runs locally, we practice these concepts safely without needing SSH keys, remote servers, or cloud infrastructure.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">import os, sys, subprocess, textwrap, stat\nBASE = \"\/content\/ansible_lab\" if os.path.isdir(\"\/content\") else os.path.expanduser(\"~\/ansible_lab\")\nos.makedirs(BASE, exist_ok=True)\nENV = os.environ.copy()\nENV[\"ANSIBLE_CONFIG\"]      = os.path.join(BASE, \"ansible.cfg\")\nENV[\"ANSIBLE_FORCE_COLOR\"] = \"1\"\nENV[\"PY_COLORS\"]           = \"0\"\ndef banner(title):\n   print(\"n\" + \"=\" * 78 + f\"n  {title}n\" + \"=\" * 78)\ndef write(relpath, content):\n   \"\"\"Write a dedented file under BASE, creating parent dirs.\"\"\"\n   path = os.path.join(BASE, relpath)\n   os.makedirs(os.path.dirname(path), exist_ok=True)\n   with open(path, \"w\") as f:\n       f.write(textwrap.dedent(content).lstrip(\"n\"))\n   return path\ndef sh(cmd, title=None):\n   \"\"\"Run a shell command from BASE, stream stdout, never raise.\"\"\"\n   if title:\n       banner(title)\n   print(f\"$ {cmd}n\")\n   p = subprocess.run(cmd, shell=True, cwd=BASE, env=ENV,\n                      stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n   print(p.stdout)\n   return p.returncode\nbanner(\"STEP 1 \u2014 Installing ansible-core\")\nsubprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"ansible-core\"], check=True)\nsh(\"ansible --version\")\nwrite(\"ansible.cfg\", \"\"\"\n   [defaults]\n   inventory              = .\/inventory.ini\n   roles_path             = .\/roles\n   library                = .\/library\n   filter_plugins         = .\/filter_plugins\n   vault_password_file    = .\/vault_pass.txt\n   host_key_checking      = False\n   retry_files_enabled    = False\n   interpreter_python     = auto_silent\n   callback_result_format = yaml\n   deprecation_warnings   = False\n   localhost_warning      = False\n   nocows                 = 1\n   [privilege_escalation]\n   become = False\n\"\"\")\nwrite(\"inventory.ini\", \"\"\"\n   [webservers]\n   web1 ansible_connection=local\n   web2 ansible_connection=local\n   [dbservers]\n   db1 ansible_connection=local\n   [datacenter:children]\n   webservers\n   dbservers\n\"\"\")<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We start by preparing the Ansible workspace, setting environment variables, and defining helper functions that make the tutorial easier to run. We install ansible-core, verify the installation, and create the main Ansible configuration file. We also define a static inventory with local web and database host groups so that we can practice Ansible concepts without using remote servers.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">write(\"group_vars\/all.yml\", \"\"\"\n   ---\n   app_name: \"Colab Demo App\"\n   app_version: \"2.0.1\"\n   admin_email: \"admin@example.com\"\n   packages:\n     - nginx\n     - git\n     - htop\n   feature_flags:\n     enable_cache: true\n     enable_metrics: false\n\"\"\")\nwrite(\"host_vars\/web1.yml\", \"\"\"\n   ---\n   server_id: 101\n   max_connections: 512\n\"\"\")\nwrite(\"filter_plugins\/custom_filters.py\", '''\n   import re\n   def to_slug(value):\n       return re.sub(r\"[^a-z0-9]+\", \"-\", str(value).lower()).strip(\"-\")\n   def human_bytes(value):\n       n = float(value)\n       for unit in [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"]:\n           if n &lt; 1024:\n               return f\"{n:.1f}{unit}\"\n           n \/= 1024\n       return f\"{n:.1f}PB\"\n   class FilterModule(object):\n       def filters(self):\n           return {\"to_slug\": to_slug, \"human_bytes\": human_bytes}\n''')\nwrite(\"library\/system_report.py\", '''\n   #!\/usr\/bin\/python\n   from ansible.module_utils.basic import AnsibleModule\n   import platform, os\n   def main():\n       module = AnsibleModule(\n           argument_spec=dict(\n               label=dict(type=\"str\", required=True),\n               threshold=dict(type=\"int\", required=False, default=80),\n           ),\n           supports_check_mode=True,\n       )\n       report = {\n           \"label\": module.params[\"label\"],\n           \"system\": platform.system(),\n           \"release\": platform.release(),\n           \"python\": platform.python_version(),\n           \"cpu_count\": os.cpu_count(),\n           \"threshold\": module.params[\"threshold\"],\n       }\n       module.exit_json(changed=False,\n                        report=report,\n                        message=\"Report generated for %s\" % module.params[\"label\"])\n   if __name__ == \"__main__\":\n       main()\n''')<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We define shared group variables and host-specific variables to show how Ansible manages configuration data and applies variable precedence. We then create a custom Jinja2 filter plugin that converts text into slugs and formats byte values into readable units. We also built a custom Python-based Ansible module that generates a simple system report for each host.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">write(\"roles\/webserver\/defaults\/main.yml\", \"\"\"\n   ---\n   listen_port: 8080\n\"\"\")\nwrite(\"roles\/webserver\/vars\/main.yml\", \"\"\"\n   ---\n   doc_root: \"\/tmp\/www\"\n\"\"\")\nwrite(\"roles\/webserver\/tasks\/main.yml\", \"\"\"\n   ---\n   - name: Ensure docroot exists\n     ansible.builtin.file:\n       path: \"{{ doc_root }}\"\n       state: directory\n       mode: \"0755\"\n   - name: Deploy index.html from a Jinja2 template\n     ansible.builtin.template:\n       src: index.html.j2\n       dest: \"{{ doc_root }}\/index.html\"\n     notify: Restart web service\n   - name: Run handlers immediately (instead of end of play)\n     ansible.builtin.meta: flush_handlers\n\"\"\")\nwrite(\"roles\/webserver\/handlers\/main.yml\", \"\"\"\n   ---\n   - name: Restart web service\n     ansible.builtin.debug:\n       msg: \"(simulated) restarting web service on port {{ listen_port }}\"\n\"\"\")\nwrite(\"roles\/webserver\/templates\/index.html.j2\", \"\"\"\n   &lt;!DOCTYPE html&gt;\n   &lt;html&gt;\n     &lt;head&gt;&lt;title&gt;{{ app_name }}&lt;\/title&gt;&lt;\/head&gt;\n     &lt;body&gt;\n       &lt;h1&gt;{{ app_name }} v{{ app_version }}&lt;\/h1&gt;\n       &lt;p&gt;Served on port {{ listen_port }} from {{ doc_root }}&lt;\/p&gt;\n       &lt;p&gt;Host: {{ inventory_hostname }}&lt;\/p&gt;\n     &lt;\/body&gt;\n   &lt;\/html&gt;\n\"\"\")\nwrite(\"templates\/report.txt.j2\", \"\"\"\n   Deployment Report\n   =================\n   App:        {{ app_name }} ({{ app_version }})\n   Host:       {{ inventory_hostname }}\n   Generated:  {{ ansible_date_time.iso8601 | default('n\/a') }}\n   Slug:       {{ app_name | to_slug }}\n   Packages:\n   {% for p in packages %}\n     - {{ p }}\n   {% endfor %}\n   Cache enabled:   {{ feature_flags.enable_cache }}\n   Metrics enabled: {{ feature_flags.enable_metrics }}\n\"\"\")\ndyn = write(\"dynamic_inventory.py\", '''\n   #!\/usr\/bin\/env python3\n   import json, sys\n   INV = {\n       \"webservers\": {\"hosts\": [\"web1\", \"web2\"], \"vars\": {\"role\": \"frontend\"}},\n       \"dbservers\":  {\"hosts\": [\"db1\"],          \"vars\": {\"role\": \"backend\"}},\n       \"_meta\": {\n           \"hostvars\": {\n               \"web1\": {\"ansible_connection\": \"local\", \"tier\": \"gold\"},\n               \"web2\": {\"ansible_connection\": \"local\", \"tier\": \"silver\"},\n               \"db1\":  {\"ansible_connection\": \"local\", \"tier\": \"gold\"},\n           }\n       },\n   }\n   if \"--host\" in sys.argv:\n       print(json.dumps({}))\n   else:\n       print(json.dumps(INV, indent=2))\n''')\nos.chmod(dyn, os.stat(dyn).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We create a complete web server role with defaults, variables, tasks, handlers, and templates to demonstrate how to build reusable Ansible automation. We use Jinja2 templates to generate an HTML page and a deployment report from Ansible variables. We also add a dynamic inventory script that returns host and group information in JSON format.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">write(\"playbook.yml\", \"\"\"\n   ---\n   - name: Advanced concepts demo\n     hosts: webservers\n     gather_facts: true\n     vars:\n       deploy_user: colab\n     tasks:\n       - name: Merged variables (group_vars + host_vars precedence)\n         ansible.builtin.debug:\n           msg: \"App={{ app_name }} v{{ app_version }} | server_id={{ server_id | default('n\/a') }}\"\n       - name: CUSTOM filter -&gt; to_slug\n         ansible.builtin.debug:\n           msg: \"slug =&gt; {{ app_name | to_slug }}\"\n       - name: CUSTOM filter -&gt; human_bytes\n         ansible.builtin.debug:\n           msg: \"size =&gt; {{ 1536000 | human_bytes }}\"\n       - name: LOOP with an index variable\n         ansible.builtin.debug:\n           msg: \"package #{{ idx + 1 }} = {{ item }}\"\n         loop: \"{{ packages }}\"\n         loop_control:\n           index_var: idx\n       - name: CONDITIONAL (when) \u2014 only if caching is enabled\n         ansible.builtin.debug:\n           msg: \"cache is ON\"\n         when: feature_flags.enable_cache | bool\n       - name: Run a command and REGISTER its output\n         ansible.builtin.command: date +%Y-%m-%d\n         register: date_out\n         changed_when: false\n       - name: SET a derived fact from the registered value\n         ansible.builtin.set_fact:\n           deploy_stamp: \"{{ app_name | to_slug }}-{{ date_out.stdout }}\"\n       - name: Show the derived fact\n         ansible.builtin.debug:\n           var: deploy_stamp\n       - name: Run our CUSTOM MODULE (system_report)\n         system_report:\n           label: \"{{ inventory_hostname }}\"\n           threshold: 90\n         register: sysrep\n       - name: Show custom module output\n         ansible.builtin.debug:\n           var: sysrep.report\n       - name: BLOCK with rescue\/always (error handling)\n         block:\n           - name: This fails on purpose\n             ansible.builtin.command: \/bin\/false\n           - name: Never reached\n             ansible.builtin.debug:\n               msg: \"unreachable\"\n         rescue:\n           - name: Recover gracefully\n             ansible.builtin.debug:\n               msg: \"caught the failure \u2014 recovering\"\n         always:\n           - name: Always run cleanup\n             ansible.builtin.debug:\n               msg: \"cleanup runs no matter what\"\n       - name: Use a VAULT-encrypted secret (decrypted at runtime)\n         ansible.builtin.debug:\n           msg: \"token prefix={{ api_secret_token[:3] }}*** len={{ api_secret_token | length }}\"\n       - name: TEMPLATE a report file (tagged 'report')\n         ansible.builtin.template:\n           src: report.txt.j2\n           dest: \"\/tmp\/{{ inventory_hostname }}_report.txt\"\n         tags: [report]\n   - name: Role demo\n     hosts: web1\n     gather_facts: false\n     roles:\n       - role: webserver\n\"\"\")<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We write the main playbook that brings together variables, custom filters, loops, conditionals, registered outputs, derived facts, and a custom module. We intentionally include a failing command to demonstrate error handling through block, rescue, and always. We also use a Vault-encrypted secret and apply the web server role to demonstrate how role-based automation works in a real workflow.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">banner(\"STEP 2 \u2014 Ansible Vault: encrypting an inline secret\")\nwrite(\"vault_pass.txt\", \"colab-demo-vault-passn\")\nenc = subprocess.run(\n   \"ansible-vault encrypt_string 'S3cr3t-Token-42' --name 'api_secret_token'\",\n   shell=True, cwd=BASE, env=ENV, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True\n).stdout\nwith open(os.path.join(BASE, \"group_vars\/webservers.yml\"), \"w\") as f:\n   f.write(\"---n\")\n   f.write(enc)\nprint(\"group_vars\/webservers.yml now contains:n\")\nprint(open(os.path.join(BASE, \"group_vars\/webservers.yml\")).read())\nsh(\"ansible-inventory -i inventory.ini --graph\",      \"STEP 3 \u2014 Static inventory graph\")\nsh(\"ansible-inventory -i dynamic_inventory.py --list\", \"STEP 4 \u2014 Dynamic inventory (JSON)\")\nsh(\"ansible all -m ping\",                 \"STEP 5 \u2014 Ad-hoc: ping all hosts\")\nsh(\"ansible web1 -m setup -a 'filter=ansible_python_version'\",\n  \"STEP 6 \u2014 Ad-hoc: gather a single fact\")<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We create a Vault password file and encrypt an inline secret that Ansible decrypts automatically when the playbook runs. We inspect both the static and dynamic inventories to understand how Ansible reads hosts, groups, and metadata. We then run ad-hoc commands to ping all hosts and gather a specific Python version fact from web1.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">sh(\"ansible-playbook playbook.yml --check --diff\", \"STEP 7 \u2014 Dry run (--check)\")\nsh(\"ansible-playbook playbook.yml\",                \"STEP 8 \u2014 Real run\")\nsh(\"ansible-playbook playbook.yml\",                \"STEP 9 \u2014 Re-run (idempotency: expect 0 changed)\")\nsh(\"ansible-playbook playbook.yml --tags report\",  \"STEP 10 \u2014 Run only tasks tagged 'report'\")\nsh(\"echo '--- \/tmp\/www\/index.html ---'; cat \/tmp\/www\/index.html; \"\n  \"echo; echo '--- \/tmp\/web1_report.txt ---'; cat \/tmp\/web1_report.txt\",\n  \"STEP 11 \u2014 Generated files\")\nsh('ansible webservers --limit web1 -m debug -a \"var=api_secret_token\"',\n  \"STEP 12a \u2014 Inline vault secret decrypted at runtime\")\nwrite(\"secrets.yml\", \"\"\"\n   ---\n   db_password: full-file-secret-99\n   api_key: abc123\n\"\"\")\nsh(\"ansible-vault encrypt secrets.yml\",        \"STEP 12b \u2014 Encrypt a WHOLE file\")\nsh(\"head -c 60 secrets.yml; echo ' ...'\")\nsh(\"ansible-vault view secrets.yml\",           \"STEP 12c \u2014 View the fully-encrypted file\")\nbanner(\"DONE \u2014 you now have a working advanced Ansible lab in Colab\")\nprint(f\"Workspace: {BASE}nEdit any file there and re-run a step with the sh() helper.\")<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We run the playbook in check mode, execute it for real, and rerun it to confirm that the workflow is idempotent. We use tags to run only the report-related task and then inspect the generated HTML and text report files. We also demonstrate full-file Vault encryption, safely view the encrypted file, and complete the advanced Ansible lab.<\/p>\n<p class=\"wp-block-paragraph\">In conclusion, we have a working Ansible lab that demonstrates how automation workflows are structured and executed in real projects. We created reusable roles, generated files from Jinja2 templates, ran custom Python-based Ansible modules, handled errors with rescue and always blocks, encrypted secrets with Ansible Vault, and validated our setup through dry runs and repeated idempotent executions. We also learned how static and dynamic inventories work, how tags help us run selected tasks, and how Ansible organizes infrastructure automation in a clean, repeatable, and production-friendly way.<\/p>\n<p class=\"wp-block-paragraph\">\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<\/p><p class=\"wp-block-paragraph\">\n<\/p><p class=\"wp-block-paragraph\">Check out\u00a0the\u00a0<strong><a href=\"https:\/\/github.com\/Marktechpost\/AI-Agents-Projects-Tutorials\/blob\/main\/Distributed%20Systems\/end_to_end_ansible_automation_lab_playbooks_roles_vault_custom_modules_Marktechpost.ipynb\" target=\"_blank\" rel=\"noreferrer noopener\">Full Codes with Notebook here<\/a>.\u00a0<\/strong>Also,\u00a0feel free to follow us on\u00a0<strong><a href=\"https:\/\/x.com\/intent\/follow?screen_name=marktechpost\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Twitter<\/mark><\/a><\/strong>\u00a0and don\u2019t forget to join our\u00a0<strong><a href=\"https:\/\/www.reddit.com\/r\/machinelearningnews\/\" target=\"_blank\" rel=\"noreferrer noopener\">150k+ ML SubReddit<\/a><\/strong>\u00a0and Subscribe to\u00a0<strong><a href=\"https:\/\/www.aidevsignals.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">our Newsletter<\/a><\/strong>. Wait! are you on telegram?\u00a0<strong><a href=\"https:\/\/t.me\/machinelearningresearchnews\" target=\"_blank\" rel=\"noreferrer noopener\">now you can join us on telegram as well.<\/a><\/strong><\/p>\n<p class=\"wp-block-paragraph\">Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.?\u00a0<strong><a href=\"https:\/\/forms.gle\/wbash1wF6efRj8G58\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Connect with us<\/mark><\/a><\/strong><\/p>\n<p>The post <a href=\"https:\/\/www.marktechpost.com\/2026\/05\/28\/how-to-design-an-end-to-end-ansible-automation-lab-with-playbooks-inventories-roles-vault-dynamic-inventory-and-custom-modules\/\">How to Design an End-to-End Ansible Automation Lab with Playbooks, Inventories, Roles, Vault, Dynamic Inventory, and Custom Modules<\/a> appeared first on <a href=\"https:\/\/www.marktechpost.com\/\">MarkTechPost<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we build a c&hellip;<\/p>\n","protected":false},"author":1,"featured_media":29,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-997","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts\/997","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=997"}],"version-history":[{"count":0,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts\/997\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/media\/29"}],"wp:attachment":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=997"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=997"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=997"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}