--- - name: Data manipulation tags: - data_manipulation - never hosts: localhost connection: local gather_facts: false tasks: - name: Return the input data's type tags: type_return ansible.builtin.set_fact: should_return_str: "{{ 'this' | type_debug }}" - name: Test types tags: type_test ansible.builtin.set_fact: # strings are classified as 'string', 'iterable' and 'sequence', but not 'mapping' aa_is_string: "{{ 'aa' is string }}" aa_is_iterable: "{{ 'aa' is iterable }}" aa_is_sequence: "{{ 'aa' is sequence }}" # numbers are classified as 'numbers', with 'integer' and 'float' being subclasses i42_is_number: "{{ 42 is number }}" i5_is_integer: "{{ 5 is integer }}" f21_34_is_number: "{{ 21.34 is number }}" f12_1_is_float: "{{ 12.1 is float }}" # lists are classified as 'iterable' and 'sequence', but not as 'string' nor 'mapping' list_is_iterable: "{{ ['list'] is iterable }}" list_is_sequence: "{{ ['list'] is sequence }}" list_is_string: "{{ ['list'] is string }}" list_is_mapping: "{{ ['list'] is mapping }}" # dictionaries are classified as 'iterable', 'sequence' and 'mapping', but not as 'string' dict_is_iterable: "{{ {'a': 'dict'} is iterable }}" dict_is_sequence: "{{ {'a': 'dict'} is sequence }}" dict_is_mapping: "{{ {'a': 'dict'} is mapping }}" dict_is_string: "{{ {'a': 'dict'} is string }}" # native booleans true_is_boolean: "{{ true is boolean }}" upper_true_is_boolean: "{{ True is boolean }}" false_is_boolean: "{{ false is boolean }}" upper_false_is_boolean: "{{ False is boolean }}" # null is None in ansible aa_is_not_null: "{{ 'aa' != None }}" aa_is_not_null_nor_empty: "{{ 'aa' not in [ None, '' ] }}" - name: Convert between types tags: type_conversion ansible.builtin.set_fact: string_to_int_is_integer: "{{ 'string' | int is integer }}" string_to_float_is_float: "{{ 'string' | float is float }}" integer_to_float_is_float: "{{ 12 | float is float }}" float_to_int_is_integer: "{{ 21.02 | int is integer }}" integer_to_string_is_string: "{{ 43 | string is string }}" float_to_string_is_string: "{{ 74.93 | string is string }}" integer_to_bool_is_boolean: "{{ 4 | bool is boolean }}" - name: Test truthfulness tags: truthfulness ansible.builtin.set_fact: this_is_true: true this_is_false: false this_is_true_again: "{{ not false }}" true_is_truthy: "{{ true is truthy }}" false_is_falsy: "{{ false is falsy }}" - name: Elvis operator tags: - elvis_operator - ternary # (condition) | ternary(value_for_true_condition, value_for_false_condition, optional_value_for_null_condition) ansible.builtin.set_fact: acme_directory: >- {{ this_is_a_test_run | default(true) | bool | ternary( 'https://acme-staging-v02.api.letsencrypt.org/directory', 'https://acme-v02.api.letsencrypt.org/directory' ) }} - name: Manipulate strings tags: string_manipulation vars: module_output: >- u001b]0;@smth:/u0007{ "failed": 0, "started": 1, "finished": 0, "ansible_job_id": "j968817333249.114504", "results_file": "/home/ssm-user/.ansible_async/j968817333249.114504", "_ansible_suppress_tmpdir_delete": true }\r\r pattern: >- {{ '"failed": 0, "started": 1, "finished": 0' | regex_escape() }} ansible.builtin.set_fact: first_letter_to_uppercase: "{{ 'all_lowercase' | capitalize }}" something_replaced: "{{ 'dots.to.dashes' | replace('.','-') }}" split_string: "{{ 'testMe@example.com' | split('@') | first }}" pattern_replaced: >- {{ '*.domain.com...' | regex_replace('*' | regex_escape, 'star') | regex_replace('\.+$', '') }} pattern_is_anywhere_in_module_output: "{{ module_output is search(pattern) }}" pattern_is_at_the_beginning_of_string: "{{ 'sator arepo tenet opera rotas' is match('sator arepo') }}" regex_is_anywhere_in_string: "{{ 'sator arepo tenet opera rotas' is regex('\\stenet\\s') }}" first_substr_matching_regex: "{{ 'sator arepo tenet opera rotas' | regex_search('\\stenet\\s') }}" password_obfuscated: "{{ 'sensitiveString' | regex_replace('^(.{2}).*(.{2})$', '\\1…\\2') }}" value_from_json_string_in_module_output: >- {{ 'ansible_job_id' | extract(module_output | regex_search('{.*}') | from_json) }} base64_encoded_string: "{{ 'some string' | ansible.builtin.b64encode }}" base64_decoded_string: "{{ 'c29tZSBzdHJpbmc=' | ansible.builtin.b64decode }}" - name: Manipulate lists tags: list_manipulation block: - name: Add elements to lists vars: programming_languages: - C - Python ansible.builtin.set_fact: programming_languages: "{{ programming_languages + ['Ruby'] }}" - name: Remove elements from lists vars: dbs_list: ['primary', 'sales'] ansible.builtin.set_fact: list_without_items: "{{ dbs_list | difference(['template0','template1','postgres','rdsadmin']) }}" - name: Get elements ansible.builtin.set_fact: random_item: "{{ ['a','b','c'] | random }}" last_item_array_mode: "{{ ['a','b','c'][-1] }}" last_item_filter: "{{ ['a','b','c'] | last }}" first_item_filter: "{{ ['a','b','c'] | first }}" - name: Sort dict elements in lists by attribute tags: order_by vars: snapshots: - name: sales create_time: '2024-06-25T00:52:55.127000+00:00' - name: test create_time: '2024-05-17T01:53:12.103220+00:00' ansible.builtin.set_fact: snapshot_latest: "{{ snapshots | sort(attribute='create_time') | last }}" - name: Give back the first not null value tags: coalesce vars: list_with_null_values: - null - null - something - something else ansible.builtin.set_fact: first_non_null_value: "{{ list_with_null_values | select | first }}" - name: Get values for a specific attribute in a list of dictionaries ansible.builtin.set_fact: vpc_security_group_ids: >- {{ instance_information.vpc_security_groups | map(attribute='vpc_security_group_id') }} volume_ids: >- {{ instances_information.instances[0].block_device_mappings | map(attribute='ebs.volume_id') }} - name: Return only elements with specific attributes matching a filter ansible.builtin.set_fact: available_rds_snapshots: "{{ snapshots_list | selectattr('status', 'equalto', 'available') }}" mounts_with_path: "{{ ansible_facts.mounts | selectattr('mount', 'in', path) }}" - name: Return all elements *but* the ones with specific attributes matching a filter ansible.builtin.set_fact: available_rds_snapshots: "{{ snapshots_list | rejectattr('status', 'equalto', 'creating') }}" mounts_without_path: "{{ ansible_facts.mounts | rejectattr('mount', 'in', path) }}" - name: Remove lines about RDS protected users and permissions from a dump file # remove empty lines # remove comments # remove creation of the master user # remove anything involving 'rdsadmin' # remove changes to protected RDS users # remove protected 'superuser' and 'replication' assignments vars: # **Hack notice**: Ansible has issues with splitting on new lines if this template is quoted differently permissions_dump_content_as_lines: "{{ dump_file.content | ansible.builtin.b64decode | split('\n') }}" master_username: postgresql ansible.builtin.set_fact: permissions_commands: >- {{ permissions_dump_content_as_lines | reject('match', '^$') | reject('match', '^--') | reject('match', '^CREATE ROLE ' + master_username) | reject('match', '.*rdsadmin.*') | reject('match', '^(CREATE|ALTER) ROLE rds_') | map('regex_replace', '(NO)(SUPERUSER|REPLICATION)\s?', '') }} - name: Manipulate dictionaries tags: dictionary_manipulation vars: organization: address: 123 common lane id: 123abc block: - name: Add keys to dictionaries ansible.builtin.set_fact: organization: "{{ organization | combine({ 'name': 'ExampleOrg' }) }}" - name: Sort keys in dictionaries ansible.builtin.set_fact: organization: "{{ organization | dictsort }}" - name: Pretty print dictionaries ansible.builtin.set_fact: organization: "{{ organization | to_nice_json }}" - name: Merge dictionaries vars: dict_1: a: 43 b: some string dict_2: y: true z: - 4 - test ansible.builtin.set_fact: merged_dict: "{{ dict1 | ansible.builtin.combine(dict_2, {'z':'new_value','w':[44]}) }}" recursively_merged_dict: >- {{ {'rest':'test'} | ansible.builtin.combine({'z':'newValue','w':[44]}, dict_1, dict_2, recursive=true) }} - name: Register the list of extensions per DB vars: db_extensions: {} ansible.builtin.set_fact: db_extensions: >- {{ db_extensions | combine({ item.item: item.query_result | map(attribute='extname') }) }} with_items: "{{ db_extensions_query.results }}" - name: Register the list of extensions per DB as 'db:extensions[]' pairs vars: db_extensions: sales: - pgaudit - plpgsql countries: - pgcrypto - postgis - pg_stat_statements ansible.builtin.set_fact: db_extension_pairs: # Refer https://jinja.palletsprojects.com/en/3.0.x/templates/#assignments for the namespace object's # reason >- {%- set ns = namespace(output = []) -%} {%- for db in db_extensions.keys() -%} {%- for extension in db_extensions[db] -%} {{- ns.output.append({'db':db, 'extension': extension}) -}} {%- endfor -%} {%- endfor -%} {{- ns.output -}} - name: Get the device name and last snapshot id for all block devices in an EC2 instance # Useful to create AMIs from instance snapshots tags: - aws - ec2 - snapshot - ami ansible.builtin.set_fact: last_snap_for_device: # Refer https://jinja.palletsprojects.com/en/3.0.x/templates/#assignments for the namespace object's # reason >- {%- set ns = namespace(devices_list = []) -%} {%- for result in current_instance_snapshots.results -%} {%- for device in current_instance_information.instances[0].block_device_mappings | selectattr('ebs.volume_id', 'equalto', result.volume_id) -%} {{- ns.devices_list.append({ 'device_name': device.device_name, 'snapshot_id': result.snapshots | sort(attribute='start_time') | last | json_query('snapshot_id'), }) -}} {%- endfor -%} {%- endfor -%} {{ ns.devices_list }} - name: Flow control tags: - flow_control - never hosts: localhost connection: local gather_facts: false ignore_errors: true tasks: - name: Take pauses tags: - pause - sleep ansible.builtin.pause: seconds: 1 - name: Only run in check mode tags: check_mode_only when: ansible_check_mode is truthy ansible.builtin.set_fact: check_mode_active: ansible_check_mode - name: Enforce failures tags: failure ansible.builtin.fail: msg: Manually enforced failure - name: Fail task on any non-compliance tags: assertion vars: installation_method: package url: https://www.google.com/ ansible.builtin.assert: that: - installation_method in ['container', 'package'] - "'https://www.google.com/' is ansible.builtin.url" - "'domain.example.com' is community.general.fqdn_valid(min_labels=2)" - url is regex('\w\.com/') fail_msg: What to say if any of the above conditions fail success_msg: What to say if all of the above conditions succeed - name: Execute notified handlers now tags: handler ansible.builtin.meta: flush_handlers - name: Do nothing tags: noop ansible.builtin.meta: noop - name: Retry failing tasks tags: - failure - retry changed_when: false ansible.builtin.command: /usr/bin/false retries: 3 delay: 1 register: command_result until: command_result is not failed - name: Error handling in blocks tags: error_handling block: - name: This executes normally ansible.builtin.debug: msg: I execute normally - name: This errors out changed_when: false ansible.builtin.command: /bin/false - name: This never executes ansible.builtin.debug: msg: I never execute due to the above task failing rescue: - name: This executes if any errors arose in the block ansible.builtin.debug: msg: I caught an error and can do stuff here to fix it always: - name: This always executes ansible.builtin.debug: msg: I always execute - name: Long-running tasks tags: long-running vars: ansible_async_dir: /tmp/.ansible/async # defaults to '~/.ansible_async' block: - name: Long-running task with integrated poll tags: async_with_self_poll when: ansible_check_mode is falsy # check mode and async cannot be used on same task ansible.builtin.command: /bin/sleep 15 changed_when: false async: 45 # run max 45s poll: 5 # check once every 5s - name: Long-running task with external poll tags: async_with_external_poll block: - name: Long-running task with external poll when: ansible_check_mode is falsy # check mode and async cannot be used on same task ansible.builtin.command: /bin/sleep 15 changed_when: false async: 45 # run max 45s poll: 0 # fire and forget register: long_running_task_with_external_poll - name: Check on long_running_task_with_external_poll when: long_running_task_with_external_poll is not skipped ansible.builtin.async_status: jid: "{{ long_running_task_with_external_poll.ansible_job_id }}" register: job_result until: job_result.finished retries: 9 delay: 5 - name: Run this play on up to 3 hosts at a time hosts: large_group_of_hosts serial: 3 tasks: [] - name: Run this play on a batch of 4 hosts, then a batch of 8, then the rest hosts: large_group_of_hosts serial: - 4 - 8 - 100% strategy: linear tasks: - name: Limit this task to 3 workers (or up to the current batch if lower) throttle: 3 ansible.builtin.set_fact: greetings: hi from {{ ansible_hostname }} # - name: Run this task only on one single host # run_once: true # ansible.builtin.set_fact: # confirm: only run on {{ ansible_hostname }} - name: Debugging tags: - debug - never hosts: localhost connection: local gather_facts: false ignore_errors: true tasks: - name: Output messages ansible.builtin.debug: msg: I always display! - name: Pretty print messages vars: install_method: package supported_install_methods: ['package'] ansible.builtin.debug: msg: >- {{ dict([ [ 'install_method', install_method ], [ 'install_method in supported_install_methods', install_method in supported_install_methods ], ]) }} - name: Output variable values vars: install_method: package ansible.builtin.debug: var: install_method - name: Output messages depending on the verbosity level ansible.builtin.debug: msg: I only display with 'ansible-playbook -vvv' or with more 'v's verbosity: 3 - name: Start the debugger on failure # print all variables at this point => p task_vars # continue => c # abort and quit => q debugger: on_failed ansible.builtin.fail: msg: Manually enforced failure - name: Prompt for vars tags: - prompt - never hosts: localhost connection: local gather_facts: false vars_prompt: # only works at playbook level - name: target_db prompt: Target DB private: false default: localhost - name: db_password prompt: DB password default: whatever tasks: [] - name: Run tasks on different targets than the current one # Only *single* target patterns allowed # No groups tags: - never hosts: localhost connection: local gather_facts: false tasks: - name: Run on the controller delegate_to: 127.0.0.1 connection: local changed_when: false ansible.builtin.command: hostname - name: Run on other targets delegate_to: copernicus changed_when: false ansible.builtin.command: hostname - name: Reuse tasks tags: - never hosts: localhost connection: local gather_facts: false tasks: [] # - name: Statically use tasks from files # when: false # block: # - name: By using absolute paths and special variables (preferred) # ansible.builtin.import_tasks: # file: "{{ role_path }}/tasks/install/{{ install_method }}.yml" # - name: By using paths relative to the including file # ansible.builtin.import_tasks: # file: pre-flight.yml # - name: Conditionally include tasks # block: # - name: by leveraging the 'with_fileglob' loop filter (preferred) # ansible.builtin.include_tasks: # file: "{{ item }}" # with_fileglob: "{{ install_method }}.yml" # - name: by checking the files' existence # vars: # filename: "{{ install_method }}.yml" # when: lookup('ansible.builtin.fileglob', filename) != [] # ansible.builtin.import_tasks: # file: "{{ filename }}" # - name: Reuse playbooks # # only works at playbook level # vars: # var_for_playbook_1: value1 # ansible.builtin.import_playbook: path/to/playbook.yml # - name: Apply roles # hosts: localhost # connection: local # gather_facts: false # roles: # - role_name # - path/to/role # - role: role_name # - role: role_with_vars # vars: # var1_for_role_with_vars: value # tasks: # - name: Apply a role now # ansible.builtin.import_role: # name: role_name - name: Integrate Ansible Vault tags: - vault - never hosts: localhost connection: local gather_subset: - '!all' - min check_mode: true tasks: - name: Use encrypted values ansible.builtin.set_fact: var_from_encrypted_value: # password: '1q2w3e4r', plaintext value: 'very secret string' !vault | $ANSIBLE_VAULT;1.1;AES256 34646464653830386631363430386432666530356364313532313336373665613038633464376335 3539363530613130623638313063363165386230646566640a313438386133366137383939336637 33333365393337326239336264623462373064383663363234353635316538356461353061646563 3037306464363439340a663430313739393439363936613862316361353330363638323065383063 39613935613035343637336537643266313737666635313730353034373736353736 - name: Use encrypted files # The 'unvault' lookup plugin requires files to exist beforehand, but it is fine for them to be plaintext. # The 'file' lookup plugin requires files to exist beforehand, but decrypts vault-encryped files. tags: tls_certificate ansible.builtin.copy: dest: /etc/haproxy/certificate.pem content: | {{ lookup('ansible.builtin.unvault', 'path/to/cert/key.pem') | string | trim }} {{ lookup('ansible.builtin.unvault', 'path/to/cert/full_chain.pem') | string | trim }} mode: '0700' - name: Save data to encrypted files # Of fu*king course the 'vault' filter would use the 'filter_default' vault ID by default to encrypt content. # Set that parameter to '' to *not* specify a vault ID. vars: ansible_vault_password: >- {{ lookup('ansible.builtin.file', [playbook_dir, 'ansible_vault_password_file.txt'] | path_join) }} ansible.builtin.copy: dest: path/to/file decrypt: false # necessary if the file does not exist beforehand content: "{{ 'some string' | ansible.builtin.vault(ansible_vault_password, vault_id='') }}" mode: '0644' - name: Use YAML anchors hosts: localhost connection: local vars: some_reusable_task: &some_reusable_task name: Some reusable task tags: some_reusable_task check_mode: false ansible.builtin.set_fact: some_fact: "{{ some_var | default('some value') }}" some_reusable_tasks_block: &some_reusable_tasks_block name: Some reusable tasks block tags: some_reusable_tasks_block block: - name: Some first reusable task in block tags: some_first_reusable_task_in_block check_mode: false ansible.builtin.set_fact: some_first_fact: "{{ some_first_var | default('some first value') }}" - name: Some nth reusable task in block tags: some_nth_reusable_task_in_block check_mode: false ansible.builtin.set_fact: some_nth_fact: "{{ some_nth_var | default('some nth value') }}" tasks: - *some_reusable_task - <<: *some_reusable_task vars: some_var: some overridden value - *some_reusable_tasks_block - name: Common operations tags: never hosts: localhost connection: local gather_subset: - '!all' - min check_mode: true tasks: # ordered alphabetically by name - name: Add authorized keys become: true ansible.posix.authorized_key: user: ansible key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI0123456789abcdefghijkl/ABCDEFGHIJKL01234567 ansible@example.org - name: Add repositories block: - name: To DNF/YUM when: ansible_pkg_mgr | lower in ['dnf', 'yum'] ansible.builtin.yum_repository: name: epel description: EPEL YUM repo baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/ - name: Add users to the sudoers become: true community.general.sudoers: name: ansible user: ansible nopassword: true commands: ALL - name: Create directories recursively ansible.builtin.file: path: /tmp/path/to/final/dir state: directory mode: '0775' - name: Create users become: true ansible.builtin.user: name: ansible - name: Define files content in tasks ansible.builtin.copy: dest: "{{ ansible_user_dir }}/.tmux.conf" mode: u=rw,go=r content: | … - name: Generate random strings ansible.builtin.set_fact: random_alphanumerical_lowercase_string_of_12_chars: >- query('community.general.random_string', upper=false, special=false, length=12) - name: Generate passwords ansible.builtin.set_fact: random_password: "{{ lookup('ansible.builtin.password', '/dev/null') }}" random_password_with_requirements: >- {{ lookup('ansible.builtin.password', '/dev/null length=32 chars=ascii_letters,digits,punctuation') }} random_but_idempotent_password: >- {{ lookup('ansible.builtin.password', '/dev/null', seed=inventory_hostname, length=16) }} - name: Get the PID of the current play ansible.builtin.set_fact: current_play_pid: lookup('pipe', 'echo $PPID') - name: Look for files ansible.builtin.set_fact: path_list_of_all_txt_files_in_dir: "{{ lookup('ansible.builtin.fileglob', '/my/path/*.txt') }}" - name: Install Docker Compose block: - name: Create Docker's CLI plugins directory become: true ansible.builtin.file: dest: /usr/local/lib/docker/cli-plugins state: directory owner: root group: root mode: u=rwx,g=rx,o=rx - name: Get Docker compose from its official binaries become: true ansible.builtin.get_url: url: https://github.com/docker/compose/releases/latest/download/docker-compose-{{ ansible_system }}-{{ ansible_architecture }} dest: /usr/local/lib/docker/cli-plugins/docker-compose owner: root group: root mode: u=rwx,g=rx,o=rx - name: Install packages block: - name: Via package manager on any supported system ansible.builtin.package: name: - tmux - screen - name: Via PIP ansible.builtin.pip: name: - bottle - django>1.11.0,<1.12.0 - svn+http://myrepo/svn/MyApp#egg=MyApp - git+http://myrepo/app/MyApp - file:///path/to/MyApp.tar.gz - name: Replace multiple strings in files ansible.builtin.replace: path: /path/to/file.yml regexp: '(\s+image\:.*)stable-v.*' replace: \g<1>custom backup: true - name: Gather facts about hosts previously not in inventory delegate_to: host_previously_not_in_inventory ansible.builtin.setup: filter: - '!all' - min - name: Apply SQL migrations from files in folder # take files in in alphabetical order so they can have a priority set tags: apply_sql_migrations_from_files_in_folder community.postgresql.postgresql_query: login_host: "{{ login_host }}" login_port: "{{ login_port }}" login_user: "{{ login_user }}" login_password: "{{ login_password }}" login_db: "{{ login_db }}" query: "{{ lookup('ansible.builtin.file', item) }}" loop: >- {{ query( 'fileglob', [ role_path, 'files', '*.sql' ] | path_join, [ role_path, 'files', country|lower, '*.sql' ] | path_join, ) | sort }} - name: Unarchive files tags: unarchive when: - archive_file.stat.mimetype in ["application/x-tar"] - archive_file.stat.path is regex('\\.tar') block: - name: Unarchive file '{{ archive_file.stat.path | basename }}' ansible.builtin.unarchive: remote_src: true src: "{{ archive_file.stat.path }}" dest: "{{ archive_file.stat.path | dirname }}" - name: Get the name of extracted directories ending in '.dir' tags: find ansible.builtin.find: paths: "{{ archive_file.stat.path | dirname }}" recurse: false file_type: directory patterns: '*.dir' register: extracted_dirs - name: Save the first extracted directory found ansible.builtin.stat: path: "{{ extracted_dirs.files[0].path }}" register: first_extracted_dir - name: Run containers block: - name: Directly community.docker.docker_container: name: gitlab image: gitlab/gitlab-ce:16.11.2-ce.0 hostname: gitlab.lan published_ports: - "8022:22" - "8080:80" - "8443:443" env: GITLAB_OMNIBUS_CONFIG: >- external_url 'http://gitlab.lan'; shm_size: 256m volumes: - ./config:/etc/gitlab:Z - ./logs:/var/log/gitlab:Z - ./data:/var/opt/gitlab:Z auto_remove: true - name: With Compose community.docker.docker_compose_v2: project_src: /home/user/flask - name: Send messages to Slack channels vars: slack_notification_hook_url: https://hooks.slack.com/services/AB01CD23EF4/ABCD0123E/aBcDefGh0123456789iJKLmn block: - name: Send plain messages ansible.builtin.uri: url: "{{ slack_notification_hook_url }}" method: POST body_format: json body: text: (╥╯ᗝ╰╥) task XYZ failed - name: Send mrkdwn (Slack-specific markdown) text # FIXME: still to be tested ansible.builtin.uri: url: "{{ slack_notification_hook_url }}" method: POST body_format: json body: blocks: - type: section text: type: mrkdwn text: This is a *_fancy_* message - name: Setup cronjobs block: - name: At specific times # Mind this is based on the *hosts'* time. become: true ansible.builtin.cron: name: Prometheus manual data backup cron_file: prometheus-manual-data-backup hour: 4 minute: 0 user: root job: # - Keep '%' characters escaped or they'll be treated as newlines. # - Archive creation returns 1 if it detects changes to read files. # Using ';' instead of '&&' to ignore. > FILENAME="/tmp/prometheus-data-$(date +'\%s-\%F-\%H-\%m-\%S').tar.gz" && tar -czf "$FILENAME" '/var/lib/prometheus/data' ; tar -tf "$FILENAME" > '/dev/null' && aws s3 cp "$FILENAME" 's3://backups/prometheus/' && rm "$FILENAME" - name: Use the users' home directory for something block: - name: Executing commands from specified users block: - name: Get users' homedir back become: true become_user: "{{ item }}" become_flags: -iH check_mode: false ansible.builtin.command: >- echo "{{ item }}: $HOME" changed_when: false with_items: - root - ec2-user register: users_homedir_retrieve - name: Compute and register the results tags: AnsibleUnsafeText_to_Dict ansible.builtin.set_fact: users_homedir: >- {{ users_homedir_retrieve | community.general.json_query('results[].stdout') | map('from_yaml') | combine }} - name: Do your thing! become: true become_user: "{{ item.key }}" ansible.builtin.file: path: "{{ item.value }}/placeholder" state: touch mode: '0755' with_dict: "{{ users_homedir }}" - name: From the system's entries block: - name: Get raw information from the system's entries ansible.builtin.getent: database: passwd key: "{{ item }}" split: ':' with_items: - root - ec2-user register: users_entries - name: Compute and register the results ansible.builtin.set_fact: users_info: >- {{ users_entries | community.general.json_query('results[].ansible_facts.getent_passwd[]') | combine }} - name: Do your thing! ansible.builtin.file: path: "{{ item.value[4] }}/placeholder" owner: "{{ item.key }}" state: touch mode: '0755' with_dict: "{{ users_info }}" - name: Work with time # https://docs.ansible.com/ansible/latest/collections/ansible/builtin/to_datetime_filter.html # https://docs.python.org/3/library/datetime.html # The datetime format defaults to '%Y-%m-%d %H:%M:%S'. tags: datetime block: - name: Add or remove time ansible.builtin.set_fact: one_year_from_now: "{{ '%Y-%m-%dT%H:%M:%S' | strftime( (ansible_date_time.epoch_int) + (60*60*24*365) ) }}" one_year_from_now_date_only: "{{ '%Y-%m-%d' | strftime( now(fmt='%s')|int + (60*60*24*365) ) }}" - name: Compare date and times ansible.builtin.set_fact: total_seconds_between_dates: >- {{ ( ('2016-08-14 20:00:12' | to_datetime) - ('2015-12-25' | to_datetime('%Y-%m-%d')) ).total_seconds() }} remaining_seconds_after_delta: # Does NOT convert years and days to seconds. # Use total_seconds() for that. "{{ ( ('2016-08-14 20:00:12' | to_datetime) - ('2016-08-14 18:00:00' | to_datetime) ).seconds }}" # This returns "7212", delta is 2 hours and 12 seconds days_between_dates: # Discards remaining hours, minutes, and seconds "{{ ( ('2016-08-14 20:00:12' | to_datetime) - ('2015-12-25' | to_datetime('%Y-%m-%d')) ).days }}" - name: Take difference between dotnet (100ns precision) and iso8601 microsecond timestamps vars: date1: '2022-11-15T03:23:13.6869568Z' date2: '2021-12-15 16:06:24.400087Z' date1_short: # Cap to microseconds for any timestamp with higher precision '{{ date1 | regex_replace("([^.]+)(\.\d{6})\d*(.+)", "\1\2\3") }}' iso8601format: '%Y-%m-%dT%H:%M:%S.%fZ' rfc3339format: '%Y-%m-%d %H:%M:%S.%fZ' ansible.builtin.set_fact: date_diff: >- {{ (date1_short|to_datetime(iso8601format) - date2|to_datetime(rfc3339format)).total_seconds() }} - name: AWS-specific operations tags: - aws - never hosts: localhost connection: local gather_facts: false check_mode: true tasks: - name: Apply roles on different targets than the current one block: [] # - name: Gather facts about the target EC2 instance # when: instance_information.instance_ids | length > 0 # delegate_to: "{{ instance_information.instance_ids | first }}" # vars: # ansible_connection: aws_ssm # ansible_python_interpreter: /usr/bin/python3 # ansible_remote_tmp: /tmp/.ansible-ssm-user/tmp # ansible.builtin.gather_facts: {} # register: fact_gathering # - name: Apply the role to the EC2 instance # when: fact_gathering is not skipped # delegate_to: "{{ instance_information.instance_ids | first }}" # delegate_facts: true # vars: # ansible_connection: aws_ssm # ansible_aws_ssm_timeout: 900 # ansible_python_interpreter: /usr/bin/python3 # ansible_remote_tmp: /tmp/.ansible-ssm-user/tmp # ansible_async_dir: /tmp/.ansible-ssm-user/async # some_role_var: some value # some_other_role_var: some value # ansible.builtin.import_role: # name: role name - name: Get the list of current public IP ranges # too many to be put into security group rules ansible.builtin.set_fact: ip_ranges: >- lookup('url', 'https://ip-ranges.amazonaws.com/ip-ranges.json', split_lines=False) | from_json | json_query('prefixes') | selectattr('region', 'equalto', 'eu-west-1') | selectattr('service', 'equalto', 'AMAZON') | map(attribute='ip_prefix') - name: Work with time tags: datetime # AWS' datetime format is '%Y-%m-%dT%H:%M:%S'. # https://stackoverflow.com/questions/48101921/ansible-compare-difference-between-two-dates-for-the-last-hour block: [] - name: Check the caller can access AWS' APIs tags: check_access_to_aws_api amazon.aws.aws_caller_info: - name: Check the caller can connect to EC2 instances via SSM tags: check_access_to_ec2_instances_via_ssm vars: ssm_bucket: someBucketWhereSsmStoresData block: - name: Check the caller can get information about the S3 bucket used by SSM tags: check_list_on_ssm_bucket amazon.aws.s3_bucket_info: name: "{{ ssm_bucket }}" - name: "Check the caller can act upon objects in the S3 bucket used by SSM" tags: check_usage_on_ssm_bucket check_mode: false amazon.aws.s3_object: bucket: "{{ ssm_bucket }}" object: whatever/test.txt content: just a test file, nothing to see here overwrite: latest expiry: 15 mode: "{{ item }}" loop: - put - getstr - delobj - name: Assume roles tags: assume_role block: - name: Get session tokens amazon.aws.sts_assume_role: access_key: AKIA1EXAMPLE1EXAMPLE # optional if defined as environment variable secret_key: 123456789abcdefghijklmnopqrstuvwxyzABCDE # optional if defined as environment variable profile: someProfile # optional if defined as environment variable role_arn: "arn:aws:iam::123456789012:role/someRole" role_session_name: someRoleSession duration_seconds: "{{ (60 * 60 * 1) | int }}" # min 900s, must be int register: assumed_role - name: Use the assumed role to take action amazon.aws.ec2_tag: access_key: "{{ assumed_role.sts_creds.access_key }}" secret_key: "{{ assumed_role.sts_creds.secret_key }}" profile: null # required to use the assumed role's token, if profile is specified via environment variable session_token: "{{ assumed_role.sts_creds.session_token }}" resource: i-xyzxyz01 tags: MyNewTag: value - name: Test the remaining time for role assumption is > 5m # https://docs.ansible.com/ansible/latest/collections/ansible/builtin/to_datetime_filter.html # https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior ansible.builtin.debug: msg: >- {{ dict([ [ 'assumed_role.sts_creds.expiration', assumed_role.sts_creds.expiration | to_datetime('%Y-%m-%dT%H:%M:%S+00:00') ], [ 'now()', now() ], [ 'assumed_role.sts_creds.expiration - now()', assumed_role.sts_creds.expiration | to_datetime('%Y-%m-%dT%H:%M:%S+00:00') - now() ], [ 'condition', ( assumed_role.sts_creds.expiration | to_datetime('%Y-%m-%dT%H:%M:%S+00:00') - now() ).total_seconds() > 300 ], ]) }} - name: Update the play's inventory with a newly started instance tags: - update_inventory - new_instance when: new_instance.instance_id is defined block: - name: Add the new instance to the play's inventory tags: add_instance_to_inventory ansible.builtin.add_host: name: "{{ new_instance.instance_id }}" ansible_python_interpreter: /usr/bin/python3 ansible_connection: community.aws.aws_ssm ansible_aws_ssm_bucket_name: company-ssm-logs ansible_aws_ssm_region: eu-west-1 ansible_remote_tmp: /home/ssm-user/.cache/ansible/tmp ansible_async_dir: /home/ssm-user/.cache/ansible/async - name: Gather facts from the instance tags: gather_facts delegate_to: "{{ new_instance.instance_id }}" delegate_facts: true ansible.builtin.gather_facts: # alternatively, use 'ansible.builtin.setup' to allow for subsets gathering - name: DEBUG Print the new instance's host variables delegate_to: "{{ new_instance.instance_id }}" ansible.builtin.debug: verbosity: 3 var: hostvars[new_instance.instance_id] - name: Wait for AWS to realize some requests have been made ansible.builtin.pause: seconds: 60 - name: EC2-specific operations block: - name: Get running instances with 'K8S' as the 'Application' tag amazon.aws.ec2_instance_info: filters: "tag:Application": K8S instance-state-name: ["running"] - name: Clone EC2 instances vars: source_instance_id: i-0123456789abcdef0 block: - name: Get instance information from the original instance amazon.aws.ec2_instance_info: instance_ids: - "{{ source_instance_id }}" register: source_instance_info - name: Create an AMI of the original instance amazon.aws.ec2_ami: instance_id: "{{ source_instance_id }}" no_reboot: true # remove if the instance rebooting upon AMI creation is no biggie wait: true wait_timeout: 3600 # big volumes call for bit wait times (a 200GiB volume took ) name: ami-source register: source_ami - name: Use the AMI to launch clones identical to the original when: source_ami.image_id is defined amazon.aws.ec2_instance: name: clone vpc_subnet_id: "{{ source_instance_info.instances[0].subnet_id }}" instance_type: "{{ source_instance_info.instances[0].instance_type }}" image: id: "{{ source_ami.image_id }}" - name: Long-running tasks via SSM vars: ansible_connection: community.aws.aws_ssm ansible_remote_tmp: /tmp/.ansible-ssm-user/tmp ansible_async_dir: /tmp/.ansible-ssm-user/async block: - name: Dump a DB from an RDS instance to a temporary file when: - ansible_check_mode is falsy # check mode and async cannot be used on same task - rds_instance.endpoint is defined vars: wanted_pattern_in_module_output: >- {{ '"failed": 0, "started": 1, "finished": 0' | regex_escape() }} community.postgresql.postgresql_db: login_host: "{{ rds_instance.endpoint.address }}" login_port: "{{ rds_instance.endpoint.port }}" login_user: "{{ rds_instance.master_username }}" login_password: "{{ db_password }}" name: sales state: dump target: >- [ {{ ansible_user_dir }}, dump.{{ db_instance_identifier }}.{{ ansible_date_time.iso8601_basic_short }}.dir ] | path_join target_opts: >- --exclude-table … --exclude-schema archived --no-publications --format d --jobs $(nproc) async: "{{ 60 * 60 * 2 }}" # wait up to 2 hours -- 60s * 60m * 2h poll: 0 # fire and forget; ssm would not allow self-checking anyways register: dump changed_when: - dump.rc == 0 - dump.module_stderr == '' - "'started' | extract(dump.module_stdout | regex_search('{.*}') | from_json) == 1" - "'failed' | extract(dump.module_stdout | regex_search('{.*}') | from_json) == 0" failed_when: dump.rc != 0 - name: Check on the dump task vars: max_wait: "{{ (60 * 60 * 12) }}" # wait for the async task to end ansible_aws_ssm_timeout: "{{ max_wait }}" # ssm uses a single connection, keep active until the end dump_stdout_as_obj: "{{ dump.module_stdout | regex_search('{.*}') | from_json }}" ansible_job_id: "{{ dump_stdout_as_obj.ansible_job_id }}" ansible.builtin.async_status: jid: "{{ ansible_job_id }}" register: dump_result until: dump_result.finished retries: "{{ max_wait | int }}" delay: 300 # check once every 5m to avoid overloading the ssm agent - name: RDS-specific operations block: - name: Create an instance's snapshot block: - name: Create the snapshot amazon.aws.rds_instance_snapshot: db_instance_identifier: identifier-for-db-instance db_snapshot_identifier: identifier-for-db-snapshot register: snapshot_creation - name: Wait for the snapshot to be in the 'available' state when: snapshot_creation.snapshot_create_time is defined amazon.aws.rds_snapshot_info: db_snapshot_identifier: "{{ snapshot_creation.db_snapshot_identifier }}" register: snapshot_check retries: 3 delay: 120 until: snapshot_check.snapshots | selectattr('status', 'equalto', 'available') | length > 0 - name: Restore instance with automatic backup enabled to point in time block: - name: Restore DB instance amazon.aws.rds_instance: db_instance_identifier: restored-to-pitr creation_source: instance source_db_instance_identifier: source-instance use_latest_restorable_time: true tags: "{{ omit }}" # avoid setting tags, it errors out when restoring to pitr wait: # avoid waiting for db instances with automatic backup enabled to finish backing up the restored # instance right after creation - db instances' first backup can take unbearably long (3h for 100GB) false register: pitr_restored_instance - name: Wait for the restored DB instance to be ready when: pitr_restored_instance.db_instance_identifier is defined block: - name: Wait for the restored DB instance to be ready amazon.aws.rds_instance_info: db_instance_identifier: "{{ pitr_restored_instance.db_instance_identifier }}" register: pitr_restored_instance_ready_check retries: 15 delay: 60 until: - pitr_restored_instance_ready_check.instances[0].db_instance_status in ['available', 'backing-up'] - pitr_restored_instance_ready_check.instances[0].pending_modified_values.keys() | length == 0 - name: Update restored DB instance information # 'amazon.aws.rds_instance' will *not* have the 'endpoint' key defined if not waiting ansible.builtin.set_fact: pitr_restored_instance: "{{ pitr_restored_instance_ready_check.instances[0] }}" - name: Dump roles' privileges block: - name: Dump to file environment: PGPASSWORD: someRandomString vars: out_file: /tmp/instance-id_roles.sql ansible.builtin.command: >- pg_dumpall --host 'instance-id.0123456789ab.eu-west-1.rds.amazonaws.com' --port '5432' --user 'postgres' --database 'postgres' --no-password --roles-only --no-role-passwords --file '{{ out_file }}' changed_when: false - name: Dump to variable for later use through 'dump_execution.stdout_lines' environment: PGPASSWORD: someRandomString ansible.builtin.command: >- pg_dumpall -h 'instance-id.0123456789ab.eu-west-1.rds.amazonaws.com' -p '5432' -U 'postgres' -l 'postgres' -w -r --no-role-passwords changed_when: false register: dump_execution - name: Wait for pending changes to be applied amazon.aws.rds_instance_info: db_instance_identifier: identifier-for-db-instance register: instance_check retries: 12 delay: 15 until: instance_check.instances[0].pending_modified_values.keys() | length == 0 - name: S3-specific operations block: - name: Check S3 object exists amazon.aws.s3_object_info: bucket_name: my-bucket object_name: prefix/object.tar - name: Download objects from S3 # The 'amazon.aws.s3_object' module might be *not* suitable for files bigger than the executor's currently # available memory. See . # TL:DR: at the time of writing, the module keeps downloaded data in memory before flushing it to disk, # filling up the host's memory when downloading big files and causing it to stall or crash. amazon.aws.s3_object: bucket: my-bucket object: prefix/object.tar dest: /tmp/object.tar mode: get - name: Upload objects to S3 amazon.aws.s3_object: bucket: my-bucket object: prefix/object.tar src: /tmp/object.tar mode: put - name: "AWS: Start stopped instances and add the first of them to the inventory for the next play" # works at playbook level tags: - never hosts: localhost connection: local check_mode: false handlers: - name: Add the first started host ansible.builtin.add_host: groups: - tag_Application_Postgres - tag_Component_Dumper name: "{{ started_instances.instance_ids[0] }}" - name: Add all started hosts with host variables loop: "{{ started_instances.instance_ids }}" ansible.builtin.add_host: groups: - tag_Application_Postgres - tag_Component_Dumper name: "{{ item }}" ansible_connection: aws_ssm ansible_aws_ssm_bucket_name: company-ssm-logs ansible_aws_ssm_region: eu-west-1 ansible_aws_ssm_timeout: 900 ansible_remote_tmp: /tmp/.ansible-ssm-user/tmp ansible_async_dir: /tmp/.ansible-ssm-user/async tasks: - name: Start the PG dumper instance tags: dumper amazon.aws.ec2_instance: filters: tag:Application: Postgres tag:Component: Dumper state: started register: started_instances notify: - Add the first started host - Add all started hosts with host variables # follow up with play using hosts 'tag_Application_Postgres,&tag_Component_Dumper' - name: AWX-specific operations tags: never hosts: localhost connection: local gather_facts: false check_mode: true environment: CONTROLLER_HOST: https://awx.example.org/ CONTROLLER_VERIFY_SSL: false CONTROLLER_USERNAME: admin CONTROLLER_PASSWORD: somethingSecret tasks: - name: Export all data from existing instances # At the time of writing: applications, credential_types, credentials, execution_environments, inventory, # inventory_sources, job_templates, notification_templates, organizations, projects, schedules, teams, and users. awx.awx.export: all: true register: awx_export_output - name: DBLab-specific operations tags: never hosts: localhost connection: local vars: dblab_api_port: 2346 # some operations (reset) are not available on the non-api endpoints max_snapshot_age_in_days: 15 vars_prompt: - name: dblab_host private: false - name: dblab_token pre_tasks: - name: Print out run's variables ansible.builtin.debug: verbosity: 3 msg: >- {{ dict([ [ 'host', dblab_host ], [ 'port', dblab_api_port ], [ 'token', dblab_token ], [ 'max_snapshot_age_in_days', max_snapshot_age_in_days ], ]) }} - name: Check the snapshots block: - name: Gather snapshots' status check_mode: false ansible.builtin.uri: url: https://{{ dblab_host }}:{{ dblab_api_port }}/api/snapshots headers: 'verification-token': "{{ dblab_token }}" register: snapshots_status - name: Print out the request's results ansible.builtin.debug: verbosity: 3 var: snapshots_status.json - name: Check the data is recent enough on both snapshots ansible.builtin.assert: that: >- ( ansible_date_time.iso8601|to_datetime('%Y-%m-%dT%H:%M:%SZ') - item.dataStateAt|to_datetime('%Y-%m-%dT%H:%M:%SZ') ).days < max_snapshot_age_in_days fail_msg: snapshot {{ item.id }} for pool {{ item.pool }} is too old loop: "{{ snapshots_status.json }}" tasks: - name: Reset clones on {{ dblab_host }} tags: reset_clones block: - name: Gather clones on {{ dblab_host }} check_mode: false ansible.builtin.uri: url: http://{{ dblab_host }}:{{ dblab_api_port }}/api/status headers: 'verification-token': "{{ dblab_token }}" register: instance_status - name: Print out the request's results ansible.builtin.debug: verbosity: 3 var: instance_status.json - name: Reset protected clones vars: clones: "{{ instance_status.json.cloning.clones | selectattr('protected', 'equalto', true) }}" ansible.builtin.uri: url: http://{{ dblab_host }}:{{ dblab_api_port }}/api/clone/{{ item.id }}/reset method: POST headers: 'verification-token': "{{ dblab_token }}" body_format: json body: latest: true loop: "{{ clones }}" register: reset_requests - name: Gitea-specific operations tags: - gitea - never hosts: localhost connection: local gather_facts: false check_mode: true tasks: - name: Create a GitLab mirror repository in Gitea tags: create_gitea_mirror_gitlab ansible.builtin.uri: url: https://gitea.example.org/api/v1/repos/migrate method: POST headers: Authorization: token abcdef0123456789abcdef0123456789abcdef01 body_format: json body: auth_username: ro-access auth_token: glpat-projAccesToken012345 clone_addr: https://gitlab.example.org/jimmy/secret-project issues: false labels: false lfs: false milestones: false mirror: true mirror_interval: 8h0m0s private: true pull_requests: false releases: false repo_name: secret-project repo_owner: jimmy service: gitlab wiki: false status_code: 201 - name: GitLab-specific operations tags: - gitlab - never hosts: localhost connection: local gather_facts: false check_mode: true tasks: - name: Install configured fleeting plugins when: runner_executor in ["docker-autoscaler", "instance"] become: true ansible.builtin.command: chdir: /root cmd: gitlab-runner fleeting install creates: /root/.config/fleeting/plugins - name: Create a Project Access Token for Gitea in GitLab tags: create_gitlab_project_access_token block: - name: Create the access token community.general.gitlab_project_access_token: api_url: https://gitlab.example.org api_token: glpat-m-PLACEHOLDER0123456 project: jimmy/secret-project name: ro-access expires_at: "{{ '%Y-%m-%d' | strftime( now(fmt='%s')|int + (60*60*24*365) ) }}" # 1y from now access_level: guest scopes: - read_api - read_repository register: gitlab_project_access_token_creation - name: Save the access token for later use ansible.builtin.set_fact: gitlab_project_access_token: "{{ gitlab_project_access_token_creation.access_token.token }}" - name: Let's Encrypt-specific operations tags: never hosts: localhost connection: local gather_facts: false check_mode: true tasks: # The 'acme_certificate' module takes in file paths for the certificate's files; those need either to *not* exist # beforehand, or their content to be in specific formats. - name: Revoke test certificates with account key community.crypto.acme_certificate_revoke: acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory acme_version: 2 account_key_src: path/to/acme_account.key.pem certificate: path/to/certificate.crt.pem