Infrastructure App Development
An infrastructure app is a Jinja template that generates one or more Resoto commands which are then executed by Resoto one after the other.
Check out the Resoto Apps git repository for a list of apps.
Using resotoappbundler
(pip install resotoappbundler
) you can bundle and dry-run (using resotoapprunner
) your app locally.
resotoappbundler
takes one or more Resoto app directories and bundles them into a single JSON file. This file can be hosted on any http server and used as an app index URL.
Development Workflow​
Create a new directory for app development:
$ mkdir resoto-app-development
$ cd resoto-app-developmentCreate and activate a new Python virtual environment:
$ python3 -m venv resoto-apps-venv
$ source resoto-apps-venv/bin/activateInstall
resotoappbundler
:$ pip install resotoappbundler
Check out the
resoto-apps
GitHub repository:$ git clone https://github.com/someengineering/resoto-apps.git
Add or modify an app in the
resoto-apps
directory.tipYou can perform a dry run of the
cleanup-untagged
app for sample infrastructure app output:$ resotoapprunner --path resoto-apps/cleanup-untagged/
Bundle all apps into a single
index.json
file:$ resotoappbundler --path resoto-apps/ --discover > index.json
From within Resoto Shell, install an app using the
app install
command:Installing an app from a custom index URL> app install <app_name> --index-url <file://...>
noteThe
--index-url
argument can be used to specify a custom app index URL.By default, Resoto uses the official app index URL.
For local development, the index URL can point to a local JSON file using
file://...
.
Extra Functions and Variables​
A Resoto Infrastructure App has access to a couple of extras that are not part of the standard Jinja library:
search()
- Search the Resoto Graph for resources. Returns a generator that yields resources (and edges if requested).parse_duration()
- Parse a duration string (e.g.2d4h
or7 weeks
) into a timedelta object that can be compared.config
- The app configuration (if any).args
- The command line arguments passed to the app (if any).stdin
- A generator representing the standard input passed to the app (if any).
Resoto Infrastructure Apps also have access to the Expression Statement and Loop Controls Jinja extensions.
Directory Structure​
An infrastructure app is a directory that contains the following files:
README.md
- A markdown file that describes the app.app.yaml
- A YAML file that contains the app manifest.app.jinja2
- A Jinja template that generates Resoto commands.app.svg
- A vector graphics icon for the app.
App Manifest​
The app manifest is a YAML file that contains the following fields:
name
- The name of the app.description
- A short description of the app.version
- The version of the app.language
- The language of the app. Currently onlyjinja2
is supported.license
- The license of the app.authors
- A list of authors.url
- The URL of the app.categories
- A list of categories the app belongs to.default_config
- The default configuration of the app.config_schema
- The configuration schema of the app.args_schema
- The command line arguments schema of the app.
Example Apps​
Execute a Static Search​
search /metadata.expires < "@NOW@" | clean "Resource is expired"
name: cleanup-expired
description: "This app cleans up resources that have expired."
version: "1.0.0"
language: jinja2
license: "Apache 2.0"
authors: ["someengineering"]
url: "https://resoto.com/"
categories: ["cleanup"]
default_config:
config_schema:
args_schema:
Clean Up Abandoned AWS CloudWatch Instance Alarms​
{%- set config = config["cleanup_aws_alarms"] %}
{%- for cloud, accounts in config["clouds_and_accounts"].items() %}
{%- for account in accounts %}
search is(aws_cloudwatch_alarm) and /ancestors.account.reported.id == "{{account}}" and /ancestors.cloud.reported.id == "{{cloud}}" and cloudwatch_dimensions[*].name = InstanceId with (empty, <-- is(aws_ec2_instance)) | clean "Abandoned CloudWatch Instance Alarm"
{%- endfor %}
{%- endfor %}
name: cleanup-aws-alarms
description: "This plugin marks all abandoned AWS CloudWatch Instance Alarms for cleanup."
version: "1.0.0"
language: jinja2
license: "Apache 2.0"
authors: ["someengineering"]
url: "https://resoto.com/"
categories: ["cleanup"]
default_config:
cleanup_aws_alarms:
clouds_and_accounts:
aws:
- '1234567'
- '567890'
config_schema:
- fqn: cleanup_aws_alarms
bases: []
properties:
- name: clouds_and_accounts
kind: dictionary[string, string[]]
required: false
description: Clouds and accounts to cleanup AWS alarms in.
args_schema:
Send a Message to a Discord Channel​
{%- set config = config["discord"] %}
{%- set message = [] %}
{%- for resource in stdin %}
{%- set reported = resource.get("reported", {}) %}
{%- set ancestors = resource.get("ancestors", {}) %}
{%- set account = ancestors.get("account", {}).get("reported", {}).get("name") %}
{%- set kind = reported.get("kind") %}
{%- set id = reported.get("id") %}
{%- set name = reported.get("id") %}
{%- if id == name %}
{%- set dname = id %}
{%- else %}
{%- set dname = name ~ ' (' ~ id ~ ')' %}
{%- endif %}
{%- set kdname = account ~ ' - ' ~ kind ~ ' ' ~ dname %}
{%- do message.append(kdname) %}
{%- endfor %}
{%- set message = message | join(" | ") %}
{%- set discord_data = {"embeds": [{"type": "rich", "title": args.title, "description": message, "footer": {"text": "Message created by Resoto"}}]} %}
json {{ discord_data | tojson }} | http POST {{ config["webhook"] }}
name: discord
description: "Discord client for Resoto"
version: "1.0.0"
language: jinja2
license: "Apache 2.0"
authors: ["someengineering"]
url: "https://resoto.com/"
categories: ["tools"]
default_config:
discord:
webhook: 'https://discordapp.com/api/webhooks/1234567890/abcdefghijklmnopqrstuvwxyz'
config_schema:
- fqn: discord
bases: []
properties:
- name: webhook
kind: string
required: true
description: Discord Webhook URL.
args_schema:
title:
help: "Title of the message to send to Discord."
type: str
required: true
message:
help: "Message to send to Discord."
type: str
required: true
num:
help: "Message to send to Discord."
type: int
default: 10