├── .ansible-lint ├── .github └── workflows │ ├── issue_template_broken_link.md │ ├── make_docs.yml │ ├── make_preview_docs.yml │ ├── test_docs.yml │ ├── test_html_links.yml │ └── test_markdown_links.yml ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTE.adoc ├── Makefile ├── README.adoc ├── _style └── render.adoc ├── coding_style └── README.adoc ├── collections └── README.adoc ├── images ├── ansible_structures.plantuml └── variable_precedences.plantuml ├── inventories ├── README.adoc ├── inventory_example │ ├── dynamic_inventory_plugin.yml │ ├── dynamic_inventory_script.py │ ├── group_vars │ │ ├── alephs │ │ │ └── capital_letter.yml │ │ ├── all │ │ │ └── ansible.yml │ │ ├── alphas │ │ │ ├── capital_letter.yml │ │ │ └── small_caps_letter.yml │ │ ├── betas │ │ │ └── capital_letter.yml │ │ ├── greek_letters │ │ │ └── small_caps_letter.yml │ │ └── hebrew_letters │ │ │ └── small_caps_letter.yml │ ├── groups_and_hosts │ └── host_vars │ │ ├── host1.example.com │ │ └── ansible.yml │ │ ├── host2.example.com │ │ └── ansible.yml │ │ └── host3.example.com │ │ ├── ansible.yml │ │ └── capital_letter.yml ├── inventory_loop_hosts │ ├── inventory_bad │ │ ├── group_vars │ │ │ └── all │ │ │ │ └── ansible.yml │ │ ├── groups_and_hosts │ │ └── host_vars │ │ │ ├── host1 │ │ │ └── provision.yml │ │ │ ├── host2 │ │ │ └── provision.yml │ │ │ ├── host3 │ │ │ └── provision.yml │ │ │ ├── manager_a │ │ │ └── provision.yml │ │ │ └── manager_b │ │ │ └── provision.yml │ ├── inventory_good │ │ ├── group_vars │ │ │ ├── all │ │ │ │ └── ansible.yml │ │ │ ├── managed_hosts_a │ │ │ │ └── provision.yml │ │ │ └── managed_hosts_b │ │ │ │ └── provision.yml │ │ ├── groups_and_hosts │ │ └── host_vars │ │ │ ├── host1 │ │ │ └── provision.yml │ │ │ ├── host2 │ │ │ └── provision.yml │ │ │ └── host3 │ │ │ └── provision.yml │ ├── inventory_not_so_bad │ │ ├── group_vars │ │ │ └── all │ │ │ │ └── ansible.yml │ │ ├── groups_and_hosts │ │ └── host_vars │ │ │ ├── host1 │ │ │ └── provision.yml │ │ │ ├── host2 │ │ │ └── provision.yml │ │ │ ├── host3 │ │ │ └── provision.yml │ │ │ ├── manager_a │ │ │ └── provision.yml │ │ │ └── manager_b │ │ │ └── provision.yml │ ├── playbook_bad.yml │ ├── playbook_good.yml │ └── playbook_not_so_bad.yml └── inventory_satellite │ ├── groups_and_hosts │ └── host_vars │ └── sat6.example.com │ ├── ansible.yml │ └── satellite │ ├── content_views.yml │ ├── hostgroups.yml │ └── locations.yml ├── playbooks ├── README.adoc └── playbook_role_tags │ ├── playbook_import.yml │ ├── playbook_include.yml │ └── roles │ ├── role1 │ └── tasks │ │ └── main.yml │ ├── role2 │ └── tasks │ │ └── main.yml │ └── role3 │ └── tasks │ └── main.yml ├── plugins └── README.adoc ├── roles ├── README.adoc ├── dont_use_groups │ ├── inventory │ ├── playbook.yml │ └── playbook2.yml └── prefix_subtasks │ ├── playbook.yml │ └── roles │ └── prefix_show │ └── tasks │ ├── main.yml │ ├── sub_noprefix.yml │ └── sub_prefix.yml └── structures └── README.adoc /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | # .ansible-lint 3 | 4 | profile: production # min, basic, moderate,safety, shared, production 5 | 6 | # Allows dumping of results in SARIF format 7 | # sarif_file: result.sarif 8 | 9 | # exclude_paths included in this file are parsed relative to this file's location 10 | # and not relative to the CWD of execution. CLI arguments passed to the --exclude 11 | # option are parsed relative to the CWD of execution. 12 | exclude_paths: 13 | - .github/ 14 | - .vscode/ 15 | - changelogs/ 16 | - images/ 17 | 18 | # parseable: true 19 | # quiet: true 20 | # strict: true 21 | # verbosity: 1 22 | 23 | # Mock modules or roles in order to pass ansible-playbook --syntax-check 24 | mock_modules: 25 | - community.vmware.vmware_guest_snapshot 26 | 27 | # mock_roles: 28 | # - mocked_role 29 | # - author.role_name # old standalone galaxy role 30 | # - fake_namespace.fake_collection.fake_role # role within a collection 31 | 32 | # Enable checking of loop variable prefixes in roles 33 | loop_var_prefix: ^(__|{role}_) 34 | 35 | # Enforce variable names to follow pattern below, in addition to Ansible own 36 | # requirements, like avoiding python identifiers. To disable add `var-naming` 37 | # to skip_list. 38 | var_naming_pattern: ^[a-z_][a-z0-9_]*$ 39 | 40 | use_default_rules: true 41 | # Load custom rules from this specific folder 42 | # rulesdir: 43 | # - ./rule/directory/ 44 | 45 | # Ansible-lint is able to recognize and load skip rules stored inside 46 | # `.ansible-lint-ignore` (or `.config/ansible-lint-ignore.txt`) files. 47 | # To skip a rule just enter filename and tag, like "playbook.yml package-latest" 48 | # on a new line. 49 | # Optionally you can add comments after the tag, prefixed by "#". We discourage 50 | # the use of skip_list below because that will hide violations from the output. 51 | # When putting ignores inside the ignore file, they are marked as ignored, but 52 | # still visible, making it easier to address later. 53 | skip_list: 54 | - yaml[colons] # Violations reported by yamllint. 55 | - yaml[line-length] # Violations reported by yamllint. 56 | - var-naming 57 | 58 | # Ansible-lint does not automatically load rules that have the 'opt-in' tag. 59 | # You must enable opt-in rules by listing each rule 'id' below. 60 | enable_list: 61 | - args 62 | - empty-string-compare # opt-in 63 | - no-log-password # opt-in 64 | - no-same-owner # opt-in 65 | - name[prefix] # opt-in 66 | # add yaml here if you want to avoid ignoring yaml checks when yamllint 67 | # library is missing. Normally its absence just skips using that rule. 68 | - yaml 69 | # Report only a subset of tags and fully ignore any others 70 | # tags: 71 | # - jinja[spacing] 72 | 73 | # Ansible-lint does not fail on warnings from the rules or tags listed below 74 | warn_list: 75 | - experimental # experimental is included in the implicit list 76 | - git-latest # Allow for newest git version 77 | - package-latest # Allow newest package version 78 | - risky-file-permissions # File permissions unset or incorrect. 79 | - template-instead-of-copy # Templated files should use template instead of copy 80 | - sanity[cannot-ignore] # cope with shebang test bug 81 | 82 | # Some rules can transform files to fix (or make it easier to fix) identified 83 | # errors. `ansible-lint --fix` will reformat YAML files and run these transforms. 84 | # By default it will run all transforms (effectively `write_list: ["all"]`). 85 | # You can disable running transforms by setting `write_list: ["none"]`. 86 | # Or only enable a subset of rule transforms by listing rules/tags here. 87 | # write_list: 88 | # - all 89 | 90 | # Offline mode disables installation of requirements.yml and schema refreshing 91 | offline: false 92 | 93 | # Define required Ansible's variables to satisfy syntax check 94 | # extra_vars: 95 | # foo: bar 96 | # multiline_string_variable: | 97 | # line1 98 | # line2 99 | # complex_variable: ":{;\t$()" 100 | 101 | # Uncomment to enforce action validation with tasks, usually is not 102 | # needed as Ansible syntax check also covers it. 103 | # skip_action_validation: false 104 | 105 | # List of additional kind:pattern to be added at the top of the default 106 | # match list, first match determines the file kind. 107 | kinds: 108 | # - playbook: "**/examples/*.{yml,yaml}" 109 | # - galaxy: "**/folder/galaxy.yml" 110 | # - tasks: "**/tasks/*.yml" 111 | # - vars: "**/vars/*.yml" 112 | # - meta: "**/meta/main.yml" 113 | - yaml: "**/*.yaml-too" 114 | 115 | # List of additional collections to allow in only-builtins rule. 116 | # only_builtins_allow_collections: 117 | # - example_ns.example_collection 118 | 119 | # List of additions modules to allow in only-builtins rule. 120 | # only_builtins_allow_modules: 121 | # - example_module 122 | 123 | # Allow setting custom prefix for name[prefix] rule 124 | task_name_prefix: "{stem} | " 125 | # Complexity related settings 126 | 127 | # Limit the depth of the nested blocks: 128 | # max_block_depth: 20 129 | ... 130 | -------------------------------------------------------------------------------- /.github/workflows/issue_template_broken_link.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Website Contains Broken Links 3 | labels: Bug 4 | --- 5 | 6 | ## Broken Links Detected 7 | 8 | Broken Link Checker found broken links on https://redhat-cop.github.io/automation-good-practices/ 9 | 10 | [View Results](https://github.com/redhat-cop/automation-good-practices/actions/workflows/test_html_links.yml) 11 | 12 | _Use search filter `─BROKEN─` to highlight failures_ 13 | -------------------------------------------------------------------------------- /.github/workflows/make_docs.yml: -------------------------------------------------------------------------------- 1 | name: publish document 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | container: fedora:latest 13 | 14 | steps: 15 | - name: Install requirements 16 | run: sudo dnf install -y git graphviz make plantuml rubygem-asciidoctor rubygem-asciidoctor-pdf rubygem-rouge 17 | 18 | - name: Repository checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Move to the correct folder 22 | run: cd $GITHUB_WORKSPACE 23 | 24 | - name: Ensure git folder is considered safe 25 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 26 | 27 | - name: Render HTML 28 | run: make release 29 | 30 | - name: Git add and force push to docs 31 | run: git add -f docs 32 | 33 | - name: Push changes 34 | uses: stefanzweifel/git-auto-commit-action@v4 35 | with: 36 | commit_message: publish release 37 | file_pattern: docs/* 38 | add_options: '-A --force' 39 | branch: docs # main branch is protected, make sure this one is used for GitHub pages 40 | # the following options are necessary to forcefully overwrite each time the branch 41 | skip_fetch: true 42 | skip_checkout: true 43 | push_options: '--force' 44 | -------------------------------------------------------------------------------- /.github/workflows/make_preview_docs.yml: -------------------------------------------------------------------------------- 1 | name: create preview renders 2 | 3 | on: 4 | push: 5 | branches: 6 | - '!main' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install requirements 17 | run: sudo apt-get install asciidoctor ruby-asciidoctor-pdf 18 | 19 | - name: Install requirements 20 | run: cd $GITHUB_WORKSPACE 21 | 22 | - name: render preview 23 | run: make preview 24 | 25 | - uses: actions/upload-artifact@v3 26 | with: 27 | name: preview-pdf 28 | path: docs/preview/*.pdf 29 | 30 | - uses: actions/upload-artifact@v3 31 | with: 32 | name: preview-html 33 | path: docs/preview/*.html 34 | -------------------------------------------------------------------------------- /.github/workflows/test_docs.yml: -------------------------------------------------------------------------------- 1 | name: test document generation 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test-build-docs: 8 | 9 | runs-on: ubuntu-latest 10 | container: fedora:latest 11 | 12 | steps: 13 | - name: Install requirements 14 | run: sudo dnf install -y git graphviz make plantuml rubygem-asciidoctor rubygem-asciidoctor-pdf rubygem-rouge 15 | 16 | - name: Repository checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Move to the correct folder 20 | run: cd $GITHUB_WORKSPACE 21 | 22 | - name: Ensure git folder is considered safe 23 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 24 | 25 | - name: Render HTML 26 | run: make release 27 | -------------------------------------------------------------------------------- /.github/workflows/test_html_links.yml: -------------------------------------------------------------------------------- 1 | #Check the rendered page for dead links 2 | name: Broken Links Checker 3 | permissions: 4 | contents: read 5 | issues: write 6 | on: 7 | schedule: 8 | - cron: '0 16 * * *' 9 | workflow_dispatch: 10 | env: 11 | WEBSITE_URL: "https://redhat-cop.github.io/automation-good-practices/" 12 | ISSUE_TEMPLATE: ".github/workflows/issue_template_broken_link.md" 13 | 14 | jobs: 15 | check_links: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Run Broken Links Checker 20 | run: npx broken-link-checker $WEBSITE_URL --ordered --recursive --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0" --exclude "https://opensource.org/licenses/BSD-2-Clause" --exclude "github.com" 21 | 22 | - uses: actions/checkout@v3 23 | if: failure() 24 | 25 | - uses: JasonEtco/create-an-issue@v2 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | with: 29 | filename: ${{ env.ISSUE_TEMPLATE }} 30 | if: failure() 31 | -------------------------------------------------------------------------------- /.github/workflows/test_markdown_links.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action checks all Markdown files in the repository for broken links. 3 | # (Uses https://github.com/tcort/markdown-link-check) 4 | name: markdown link check 5 | 6 | 7 | on: 8 | push: 9 | pull_request: 10 | 11 | jobs: 12 | markdown-link-check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: gaurav-nelson/github-action-markdown-link-check@v1 17 | with: 18 | use-quiet-mode: 'yes' 19 | use-verbose-mode: 'yes' 20 | config-file: '.mlc_config.json' 21 | ... 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # generated files 2 | *.html 3 | *.pdf 4 | *.png 5 | *.svg 6 | docs 7 | 8 | # temporary files 9 | .*.swp 10 | *~ 11 | 12 | # my own files, only for local usage 13 | *[_.]myown[_.]* 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /CONTRIBUTE.adoc: -------------------------------------------------------------------------------- 1 | = Contribution guidelines 2 | include::_style/render.adoc[] 3 | 4 | Before you suggest _automation_ guidelines, please consider the _contribution_ guidelines layed out in this document. 5 | 6 | == Writing 7 | 8 | . The guidelines are written in https://asciidoctor.org[asciidoc as described by Asciidoctor]. 9 | . each guideline is made of one sentence, as easy to remember as possible, followed by a collapsible description, made of: 10 | ** explanations 11 | ** rationale 12 | ** examples 13 | + 14 | The result looks then as the following template shows (you may copy & paste): 15 | + 16 | [source,asciidoc] 17 | ------------------------------------------------------------------------ 18 | == Do this and do not do that is the guideline 19 | [%collapsible] 20 | ==== 21 | Explanations:: These are explanations 22 | 23 | Rationale:: This is the rationale 24 | 25 | Examples:: These are examples 26 | + 27 | .A mini playbook example 28 | [source,yaml] 29 | ---- 30 | --- 31 | - name: A mini example of playbook 32 | hosts: all 33 | gather_facts: false 34 | become: false 35 | tasks: 36 | - name: Say what we all think 37 | ansible.builtin.debug: 38 | msg: asciidoctor is {{ my_private_thoughts }} 39 | ---- 40 | + 41 | Even more examples... 42 | ==== 43 | ------------------------------------------------------------------------ 44 | // maintain the code above in sync with the example below 45 | + 46 | NOTE: see how it looks like in the <<_example>> section below. 47 | + 48 | . Those guidelines are grouped into sections and optionally sub-sections, as far as required for maintainability. 49 | . Those (sub-)sections can be written in their own source file, but then are included with `include::directory/file.adoc[leveloffset=1]` in the parent section's file. 50 | This makes sure that all source files are interlinked and can be rendered all together by rendering the top `README.adoc`, either with `asciidoctor` or with `asciidoctor-pdf`. 51 | + 52 | NOTE: this contribution file is obviously not meant for inclusion in the overall document. 53 | + 54 | . Each source file has a single title (the line starting with one equal sign) and can be rendered individually (the `leveloffset` is set such that it fits in the overall headings structure when included). 55 | . The source code is written as readable as possible in its raw form, without impacting maintainability. 56 | . We follow the https://asciidoctor.org/docs/asciidoc-recommended-practices/[Asciidoc recommended practices]. 57 | . Sentences are written in the present tense form, avoid "should", "must", etc. 58 | For example, "Sentences are written", not "Sentences should be written" or "Sentences must be written". This avoids filler words. 59 | . The https://en.wikipedia.org/wiki/Singular_they[singular "they"] is used to avoid the unreadable "he/she/it" construct and still be neutral. 60 | 61 | == Contributing 62 | 63 | . Just fork the repository, create a Pull Request (PR) and offer your changes. 64 | + 65 | TIP: limiting each PR to a single recommendation makes it easier to review. 66 | This furthermore speeds up approval, which is an advantage also for contributors. 67 | 68 | . Feel free to review existing PR and give your opinion 69 | . Also an issue against one of the recommendations is a valid approach 70 | 71 | == Example 72 | 73 | This is how one guideline as shown above looks like once rendered: 74 | 75 | // This is a duplicate from the above code, but rendered e.g. by GitHub, it shows how it's supposed to look like. 76 | 77 | === Do this and do not do that is the guideline 78 | [%collapsible] 79 | ==== 80 | Explanations:: These are explanations 81 | 82 | Rationale:: This is the rationale 83 | 84 | Examples:: These are examples 85 | + 86 | .A mini playbook example 87 | [source,yaml] 88 | ---- 89 | - name: a mini example of playbook 90 | hosts: all 91 | gather_facts: false 92 | become: false 93 | 94 | tasks: 95 | 96 | - name: say what we all think 97 | debug: 98 | msg: asciidoctor is {{ my_private_thoughts }} 99 | ---- 100 | + 101 | Even more examples... 102 | ==== 103 | 104 | == Publish for the website 105 | 106 | Use for now the following manual command to publish to 107 | link:++https://redhat-cop.github.io/automation-good-practices/++[the website]: 108 | 109 | [source,bash] 110 | ---- 111 | asciidoctor -a toc=left -D docs -o index.html README.adoc 112 | asciidoctor -a toc=left -D docs CONTRIBUTE.adoc 113 | mkdir -p docs/images 114 | cp -v images/*.svg docs/images 115 | ---- 116 | 117 | NOTE: it doesn't seem that there is any much better way to keep links to images correct according to the https://docs.asciidoctor.org/asciidoctor/latest/html-backend/manage-images/[HTML generation / managed images] chapter. 118 | 119 | == Creating a PDF 120 | 121 | If you run (a current) Fedora Linux, 122 | then you can use the `Makefile`. 123 | 124 | * `make view` generates both PDF files and displays the GPA guide 125 | * `make print` prints to your default printer 126 | * `make spell` runs hunspell for spellchecking 127 | * … 128 | 129 | Alternatively, use the following manual commands to generate the 2 PDFs: 130 | 131 | [source,bash] 132 | ---- 133 | asciidoctor-pdf \ 134 | --attribute=gitdate=$(git log -1 --date=short --pretty=format:%cd) \ 135 | --attribute=githash=$(git rev-parse --verify HEAD) \ 136 | --out-file Good_Practices_for_Ansible.pdf \ 137 | README.adoc 138 | asciidoctor-pdf \ 139 | --attribute=gitdate=$(git log -1 --date=short --pretty=format:%cd) \ 140 | --attribute=githash=$(git rev-parse --verify HEAD) \ 141 | --out-file Contributing-to-GPA.pdf \ 142 | CONTRIBUTE.adoc 143 | ---- 144 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # be sure to have the following RPMs installed on Fedora Linux 2 | # 3 | # okular 4 | # asciidoctor-pdf 5 | # rubygem-rugged 6 | # hunspell 7 | # hunspell-en-GB 8 | # rubygem-ffi ? 9 | # rubygem-json ? 10 | 11 | ADOCPDF = asciidoctor-pdf --attribute=gitdate=$(shell git log -1 --date=short --pretty=format:%cd) --attribute=githash=$(shell git rev-parse --verify HEAD) --failure-level=warn 12 | ADOCHTML = asciidoctor -a toc=left --attribute=gitdate=$(shell git log -1 --date=short --pretty=format:%cd) --attribute=githash=$(shell git rev-parse --verify HEAD) --failure-level=warn 13 | ACROREAD = okular 14 | VCS = git 15 | INFILE = README.adoc 16 | INFILE2 = CONTRIBUTE.adoc 17 | OUTFILE = Good_Practices_for_Ansible 18 | OUTFILE2 = Contributing-to-GPA 19 | PRINT = lpr 20 | SPELL = hunspell 21 | SPELLOPTS = -d en_GB 22 | 23 | all: $(OUTFILE) 24 | 25 | $(OUTFILE): $(INFILE) *.adoc */*.adoc _images/* Makefile .git/index 26 | $(ADOCPDF) --out-file $(OUTFILE).pdf $(INFILE) 27 | $(ADOCPDF) --out-file $(OUTFILE2).pdf $(INFILE2) 28 | 29 | view: viewpdf 30 | 31 | print: $(OUTFILE) 32 | $(PRINT) $(OUTFILE).pdf 33 | 34 | viewpdf: $(OUTFILE) 35 | $(ACROREAD) $(OUTFILE).pdf 36 | 37 | clean: 38 | rm -f $(OUTFILE).pdf 39 | rm -f $(OUTFILE2).pdf 40 | rm -rf .AppleDouble 41 | 42 | spell: 43 | $(SPELL) $(SPELLOPTS) *.adoc */*.adoc 44 | 45 | commit: clean 46 | $(VCS) commit . 47 | 48 | push: clean 49 | $(VCS) push 50 | 51 | pull: 52 | $(VCS) pull 53 | 54 | plantuml: 55 | for f in images/*.plantuml; do \ 56 | plantuml $${f} -tsvg; \ 57 | done 58 | 59 | release: plantuml 60 | mkdir -p docs 61 | $(ADOCHTML) -D docs --out-file index.html $(INFILE) 62 | $(ADOCHTML) -D docs --out-file CONTRIBUTE.html $(INFILE2) 63 | mkdir -p docs/images 64 | cp -v images/*.svg docs/images 65 | 66 | preview: 67 | mkdir -p docs 68 | $(ADOCPDF) --out-file docs/preview/$(OUTFILE).pdf $(INFILE) 69 | $(ADOCPDF) --out-file docs/preview/$(OUTFILE2).pdf $(INFILE2) 70 | $(ADOCHTML) --out-file docs/preview/$(OUTFILE).html $(INFILE) 71 | $(ADOCHTML) --out-file docs/preview/$(OUTFILE2).html $(INFILE2) 72 | mkdir -p docs/preview/images 73 | cp -v images/*.svg docs/preview/images 74 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Good Practices for Ansible - GPA 2 | include::_style/render.adoc[] 3 | 4 | == Introduction 5 | 6 | https://ansible.com/[Ansible] is simple, flexible, and powerful. Like any powerful tool, there are many ways to use it, some better than others. 7 | 8 | This document aims to gather good practices from the field of Ansible practitioners at Red Hat, consultants, developers, and others. 9 | And thus it strives to give any Red Hat employee, partner or customer (or any Ansible user) a guideline from which to start in good conditions their automation journey. 10 | 11 | Those are opinionated guidelines based on the experience of many people. 12 | They are not meant to be followed blindly if they don't fit the reader's specific use case, organization or needs; 13 | there is a reason why they are called _good_ and not _best_ practices. 14 | 15 | The reader of this document is expected to have working practice of Ansible. 16 | If they are new to Ansible, the https://docs.ansible.com/ansible/latest/user_guide/index.html#getting-started[Getting started] section of 17 | the https://docs.ansible.com/[official Ansible documentation] is a better place to start. 18 | 19 | This document is split in six main sections. 20 | Each section covers a different aspect of automation using Ansible (and in a broader term the whole https://www.redhat.com/en/technologies/management/ansible[Red Hat Ansible Automation Platform], including Ansible Tower): 21 | 22 | . structures: we need to know what to use for which purpose before we can delve into the details, this section explains this. 23 | . roles: as we recommend to use roles to host the most actual Ansible code, this is also where we'll cover the more low level aspects of code (tasks, variables, etc...). 24 | . collections 25 | . playbooks 26 | . inventories 27 | . plugins 28 | 29 | // TODO add a few more words about the content of each section once we know 30 | // what we write in there... 31 | 32 | Each section is then made of guidelines, one sentence hopefully easy to remember, followed by description, rationale and examples. 33 | The HTML version of this document makes the content collapsable so that all guidelines can be seen at once in a very overseeable way, for the reader to uncollapse the content of guidelines they are interested in. 34 | 35 | A rationale is expected for each good practice, with a reference if applicable. 36 | It is really helpful to know not only how to do certain things, but why to do them in this way. 37 | It will also help with further revisions of the standards as some items may become obsolete or no longer applicable. 38 | If the reason is not included, there is a risk of keeping items that are no longer applicable, or alternatively blindly removing items that should be kept. 39 | It also has great educational value for understanding how things actually work (or how they don't). 40 | 41 | // If you're a potential author check the CONTRIBUTE.adoc document before... contributing. 42 | 43 | === Where to get and maintain this document 44 | 45 | This document is published to https://redhat-cop.github.io/automation-good-practices/, it is open source and its source code is maintained at https://github.com/redhat-cop/automation-good-practices/. 46 | 47 | include::structures/README.adoc[leveloffset=1] 48 | 49 | include::roles/README.adoc[leveloffset=1] 50 | 51 | include::collections/README.adoc[leveloffset=1] 52 | 53 | include::playbooks/README.adoc[leveloffset=1] 54 | 55 | include::inventories/README.adoc[leveloffset=1] 56 | 57 | include::plugins/README.adoc[leveloffset=1] 58 | 59 | include::coding_style/README.adoc[leveloffset=1] 60 | -------------------------------------------------------------------------------- /_style/render.adoc: -------------------------------------------------------------------------------- 1 | :doctype: book 2 | :toc: auto 3 | :toclevels: 4 4 | :sectnumlevels: 6 5 | :numbered: 6 | :chapter-label: 7 | :icons: font 8 | :pdf-page-size: A4 9 | :source-highlighter: rouge 10 | :rouge-style: github 11 | :listing-caption: Listing 12 | :imagesdir: images/ 13 | 14 | :revnumber: {gitdate} (commit: {githash}) 15 | :!last-update-label: 16 | 17 | // The following lines could become relevant in the future 18 | 19 | //// 20 | :pdf-style: redhat 21 | :pdf-stylesdir: _styles/pdf/ 22 | :pdf-fontsdir: fonts/ 23 | 24 | ifdef::backend-pdf[] 25 | :autofit-option: 26 | endif::[] 27 | //// 28 | -------------------------------------------------------------------------------- /coding_style/README.adoc: -------------------------------------------------------------------------------- 1 | = Coding Style Good Practices for Ansible 2 | 3 | It has proven useful to agree on certain guiding principles as early as possible in any automation project. 4 | Doing so makes it much easier to onboard new Ansible developers. 5 | Project guidelines can also be shared with other departments working on automation which in turn improves the re-usability of playbooks, roles, modules, and documentation. 6 | 7 | Another major benefit is that it makes code review process less time-consuming and more reliable; making both the developer and reviewer more likely to engage in a constructive review conversation. 8 | 9 | This section contains suggestions for such coding-style guidelines. 10 | The list is neither complete nor are all of the guidelines necessary in every automation project. 11 | Experience shows that it makes sense to start with a minimum set of guidelines because the longer the list the lower the chance of people actually reading through it. 12 | Additional guidelines can always be added later should the situation warrant it. 13 | 14 | == Naming things 15 | 16 | * Use valid Python identifiers following standard naming conventions of being in `snake_case_naming_schemes` for all YAML or Python files, variables, arguments, repositories, and other such names (like dictionary keys). 17 | * Do not use special characters other than underscore in variable names, even if YAML/JSON allow them. 18 | + 19 | [%collapsible] 20 | ==== 21 | Explanation:: Using such variables in Jinja2 or Python would be then very confusing and probably not functional. 22 | Rationale:: even when Ansible currently allows names that are not valid identifier, it may stop allowing them in the future, as it happened in the past already. 23 | Making all names valid identifiers will avoid encountering problems in the future. Dictionary keys that are not valid identifiers are also less intuitive to use in Jinja2 (a dot in a dictionary key would be particularly confusing). 24 | ==== 25 | 26 | * Use mnemonic and descriptive names that are human-readable and do not shorten more than necessary. 27 | A pattern `object[_feature]_action` has proven useful as it guarantees a proper sorting in the file system for roles and playbooks. 28 | Systems support long identifier names, so use them! 29 | * Avoid numbering roles and playbooks, you'll never know how they'll be used in the future. 30 | * Name all tasks, plays, and task blocks to improve readability. 31 | * Write task names in the imperative (e.g. "Ensure service is running"), this communicates the action of the task. 32 | * Avoid abbreviations in names, or use capital letter for abbreviations where it cannot be avoided. 33 | 34 | == YAML and Jinja2 Syntax 35 | 36 | * Indent at two spaces 37 | * Indent list contents beyond the list definition 38 | + 39 | [%collapsible] 40 | ==== 41 | .Do this: 42 | [source,yaml] 43 | ---- 44 | example_list: 45 | - example_element_1 46 | - example_element_2 47 | - example_element_3 48 | - example_element_4 49 | ---- 50 | 51 | .Don't do this: 52 | [source,yaml] 53 | ---- 54 | example_list: 55 | - example_element_1 56 | - example_element_2 57 | - example_element_3 58 | - example_element_4 59 | ---- 60 | ==== 61 | 62 | * Split long expressions into multiple lines. 63 | + 64 | [%collapsible] 65 | ==== 66 | Rationale:: Long lines are difficult to read, and many teams even ask for a line length limit around 120-150 characters. 67 | ansible-lint defaults to 82 characters per line - see (https://ansible-lint.readthedocs.io/rules/yaml/[Ansible Lint YAML rules]). 68 | You don't want to have to skip `yaml[line-length]` in your `.ansible-lint`, or litter your code with `# noqa yaml[line-length]`. 69 | Examples:: there are multiple ways to avoid long lines but the most generic one is to use the YAML folding sign (`>-`): 70 | + 71 | .Usage of the YAML folding sign 72 | [source,yaml] 73 | ---- 74 | - name: Call a very long command line 75 | ansible.builtin.command: >- 76 | echo Lorem ipsum dolor sit amet, consectetur adipiscing elit. 77 | Maecenas mollis, ante in cursus congue, mauris orci tincidunt nulla, 78 | non gravida tortor mi non nunc. 79 | 80 | - name: Set a very long variable 81 | ansible.builtin.set_fact: 82 | meaningless_variable: >- 83 | Ut ac neque sit amet turpis ullamcorper auctor. 84 | Cras placerat dolor non ipsum posuere malesuada at ac ipsum. 85 | Duis a neque fermentum nulla imperdiet blandit. 86 | ---- 87 | + 88 | TIP: Always use the sign `>-` instead of `>` unless you are absolutely sure the trailing newline is not significant. 89 | The sign `>` adds a newline character to the last line, effectively turning `non nunc.` at the end of the example string above into `"non nunc.\n"`, and `>-` doesn't add the newline character. 90 | It is really easy to introduce an error by using `>` and silently add a newline to a variable, like a filename, which leads to strange, hard to decipher errors. 91 | See section "Wrap longer lines of code" for more information about line wrapping. 92 | ==== 93 | 94 | * If the `when:` condition results in a line that is too long, and is an `and` expression, then break it into a list of conditions. 95 | + 96 | [%collapsible] 97 | ==== 98 | Rationale:: Ansible will `and` the list elements together (https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html#basic-conditionals-with-when[Ansible UseGuide » Conditionals]). 99 | Multiple conditions that all need to be true (a logical `and`) can also be specified as a list, but beware of bare variables in `when:`. 100 | Examples:: 101 | + 102 | .Do this 103 | [source,yaml] 104 | ---- 105 | when: 106 | - myvar is defined 107 | - myvar | bool 108 | ---- 109 | + 110 | .instead of this 111 | [source,yaml] 112 | ---- 113 | when: myvar is defined and myvar | bool 114 | ---- 115 | ==== 116 | 117 | * All roles need to, minimally, pass a basic ansible-playbook syntax check run 118 | * Spell out all task arguments in YAML style and do not use `key=value` type of arguments 119 | + 120 | [%collapsible] 121 | ==== 122 | .Do this: 123 | [source,yaml] 124 | ---- 125 | tasks: 126 | - name: Print a message 127 | ansible.builtin.debug: 128 | msg: This is how it's done. 129 | ---- 130 | 131 | .Don't do this: 132 | [source,yaml] 133 | ---- 134 | tasks: 135 | - name: Print a message 136 | ansible.builtin.debug: msg="This is the exact opposite of how it's done." 137 | ---- 138 | ==== 139 | 140 | * Use `true` and `false` for boolean values in playbooks. 141 | + 142 | [%collapsible] 143 | ==== 144 | Explanation:: Do not use the Ansible-specific `yes` and `no` as boolean values in YAML as these are completely custom extensions used by Ansible and are not part of the YAML spec and also avoid the use of the Python-style `True` and `False` for boolean values in playbooks. 145 | 146 | Rationale:: https://yaml.org/type/bool.html[YAML 1.1] allows all variants whereas https://yaml.org/spec/1.2/spec.html#id2803629[YAML 1.2] allows only true/false, and we want to be ready for when it becomes the default, and avoid a massive migration effort. 147 | ==== 148 | 149 | * Avoid comments in playbooks when possible. 150 | Instead, ensure that the task `name` value is descriptive enough to tell what a task does. 151 | Variables are commented in the `defaults` and `vars` directories and, therefore, do not need explanation in the playbooks themselves. 152 | * Use a single space separating the template markers from the variable name inside all Jinja2 template points. 153 | For instance, always write it as `{{ variable_name_here }}`. 154 | The same goes if the value is an expression. `{{ variable_name | default('hiya, doc') }}` 155 | * When naming files, use the `.yml` extension and _not_ `.yaml`. 156 | `.yml` is what `ansible-galaxy init` does when creating a new role template. 157 | * Use double quotes for YAML strings with the exception of Jinja2 strings which will use single quotes. 158 | * Do not use quotes unless you have to, especially for short module-keyword-like strings like `present`, `absent`, etc. 159 | But do use quotes for user-side strings such as descriptions, names, and messages. 160 | * Even if JSON is valid YAML and Ansible understands it, do only use JSON syntax if it makes sense (e.g. a variable file automatically generated) or adds to the readability. 161 | In doubt, nobody expects JSON so stick to YAML. 162 | * Break up lengthy Jinja templates into multiple templates when there are distinct logical sections. 163 | + 164 | [%collapsible] 165 | ==== 166 | Rationale:: Long and complex Jinja templates can be difficult to maintain and debug. By splitting excessively long templates into logical componets that can be included as-needed, each template will be easier to maintain. 167 | ==== 168 | 169 | * Jinja templates should not be used to create structured data but instead text and semi-structured data. Filter plugins are preferred over Jinja templates for the use of data manipulation or transformation. 170 | + 171 | [%collapsible] 172 | ==== 173 | Rationale:: When working with structured data or data transformations it is preferable to use a programming language (such as Python) that has better support and tooling to do this kind of work. 174 | Custom filter plugins can be written to handle complex or unique use-cases. 175 | Tasks will be much more legible if data is managed and manipulated via plugins than with in-line Jinja. 176 | ==== 177 | 178 | == Ansible Guidelines 179 | 180 | * Ensure that all tasks are idempotent. 181 | * https://github.com/ansible/ansible/issues/10374[Ansible variables use lazy evaluation.] 182 | * Prefer the command module over the shell module unless you explicitly need shell functionality such as, e.g., piping. 183 | Even better, use a dedicated module, if it exists. 184 | If not, see the <> about idempotency and check mode and make sure that your task is idempotent and supports check mode properly; 185 | your task will likely need options such as `changed_when:` and maybe `check_mode:`). 186 | * Anytime `command` or `shell` modules are used, add a comment in the code with justification to help with future maintenance. 187 | * Use the `| bool` filter when using bare variables (expressions consisting of just one variable reference without any operator) in `when`. 188 | * Break complex task files down into discrete parts. 189 | + 190 | [%collapsible] 191 | ==== 192 | Rationale:: 193 | Task files that are very or and/or contain highly nested blocks are difficult to maintain. 194 | Breaking a large or complex task file into multiple discrete files makes it easier to read and understand what is being done in each part. 195 | ==== 196 | 197 | * Use bracket notation instead of dot notation for value retrieval (e.g. `item['key']` vs. `item.key`) 198 | + 199 | [%collapsible] 200 | ==== 201 | Rationale:: 202 | Dot notation will fail in some cases (such as when a variable name includes a hyphen) and it's better to stay consistent than to switch between the two options within a role or playbook. 203 | Additionally, some key names collide with attributes and methods of Python dictionaries such as `count`, `copy`, `title`, and others (refer to the https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#referencing-key-value-dictionary-variables[Ansible User Guide] for an extended list) 204 | 205 | Example:: 206 | This https://blog.networktocode.com/post/Exploring-Jinja-Variable-Syntax-in-Ansible[post] provides an excellent demonstration of how using dot notation syntax can impact your playbooks. 207 | ==== 208 | 209 | * Do not use `meta: end_play`. 210 | + 211 | [%collapsible] 212 | ==== 213 | Rationale:: It aborts the whole play instead of a given host (with multiple hosts in the inventory). 214 | If absolutely necessary, consider using `meta: end_host`. 215 | ==== 216 | 217 | * Task names can be made dynamic by using variables wrapped in Jinja2 templates at the end of the string 218 | + 219 | [%collapsible] 220 | ==== 221 | Rationale:: This can help with reading the logs. 222 | For example, if the task is managing one of several devices, and you want the task name output to show the device being managed. 223 | However, the template must come at the *end* of the string - see (https://ansible-lint.readthedocs.io/rules/name/[Ansible Lint name template rule]). 224 | Note that in some cases, it can make it harder for users to correlate the logs to the code. 225 | For example, if there is a log message like "Manage the disk device /dev/dsk/0001", and the user tries to do something like `grep "Manage the disk device /dev/dsk/0001" rolename/tasks/*.yml` to figure out which task this comes from, they will not find it. 226 | If the template comes at the end of the string, the user will know to omit the device name from `grep`. 227 | A better way to debug is to use `ansible-playbook -vv`, which will show the exact file and line number of the task. 228 | 229 | Example:: 230 | .Do this: 231 | [source,yaml] 232 | ---- 233 | tasks: 234 | - name: Manage the disk device {{ storage_device_name }} 235 | some.module: 236 | device: "{{ storage_device_name }}" 237 | ---- 238 | 239 | .Don't do this: 240 | [source,yaml] 241 | ---- 242 | tasks: 243 | - name: Manage {{ storage_device_name }}, the disk device 244 | some.module: 245 | device: "{{ storage_device_name }}" 246 | ---- 247 | ==== 248 | 249 | * Do not use variables (wrapped in Jinja2 templates) for play names; variables don't get expanded properly there. 250 | The same applies to loop variables (by default `item`) in task names within a loop. 251 | They, too, don't get properly expanded and hence are not to be used there. 252 | * Do not override role defaults or vars or input parameters using `set_fact`. 253 | Use a different name instead. 254 | + 255 | [%collapsible] 256 | ==== 257 | Rationale:: a fact set using `set_fact` can not be unset and it will override the role default or role variable in all subsequent invocations of the role in the same playbook. 258 | A fact has a different priority than other variables and not the highest, so in some cases overriding a given parameter will not work because the parameter has a higher priority (https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable[Ansible User Guide » Using Variables]) 259 | ==== 260 | 261 | * Use the smallest scope for variables. 262 | Facts are global for playbook run, so it is preferable to use other types of variables. Therefore limit (preferably avoid) the use of `set_fact`. 263 | Role variables are exposed to the whole play when the role is applied using `roles:` or `import_role:`. A more restricted scope such as task or block variables is preferred. 264 | * Beware of `ignore_errors: true`; especially in tests. 265 | If you set on a block, it will ignore all the asserts in the block ultimately making them pointless. 266 | * Do not use the `eq`, `equalto`, or `==` Jinja tests introduced in Jinja 2.10, use Ansible built-in `match`, `search`, or `regex` instead. 267 | + 268 | [%collapsible] 269 | ==== 270 | Explanation:: The issue is only with Jinja versions older than 2.10. 271 | RPM distributions of Ansible generally use the underlying OS platform python library for Jinja e.g. python-jinja2. 272 | This is especially problematic on EL7. 273 | The only supported Ansible RPM on that platform is 2.9, which uses the EL7 platform python-jinja2 library, which is 2.7 (and will likely never be upgraded). 274 | As of mid-2022, there are many users using EL7 for the control node. 275 | I believe this means AAP 1.x users will also be affected. 276 | Users not affected: 277 | * AAP 2.x users - there should be an option to use EL8 runners, or otherwise, build the EEs in such a way as to use Jinja 2.11 or later 278 | * Users running Ansible from a pip install 279 | * Users running Ansible installed via RPM on EL8 or later 280 | Rationale:: These tests are not present in versions of Jinja older than 2.10, which are used on older controller platforms, such as EL7. 281 | If you want to ensure that your code works on older platforms, use the built-in Ansible tests such as (https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html#testing-strings[match]), (https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html#testing-strings[search]), or (https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html#testing-strings[regex]) instead. 282 | Example:: 283 | You have a `list` of `dict`, and you want to filter out elements that have the key `type` with the value `bad_type`. 284 | 285 | .Do this: 286 | [source,yaml] 287 | ---- 288 | tasks: 289 | - name: Do something 290 | some.module: 291 | param: "{{ list_of_dict | rejectattr('type', 'search', '^bad_type$') | list }}" 292 | ---- 293 | 294 | .Don't do this: 295 | [source,yaml] 296 | ---- 297 | tasks: 298 | - name: Do something 299 | some.module: 300 | param: "{{ list_of_dict | rejectattr('type', 'eq', 'bad_type') | list }}" 301 | ---- 302 | When using `match`, `search`, or `regex`, and you want an exact match, you must specify the regex `^STRING$`, otherwise, you will match partial strings. 303 | ==== 304 | 305 | * Avoid the use of `when: foo_result is changed` whenever possible. 306 | Use handlers, and, if necessary, handler chains to achieve this same result. 307 | * Use the various include/import statements in Ansible. 308 | + 309 | [%collapsible] 310 | ==== 311 | Explanation:: Doing so can lead to simplified code and a reduction of repetition. 312 | This is the closest that Ansible comes to callable sub-routines, so use judgment about callable routines to know when to similarly include a sub playbook. 313 | Some examples of good times to do so are 314 | * When a set of multiple commands share a single `when` conditional 315 | * When a set of multiple commands are being looped together over a list of items 316 | * When a single large role is doing many complicated tasks and cannot easily be broken into multiple roles, but the process proceeds in multiple related stages 317 | ==== 318 | 319 | * Avoid calling the `package` module iteratively with the `{{ item }}` argument, as this is impressively more slow than calling it with the line `name: "{{ foo_packages }}"`. 320 | The same can go for many other modules that can be given an entire list of items all at once. 321 | * Use meta modules when possible. 322 | + 323 | [%collapsible] 324 | ==== 325 | Rationale:: This will allow our playbooks to run on the widest selection of operating systems possible without having to modify any more tasks than is necessary. 326 | Examples:: 327 | * Instead of using the `upstart` and `systemd` modules, use the `service` 328 | module when at all possible. 329 | * Similarly for package management, use `package` instead of `yum` or `dnf` or 330 | similar. 331 | ==== 332 | 333 | * Avoid the use of `lineinfile` wherever that might be feasible. 334 | + 335 | [%collapsible] 336 | ==== 337 | Rationale:: Slight miscalculations in how it is used can lead to a loss of idempotence. 338 | Modifying config files with it can cause the Ansible code to become arcane and difficult to read, especially for someone not familiar with the file in question. 339 | Try editing files directly using other built-in modules (e.g. `ini_file`, `blockinfile`, `xml`), or reading and parsing. 340 | If you are modifying more than a tiny number of lines or in a manner more than trivially complex, try leveraging the `template` module, instead. 341 | This will allow the entire structure of the file to be seen by later users and maintainers. 342 | The use of `lineinfile` should include a comment with justification. 343 | Alternatively, most configuration files have their own modules, such as https://docs.ansible.com/ansible/latest/collections/community/general/ssh_config_module.html[community.general.ssh_config] or https://docs.ansible.com/ansible/latest/collections/community/general/nmcli_module.html[community.general.nmcli]. 344 | Using these make code cleaner to read and ensure idempotence. 345 | ==== 346 | 347 | * Limit use of the `copy` module to copying remote files, static files, and to uploading binary blobs. 348 | For most file pushes, use the `template` module. 349 | Even if there currently is nothing in the file that is being templated, if there is the possibility in the future that it might be added, having the file handled by the `template` module now makes adding that functionality much simpler than if the file is initially handled by the `copy` module and then needs to be moved before it can be edited. 350 | * When using the `template` module, append `.j2` to the template file name. 351 | + 352 | [%collapsible] 353 | ==== 354 | Example:: If you want to use the `ansible.builtin.template` module to create a file called `example.conf` somewhere on the managed host, name the template for this file `templates/example.conf.j2`. 355 | Rationale:: When you are at the stage of writing a template file you usually already know how the file should end up looking on the file system, so at that point it is convenient to use Jinja2 syntax highlighting to make sure your templating syntax checks out. 356 | Should you need syntax highlighting for whatever language the target file should be in, it is very easy to define in your editor settings to use, e.g., HTML syntax highlighting for all files ending in `.html.j2`. 357 | It is much less straightforward to automatically enable Jinja2 syntax highlighting for _some_ files ending on `.html`. 358 | ==== 359 | 360 | * Keep filenames and templates as close to the name on the destination system as possible. 361 | + 362 | [%collapsible] 363 | ==== 364 | Rationale:: This will help with both editor highlighting as well as identifying source and destination versions of the file at a glance. 365 | Avoid duplicating the remote full path in the role directory, however, as that creates unnecessary depth in the file tree for the role. 366 | Grouping sets of similar files into a subdirectory of `templates` is allowable, but avoid unnecessary depth to the hierarchy. 367 | ==== 368 | 369 | * Using agnostic modules like `package` only makes sense if the features required are very limited. 370 | In many cases, if the platform is different, the package name is also different so that using `package` doesn't help a lot. 371 | Prefer then the more specific `yum`, `dnf` or `apt` module if you anyway need to differentiate. 372 | 373 | * Use `float`, `int`, and `bool` filters to "cast" public API variables to ensure type safety, especially for numeric operations in Jinja. 374 | + 375 | [%collapsible] 376 | ==== 377 | Example:: Variables set by users in the public API are not guaranteed to be any specific data type, and may be `str` type when some numeric type is expected: 378 | ``` 379 | > ansible -c local -i localhost --extra-vars int_val=1 localhost -m debug -a "msg={{ int_val < 0 }}" 380 | localhost | FAILED! => { 381 | "msg": "Unexpected templating type error occurred on ({{ int_val < 0 }}): '<' not supported between instances of 'str' and 'int'" 382 | } 383 | ``` 384 | 385 | Rationale:: It is generally not possible to guarantee that all user inputs retain their desired numeric type, and if not, will likely be `str` type. 386 | If you use numeric variables where the value comes from user input, use the `float`, `int`, and `bool` filters to "cast" the values to the type for numeric operations. 387 | If you are simply converting the value to a string, you do not have to use the cast. 388 | Numeric operations include: 389 | 390 | * arithmetic: `int_var + 3`, `float_var * 3.14159` 391 | * comparison: `int_var == 0`, `float_var >= 2.71828` 392 | * unary: `-int_var`, `+float_var` 393 | 394 | Here are some examples: 395 | ``` 396 | > ansible -c local -i localhost --extra-vars int_val=1 localhost -m debug -a "msg={{ int_val | int < 0 }}" 397 | localhost | SUCCESS => { 398 | "msg": false 399 | } 400 | 401 | > ansible -c local -i localhost -e float_val=0.5 localhost -m debug -a "msg='float_val is less than 1.0 {{ float_val | float + 0.1 < 1.0 }}'" 402 | localhost | SUCCESS => { 403 | "msg": "float_val is less than 1.0 True" 404 | } 405 | 406 | ``` 407 | ==== 408 | 409 | == Wrap longer lines of code 410 | 411 | ansible-lint has a pretty short line length, which causes problems if you are trying to use good programming practices by having descriptive variable names, which usually end up being quite long. 412 | Here are some examples of how to deal with line wrapping in common scenarios: 413 | 414 | * Jinja expressions can be wrapped. 415 | Within the Jinja expression, whitespace and newline characters aren't significant, so take advantage of this to wrap lines into as readable a form as possible. 416 | Remember, in a `when`, `that`, `failed_when`, or other such keywords, you can just write Jinja code - you do not need the `"{{ ... }}"` 417 | 418 | * Start an expression with '{{' followed by newline if the line will otherwise be too long. 419 | But what if the code is already indented a lot, and the variable I'm assigning to is already very long, and I can't put anything else on the line? 420 | Just start the assignment on the next line. 421 | 422 | * Use backslash escapes in double quoted strings. 423 | But what if I have a very long string that I cannot use `>-` to wrap because I cannot have extra spaces in the value e.g. like a url value? 424 | Use a backslash escape in a double quoted string. 425 | YAML will concatenate the values with no spaces. 426 | 427 | [%collapsible] 428 | ==== 429 | Rationale:: Use of whitespace and multi-line indentation makes expressions easier to read. 430 | 431 | .Do this: 432 | [source,yaml] 433 | ---- 434 | - name: Wrap long Jinja expressions 435 | foo: "{{ a_very.long_variable.name | 436 | somefilter('with', 'many', 'arguments') | 437 | another_filter | list }}" 438 | when: a_very.long_variable.name | 439 | somefilter('with', 'many', 'arguments') | 440 | another_filter | list 441 | 442 | - name: Wrap when first line is already too long 443 | very_indented_foo: "{{ 444 | a_very.long_variable.name | 445 | somefilter('with', 'many', 'arguments') | 446 | another_filter | list }}" 447 | when: \ 448 | a_very.long_variable.name | 449 | somefilter('with', 'many', 'arguments') | 450 | another_filter | list | length > 0 451 | 452 | - name: Set some test variables 453 | set_fact: 454 | my_very_long_variable_1: "{{ __pre_digest | filter1 }}" 455 | my_very_long_variable_2: "{{ __pre_digest | filter2 }}" 456 | vars: 457 | __pre_digest: "{{ a_very.long_variable.name | some_filter }}" 458 | 459 | - name: Use a very long URL 460 | uri: 461 | url: "https://{{ my_very_long_value_for_hostname }}:\ 462 | {{ my_very_long_value_for_port }}\ 463 | {{ my_very_long_value_for_uri }}?\ 464 | {{ my_very_long_value_for_query }}" 465 | 466 | ---- 467 | 468 | .Don't do this: 469 | [source,yaml] 470 | ---- 471 | - name: Very long line with Jinja expression 472 | foo: "{{ a_very.long_variable.name | somefilter('with', 'many', 'arguments') | another_filter | list }}" 473 | 474 | - name: First line is already too long 475 | very_indented_foo: "{{ a_very.long_variable.name | 476 | somefilter('with', 'many', 'arguments') | 477 | another_filter | list }}" 478 | when: a_very.long_variable.name | 479 | somefilter('with', 'many', 'arguments') | 480 | another_filter | list | length > 0 481 | 482 | - name: Redundancy in expressions 483 | set_fact: 484 | my_very_long_variable_1: "{{ a_very.long_variable.name | some_filter | filter1 }}" 485 | my_very_long_variable_2: "{{ a_very.long_variable.name | some_filter | filter2 }}" 486 | 487 | - name: URL string is too long 488 | uri: 489 | url: "https://{{ my_very_long_value_for_hostname }}:{{ my_very_long_value_for_port }}{{ my_very_long_value_for_uri }}{{ my_very_long_value_for_query }}" 490 | ---- 491 | ==== 492 | -------------------------------------------------------------------------------- /collections/README.adoc: -------------------------------------------------------------------------------- 1 | = Collections good practices 2 | 3 | Note: Unreviewed work. Please contribute to the discussion in the Automation Red Hat COP 4 | 5 | == Collection Structure should be at the type or landscape level 6 | [%collapsible] 7 | ==== 8 | Explanations:: Collections should be comprised of roles collected either at the type or landscape level. See <> 9 | 10 | Rationale:: Gathering and publishing collections, rather than individual roles, allows for easier distribution and particularly becomes more important when we discuss Execution Environments. 11 | 12 | ==== 13 | 14 | == Create implicit collection variables and reference them in your roles' defaults variables 15 | [%collapsible] 16 | ==== 17 | Explanations:: Often, variables will want to be defined on a collection level, but this can cause issues with roles being able to be reused. 18 | By defining collection wide variables and referencing them in roles' defaults variables, this can be made clear and roles can remain reusable. 19 | Collection variables are nowhere defined explicitly and are to be documented in the collection's documentation. 20 | 21 | Rationale:: Variables that are shared across collections can cause collisions when roles are reused outside of the original collection. 22 | Role variables should continue to be named according to our <> 23 | It still remains possible to overwrite collection variable values for a specific role. 24 | Each role has it's own set of defaults for the variable. 25 | 26 | Examples:: 27 | For a collection "mycollection", two roles exist. "alpha" and "beta". For this example, there is no default for the controller_username 28 | and would have to be defined in one's inventory. The no_log variable does have defaults defined, and thus only needs to be defined if the default 29 | is being overwritten. 30 | + 31 | .Alpha defaults/main.yml 32 | [source,yaml] 33 | ---- 34 | # specific role variables 35 | alpha_job_name: 'some text' 36 | # collection wide variables 37 | alpha_controller_username: "{{ mycollection_controller_username }}" 38 | alpha_no_log: "{{ mycollection_no_log | default('true') }}" 39 | ---- 40 | + 41 | .Beta defaults/main.yml 42 | [source,yaml] 43 | ---- 44 | # specific role variables 45 | beta_job_name: 'some other text' 46 | # collection wide variables 47 | beta_controller_username: "{{ mycollection_controller_username }}" 48 | beta_no_log: "{{ mycollection_no_log | default('false') }}" 49 | ---- 50 | ==== 51 | 52 | == Include a README file in each collection 53 | [%collapsible] 54 | ==== 55 | Explanation:: 56 | Include a README file that is in the root of the collection and which contains: 57 | * Information about the purpose of the collection 58 | * A link to the collection license file 59 | * General usage information such as which versions of ansible-core are supported and any libraries or SDKs which are required by the collection 60 | 61 | + 62 | Generating the README's plugin documentation from the plugin code helps eliminate documentation errors. 63 | Supplemental documentation such as user guides may be written in reStructured Text (rst) and located in the docs/docsite/rst/ directory of the collection. 64 | 65 | Examples:: 66 | Use https://github.com/ansible-network/collection_prep to generate the documentation for the collection 67 | ==== 68 | 69 | == Include a license file in a collection root directory 70 | [%collapsible] 71 | ==== 72 | Explanation:: 73 | Include a license file in the root directory 74 | Name the license file either LICENSE or COPYING. 75 | The contents may be either the text of the applicable license, or a link to the canonical reference for the license on the Internet (such as https://opensource.org/licenses/BSD-2-Clause ) 76 | If any file in the collection is licensed differently from the larger collection it is a part of (such as module utilities), note the applicable license in the header of the file. 77 | ==== 78 | -------------------------------------------------------------------------------- /images/ansible_structures.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | Landscape *- Type 3 | Type *- Function 4 | Function *- Component 5 | class Landscape << (L,orchid) >> { 6 | {field} Workflow 7 | {method} Playbook of playbooks 8 | } 9 | class Type << (T,orange) >> { 10 | Playbook 11 | {method} _ 12 | } 13 | class Function << (F,yellow) >> { 14 | Role 15 | {method} _ 16 | } 17 | class Component { 18 | {field} Task file 19 | {method} Role 20 | } 21 | hide empty members 22 | scale 750 width 23 | skinparam classBackgroundColor Wheat/PowderBlue 24 | skinparam minClassWidth 150 25 | skinparam classFontSize 16 26 | skinparam defaultFontSize 12 27 | @enduml 28 | -------------------------------------------------------------------------------- /images/variable_precedences.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | start 3 | split 4 | split 5 | :role defaults; 6 | :inventory vars; 7 | :role vars; 8 | split again 9 | :scoped vars 10 | block <&arrow-right> task; 11 | :runtime vars 12 | set_fact+register; 13 | end split 14 | :scoped params 15 | role <&arrow-right> include; 16 | split again 17 | :host facts; 18 | end split 19 | :extra vars; 20 | stop 21 | @enduml 22 | -------------------------------------------------------------------------------- /inventories/README.adoc: -------------------------------------------------------------------------------- 1 | = Inventories and Variables Good Practices for Ansible 2 | 3 | == Identify your Single Source(s) of Truth and use it/them in your inventory 4 | [%collapsible] 5 | ==== 6 | Explanations:: 7 | A Single Source of Truth (SSOT) is the place where the "ultimate" truth about a certain data is generated, stored and maintained. 8 | There can be more than one SSOT, each for a different piece of information, but they shouldn't overlap and even less conflict. 9 | As you create your inventory, you identify these SSOTs and combine them into one inventory using dynamic inventory sources (we'll see how later on). 10 | Only the aspects which are not already provided by other sources are kept statically in your inventory. 11 | Doing this, your inventory becomes another source of truth, but only for the data it holds statically, because there is no other place to keep it. 12 | 13 | Rationale:: 14 | You limit your effort to maintain your inventory to its absolute minimum and you avoid generating potentially conflicting information with the rest of your IT. 15 | 16 | Examples:: 17 | You can typically identify three kinds of candidates as SSOTs: 18 | + 19 | * technical ones, where your managed devices live anyway, like a cloud or virtual manager (OpenStack, RHV, Public Cloud API, ...) or management systems (Satellite, monitoring systems, ...). Those sources provide you with technical information like IP addresses, OS type, etc. 20 | * managed ones, like a Configuration Management Database (CMDB), where your IT anyway manages a lot of information of use in an inventory. A CMDB provides you with more organizational information, like owner or location, but also with "to-be" technical information. 21 | * the inventory itself, only for the data which doesn't exist anywhere else. 22 | + 23 | Ansible provides a lot of https://docs.ansible.com/ansible/latest/plugins/inventory.html[inventory plugins] to pull data from those sources and they can be combined into one big inventory. 24 | This gives you a complete model of the environment to be automated, with limited effort to maintain it, and no confusion about where to modify it to get the result you need. 25 | ==== 26 | 27 | 28 | [[differentiate]] 29 | == Differentiate clearly between "As-Is" and "To-Be" information 30 | [%collapsible] 31 | ==== 32 | Explanations:: 33 | As you combine multiple sources, some will represent: 34 | + 35 | * discovered information grabbed from the existing environment, this is the "As-Is" information. 36 | * managed information entered in a tool, expressing the state to be reached, hence the "To-Be" information. 37 | + 38 | In general, the focus of an inventory is on the managed information because it represents the desired state you want to reach with your automation. This said, some discovered information is required for the automation to work. 39 | 40 | Rationale:: 41 | Mixing up these two kind of information can lead to your automation taking the wrong course of action by thinking that the current situation is aligned with the desired state. 42 | That can make your automation go awry and your automation engineers confused. 43 | There is a reason why Ansible makes the difference between "facts" (As-Is) and "variables" (To-Be), and so should you. 44 | In the end, automation is making sure that the As-Is situation complies to the To-Be description. 45 | + 46 | NOTE: many CMDBs have failed because they don't respect this principle. 47 | This and the lack of automation leads to a mix of unmaintained As-Is and To-Be information with no clear guideline on how to keep them up-to-date, and no real motivation to do so. 48 | 49 | Examples:: 50 | The technical tools typically contain a lot of discovered information, like an IP address or the RAM size of a VM. 51 | In a typical cloud environment, the IP address isn't part of the desired state, it is assigned on the fly by the cloud management layer, so you can only get it dynamically from the cloud API and you won't manage it. 52 | In a more traditional environment nevertheless, the IP address will be static, managed more or less manually, so it will become part of your desired state. 53 | In this case, you shouldn't use the discovered information or you might not realize that there is a discrepancy between As-Is and To-Be. 54 | + 55 | The RAM size of a VM will be always present in two flavours, e.g. As-Is coming from the technical source and To-Be coming from the CMDB, or your static inventory, and you shouldn't confuse them. 56 | By lack of doing so, your automation might not correct the size of the VM where it should have aligned the As-Is with the To-Be. 57 | ==== 58 | 59 | 60 | == Define your inventory as structured directory instead of single file 61 | [%collapsible] 62 | ==== 63 | Explanations:: 64 | Everybody has started with a single file inventory in ini-format (the courageous ones among us in YAML format), combining list of hosts, groups and variables. 65 | An inventory can nevertheless be also a directory containing: 66 | + 67 | * list(s) of hosts 68 | * list(s) of groups, with sub-groups and hosts belonging to those groups 69 | * dynamic inventory plug-ins configuration files 70 | * dynamic inventory scripts (deprecated but still simple to use) 71 | * structured `host_vars` directories 72 | * structured `group_vars` directories 73 | + 74 | The recommendation is to start with such a structure and extend it step by step. 75 | 76 | Rationale:: 77 | It is the only way to combine simply multiple sources into one inventory, without the trouble to call ansible with multiple `-i {inventory_file}` parameters, and keep the door open for extending it with dynamic elements. 78 | + 79 | It is also simpler to maintain in a Git repository with multiple maintainers as the chance to get a conflict is reduced because the information is spread among multiple files. 80 | You can drop roles' `defaults/main.yml` file into the structure and adapt it to your needs very quickly. 81 | + 82 | And finally it gives you a better overview of what is in your inventory without having to dig deeply into it, because already the structure (as revealed with `tree` or `find`) gives you a first idea of where to search what. This makes on-boarding of new maintainers a lot easier. 83 | 84 | Examples:: 85 | The following is a complete inventory as described before. 86 | You don't absolutely need to start at this level of complexity, but the experience shows that once you get used to it, it is actually a lot easier to understand and maintain than a single file. 87 | + 88 | .Tree of a structured inventory directory 89 | ---- 90 | inventory_example/ <1> 91 | ├── dynamic_inventory_plugin.yml <2> 92 | ├── dynamic_inventory_script.py <3> 93 | ├── groups_and_hosts <4> 94 | ├── group_vars/ <5> 95 | │   ├── alephs/ 96 | │   │   └── capital_letter.yml 97 | │   ├── all/ 98 | │   │   └── ansible.yml 99 | │   ├── alphas/ 100 | │   │   ├── capital_letter.yml 101 | │   │   └── small_caps_letter.yml 102 | │   ├── betas/ 103 | │   │   └── capital_letter.yml 104 | │   ├── greek_letters/ 105 | │   │   └── small_caps_letter.yml 106 | │   └── hebrew_letters/ 107 | │   └── small_caps_letter.yml 108 | └── host_vars/ <6> 109 | ├── host1.example.com/ 110 | │   └── ansible.yml 111 | ├── host2.example.com/ 112 | │   └── ansible.yml 113 | └── host3.example.com/ 114 | ├── ansible.yml 115 | └── capital_letter.yml 116 | ---- 117 | <1> this is your inventory directory 118 | <2> a configuration file for a dynamic inventory plug-in 119 | <3> a dynamic inventory script, old style and deprecated but still used (and supported) 120 | <4> a file containing a static list of hosts and groups, the name isn't important (often called `hosts` but some might confuse it with `/etc/hosts` and it also contains groups). 121 | See below for an example. 122 | <5> the `group_vars` directory to define group variables. 123 | Notice how each group is represented by a directory of its name containing one or more variable files. 124 | <6> the `host_vars` directory to define host variables. 125 | Notice how each host is represented by a directory of its name containing one or more variable files. 126 | + 127 | The groups and hosts file could look as follows, important is to not put any variable definition in this file. 128 | + 129 | .Content of the `groups_and_hosts` file 130 | [source,ini] 131 | ---- 132 | include::inventory_example/groups_and_hosts[] 133 | ---- 134 | + 135 | Listing the hosts under `[all]` isn't really required but makes sure that no host is forgotten, should it not belong to any other group. 136 | The ini-format isn't either an obligation but it seems easier to read than YAML, as long as no variable is involved, and makes it easier to maintain in an automated manner using `lineinfile` (without needing to care for the indentation). 137 | + 138 | Regarding the group and host variables, the name of the variable files is actually irrelevant, you can verify it by calling `ansible-inventory -i inventory_example --list`: 139 | you will see nowhere the name `capital_letter` or `small_caps_letter` (you might see `ansible` though, but for other reasons...). 140 | We nevertheless follow the convention to name our variable files after the role they are steering (so we assume the roles `capital_letter` and `small_caps_letter`). 141 | If correctly written, the `defaults/main.yml` file from those roles can be simply "dropped" into our inventory structure and adapted accordingly to our needs. 142 | We reserve the name `ansible.yml` for the Ansible related variables (user, connection, become, etc). 143 | + 144 | TIP: you can even create a sub-directory in a host's or group's variable directory and put _there_ the variable files. 145 | This is useful if you have many variables related to the same topic you want to group together but maintain in separate files. 146 | For example Satellite requires many variables to be fully configured, so you can have a structure as follows (again, the name of the sub-directory `satellite` and of the files doesn't matter): 147 | + 148 | .Example of a complex tree of variables with sub-directory 149 | ---- 150 | inventory_satellite/ 151 | ├── groups_and_hosts 152 | └── host_vars/ 153 | └── sat6.example.com/ 154 | ├── ansible.yml 155 | └── satellite/ 156 | ├── content_views.yml 157 | ├── hostgroups.yml 158 | └── locations.yml 159 | ---- 160 | ==== 161 | 162 | == Rely on your inventory to loop over hosts, don't create lists of hosts 163 | [%collapsible] 164 | ==== 165 | Explanations:: 166 | To perform the same task on multiple hosts, don't create a variable with a list of hosts and loop over it. 167 | Instead use as much as possible the capabilities of your inventory, which is already a kind of list of hosts. 168 | + 169 | The anti-pattern is especially obvious in the example of provisioning hosts on some kind of manager. 170 | Commonly seen automation tasks of this kind are spinning up a list of VMs via a hypervisor manager like oVirt/RHV or vCenter, or calling a management tool like Foreman/Satellite or even our beloved AWX/Tower/controller. 171 | 172 | Rationale:: 173 | There are 4 main reasons for following this advice: 174 | + 175 | . a list of hosts is more difficult to maintain than an inventory structure, and tends to become very quickly difficult to oversee. 176 | This is especially true as you generally need to maintain your hosts also in your inventory. 177 | This brings us to the 2nd advantage: 178 | . you avoid duplicating information, as you often need the same kind of information in your inventory that you also need in order to provision your VMs. 179 | In your inventory, you can also use groups to define group variables, automatically inherited by hosts. 180 | You can try to implement a similar inheritance pattern with your list of hosts, but it quickly becomes difficult and _hand-crafted_. 181 | . as you loop through the hosts of an inventory, Ansible helps you with https://docs.ansible.com/ansible/latest/user_guide/playbooks_strategies.html[parallelization, throttling, etc], all of which you can't do easily with your own list (technically, you _can_ combine https://docs.ansible.com/ansible/latest/user_guide/playbooks_async.html[async and loop] to reach something like this, but it's a lot more complex to handle than letting Ansible do the heavy lifting for you). 182 | . you can very simply _limit_ the play to certain hosts, using for example the `--limit` parameter of `ansible-playbook` (or the 'limit' field in Tower/controller), even using groups and patterns. 183 | You can't really do this with your own list of hosts. 184 | 185 | Examples:: 186 | Our first idea could be to define managers and hosts first in an inventory: 187 | + 188 | .Content of the "bad" `groups_and_hosts` file 189 | [source,ini] 190 | ---- 191 | include::inventory_loop_hosts/inventory_bad/groups_and_hosts[] 192 | ---- 193 | + 194 | Each manager has a list of hosts, which can look like this: 195 | + 196 | .List of hosts in `inventory_bad/host_vars/manager_a/provision.yml` 197 | [source,yaml] 198 | ---- 199 | include::inventory_loop_hosts/inventory_bad/host_vars/manager_a/provision.yml[] 200 | ---- 201 | + 202 | So that we can loop over the list in this way: 203 | + 204 | .The "bad" way to loop over hosts 205 | [source,yaml] 206 | ---- 207 | include::inventory_loop_hosts/playbook_bad.yml[] 208 | ---- 209 | + 210 | TIP: check the resulting files using e.g. `head -n-0 /tmp/bad_*`. 211 | + 212 | As said, no way to limit the hosts provisioned, and no parallelism. 213 | Compare then with the recommended approach, with a slightly different structure: 214 | + 215 | .Content of the "good" `groups_and_hosts` file 216 | [source,ini] 217 | ---- 218 | include::inventory_loop_hosts/inventory_good/groups_and_hosts[] 219 | ---- 220 | + 221 | It is now the hosts and their groups which carry the relevant information, it is not anymore parked in one single list (and can be used for other purposes): 222 | + 223 | .The "good" variable structure 224 | ---- 225 | $ cat inventory_good/host_vars/host1/provision.yml 226 | provision_value: uno 227 | $ cat inventory_good/group_vars/managed_hosts_a/provision.yml 228 | manager_hostname: manager_a 229 | ---- 230 | + 231 | And the provisioning playbook now runs in parallel and can be limited to specific hosts: 232 | + 233 | .The "good" way to loop over hosts 234 | [source,yaml] 235 | ---- 236 | include::inventory_loop_hosts/playbook_good.yml[] 237 | ---- 238 | + 239 | The result isn't overwhelming in this simple setup but you would of course better appreciate if the provisioning would take half an hour instead of a fraction of seconds: 240 | + 241 | .Comparison of the execution times between the "good" and the "bad" implementation 242 | ---- 243 | $ ANSIBLE_STDOUT_CALLBACK=profile_tasks \ 244 | ansible-playbook -i inventory_bad playbook_bad.yml 245 | Saturday 23 October 2021 13:11:45 +0200 (0:00:00.040) 0:00:00.040 ****** 246 | Saturday 23 October 2021 13:11:45 +0200 (0:00:00.858) 0:00:00.899 ****** 247 | =============================================================================== 248 | create some file to simulate an API call to provision a host ------------ 0.86s 249 | $ ANSIBLE_STDOUT_CALLBACK=profile_tasks \ 250 | ansible-playbook -i inventory_good playbook_good.yml 251 | Saturday 23 October 2021 13:11:55 +0200 (0:00:00.040) 0:00:00.040 ****** 252 | Saturday 23 October 2021 13:11:56 +0200 (0:00:00.569) 0:00:00.610 ****** 253 | =============================================================================== 254 | create some file to simulate an API call to provision a host ------------ 0.57s 255 | ---- 256 | + 257 | TIP: if for some reason, you can't follow the recommendation, you can at least avoid duplicating too much information by indirectly referencing the hosts' variables as in `"{{ hostvars[item.name]['provision_value'] }}"`. Not so bad... 258 | ==== 259 | 260 | == Restrict your usage of variable types 261 | [%collapsible] 262 | ==== 263 | Explanations:: 264 | * Avoid playbook and play variables, as well as `include_vars`. 265 | Opt for inventory variables instead. 266 | * Avoid using scoped variables unless required for runtime reasons, e.g. for loops and for temporary variables based on runtime variables. 267 | Another valid exception is when nested variables are too complicated to be defined at once. 268 | 269 | Rationale:: 270 | There are https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#understanding-variable-precedence[22 levels of variable precedence]. 271 | This is almost impossible to keep in mind for a "normal" human and can lead to all kind of weird behaviors if not under control. 272 | In addition, the use of play(book) variables is not recommended as it blurs the separation between code and data. 273 | The same applies to all constructs including specific variable files as part of the play (i.e. `include_vars`). 274 | By reducing the number of variable types, you end up with a more simple and overseeable list of variables. 275 | Together with some explanations why they have their specific precedence, so that they become easier to remember and use wisely: 276 | + 277 | . role defaults (defined in `defaults/main.yml`), they are... defaults and can be overwritten by anything. 278 | . inventory vars, they truly represent your desired state. 279 | They have their own internal precedence (group before host) but that's easy to remember. 280 | . host facts don't represent a desired state but the current state, and no other variable should have the same name because of <> so that the precedence doesn't really matter. 281 | . role vars (defined in `vars/main.yml`) represent constants used by the role to separate code from data, and shouldn't either collide with the inventory variables, but can be overwritten by extra vars if you know what you're doing. 282 | . scoped vars, at the block or task level, are local to their scope and hence internal to the role, and can't collide with other variable types. 283 | . runtime vars, defined by register or set_facts, are taking precedence over almost everything defined previously, which makes sense as they represent the current state of the automation. 284 | . scoped params, at the role or include level this time, are admittedly a bit out of order and should be avoided to limit surprises. 285 | . and lastly, extra_vars overwrite everything else (even runtime vars, which can be quite surprising) 286 | 287 | NOTE: we didn't explicitly consider https://docs.ansible.com/automation-controller/4.4/html/userguide/workflow_templates.html#ug-wf-templates-extravars[Workflow and Job Template variables] but they are all extra vars in this consideration. 288 | 289 | The following picture summarizes this list in a simplified and easier to keep in mind way, highlighting which variables are meant to overwrite others: 290 | 291 | .Flow of variable precedences 292 | image::variable_precedences.svg[flow of variable precedences in 3 lanes] 293 | 294 | CAUTION: even if we write that variables _shouldn't_ overwrite each other, they still all share the same namespace and _can_ potentially overwrite each other. 295 | It is your responsibility as automation author to make sure they don't. 296 | ==== 297 | 298 | == Prefer inventory variables over extra vars to describe the desired state 299 | [%collapsible] 300 | ==== 301 | Explanations:: 302 | Don't use extra vars to define your desired state. 303 | Make sure your inventory completely describes how your environment is supposed to look like. 304 | Use extra vars only for troubleshooting, debugging or validation purposes. 305 | 306 | Rationale:: 307 | Inventory variables are typically in some kind of persistent tracked storage (be it a database or Git), and should be your sole source representing your desired state so that you can refer to it non-ambiguously. 308 | On the other hand, extra vars are bound to a specific job or ansible-call and disappear together with history. 309 | 310 | Examples:: 311 | Don't use extra vars for the RAM size of VM to create, because this is part of the desired state of your environment, and nobody would know one year down the line if the VM was really created with the proper RAM size according to the state of the inventory. 312 | You may use an extra variable to protect a critical part of a destructive playbook, something like `are_you_really_really_sure: true/false`, which is validated before e.g. a VM is destroyed and recreated to change parameters which can't be changed on the fly. 313 | You can also use extra vars to enforce fact values which can't be reproduced easily, like overwriting `ansible_memtotal_mb` to simulate a RAM size fact of terabytes to validate that your code can cope with it. 314 | + 315 | Another example could be the usage of `no_log: "{{ no_log_in_case_of_trouble | default(true) }}` to exceptionally "uncover" the output of failing tasks even though they are security relevant. 316 | ==== 317 | -------------------------------------------------------------------------------- /inventories/inventory_example/dynamic_inventory_plugin.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-cop/automation-good-practices/7b44f1d2333680052c967964d84301ad7d2128e7/inventories/inventory_example/dynamic_inventory_plugin.yml -------------------------------------------------------------------------------- /inventories/inventory_example/dynamic_inventory_script.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-cop/automation-good-practices/7b44f1d2333680052c967964d84301ad7d2128e7/inventories/inventory_example/dynamic_inventory_script.py -------------------------------------------------------------------------------- /inventories/inventory_example/group_vars/alephs/capital_letter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # variables for the capital_letter role 3 | # size of the capital letter (default: 12) 4 | capital_letter_size: 15 5 | -------------------------------------------------------------------------------- /inventories/inventory_example/group_vars/all/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ansible specific variables 3 | ansible_user: wall-e 4 | -------------------------------------------------------------------------------- /inventories/inventory_example/group_vars/alphas/capital_letter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # variables for the capital_letter role 3 | # size of the capital letter (default: 12) 4 | capital_letter_size: 16 5 | -------------------------------------------------------------------------------- /inventories/inventory_example/group_vars/alphas/small_caps_letter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # variables for the small_caps_letter role 3 | # font of the small caps letter (default: Sans) 4 | small_caps_letter_font: DejaVu 5 | -------------------------------------------------------------------------------- /inventories/inventory_example/group_vars/betas/capital_letter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # variables for the capital_letter role 3 | # size of the capital letter (default: 12) 4 | capital_letter_size: 18 5 | -------------------------------------------------------------------------------- /inventories/inventory_example/group_vars/greek_letters/small_caps_letter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # variables for the small_caps_letter role 3 | # font of the small caps letter (default: Sans) 4 | small_caps_letter_font: Oxygen Mono 5 | -------------------------------------------------------------------------------- /inventories/inventory_example/group_vars/hebrew_letters/small_caps_letter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # variables for the small_caps_letter role 3 | # font of the small caps letter (default: Sans) 4 | small_caps_letter_font: Culmus 5 | -------------------------------------------------------------------------------- /inventories/inventory_example/groups_and_hosts: -------------------------------------------------------------------------------- 1 | [all] 2 | host1.example.com 3 | host2.example.com 4 | host3.example.com 5 | 6 | [alphas] 7 | host1.example.com 8 | 9 | [betas] 10 | host2.example.com 11 | 12 | [greek_letters:children] 13 | alphas 14 | betas 15 | 16 | [alephs] 17 | host3.example.com 18 | 19 | [hebrew_letters:children] 20 | alephs 21 | -------------------------------------------------------------------------------- /inventories/inventory_example/host_vars/host1.example.com/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ansible specific variables 3 | ansible_host: 192.0.2.1 4 | -------------------------------------------------------------------------------- /inventories/inventory_example/host_vars/host2.example.com/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ansible specific variables 3 | ansible_host: 192.0.2.2 4 | -------------------------------------------------------------------------------- /inventories/inventory_example/host_vars/host3.example.com/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ansible specific variables 3 | ansible_host: 192.0.2.3 4 | -------------------------------------------------------------------------------- /inventories/inventory_example/host_vars/host3.example.com/capital_letter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # variables for the capital_letter role 3 | # size of the capital letter (default: 12) 4 | capital_letter_size: 20 5 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_bad/group_vars/all/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_host: localhost 3 | ansible_connection: local 4 | ansible_python_interpreter: /usr/bin/python3 5 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_bad/groups_and_hosts: -------------------------------------------------------------------------------- 1 | [managers] 2 | manager_a 3 | manager_b 4 | 5 | [managed_hosts] 6 | host1 7 | host2 8 | host3 9 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_bad/host_vars/host1/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # duplicated information we might need in another context 3 | provision_value: uno 4 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_bad/host_vars/host2/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # duplicated information we might need in another context 3 | provision_value: due 4 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_bad/host_vars/host3/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # duplicated information we might need in another context 3 | provision_value: tres 4 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_bad/host_vars/manager_a/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_list_of_hosts: 3 | - name: host1 4 | provision_value: uno 5 | - name: host2 6 | provision_value: due 7 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_bad/host_vars/manager_b/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_list_of_hosts: 3 | - name: host3 4 | provision_value: tres 5 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_good/group_vars/all/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_host: localhost 3 | ansible_connection: local 4 | ansible_python_interpreter: /usr/bin/python3 5 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_good/group_vars/managed_hosts_a/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | manager_hostname: manager_a 3 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_good/group_vars/managed_hosts_b/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | manager_hostname: manager_b 3 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_good/groups_and_hosts: -------------------------------------------------------------------------------- 1 | [managers] 2 | manager_a 3 | manager_b 4 | 5 | [managed_hosts_a] 6 | host1 7 | host2 8 | 9 | [managed_hosts_b] 10 | host3 11 | 12 | [managed_hosts:children] 13 | managed_hosts_a 14 | managed_hosts_b 15 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_good/host_vars/host1/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_value: uno 3 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_good/host_vars/host2/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_value: due 3 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_good/host_vars/host3/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_value: tres 3 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_not_so_bad/group_vars/all/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_host: localhost 3 | ansible_connection: local 4 | ansible_python_interpreter: /usr/bin/python3 5 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_not_so_bad/groups_and_hosts: -------------------------------------------------------------------------------- 1 | [managers] 2 | manager_a 3 | manager_b 4 | 5 | [managed_hosts] 6 | host1 7 | host2 8 | host3 9 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_not_so_bad/host_vars/host1/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_value: uno 3 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_not_so_bad/host_vars/host2/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_value: due 3 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_not_so_bad/host_vars/host3/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_value: tres 3 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_not_so_bad/host_vars/manager_a/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_list_of_hosts: 3 | - name: host1 4 | - name: host2 5 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/inventory_not_so_bad/host_vars/manager_b/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provision_list_of_hosts: 3 | - name: host3 4 | provision_value: tres 5 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/playbook_bad.yml: -------------------------------------------------------------------------------- 1 | - name: Provision hosts in a bad way 2 | hosts: managers 3 | gather_facts: false 4 | become: false 5 | tasks: 6 | - name: Create some file to simulate an API call to provision a host 7 | ansible.builtin.copy: 8 | content: "{{ item.provision_value }}\n" 9 | dest: "/tmp/bad_{{ inventory_hostname }}_{{ item.name }}.txt" 10 | force: true 11 | owner: root 12 | group: root 13 | mode: "0644" 14 | loop: "{{ provision_list_of_hosts }}" 15 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/playbook_good.yml: -------------------------------------------------------------------------------- 1 | - name: Provision hosts in a good way 2 | hosts: managed_hosts 3 | gather_facts: false 4 | become: false 5 | tasks: 6 | - name: Create some file to simulate an API call to provision a host 7 | ansible.builtin.copy: 8 | content: "{{ provision_value }}\n" 9 | dest: "/tmp/good_{{ manager_hostname }}_{{ inventory_hostname }}.txt" 10 | force: true 11 | owner: root 12 | group: root 13 | mode: "0644" 14 | -------------------------------------------------------------------------------- /inventories/inventory_loop_hosts/playbook_not_so_bad.yml: -------------------------------------------------------------------------------- 1 | - name: Provision hosts in a not so bad way 2 | hosts: managers 3 | gather_facts: false 4 | become: false 5 | tasks: 6 | - name: Create some file to simulate an API call to provision a host 7 | ansible.builtin.copy: 8 | content: "{{ hostvars[item.name]['provision_value'] }}\n" 9 | dest: "/tmp/not_so_bad_{{ inventory_hostname }}_{{ item.name }}.txt" 10 | force: true 11 | owner: root 12 | group: root 13 | mode: "0644" 14 | loop: "{{ provision_list_of_hosts }}" 15 | -------------------------------------------------------------------------------- /inventories/inventory_satellite/groups_and_hosts: -------------------------------------------------------------------------------- 1 | [satellites] 2 | sat6.example.com 3 | -------------------------------------------------------------------------------- /inventories/inventory_satellite/host_vars/sat6.example.com/ansible.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-cop/automation-good-practices/7b44f1d2333680052c967964d84301ad7d2128e7/inventories/inventory_satellite/host_vars/sat6.example.com/ansible.yml -------------------------------------------------------------------------------- /inventories/inventory_satellite/host_vars/sat6.example.com/satellite/content_views.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-cop/automation-good-practices/7b44f1d2333680052c967964d84301ad7d2128e7/inventories/inventory_satellite/host_vars/sat6.example.com/satellite/content_views.yml -------------------------------------------------------------------------------- /inventories/inventory_satellite/host_vars/sat6.example.com/satellite/hostgroups.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-cop/automation-good-practices/7b44f1d2333680052c967964d84301ad7d2128e7/inventories/inventory_satellite/host_vars/sat6.example.com/satellite/hostgroups.yml -------------------------------------------------------------------------------- /inventories/inventory_satellite/host_vars/sat6.example.com/satellite/locations.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-cop/automation-good-practices/7b44f1d2333680052c967964d84301ad7d2128e7/inventories/inventory_satellite/host_vars/sat6.example.com/satellite/locations.yml -------------------------------------------------------------------------------- /playbooks/README.adoc: -------------------------------------------------------------------------------- 1 | = Playbooks good practices 2 | 3 | == Keep your playbooks as simple as possible 4 | [%collapsible] 5 | ==== 6 | Explanations:: Don't put too much logic in your playbook, put it in your roles (or even in custom modules), and try to limit your playbooks to a list of a roles. 7 | 8 | Rationale:: Roles are meant to be re-used and the structure helps you to make your code re-usable. 9 | The more code you put in roles, the higher the chances you, or others, can reuse it. 10 | Also, if you follow the <>, you can very easily create new (type) playbooks by just re-shuffling the roles. 11 | This way you can create a playbook for each purpose without having to duplicate a lot of code. 12 | This, in turn, also helps with the maintainability as there is only a single place where necessary changes need to be implemented, and that is in the role 13 | 14 | Examples:: 15 | + 16 | .An example of playbook containing only roles 17 | [source,yaml] 18 | ---- 19 | --- 20 | - name: A playbook can solely be a list of roles 21 | hosts: all 22 | gather_facts: false 23 | become: false 24 | roles: 25 | - role1 26 | - role2 27 | - role3 28 | ---- 29 | + 30 | TIP: we'll explain later why there might be a case for using `include_role`/`import_role` tasks instead of the role section. 31 | ==== 32 | 33 | == Use either the tasks or roles section in playbooks, not both 34 | 35 | [%collapsible] 36 | ==== 37 | Explanations:: A playbook can contain `pre_tasks`, `roles`, `tasks` and `post_tasks` sections. 38 | Avoid using both `roles` and `tasks` sections, the latter possibly containing `import_role` or `include_role` tasks. 39 | Rationale:: The order of execution between `roles` and `tasks` isn't obvious, and hence mixing them should be avoided. 40 | Examples:: Either you need only static importing of roles and you can use the `roles` section, or you need dynamic inclusion and you should use _only_ the `tasks` section. 41 | Of course, for very simple cases, you can just use `tasks` without `roles`. 42 | ==== 43 | 44 | == Use tags cautiously either for roles or for complete purposes 45 | [%collapsible] 46 | ==== 47 | Explanations:: limit your usage of tags to two aspects: 48 | + 49 | . either tags called like the roles to switch on/off single roles, 50 | . or specific tags to reach a meaningful purpose 51 | 52 | Don't set tags which can't be used on their own, or can be destructive if used on their own. 53 | 54 | Also document tags and their purpose(s). 55 | 56 | Rationale:: there is nothing worse than tags which can't be used alone, they bear the risk to destroy something by being called standalone. 57 | An acceptable exception is the pattern to use the role name as tag name, which can be useful while developing the playbook to test, or exclude, individual roles. 58 | + 59 | Important is that your users don't need to learn the right sequence of tags necessary to get a meaningful result, one tag should be enough. 60 | 61 | Examples:: 62 | + 63 | .An example of playbook importing roles with tags 64 | [source,yaml] 65 | ---- 66 | --- 67 | - name: A playbook can be a list of roles imported with tags 68 | hosts: all 69 | gather_facts: false 70 | become: false 71 | tasks: 72 | - name: Import role1 73 | ansible.builtin.import_role: 74 | name: role1 75 | tags: 76 | - role1 77 | - deploy 78 | 79 | - name: Import role2 80 | ansible.builtin.import_role: 81 | name: role2 82 | tags: 83 | - role2 84 | - deploy 85 | - configure 86 | 87 | - name: Import role3 88 | ansible.builtin.import_role: 89 | name: role3 90 | tags: 91 | - role3 92 | - configure 93 | ---- 94 | + 95 | You see that each role can be skipped/run individually, but also that the tags `deploy` and `configure` can be used to do something we'll assume to be meaningful, without having to explain at length what they do. 96 | + 97 | The same approach is also possible with `include_role` but requires additionally to `apply` the same tags to the role's tasks, which doesn't make the code easier to read: 98 | + 99 | .An example of playbook including roles with tags 100 | [source,yaml] 101 | ---- 102 | - name: a playbook can be a list of roles included with tags applied 103 | hosts: all 104 | gather_facts: false 105 | become: false 106 | 107 | tasks: 108 | - name: include role1 109 | include_role: 110 | name: role1 111 | apply: 112 | tags: 113 | - role1 114 | - deploy 115 | tags: 116 | - role1 117 | - deploy 118 | - name: include role2 119 | include_role: 120 | name: role2 121 | apply: 122 | tags: 123 | - role2 124 | - deploy 125 | - configure 126 | tags: 127 | - role2 128 | - deploy 129 | - configure 130 | - name: include role3 131 | include_role: 132 | name: role3 133 | apply: 134 | tags: 135 | - role3 136 | - configure 137 | tags: 138 | - role3 139 | - configure 140 | ---- 141 | 142 | ==== 143 | 144 | == Use the verbosity parameter with debug statements 145 | [%collapsible] 146 | ==== 147 | Explanations:: Debug messages should have a verbosity defined as appropriate for the message. 148 | 149 | Rationale:: 150 | Debug messages are useful during testing and development, and can be useful to retain as playbooks go into production for future troubleshooting. 151 | However, log messages will clutter your output, which can confuse users with non-relevant information. 152 | 153 | Examples:: 154 | + 155 | .Adding verbosity to debug messages 156 | [source, yaml] 157 | ---- 158 | - name: don't make messages always display 159 | debug: 160 | msg: "This message will clutter your log in production" 161 | 162 | - name: this message will only appear when verbosity is 2 or more 163 | debug: 164 | msg: "Some more debug information if needed" 165 | verbosity: 2 166 | ---- 167 | ==== 168 | -------------------------------------------------------------------------------- /playbooks/playbook_role_tags/playbook_import.yml: -------------------------------------------------------------------------------- 1 | - name: A playbook can be a list of roles imported with tags 2 | hosts: localhost 3 | gather_facts: false 4 | become: false 5 | tasks: 6 | - name: Import role1 7 | ansible.builtin.import_role: 8 | name: role1 9 | tags: 10 | - role1 11 | - deploy 12 | - name: Import role2 13 | ansible.builtin.import_role: 14 | name: role2 15 | tags: 16 | - role2 17 | - deploy 18 | - configure 19 | - name: Import role3 20 | ansible.builtin.import_role: 21 | name: role3 22 | tags: 23 | - role3 24 | - configure 25 | -------------------------------------------------------------------------------- /playbooks/playbook_role_tags/playbook_include.yml: -------------------------------------------------------------------------------- 1 | - name: A playbook can be a list of roles included with tags 2 | hosts: localhost 3 | gather_facts: false 4 | become: false 5 | tasks: 6 | - name: Include role1 7 | ansible.builtin.include_role: 8 | name: role1 9 | apply: 10 | tags: [role1, deploy] 11 | tags: 12 | - role1 13 | - deploy 14 | 15 | - name: Include role2 16 | ansible.builtin.include_role: 17 | name: role2 18 | apply: 19 | tags: [role2, deploy, configure] 20 | tags: 21 | - role2 22 | - deploy 23 | - configure 24 | 25 | - name: Include role3 26 | ansible.builtin.include_role: 27 | name: role3 28 | apply: 29 | tags: [role3, configure] 30 | tags: 31 | - role3 32 | - configure 33 | -------------------------------------------------------------------------------- /playbooks/playbook_role_tags/roles/role1/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for role1 3 | - name: Output my name 4 | ansible.builtin.debug: 5 | msg: I am role 1 6 | -------------------------------------------------------------------------------- /playbooks/playbook_role_tags/roles/role2/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for role2 3 | - name: Output my name 4 | ansible.builtin.debug: 5 | msg: I am role 2 6 | -------------------------------------------------------------------------------- /playbooks/playbook_role_tags/roles/role3/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for role3 3 | - name: Output my name 4 | ansible.builtin.debug: 5 | msg: I am role 3 6 | -------------------------------------------------------------------------------- /plugins/README.adoc: -------------------------------------------------------------------------------- 1 | = Plugins good practices 2 | 3 | NOTE: Work in Progress... 4 | 5 | == Python Guidelines 6 | 7 | * Review Ansible guidelines for https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_best_practices.html[modules] and https://docs.ansible.com/ansible/latest/dev_guide/index.html[development]. 8 | * Use https://pep8.org/[PEP8]. 9 | * File headers and functions should have comments for their intent. 10 | 11 | 12 | == Write documentation for all plugin types 13 | [%collapsible] 14 | ==== 15 | Explanations:: 16 | All plugins, regardless of type, need documentation that describes the input parameters, outputs, and practical examples of how to use it. 17 | 18 | Examples:: See the Ansible Developer Guide sections on https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#plugin-configuration-documentation-standards[Plugin Configuration and Documentation Standards] and https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html#module-documenting[Module Documenting] for more details. 19 | ==== 20 | 21 | == Use sphinx (reST) formatted docstrings in Python code 22 | [%collapsible] 23 | ==== 24 | Explanations:: 25 | Sphinx (reST) formatted docstring are preferred for Ansible development. This includes all parameters, yields, raises, or returns for all classes, private and public functions written in Python. 26 | 27 | Rationale:: 28 | https://peps.python.org/pep-0257/[PEP-257] states that: "All modules should normally have docstrings, and all functions and classes exported by a module should also have docstrings. Public methods (including the __init__ constructor) should also have docstrings. A package may be documented in the module docstring of the __init__.py file in the package directory." 29 | 30 | Examples:: 31 | [source,python] 32 | ---- 33 | """[Summary] 34 | 35 | :param [ParamName]: [ParamDescription], defaults to [DefaultParamVal] 36 | :type [ParamName]: [ParamType](, optional) 37 | ... 38 | :raises [ErrorType]: [ErrorDescription] 39 | ... 40 | :return: [ReturnDescription] 41 | :rtype: [ReturnType] 42 | """ 43 | ---- 44 | 45 | ==== 46 | 47 | == Use Python type hints to document variable types. 48 | [%collapsible] 49 | ==== 50 | Explanations:: Use Python type hints to document variable types. Type hints are supported in Python 3.5 and greater. 51 | 52 | Rationale:: Type hints communicate what type a variable can be expected to be in the code. They can be consumed by static analysis tools to ensure that variable usage is consistent within the code base. 53 | 54 | Examples:: 55 | [source,python] 56 | MyPy is a static type checker, which could analyze the following snippet: 57 | ---- 58 | def greeting(name: str) -> str: 59 | return 'Hello ' + name 60 | ---- 61 | 62 | ==== 63 | 64 | == The use of unittest is discouraged, use pytest instead. 65 | [%collapsible] 66 | ==== 67 | Explanations:: Use https://docs.pytest.org/[pytest] for writing unit tests for plugins 68 | 69 | Rationale:: Pytest is the testing framework used by Ansible Engineering and will provide the best experience for plugin developers. 70 | The Ansible Developer Guide section on https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html[unit testing] has detailed information on when and how to use unit tests. 71 | 72 | Examples:: 73 | [source,python] 74 | ---- 75 | from __future__ import (absolute_import, division, print_function) 76 | __metaclass__ = type 77 | 78 | import pytest 79 | 80 | from ansible.modules.copy import AnsibleModuleError, split_pre_existing_dir 81 | from ansible.module_utils.basic import AnsibleModule 82 | 83 | ONE_DIR_DATA = (('dir1', 84 | ('.', ['dir1']), 85 | ('dir1', []), 86 | ), 87 | ('dir1/', 88 | ('.', ['dir1']), 89 | ('dir1', []), 90 | ), 91 | ) 92 | 93 | @pytest.mark.parametrize('directory, expected', ((d[0], d[2]) for d in ONE_DIR_DATA)) 94 | def test_split_pre_existing_dir_one_level_exists(directory, expected, mocker): 95 | mocker.patch('os.path.exists', side_effect=[True, False, False]) 96 | split_pre_existing_dir(directory) == expected 97 | 98 | ---- 99 | 100 | ==== 101 | 102 | 103 | == Formatting of manually maintained plugin argspecs 104 | [%collapsible] 105 | ==== 106 | Explanations:: 107 | Ensure a consistent approach to the way complex argument_specs are formatted within a collection. 108 | 109 | Rationale:: 110 | When hand-writing a complex argspec, the author may choose to build up to data structure from multiple dictionaries or vars. 111 | Other authors may choose to implement a complex, nested argspec as a single dictionary. 112 | Within a single collection, select one style and use it consistently. 113 | 114 | Examples:: 115 | Use of a https://github.com/ansible-collections/cisco.nxos/blob/3.0.0/plugins/module_utils/network/nxos/argspec/bgp_global/bgp_global.py[sngle dictionary] 116 | 117 | Two different examples of using https://github.com/ansible-collections/community.aws/blob/stable-3/plugins/modules/ec2_scaling_policy.py#L355-L370[multiple] https://github.com/ansible-collections/amazon.cloud/blob/0.1.0/plugins/modules/backup_report_plan.py#L182-L234[dictionaries]. 118 | ==== 119 | 120 | 121 | == Keep plugin entry files to a minimal size. 122 | [%collapsible] 123 | ==== 124 | Explanations:: 125 | Keep the entry file to a plugin to a minimal and easily maintainable size. 126 | 127 | Rationale:: 128 | Long and complex code files can be difficult to maintain. 129 | Move reusable functions and classes, such as those for data validation or manipulation, to a https://docs.ansible.com/ansible/latest/dev_guide/developing_module_utilities.html[module_utils/] (for Ansible modules) or plugin_utils/ (for all other plugin types) file and import them into plugins. 130 | This keeps the Python code easier to read and maintain. 131 | ==== 132 | 133 | 134 | == Plugins should be initially developed using the ansible plugin builder 135 | [%collapsible] 136 | ==== 137 | Explanations:: 138 | The https://github.com/ansible-community/ansible.plugin_builder[ansible.plugin_builder] is a tool which helps developers scaffold new plugins. 139 | 140 | ==== 141 | 142 | 143 | == Use clear error/info messages 144 | [%collapsible] 145 | ==== 146 | Explanations:: This will make it easier to troubleshoot failures if they occur 147 | 148 | Rationale:: Error messages that communicate specific details of the failure will aid in resolving the problem. 149 | Unclear error messages such as "Failed!" are unnecessarily obscure. 150 | 151 | Information can be displayed to the user based on the verbosity the task is being executed at. 152 | 153 | The base AnsibleModule class from which all modules should be created provides helper methods for reporting warnings and deprecations, and for exiting the module in the case of a failure. 154 | 155 | There is a Display class available which enables the display of information at different verbosity levels in all plugin types. 156 | 157 | Examples:: 158 | [source,python] 159 | ---- 160 | # Causing a module to exit with a failure status 161 | if checksum and checksum_src != checksum: 162 | module.fail_json( 163 | msg='Copied file does not match the expected checksum. Transfer failed.', 164 | checksum=checksum_src, 165 | expected_checksum=checksum 166 | ) 167 | 168 | 169 | # Displaying a warning during module execution, without exiting 170 | try: 171 | result = get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata'] 172 | key_id = result['Arn'] 173 | except is_boto3_error_code('AccessDeniedException'): 174 | module.warn('Permission denied fetching key metadata ({0})'.format(key_id)) 175 | return None 176 | 177 | 178 | # Displaying a notice about a deprecation 179 | if importer_ssl_client_key is None and module.params['client_key'] is not None: 180 | importer_ssl_client_key = module.params['client_key'] 181 | module.deprecate("In Ansible 2.9.2 `feed_client_key` option was added. Until community.general 3.0.0 the default " 182 | "value will come from client_key option", 183 | version="3.0.0", collection_name='community.general') 184 | 185 | 186 | # Display information only when a user has set an increased level of verbosity 187 | # ansible-playbook -i inventory -vvvv test-playbook.yml 188 | from ansible.utils.display import Display 189 | 190 | display = Display() 191 | 192 | lookupfile = self.find_file_in_search_path(variables, 'files', term) 193 | display.vvvv("File lookup using {0} as file".format(lookupfile)) 194 | 195 | ---- 196 | 197 | ==== 198 | -------------------------------------------------------------------------------- /roles/README.adoc: -------------------------------------------------------------------------------- 1 | = Roles Good Practices for Ansible 2 | 3 | NOTE: this section has been rewritten, using https://github.com/oasis-roles/meta_standards[OASIS metastandards repository] as a starting place. If you have anything to add or review, please comment. 4 | 5 | == Role design considerations 6 | 7 | === Basic design 8 | [%collapsible] 9 | ==== 10 | Explanations:: Design roles focused on the functionality provided, not the software implementation. 11 | 12 | Rationale:: 13 | Try to design roles focused on the functionality, not on the software implementation behind it. 14 | This will help abstracting differences between different providers, and help the user to focus on the functionality, not on technical details. 15 | 16 | Examples:: 17 | For an example, designing a role to implement an NTP configuration on a server would be a role. 18 | The role internally would have the logic to decide whether to use ntpd, chronyd, and the ntp site configurations. 19 | However, when the underlying implementations become too divergent, for example implementing an email server with postfix or sendmail, then 20 | separate roles are encouraged. 21 | 22 | Design roles to accomplish a specific, guaranteed outcome and limit the scope of the role to that outcome. 23 | This will help abstracting differences between different providers (see above), and help the user to focus on the functionality, not on technical details. 24 | ==== 25 | 26 | * Design the interface focused on the functionality, not on the software implementation behind it. 27 | + 28 | [%collapsible] 29 | ==== 30 | Explanations:: Limit the consumer's need to understand specific implementation details about a collection, the role names within, and the file names. Presenting the collection as a low-code/no-code "automation application" provides the developer flexibility as the content grows and matures, and limits change the consumer may have to make in a later version. 31 | 32 | Examples:: 33 | * In the first example, the `mycollection.run` role has been designed to be an entry-point for the execution of multiple roles. `mycollection.run` provides a standard interface for the user, without exposing the details of the underlying automations. The implementation details of `thing_1` and `thing_2` may be changed by the developer without impacting the user as long as the interface of `mycollection.run` does not change. 34 | 35 | .Do this: 36 | [source,yaml] 37 | ---- 38 | 39 | - hosts: all 40 | gather_facts: false 41 | tasks: 42 | - name: Perform several actions 43 | include_role: mycollection.run 44 | vars: 45 | actions: 46 | - name: thing_1 47 | vars: 48 | thing_1_var_1: 1 49 | thing_1_var_2: 2 50 | - name: thing_2 51 | vars: 52 | thing_2_var_1: 1 53 | thing_2_var_2: 2 54 | 55 | ---- 56 | 57 | * In this example, the user must maintain awareness of the structure of the `thing_1` and `thing_2` roles, and the order of operations necessary for these roles to be used together. If the implementation of these roles is changed, the user will need to modify their playbook. 58 | .Don't do this: 59 | [source,yaml] 60 | ---- 61 | - hosts: all 62 | gather_facts: false 63 | tasks: 64 | - name: Do thing_1 65 | include_role: 66 | name: mycollection.thing_1 67 | tasks_from: thing_1.yaml 68 | vars: 69 | thing_1_var_1: 1 70 | thing_1_var_2: 2 71 | - name: Do thing_2 72 | include_role: 73 | name: mycollection.thing_2 74 | tasks_from: thing_2.yaml 75 | vars: 76 | thing_2_var_1: 1 77 | thing_2_var_2: 2 78 | ---- 79 | 80 | ==== 81 | 82 | * Place content common to multiple roles in a single reusable role, "common" is a typical name for this role when roles are packaged in a collection. Author loosely coupled, hierarchical content. 83 | + 84 | [%collapsible] 85 | ==== 86 | Explanations:: Roles that have hard dependencies on external roles or variables have limited flexibility and increased risk that changes to the dependency will result in unexpected behavior or failures. 87 | Coupling describes the degree of dependency between roles and variables that need to act in coordination. 88 | Hierarchical content is an architectural approach to designing your content where individual roles have parent-child relationships in an overall tree structure 89 | 90 | ==== 91 | 92 | === Role Structure 93 | [%collapsible] 94 | ==== 95 | Explanations:: New roles should be initiated in line, with the skeleton directory, which has standard boilerplate code for a Galaxy-compatible 96 | Ansible role and some enforcement around these standards 97 | 98 | Rationale:: A consistent file tree structure will help drive consistency and reusability across the entire environment. 99 | ==== 100 | 101 | === Role Distribution 102 | [%collapsible] 103 | ==== 104 | Explanations:: Use https://semver.org/[semantic versioning] for Git release tags. 105 | Use 0.y.z before the role is declared stable (interface-wise). 106 | 107 | Rationale:: There are some https://github.com/ansible/ansible/issues/67512[restrictions] for Ansible Galaxy and Automation Hub. 108 | The versioning must be in strict X.Y.Z[ab][W] format, where X, Y, and Z are integers. 109 | ==== 110 | 111 | === Naming parameters 112 | [%collapsible] 113 | ==== 114 | * All defaults and all arguments to a role should have a name that begins with the role name to help avoid collision with other names. 115 | Avoid names like `packages` in favor of a name like `foo_packages`. 116 | + 117 | Rationale:: Ansible has no namespaces, doing so reduces the potential for conflicts and makes clear what role a given variable belongs to.) 118 | * Same argument applies for modules provided in the roles, they also need a `$ROLENAME_` prefix: 119 | `foo_module`. While they are usually implementation details and not intended for direct use in playbooks, the unfortunate fact is that importing a role makes them available to the rest of the playbook and therefore creates opportunities for name collisions. 120 | * Moreover, internal variables (those that are not expected to be set by users) are to be prefixed by two underscores: `__foo_variable`. 121 | + 122 | Rationale:: role variables, registered variables, custom facts are usually intended to be local to the role, but in reality are not local to the role - as such a concept does not exist, and pollute the global namespace. 123 | Using the name of the role reduces the potential for name conflicts and using the underscores clearly marks the variables as internals and not part of the common interface. 124 | The two underscores convention has prior art in some popular roles like 125 | https://github.com/geerlingguy/ansible-role-apache/blob/f2b91ac84001db3fd4b43306a8f73f1a54f96f7d/vars/Debian.yml#L8[geerlingguy.ansible-role-apache]). 126 | This includes variables set by set_fact and register, because they persist in the namespace after the role has finished! 127 | * Prefix all tags within a role with the role name or, alternatively, a "unique enough" but descriptive prefix. 128 | * Do not use dashes in role names. This will cause issues with collections. 129 | ==== 130 | 131 | === Providers 132 | [%collapsible] 133 | ==== 134 | When there are multiple implementations of the same functionality, we call them "`providers`". 135 | A role supporting multiple providers should have an input variable called `$ROLENAME_provider`. 136 | If this variable is not defined, the role should detect the currently running provider on the system, and respect it. 137 | 138 | Rationale:: users can be surprised if the role changes the provider if they are running one already. 139 | If there is no provider currently running, the role should select one according to the OS version. 140 | 141 | Example:: on RHEL 7, chrony should be selected as the provider of time synchronization, unless there is ntpd already running on the system, or user requests it specifically. 142 | Chrony should be chosen on RHEL 8 as well, because it is the only provider available. 143 | 144 | The role should set a variable or custom fact called `$ROLENAME_provider_os_default` to the appropriate default value for the given OS version. 145 | 146 | Rationale:: users may want to set all their managed systems to a consistent 147 | state, regardless of the provider that has been used previously. 148 | Setting `$ROLENAME_provider` would achieve it, but is suboptimal, because it requires selecting the appropriate value by the user, and if the user has multiple system versions managed by a single playbook, a common value supported by all of them may not even exist. 149 | Moreover, after a major upgrade of their systems, it may force the users to change their playbooks to change their `$ROLENAME_provider` setting, if the previous value is not supported anymore. 150 | Exporting `$ROLENAME_provider_os_default` allows the users to set `$ROLENAME_provider: "{{ $ROLENAME_provider_os_default }}"` (thanks to the lazy variable evaluation in Ansible) and thus get a consistent setting for all the systems of the given OS version without having to decide what the actual value is - the decision is delegated to the role). 151 | ==== 152 | 153 | === Distributions and Versions 154 | [%collapsible] 155 | ==== 156 | Explanations:: Avoid testing for distribution and version in tasks. 157 | Rather add a variable file to "vars/" for each supported distribution and version with the variables that need to change according to the distribution and version. 158 | 159 | Rationale:: 160 | This way it is easy to add support to a new distribution by simply dropping a new file in to "vars/", see below <>. 161 | See also <> which mandates "Avoid embedding large lists or 'magic values' directly into the playbook." 162 | Since distribution-specific values are kind of "magic values", it applies to them. 163 | The same logic applies for providers: a role can load a provider-specific variable file, include a provider-specific task file, or both, as needed. 164 | Consider making paths to templates internal variables if you need different templates for different distributions. 165 | ==== 166 | 167 | === Package roles in an Ansible collection to simplify distribution and consumption 168 | [%collapsible] 169 | ==== 170 | Rationale:: 171 | Packaging roles as a collection allows you to distribute many roles in a single cohesive unit of re-usable automation. 172 | Inside a collection, you can share custom plugins across all roles in the collection instead of duplicating them in each role’s `library/` directory. 173 | Collections give your roles a namespace, which removes the potential for naming collisions when developing new roles. 174 | 175 | Example:: 176 | See the Ansible documentation on https://docs.ansible.com/ansible/devel/dev_guide/migrating_roles.html[migrating roles to collections] for details. 177 | ==== 178 | 179 | === Check Mode 180 | [%collapsible] 181 | ==== 182 | * The role should work in check mode, meaning that first of all, they should not fail check mode, and they should also not report changes when there are no changes to be done. 183 | If it is not possible to support it, please state the fact and provide justification in the documentation. 184 | This applies to the first run of the role. 185 | * Reporting changes properly is related to the other requirement: *idempotency*. 186 | Roles should not perform changes when applied a second time to the same system with the same parameters, and it should not report that changes have been done if they have not been done. 187 | Due to this, using `command:` is problematic, as it always reports changes. 188 | Therefore, override the result by using `changed_when:` 189 | * Concerning check mode, one usual obstacle to supporting it are registered variables. 190 | If there is a task which registers a variable and this task does not get executed (e.g. because it is a `command:` or another task which is not properly idempotent), the variable will not get registered and further accesses to it will fail (or worse, use the previous value, if the role has been applied before in the play, because variables are global and there is no way to unregister them). 191 | To fix, either use a properly idempotent module to obtain the information (e.g. instead of using `command: cat` to read file into a registered variable, use `slurp` and apply `.content|b64decode` to the result like https://github.com/linux-system-roles/kdump/pull/23/files#diff-d2414d4ec8ba189e1a244b0afc9aa81eL8[here]), or apply proper `check_mode:` and `changed_when:` attributes to the task. 192 | https://github.com/ansible/molecule/issues/128#issue-135906202[more_info]. 193 | * Another problem are commands that you need to execute to make changes. 194 | In check mode, you need to test for changes without actually applying them. 195 | If the command has some kind of "--dry-run" flag to enable executing without making actual changes, use it in check_mode (use the variable `ansible_check_mode` to determine whether we are in check mode). 196 | But you then need to set `changed_when:` according to the command status or output to indicate changes. 197 | See (https://github.com/linux-system-roles/selinux/pull/38/files#diff-2444ad0870f91f17ca6c2a5e96b26823L101) for an example. 198 | * Another problem is using commands that get installed during the install phase, which is skipped in check mode. 199 | This will make check mode fail if the role has not been executed before (and the packages are not there), but does the right thing if check mode is executed after normal mode. 200 | * To view reasoning for supporting why check mode in first execution may not be worthwhile: see https://github.com/ansible/molecule/issues/128#issuecomment-245009843[here]. 201 | If this is to be supported, see https://github.com/linux-system-roles/timesync/issues/27#issuecomment-472466223[hhaniel's proposal], which seems to properly guard even against such cases. 202 | ==== 203 | 204 | === Idempotency 205 | [%collapsible] 206 | ==== 207 | Explanations:: Reporting changes properly is related to the other requirement: *idempotency*. Roles should not perform changes when applied a second time to the same system with the same parameters, and it should not report that changes have been done if they have not been done. Due to this, using `command:` is problematic, as it always reports changes. Therefore, override the result by using `changed_when:` 208 | Rationale:: Additional automation or other integrations, such as with external ticketing systems, should rely on the idempotence of the ansible role to report changes accurately 209 | ==== 210 | 211 | === Supporting multiple distributions and versions 212 | [%collapsible] 213 | ==== 214 | Use Cases:: 215 | * The role developer needs to be able to set role variables to different values depending on the OS platform and version. For example, if the name of a service is different between EL8 and EL9, or a config file location is different. 216 | * The role developer needs to handle the case where the user specifies `gather_facts: false` in the playbook. 217 | * The role developer needs to access the platform specific vars in role integration tests without making a copy. 218 | 219 | NOTE: The recommended solution below requires at least some `ansible_facts` to be defined, and so relies on gathering some facts. 220 | If you just want to ensure the user always uses `gather_facts: true`, and do not want to handle this in the role, then the role documentation should state that `gather_facts: true` or `setup:` is required in order to use the role, and the role should use `fail:` with a descriptive error message if the necessary facts are not defined. 221 | 222 | If it is desirable to use roles that require facts, but fact gathering is expensive, consider using a cache plugin https://docs.ansible.com/ansible/latest/collections/index_cache.html[List of Cache Plugins], and also consider running a periodic job on the controller to refresh the cache. 223 | ==== 224 | 225 | === Platform specific variables 226 | [%collapsible] 227 | ==== 228 | Explanations:: You normally use `vars/main.yml` (automatically included) to set variables used by your role. 229 | If some variables need to be parameterized according to distribution and version (name of packages, configuration file paths, names of services), use this in the beginning of your `tasks/main.yml`: 230 | Examples:: 231 | 232 | [source,yaml] 233 | ---- 234 | - name: Ensure ansible_facts used by role 235 | setup: 236 | gather_subset: min 237 | when: not ansible_facts.keys() | list | 238 | intersect(__rolename_required_facts) == __rolename_required_facts 239 | 240 | - name: Set platform/version specific variables 241 | include_vars: "{{ __rolename_vars_file }}" 242 | loop: 243 | - "{{ ansible_facts['os_family'] }}.yml" 244 | - "{{ ansible_facts['distribution'] }}.yml" 245 | - "{{ ansible_facts['distribution'] }}_{{ ansible_facts['distribution_major_version'] }}.yml" 246 | - "{{ ansible_facts['distribution'] }}_{{ ansible_facts['distribution_version'] }}.yml" 247 | vars: 248 | __rolename_vars_file: "{{ role_path }}/vars/{{ item }}" 249 | when: __rolename_vars_file is file 250 | ---- 251 | 252 | * Add this as the first task in `tasks/main.yml`: 253 | + 254 | [source,yaml] 255 | ---- 256 | - name: Set platform/version specific variables 257 | include_tasks: tasks/set_vars.yml 258 | ---- 259 | 260 | * Add files to `vars/` for the required OS platforms and versions. 261 | 262 | The files in the `loop` are in order from least specific to most specific: 263 | 264 | * `os_family` covers a group of closely related platforms (e.g. `RedHat` covers RHEL, CentOS, Fedora) 265 | * `distribution` (e.g. `Fedora`) is more specific than `os_family` 266 | * ``distribution``_``distribution_major_version`` (e.g. `RedHat_8`) is more specific than `distribution` 267 | * ``distribution``_``distribution_version`` (e.g. `RedHat_8.3`) is the most specific 268 | 269 | See https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html#ansible-facts-distribution[Commonly Used Facts] for an explanation of the facts and their common values. 270 | 271 | Each file in the `loop` list will allow you to add or override variables to specialize the values for platform and/or version. 272 | Using the `when: item is file` test means that you do not have to provide all of the `vars/` files, only the ones you need. 273 | For example, if every platform except Fedora uses `srv_name` for the service name, you can define `myrole_service: srv_name` in `vars/main.yml` then define `myrole_service: srv2_name` in `vars/Fedora.yml`. 274 | In cases where this would lead to duplicate vars files for similar distributions (e.g. CentOS 7 and RHEL 7), use symlinks to avoid the duplication. 275 | 276 | NOTE: With this setup, files can be loaded twice. 277 | For example, on Fedora, the `distribution_major_version` is the same as `distribution_version` so the file `vars/Fedora_31.yml` will be loaded twice if you are managing a Fedora 31 host. 278 | If `distribution` is `RedHat` then `os_family` will also be `RedHat`, and `vars/RedHat.yml` will be loaded twice. 279 | This is usually not a problem - you will be replacing the variable with the same value, and the performance hit is negligible. 280 | If this is a problem, construct the file list as a list variable, and filter the variable passed to `loop` using the `unique` filter (which preserves the order): 281 | 282 | [source,yaml] 283 | ---- 284 | - name: Set vars file list 285 | set_fact: 286 | __rolename_vars_file_list: 287 | - "{{ ansible_facts['os_family'] }}.yml" 288 | - "{{ ansible_facts['distribution'] }}.yml" 289 | - "{{ ansible_facts['distribution'] }}_{{ ansible_facts['distribution_major_version'] }}.yml" 290 | - "{{ ansible_facts['distribution'] }}_{{ ansible_facts['distribution_version'] }}.yml" 291 | 292 | - name: Set platform/version specific variables 293 | include_vars: "{{ __rolename_vars_file }}" 294 | loop: "{{ __rolename_vars_file_list | unique | list }}" 295 | vars: 296 | __rolename_vars_file: "{{ role_path }}/vars/{{ item }}" 297 | when: __rolename_vars_file is file 298 | ---- 299 | 300 | Or define your `__rolename_vars_file_list` in your `vars/main.yml`. 301 | 302 | The task `Ensure ansible_facts used by role` handles the case where the user specifies `gather_facts: false` in the playbook. 303 | It gathers *only* the facts required by the role. 304 | The role developer may need to add additional facts to the list, and use a different `gather_subset`. 305 | See https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html#setup-module[Setup Module] for more information. 306 | Gathering facts can be expensive, so gather *only* the facts required by the role. 307 | 308 | Using a separate task file for `tasks/set_vars.yml` allows role integration tests to access the internal variables. 309 | For example, if the role developer wants to pre-populate a VM with the packages used by the role, the following tasks can be used: 310 | 311 | [source,yaml] 312 | ---- 313 | - hosts: all 314 | tasks: 315 | - name: Set platform/version specific variables 316 | include_role: 317 | name: my.fqcn.rolename 318 | tasks_from: set_vars.yml 319 | public: true 320 | 321 | - name: Install test packages 322 | package: 323 | name: "{{ __rolename_packages }}" 324 | state: present 325 | 326 | ---- 327 | 328 | In this way, the role developer does not have to copy and maintain a separate list of role packages. 329 | ==== 330 | 331 | === Platform specific tasks 332 | [%collapsible] 333 | ==== 334 | Platform specific tasks, however, are different. 335 | You probably want to perform platform specific tasks once, for the most specific match. 336 | In that case, use `lookup('first_found')` with the file list in order of most specific to least specific, including a "default": 337 | 338 | [source,yaml] 339 | ---- 340 | - name: Perform platform/version specific tasks 341 | include_tasks: "{{ lookup('first_found', __rolename_ff_params) }}" 342 | vars: 343 | __rolename_ff_params: 344 | files: 345 | - "{{ ansible_facts['distribution'] }}_{{ ansible_facts['distribution_version'] }}.yml" 346 | - "{{ ansible_facts['distribution'] }}_{{ ansible_facts['distribution_major_version'] }}.yml" 347 | - "{{ ansible_facts['distribution'] }}.yml" 348 | - "{{ ansible_facts['os_family'] }}.yml" 349 | - "default.yml" 350 | paths: 351 | - "{{ role_path }}/tasks/setup" 352 | ---- 353 | 354 | Then you would provide `tasks/setup/default.yml` to do the generic setup, and e.g. `tasks/setup/Fedora.yml` to do the Fedora specific setup. 355 | The `tasks/setup/default.yml` is required in order to use `lookup('first_found')`, which will give an error if no file is found. 356 | 357 | If you want to have the "use first file found" semantics, but do not want to have to provide a default file, add `skip: true`: 358 | 359 | [source,yaml] 360 | ---- 361 | - name: Perform platform/version specific tasks 362 | include_tasks: "{{ lookup('first_found', __rolename_ff_params) }}" 363 | vars: 364 | __rolename_ff_params: 365 | files: 366 | - "{{ ansible_facts['distribution'] }}_{{ ansible_facts['distribution_version'] }}.yml" 367 | - "{{ ansible_facts['os_family'] }}.yml" 368 | paths: 369 | - "{{ role_path }}/tasks/setup" 370 | skip: true 371 | ---- 372 | 373 | *NOTE*: 374 | 375 | * Use `include_tasks` or `include_vars` with `lookup('first_found')` instead of `with_first_found`. 376 | `loop` is not needed - the include forms take a string or a list directly. 377 | * Always specify the explicit, absolute path to the files to be included, 378 | using `{{ role_path }}/vars` or `{{ role_path }}/tasks`, when using these 379 | idioms. 380 | See below "Ansible Best Practices" for more information. 381 | * Use the `ansible_facts['name']` bracket notation rather than the `ansible_facts.name` or `ansible_name` form. 382 | For example, use `ansible_facts['distribution']` instead of `ansible_distribution` or `ansible.distribution`. 383 | The `ansible_name` form relies on fact injection, which can break if there is already a fact of that name. 384 | Also, the bracket notation is what is used in Ansible documentation such as https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html#ansible-facts-distribution[Commonly Used Facts] and https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#operating-system-and-distribution-variance[Operating System and Distribution Variance]. 385 | ==== 386 | 387 | === Supporting multiple providers 388 | [%collapsible] 389 | ==== 390 | Use a task file per provider and include it from the main task file, like this example from `storage:` 391 | 392 | [source,yaml] 393 | ---- 394 | - name: include the appropriate provider tasks 395 | include_tasks: "main_{{ storage_provider }}.yml" 396 | ---- 397 | 398 | The same process should be used for variables (not defaults, as defaults can 399 | not be loaded according to a variable). 400 | You should guarantee that a file exists for each provider supported, or use an explicit, absolute path using `role_path`. 401 | See below "Ansible Best Practices" for more information. 402 | ==== 403 | 404 | === Generating files from templates 405 | [%collapsible] 406 | ==== 407 | * Add ``{{ ansible_managed | comment }}`` at the top of the template file to indicate that the file is managed by Ansible roles, while making sure that multi-line values are properly commented. 408 | For more information, see https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#adding-comments-to-files[Adding comments to files]. 409 | * When commenting, don't include anything like "Last modified: {{ date }}". 410 | This would change the file at every application of the role, even if it doesn't need to be changed for other reasons, and thus break proper change reporting. 411 | * Use standard module parameters for backups, keep it on unconditionally (`backup: true`), until there is a user request to have it configurable. 412 | * Make prominently clear in the HOWTO (at the top) what settings/configuration files are replaced by the role instead of just modified. 413 | + 414 | * Use `{{ role_path }}/subdir/` as the filename prefix when including files if the name has a variable in it. 415 | + 416 | Rationale:: your role may be included by another role, and if you specify a relative path, the file could be found in the including role. 417 | For example, if you have something like `include_vars: "{{ ansible_facts['distribution'] }}.yml"` and you do not provide every possible `vars/{{ ansible_facts['distribution'] }}.yml` in your role, Ansible will look in the including role for this file. 418 | Instead, to ensure that only your role will be referenced, use `include_vars: "{{role_path}}/vars/{{ ansible_facts['distribution'] }}.yml"`. 419 | Same with other file based includes such as `include_tasks`. 420 | See https://docs.ansible.com/ansible/latest/dev_guide/overview_architecture.html#the-ansible-search-path[Ansible Developer Guide » Ansible architecture » The Ansible Search Path] for more information. 421 | ==== 422 | 423 | === Vars vs Defaults 424 | [%collapsible] 425 | ==== 426 | * Avoid embedding large lists or "magic values" directly into the playbook. 427 | Such static lists should be placed into the `vars/main.yml` file and named appropriately 428 | * Every argument accepted from outside of the role should be given a default value in `defaults/main.yml`. 429 | This allows a single place for users to look to see what inputs are expected. 430 | Document these variables in the role's README.md file copiously 431 | * Use the `defaults/main.yml` file in order to avoid use of the default Jinja2 filter within a playbook. 432 | Using the default filter is fine for optional keys on a dictionary, but the variable itself should be defined in `defaults/main.yml` so that it can have documentation written about it there and so that all arguments can easily be located and identified. 433 | * Don't define defaults in `defaults/main.yml` if there is no meaningful default. 434 | It is better to have the role fail if the variable isn't defined than have it do something dangerously wrong. 435 | Still do add the variable to `defaults/main.yml` but _commented out_, so that there is one single source of input variables. 436 | * Avoid giving default values in `vars/main.yml` as such values are very high in the precedence order and are difficult for users and consumers of a role to override. 437 | * As an example, if a role requires a large number of packages to install, but could also accept a list of additional packages, then the required packages should be placed in `vars/main.yml` with a name such as `foo_packages`, and the extra packages should be passed in a variable named `foo_extra_packages`, which should default to an empty array in `defaults/main.yml` and be documented as such. 438 | ==== 439 | 440 | === Documentation conventions 441 | [%collapsible] 442 | ==== 443 | * Use fully qualified role names in examples, like: `linux-system-roles.$ROLENAME` (with the Galaxy prefix). 444 | * Use RFC https://tools.ietf.org/html/rfc5737[5737], https://tools.ietf.org/html/rfc7042#section-2.1.1[7042] and https://tools.ietf.org/html/rfc3849[3849] addresses in examples. 445 | ==== 446 | 447 | === Create a meaningful README file for every role 448 | [%collapsible] 449 | ==== 450 | Rationale:: 451 | The documentation is essential for the success of the content. 452 | Place the README file in the root directory of the role. 453 | The README file exists to introduce the user to the purpose of the role and any important information on how to use it, such as credentials that are required. 454 | 455 | At minimum include: 456 | 457 | * Example playbooks that allow users to understand how the developed content works are also part of the documentation. 458 | * The inbound and outbound role argument specifications 459 | * List of user-facing capabilities within the role 460 | * The unit of automation the role provides 461 | * The outcome of the role 462 | * The roll-back capabilities of the role 463 | * Designation of the role as idempotent (True/False) 464 | * Designation of the role as atomic if applicable (True/False) 465 | ==== 466 | 467 | === Don't use host group names or at least make them a parameter 468 | [%collapsible] 469 | ==== 470 | Explanations:: 471 | It is relatively common to use (inventory) group names in roles: 472 | + 473 | * either to loop through the hosts in the group, generally in a cluster context 474 | * or to validate that a host is in a specific group 475 | + 476 | Instead, store the host name(s) in a (list) variable, or at least make the group name a parameter of your role. 477 | You can always set the variable at group level to avoid repetitions. 478 | 479 | Rationale:: 480 | Groups are a feature of the data in your inventory, meaning that you mingle data with code when you use those groups in your code. 481 | Rely on the inventory-parsing process to provide your code with the variables it needs instead of enforcing a specific structure of the inventory. 482 | Not all inventory sources are flexible enough to provide exactly the expected group name. 483 | Even more importantly, in a cluster context for example, if the group name is fixed, you can't describe (and hence automate) more than one cluster in each inventory. 484 | You can't possibly have multiple groups with the same name in the same inventory. 485 | On the other hand, variables can have any kind of value for each host, so that you can have as many clusters as you want. 486 | 487 | Examples:: 488 | Assuming we have the following inventory (not according to recommended practices for sake of simplicity): 489 | + 490 | .An inventory with two clusters 491 | [source,ini] 492 | ---- 493 | include::dont_use_groups/inventory[] 494 | ---- 495 | + 496 | We can then use one of the following three approaches in our role (here as playbook, again for sake of simplicity): 497 | + 498 | .A playbook showing how to loop through a group 499 | [source,yaml] 500 | ---- 501 | include::dont_use_groups/playbook.yml[] 502 | ---- 503 | + 504 | The first approach is probably best to create a cluster configuration file listing all cluster's hosts. 505 | The other approaches are good to make sure each action is performed only once, but this comes at the price of many skips. 506 | The second one fails if the first host isn't reachable (which might be what you'd want anyway), and the last one has the best chance to be executed once and only once, even if some hosts aren't available. 507 | + 508 | TIP: the variable `cluster_group_name` could have a default group name value in your role, of course properly documented, for simple use cases. 509 | + 510 | Overall, it is best to avoid this kind of constructs if the use case permits, as they are clumsy. 511 | ==== 512 | 513 | === Prefix task names in sub-tasks files of roles 514 | [%collapsible] 515 | ==== 516 | Explanation:: It is a common practice to have `tasks/main.yml` file including other tasks files, which we'll call sub-tasks files. 517 | Make sure that the tasks' names in these sub-tasks files are prefixed with a shortcut reminding of the sub-tasks file's name. 518 | 519 | Rationale:: Especially in a complex role with multiple (sub-)tasks file, it becomes difficult to understand which task belongs to which file. 520 | Adding a prefix, in combination with the role's name automatically added by Ansible, makes it a lot easier to follow and troubleshoot a role play. 521 | 522 | Examples:: In a role with one `tasks/main.yml` task file, including `tasks/sub.yml`, the tasks in this last file would be named as follows: 523 | + 524 | .A prefixed task in a sub-tasks file 525 | [source,yaml] 526 | ---- 527 | - name: sub | Some task description 528 | mytask: [...] 529 | ---- 530 | + 531 | The log output will then look something like `TASK [myrole : sub | Some task description] ****`, which makes it very clear where the task is coming from. 532 | + 533 | TIP: with a verbosity of 2 or more, ansible-playbook will show the full path to the task file, but this generally means that you need to restart the play in a higher verbosity to get the information you could have had readily available. 534 | ==== 535 | 536 | === Argument Validation 537 | [%collapsible] 538 | ==== 539 | Explanation:: Starting from ansible version 2.11, an option is available to activate argument validation for roles by utilizing an argument specification. 540 | When this specification is established, a task is introduced at the onset of role execution to validate the parameters provided for the role according to the defined specification. 541 | If the parameters do not pass the validation, the role execution will terminate. 542 | 543 | Rationale:: Argument validation significantly contributes to the stability and reliability of the automation. 544 | It also makes the playbook using the role fail fast instead of failing later when an incorrect variable is utilized. 545 | By ensuring roles receive accurate input data and mitigating common issues, we can enhance the effectiveness of the Ansible playbooks using the roles. 546 | 547 | Examples:: The specification is defined in the meta/argument_specs.yml. For more details on how to write the specification, refer to https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#specification-format. 548 | + 549 | .Argument Specification file that validates the arguments provided to the role. 550 | [source,yaml] 551 | ---- 552 | argument_specs: 553 | main: 554 | short_description: Role description. 555 | options: 556 | string_arg1: 557 | description: string argument description. 558 | type: "str" 559 | default: "x" 560 | choices: ["x", "y"] 561 | dict_arg1: 562 | description: dict argument description. 563 | type: dict 564 | required: True 565 | options: 566 | key1: 567 | description: key1 description. 568 | type: int 569 | key2: 570 | description: key2 description. 571 | type: str 572 | key3: 573 | description: key3 description. 574 | type: dict 575 | ---- 576 | ==== 577 | 578 | == References 579 | [%collapsible] 580 | ==== 581 | Links that contain additional standardization information that provide context, 582 | inspiration or contrast to the standards described above. 583 | 584 | * https://github.com/debops/debops/blob/v0.7.2/docs/debops-policy/code-standards-policy.rst). 585 | For inspiration, as the DebOps project has some specific guidance that we do not necessarily want to follow. 586 | * https://adfinis.github.io/ansible-guide/styling_guide.html 587 | * https://docs.openstack.org/openstack-ansible/latest/contributors/code-rules.html#general-guidelines-for-submitting-code 588 | 589 | ==== 590 | -------------------------------------------------------------------------------- /roles/dont_use_groups/inventory: -------------------------------------------------------------------------------- 1 | [cluster_group_A] 2 | host1 ansible_host=localhost 3 | host2 ansible_host=localhost 4 | host3 ansible_host=localhost 5 | 6 | [cluster_group_B] 7 | host4 ansible_host=localhost 8 | host5 ansible_host=localhost 9 | host6 ansible_host=localhost 10 | 11 | [cluster_group_A:vars] 12 | cluster_group_name=cluster_group_A 13 | 14 | [cluster_group_B:vars] 15 | cluster_group_name=cluster_group_B 16 | -------------------------------------------------------------------------------- /roles/dont_use_groups/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Show how to loop through a set of groups 3 | hosts: cluster_group_? 4 | gather_facts: false 5 | become: false 6 | tasks: 7 | - name: The loop happens for each host, might be too much 8 | ansible.builtin.debug: 9 | msg: do something with {{ item }} 10 | loop: "{{ groups[cluster_group_name] }}" 11 | 12 | - name: The loop happens only for the first host in each group 13 | ansible.builtin.debug: 14 | msg: do something with {{ item }} 15 | loop: "{{ groups[cluster_group_name] }}" 16 | when: inventory_hostname == groups[cluster_group_name][0] 17 | 18 | - name: Make the first host of each group fail to simulate non-availability 19 | ansible.builtin.assert: 20 | that: inventory_hostname != groups[cluster_group_name][0] 21 | 22 | - name: The loop happens only for the first _available_ host in each group 23 | ansible.builtin.debug: 24 | msg: do something with {{ item }} 25 | loop: "{{ groups[cluster_group_name] }}" 26 | when: >- 27 | inventory_hostname == (groups[cluster_group_name] 28 | | intersect(ansible_play_hosts))[0] 29 | -------------------------------------------------------------------------------- /roles/dont_use_groups/playbook2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # this approach doesn't fit a role, but is too good to not keep it 3 | - name: Show how to loop through a set of groups, avoiding skips 4 | hosts: cluster_group_? 5 | gather_facts: false 6 | become: false 7 | tasks: 8 | - name: Add last host to execution group (why not the last one?) 9 | ansible.builtin.add_host: 10 | name: "{{ groups[item][-1] }}" 11 | groups: cluster_execution_group 12 | loop: "{{ groups | select('match', '^cluster_group_.$') | list }}" 13 | 14 | - name: Now execute something on each first host of each cluster 15 | hosts: cluster_execution_group 16 | gather_facts: false 17 | become: false 18 | 19 | tasks: 20 | - name: The loop happens for each execution host 21 | ansible.builtin.debug: 22 | msg: do something with {{ item }} 23 | loop: "{{ groups[cluster_group_name] }}" 24 | -------------------------------------------------------------------------------- /roles/prefix_subtasks/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Show how prefixing sub-tasks files help 3 | hosts: localhost 4 | gather_facts: false 5 | become: false 6 | 7 | roles: 8 | - prefix_show 9 | -------------------------------------------------------------------------------- /roles/prefix_subtasks/roles/prefix_show/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for prefix_show 3 | - name: Show where we are 4 | ansible.builtin.debug: 5 | msg: we're now in the main task file 6 | - name: Include the sub task file without prefix 7 | ansible.builtin.include_tasks: 8 | file: sub_noprefix.yml 9 | - name: Include the sub task file with prefix 10 | ansible.builtin.include_tasks: 11 | file: sub_prefix.yml 12 | - name: Show where we are 13 | ansible.builtin.debug: 14 | msg: we're back in the main task file 15 | -------------------------------------------------------------------------------- /roles/prefix_subtasks/roles/prefix_show/tasks/sub_noprefix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for prefix_show 3 | - name: sub_noprefix | Do not show where we are 4 | ansible.builtin.debug: 5 | msg: we're now in a sub task file but which one? 6 | -------------------------------------------------------------------------------- /roles/prefix_subtasks/roles/prefix_show/tasks/sub_prefix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for prefix_show 3 | - name: sub_prefix | Show where we are 4 | ansible.builtin.debug: 5 | msg: we're now in a sub task file but which one? 6 | -------------------------------------------------------------------------------- /structures/README.adoc: -------------------------------------------------------------------------------- 1 | = Automation structures 2 | 3 | Before we start to describe roles, playbooks, etc, we need to decide which one we use for what. 4 | This section is meant for topics which span across multiple structures and don't fit nicely within one. 5 | 6 | == Guiding principles for Automation Good Practices 7 | [%collapsible] 8 | ==== 9 | As an overall guiding principle for designing good automation, inspired by the https://peps.python.org/pep-0020/[Zen of Python, by Tim Peters], the Zen of Ansible was created to serve as a guidepost to follow. 10 | The principles defined in this are very applicable and can give guidance when the specific practice is unclear. 11 | 12 | ---- 13 | The Zen of Ansible, by Tim Appnel 14 | 15 | Ansible is not Python. 16 | YAML sucks for coding. 17 | Playbooks are not for programming. 18 | Ansible users are (most probably) not programmers. 19 | Clear is better than cluttered. 20 | Concise is better than verbose. 21 | Simple is better than complex. 22 | Readability counts. 23 | Helping users get things done matters most. 24 | User experience beats ideological purity. 25 | “Magic” conquers the manual. 26 | When giving users options, always use convention over configuration. 27 | Declarative is always better than imperative - most of the time. 28 | Focus avoids complexity. 29 | Complexity kills productivity. 30 | If the implementation is hard to explain, it's a bad idea. 31 | Every shell command and UI interaction is an opportunity to automate. 32 | Just because something works, doesn't mean it can't be improved. 33 | Friction should be eliminated whenever possible. 34 | Automation is a continuous journey that never ends. 35 | 36 | ---- 37 | ==== 38 | 39 | == Define which structure to use for which purpose 40 | [%collapsible] 41 | ==== 42 | Explanations:: 43 | define for which use case to use roles, playbooks, potentially workflows (in Ansible Controller/Tower/AWX), and how to split the code you write. 44 | 45 | Rationale:: 46 | especially when writing automation in a team, it is important to have a certain level of consistency and make sure everybody has the same understanding. 47 | By lack of doing so, your automation becomes unreadable and difficult to grasp for new members or even for existing members. 48 | + 49 | Following a consistent structure will increase re-usability. 50 | If one team member uses roles where another one uses playbooks, they will both struggle to reuse the code of each other. 51 | Metaphorically speaking, only if stones have been cut at roughly the same size, can they be properly used to build a house. 52 | 53 | Examples:: 54 | The following is only one example of how to structure your content but has proven robust enough on multiple occasions. 55 | + 56 | .Structure of Automation 57 | image::ansible_structures.svg[a hierarchy of landscape type function and component] 58 | + 59 | * a _landscape_ is anything you want to deploy at once, the underlay of your cloud, a three tiers application, a complete application cluster... 60 | This level is represented at best by a Controller/Tower/AWX workflow, potentially by a "playbook of playbooks", i.e. a playbook made of imported _type_ playbooks, as introduced next. 61 | * a _type_ must be defined such that each managed host has one and only one type, applicable using a unique playbook. 62 | * each type is then made of multiple _functions_, represented by roles, so that the same function used by the same _type_ can be re-used, written only once. 63 | * and finally _components_ are used to split a _function_ in maintainable bits. By default a component is a task file within the _function_-role, if the role becomes too big, there is a case for splitting the _function_ role into multiple _component_ roles. 64 | + 65 | NOTE: if _functions_ are defined mostly for re-usability purposes, _components_ are more used for maintainability / readability purposes. A re-usable component might be a candidate for promotion to a function. 66 | + 67 | Let's have a more concrete example to clarify: 68 | + 69 | * as already written, a _landscape_ could be a three tier application with web-front-end, middleware and database. 70 | We would probably create a workflow to deploy this landscape at once. 71 | * our types would be relatively obvious here as we would have "web-front-end server", "middleware server" and "database server". 72 | Each type can be fully deployed by one and only one playbook (avoid having numbered playbooks and instructions on how to call them one after the other). 73 | * each server type is then made up of one or more _functions_, each implemented as a role. 74 | For example, the middleware server type could be made of a "virtual machine" (to create the virtual machine hosting the middleware server), a "base Linux OS" and a "JBoss application server" function. 75 | * and then the base OS role could be made of multiple components (DNS, NTP, SSH, etc), each represented by a separate `tasks/{component}.yml` file, included or imported from the `tasks/main.yml` file of the _function_-role. 76 | If a component becomes too big to fit within one task file, it might make sense that it gets its own component-role, included from the function-role. 77 | + 78 | NOTE: in terms of re-usability, see how you could simply create a new "integrated three tiers server" type (e.g. for test purposes), by just re-combining the "virtual machine", "base Linux OS", "JBoss application server", "PostgreSQL database" and "Apache web-server" function-roles into one new playbook. 79 | 80 | Beware that those rules, once defined, shouldn't be applied too strictly. 81 | There can always be reasons for breaking the rules, and sometimes requires discussion with your team to decide what is more important. 82 | For example if a "hardened Linux OS" and a "normal Linux OS" are two different functions, or the same function with different parameters. You could consider SSH to be a function on its own and not a component of the base OS. 83 | Also, external re-usable roles and collections, obviously not respecting your rules, might force you to bend them. 84 | Important is to break the rules not by ignorance of those but because of good and practical reasons. 85 | Respecting the rules is to know and acknowledge them, not to follow them blindly even if they don't make sense. 86 | As long as exceptions are discussed openly in the team, they won't hurt. 87 | ==== 88 | --------------------------------------------------------------------------------