Sign AWS OpenSearch requests from a custom Ansible module
The AWS Ansible collections manage the OpenSearch domain, not what runs inside it, and a domain with IAM auth rejects unsigned requests to its REST API. botocore already ships the SigV4 signer, so a custom module that signs its own requests is short. ISM policies are the payload here, but the same wrapper works for any OpenSearch API call.
#!/usr/bin/python
import json
import requests
from ansible.module_utils.basic import AnsibleModule
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.session import Session
def signed(method, url, region, body=None):
request = AWSRequest(method=method, url=url, data=body,
headers={"Content-Type": "application/json"})
SigV4Auth(Session().get_credentials(), "es", region).add_auth(request)
return requests.request(method, url, headers=dict(request.headers),
data=body, timeout=30)
def main():
module = AnsibleModule(
argument_spec=dict(
endpoint=dict(required=True),
region=dict(required=True),
name=dict(required=True),
policy=dict(type="dict", required=True),
),
supports_check_mode=True,
)
p = module.params
url = f"https://{p['endpoint']}/_plugins/_ism/policies/{p['name']}"
current = signed("GET", url, p["region"])
if current.status_code == 200:
doc = current.json()
desired = p["policy"]["policy"]
live = {k: v for k, v in doc["policy"].items() if k in desired}
if live == desired:
module.exit_json(changed=False)
url += f"?if_seq_no={doc['_seq_no']}&if_primary_term={doc['_primary_term']}"
if module.check_mode:
module.exit_json(changed=True)
resp = signed("PUT", url, p["region"], json.dumps(p["policy"]))
if resp.status_code not in (200, 201):
module.fail_json(msg=resp.text)
module.exit_json(changed=True)
if __name__ == "__main__":
main()
Two quirks. Updating an existing policy requires the current _seq_no and _primary_term as query params or OpenSearch rejects the PUT. And the GET response includes server-added fields like policy_id and last_updated_time, so compare only the keys you supplied or the module reports changed on every run.
- name: delete logs after 30 days
ism_policy:
endpoint: search-logs-abc123.us-west-2.es.amazonaws.com
region: us-west-2
name: delete-after-30d
policy:
policy:
description: delete indices after 30 days
default_state: hot
ism_template:
- index_patterns: ["logs-*"]
states:
- name: hot
actions: []
transitions:
- state_name: delete
conditions:
min_index_age: 30d
- name: delete
actions:
- delete: {}
The module runs wherever the play targets, so credentials come from the usual boto chain, an instance profile, assumed role, or environment. Nothing OpenSearch-specific in the signing, service="es" is the only tell.