Cloud Custodian

Published: Jul 19, 2022 by Isaac Johnson

Cloud Custodian is an open-source tool for managing and enforcing policy in the cloud. It’s been around for a while. Releases in Github go back to April 2016. Today c7n can be used for AWS, Azure and GCP (the later two via modules).

There are a few modules, such as Org management and a Mailer that can tie to Slack, SES and Datadog. There is also a PolicyStream module that can be used for PR automation.

Setup

We can check the install page for Cloud Custodian.

Since it’s ideally installed via Python (per Developer Install Docs).

Because in our last blog entry we spoke on how to install AWX, I figured it would be a nice follow-up to use the AWX to do some real work. We’ll create a repo, playbook then add to AWX and use it to install Cloud Custodian.

With Ansible Tower/AWX

I’ll first create a repo for the playbook

/content/images/2022/07/custodian-01.png

then a quick playbook

$ cat cloudcustodian.yaml
---
- name: Install Cloud Custodian
  hosts: all

  tasks:
  - name: Instally Python3
    ansible.builtin.shell: |
      apt-get install python3 python3-venv python3-pip
    become: true
    args:
      chdir: /tmp

  - name: Check Version
    ansible.builtin.shell: |
      python3 -m pip --version
    args:
      chdir: /tmp

  - name: Instally Custodian locally
    ansible.builtin.shell: |
      umask 0002
      python3 -m venv custodian
      source custodian/bin/activate
      pip install c7n  
    become: true
    args:
      chdir: /tmp

  - name: Pull Docker Image
    ansible.builtin.shell: |
      docker pull cloudcustodian/c7n  
    become: true
    args:
      chdir: /tmp

I’ll login to AWX and add a new Project with the repo:

/content/images/2022/07/custodian-02.png

And as is my habit, sync it right away

/content/images/2022/07/custodian-03.png

Going to Templates, I’ll add a new Template

/content/images/2022/07/custodian-04.png

and fill in the details for this project and playbook

/content/images/2022/07/custodian-05.png

Lastly, we just click the Rocket icon to launch

/content/images/2022/07/custodian-06.png

Quick tip: if you get an error about missing sudo password on commands

/content/images/2022/07/custodian-07.png

that usually means your ansible user (in my case ‘builder’) lacks nopassword in the sudoers file.

On the host you can do sudo visudo and change the entry:

builder@builder-HP-EliteBook-745-G5:~$ sudo cat /etc/sudoers | grep builder
builder ALL=(ALL:ALL) NOPASSWD: ALL

Also, if you need to install docker on Ubuntu, you can follow these quick steps

And don’t forget to add your user (in my case builder but for you likely ansible) to the docker group

$ sudo usermod -aG docker builder

When done, we should see AWX installed the Playbook successfully

/content/images/2022/07/custodian-08.png

I can now login to the host to verify both that the docker image is there as well as the binary

$ ssh builder@192.168.1.32
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.13.0-52-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

132 updates can be applied immediately.
8 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable

Your Hardware Enablement Stack (HWE) is supported until April 2025.
*** System restart required ***
Last login: Wed Jul 13 06:52:53 2022 from 192.168.1.214
builder@builder-HP-EliteBook-745-G5:~$ docker images
REPOSITORY           TAG       IMAGE ID       CREATED        SIZE
cloudcustodian/c7n   latest    610f126c34eb   3 weeks ago    895MB
hello-world          latest    feb5d9fea6a5   9 months ago   13.3kB
builder@builder-HP-EliteBook-745-G5:~$ custodian version
0.9.17

Using Custodian

Our first example will be to auto tag resources in Azure.

I’ll be using my Azure account.

The first part is easy, getting the Sub and Tenant

builder@builder-HP-EliteBook-745-G5:~$ az login
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code E2W38WRF7 to authenticate.
[
  {
    "cloudName": "AzureCloud",
    "id": "d955c0ba-1234-1234-1234-8fed74cbb22d",
    "isDefault": true,
    "name": "Pay-As-You-Go",
    "state": "Enabled",
    "tenantId": "28c575f6-1234-1234-1234-7e6d1ba0eb4a",
    "user": {
      "name": "isaac.johnson@gmail.com",
      "type": "user"
    }
  }
]
builder@builder-HP-EliteBook-745-G5:~$ export AZURE_TENANT_ID=28c575f6-1234-1234-1234-7e6d1ba0eb4a
builder@builder-HP-EliteBook-745-G5:~$ export AZURE_SUBSCRIPTION_ID=d955c0ba-1234-1234-1234-8fed74cbb22d

The steps to create an SP have changed a bit in the last year. It used to be just a quick az ad sp create-for-rbac but now they make you check the domain.

You can see what your “domain” is in the Azure Portal under AAD:

/content/images/2022/07/custodian-09.png

$ az ad sp create-for-rbac -n "CloudCustodian" --role Contributor --name cloudcustodian -n cloudcustodian.isaacjohnsongmail.onmicrosoft.com

The password (password) and Client ID (appId) will be shown in the output of the above. We can use that to set the next two variables

/content/images/2022/07/custodian-10.png

builder@builder-HP-EliteBook-745-G5:~$ export AZURE_CLIENT_ID=b**********************************************7
builder@builder-HP-EliteBook-745-G5:~$ export AZURE_CLIENT_SECRET=f******************************************a

My First example uses the VMSS I keep on standby for AzDO

/content/images/2022/07/custodian-11.png

As we can see, they have no meaningful tags

/content/images/2022/07/custodian-12.png

The following policy should tag that VMSS with a Hello World tag

$ cat vmss.policy.yml
policies:
    - name: my-first-policy
      description: |
        Adds a tag to a virtual machine scale sets
      resource: azure.vmss
      filters:
        - type: value
          key: name
          value: azdovmssexample
      actions:
       - type: tag
         tag: Hello
         value: World

If we run this now, we’ll see we are missing the Azure Support (c7n_azure)

builder@builder-HP-EliteBook-745-G5:~$ custodian run --output-dir=. vmss.policy.yml
Traceback (most recent call last):
  File "/usr/local/bin/custodian", line 8, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.8/dist-packages/c7n/cli.py", line 363, in main
    command(config)
  File "/usr/local/lib/python3.8/dist-packages/c7n/commands.py", line 51, in _load_policies
    collection = policy_load(options, fp, validate=validate, vars=vars)
  File "/usr/local/lib/python3.8/dist-packages/c7n/policy.py", line 40, in load
    load_resources(rtypes)
  File "/usr/local/lib/python3.8/dist-packages/c7n/resources/__init__.py", line 20, in load_resources
    load_providers(set(pmap))
  File "/usr/local/lib/python3.8/dist-packages/c7n/resources/__init__.py", line 77, in load_providers
    from c7n_azure.entry import initialize_azure
ModuleNotFoundError: No module named 'c7n_azure'

We could just pip install from the command line the missing module. However, I’ll instead update the playbook

/content/images/2022/07/custodian-13.png

a quick run of the updated template

/content/images/2022/07/custodian-14.png

and now it should work. However, there is a version incompatibility:

$ custodian run --output-dir=. vmss.policy.yml
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/azure/common/credentials.py", line 107, in <module>
    from msrest.authentication import (
  File "/usr/local/lib/python3.8/dist-packages/msrest/__init__.py", line 28, in <module>
    from .configuration import Configuration
  File "/usr/local/lib/python3.8/dist-packages/msrest/configuration.py", line 38, in <module>
    from .universal_http.requests import (
  File "/usr/local/lib/python3.8/dist-packages/msrest/universal_http/__init__.py", line 53, in <module>
    from ..exceptions import ClientRequestError, raise_with_traceback
  File "/usr/local/lib/python3.8/dist-packages/msrest/exceptions.py", line 31, in <module>
    from azure.core.exceptions import SerializationError, DeserializationError
ImportError: cannot import name 'SerializationError' from 'azure.core.exceptions' (/usr/lib/python3/dist-packages/azure/core/exceptions.py)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/bin/custodian", line 8, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.8/dist-packages/c7n/cli.py", line 363, in main
    command(config)
  File "/usr/local/lib/python3.8/dist-packages/c7n/commands.py", line 51, in _load_policies
    collection = policy_load(options, fp, validate=validate, vars=vars)
  File "/usr/local/lib/python3.8/dist-packages/c7n/policy.py", line 40, in load
    load_resources(rtypes)
  File "/usr/local/lib/python3.8/dist-packages/c7n/resources/__init__.py", line 20, in load_resources
    load_providers(set(pmap))
  File "/usr/local/lib/python3.8/dist-packages/c7n/resources/__init__.py", line 77, in load_providers
    from c7n_azure.entry import initialize_azure
  File "/usr/local/lib/python3.8/dist-packages/c7n_azure/entry.py", line 5, in <module>
    from c7n_azure.provider import Azure  # NOQA
  File "/usr/local/lib/python3.8/dist-packages/c7n_azure/provider.py", line 9, in <module>
    from .session import Session
  File "/usr/local/lib/python3.8/dist-packages/c7n_azure/session.py", line 12, in <module>
    from azure.common.credentials import BasicTokenAuthentication
  File "/usr/lib/python3/dist-packages/azure/common/credentials.py", line 113, in <module>
    raise ImportError("You need to install 'msrest' to use this feature")
ImportError: You need to install 'msrest' to use this feature

basically:

ERROR: c7n-azure 0.7.16 has requirement azure-core==1.24.1; python_version >= "3.6", but you'll have azure-core 1.24.2 which is incompatible.
Installing collected packages: azure-core
  Attempting uninstall: azure-core
    Found existing installation: azure-core 1.24.1
    Uninstalling azure-core-1.24.1:
      Successfully uninstalled azure-core-1.24.1
Successfully installed azure-core-1.24.2

However, this is why we brought down the docker image as well.

Note: I later tried on my laptop without issue

(custodian) builder@DESKTOP-72D2D9T:~/Workspaces/c7n$ vi vmss.policy.yml
(custodian) builder@DESKTOP-72D2D9T:~/Workspaces/c7n$ custodian run --output-dir=. vmss.policy.yml
2022-07-13 21:36:50,719: custodian.azure.session:INFO Authenticated [Principal | d955c0ba-13dc-44cf-a29a-8fed74cbb22d]
2022-07-13 21:36:52,088: custodian.policy:INFO policy:my-first-policy resource:azure.vmss region:AzureCloud count:1 time:1.37
2022-07-13 21:36:52,089: custodian.azure.tagging.Tag:INFO Action 'tag' modified 'azdovmssexample' in resource group 'AZDOVMSSEXAMPLE'.
2022-07-13 21:36:52,090: custodian.policy:INFO policy:my-first-policy action:tag resources:1 execution_time:0.00

Custodian in Docker

We can now run the VMSS policy using the docker image we already have

$ docker run -it -v $(pwd)/output:/home/custodian/output -v $(pwd)/vmss.policy.yml:/home/custodian/policy.yml --env-file <(env | grep "^AWS\|^AZURE\|^GOOGLE") cloudcustodian/c7n run -v -s /home/custodian/output /home/custodian/policy.yml
2022-07-13 12:37:05,659: custodian.commands:DEBUG Loaded file /home/custodian/policy.yml. Contains 1 policies
2022-07-13 12:37:05,659: custodian.azure.session:INFO Authenticated [Principal | d955c0ba-13dc-44cf-a29a-8fed74cbb22d]
2022-07-13 12:37:05,660: custodian.cache:DEBUG Using in-memory cache
2022-07-13 12:37:05,661: custodian.output:DEBUG Storing output with <LogFile file:///home/custodian/output/my-first-policy/custodian-run.log>
2022-07-13 12:37:05,661: custodian.policy:DEBUG Running policy:my-first-policy resource:azure.vmss region:AzureCloud c7n:0.9.17
2022-07-13 12:37:05,868: msal.authority:DEBUG openid_config = {'token_endpoint': 'https://login.microsoftonline.com/28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a/oauth2/v2.0/token', 'token_endpoint_auth_methods_supported': ['client_secret_post', 'private_key_jwt', 'client_secret_basic'], 'jwks_uri': 'https://login.microsoftonline.com/28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a/discovery/v2.0/keys', 'response_modes_supported': ['query', 'fragment', 'form_post'], 'subject_types_supported': ['pairwise'], 'id_token_signing_alg_values_supported': ['RS256'], 'response_types_supported': ['code', 'id_token', 'code id_token', 'id_token token'], 'scopes_supported': ['openid', 'profile', 'email', 'offline_access'], 'issuer': 'https://login.microsoftonline.com/28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a/v2.0', 'request_uri_parameter_supported': False, 'userinfo_endpoint': 'https://graph.microsoft.com/oidc/userinfo', 'authorization_endpoint': 'https://login.microsoftonline.com/28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a/oauth2/v2.0/authorize', 'device_authorization_endpoint': 'https://login.microsoftonline.com/28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a/oauth2/v2.0/devicecode', 'http_logout_supported': True, 'frontchannel_logout_supported': True, 'end_session_endpoint': 'https://login.microsoftonline.com/28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a/oauth2/v2.0/logout', 'claims_supported': ['sub', 'iss', 'cloud_instance_name', 'cloud_instance_host_name', 'cloud_graph_host_name', 'msgraph_host', 'aud', 'exp', 'iat', 'auth_time', 'acr', 'nonce', 'preferred_username', 'name', 'tid', 'ver', 'at_hash', 'c_hash', 'email'], 'kerberos_endpoint': 'https://login.microsoftonline.com/28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a/kerberos', 'tenant_region_scope': 'NA', 'cloud_instance_name': 'microsoftonline.com', 'cloud_graph_host_name': 'graph.windows.net', 'msgraph_host': 'graph.microsoft.com', 'rbac_url': 'https://pas.windows.net'}
2022-07-13 12:37:05,869: msal.application:DEBUG Region to be used: None
2022-07-13 12:37:05,869: msal.application:DEBUG Found 0 RTs matching {'environment': 'login.microsoftonline.com', 'home_account_id': None, 'family_id': '1'}
2022-07-13 12:37:05,869: msal.application:DEBUG Found 0 RTs matching {'environment': 'login.microsoftonline.com', 'home_account_id': None, 'client_id': 'b8e8d285-0680-4055-b516-96061d2abb67'}
2022-07-13 12:37:05,938: msal.telemetry:DEBUG Generate or reuse correlation_id: df7f0c94-7ee3-4e81-95b9-798aa9de4bf4
2022-07-13 12:37:06,039: msal.token_cache:DEBUG event={
    "client_id": "b8e8d285-0680-4055-b516-96061d2abb67",
    "data": {
        "claims": "{\"access_token\": {\"xms_cc\": {\"values\": [\"CP1\"]}}}",
        "scope": [
            "https://management.core.windows.net/.default"
        ]
    },
    "environment": "login.microsoftonline.com",
    "grant_type": "client_credentials",
    "params": null,
    "response": {
        "access_token": "********",
        "expires_in": 3599,
        "ext_expires_in": 3599,
        "token_type": "Bearer"
    },
    "scope": [
        "https://management.core.windows.net/.default"
    ],
    "token_endpoint": "https://login.microsoftonline.com/28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a/oauth2/v2.0/token"
}
2022-07-13 12:37:06,398: custodian.azure.utils.ServiceClient.send:DEBUG 200
2022-07-13 12:37:06,398: custodian.azure.utils.ServiceClient.send:DEBUG x-ms-ratelimit-remaining-resource:Microsoft.Compute/HighCostGetVMScaleSet3Min;179,Microsoft.Compute/HighCostGetVMScaleSet30Min;887
2022-07-13 12:37:06,398: custodian.azure.utils.ServiceClient.send:DEBUG x-ms-ratelimit-remaining-subscription-reads:11999
2022-07-13 12:37:06,408: custodian.resources.vmscaleset:DEBUG Filtered from 2 to 1 vmscaleset
2022-07-13 12:37:06,408: custodian.policy:INFO policy:my-first-policy resource:azure.vmss region:AzureCloud count:1 time:0.75
2022-07-13 12:37:06,412: msal.application:DEBUG Cache hit an AT
2022-07-13 12:37:06,412: msal.telemetry:DEBUG Generate or reuse correlation_id: b5b54b83-a5a0-48fd-860a-80ebab185f09
2022-07-13 12:37:06,864: custodian.azure.utils.ServiceClient.send:DEBUG 200
2022-07-13 12:37:06,864: custodian.azure.utils.ServiceClient.send:DEBUG x-ms-ratelimit-remaining-subscription-reads:11999
2022-07-13 12:37:09,758: custodian.azure.utils.ServiceClient.send:DEBUG 200
2022-07-13 12:37:09,758: custodian.azure.utils.ServiceClient.send:DEBUG x-ms-ratelimit-remaining-resource:Microsoft.Compute/VMScaleSetActions3Min;239,Microsoft.Compute/VMScaleSetActions30Min;1199,Microsoft.Compute/VmssQueuedVMOperations;0
2022-07-13 12:37:09,758: custodian.azure.utils.ServiceClient.send:DEBUG x-ms-ratelimit-remaining-subscription-writes:1199
2022-07-13 12:37:09,761: custodian.azure.tagging.Tag:INFO Action 'tag' modified 'azdovmssexample' in resource group 'AZDOVMSSEXAMPLE'. {'Hello': 'World'}
2022-07-13 12:37:09,762: custodian.policy:INFO policy:my-first-policy action:tag resources:1 execution_time:3.35
2022-07-13 12:37:09,763: custodian.output:DEBUG metric:ResourceCount Count:1 policy:my-first-policy restype:azure.vmss scope:policy

If I log back into the Azure Portal, we’ll now see the VMSS was updated

/content/images/2022/07/custodian-15.png

But only that one named VMSS

/content/images/2022/07/custodian-16.png

Destructive Operations

Tagging is useful for categorizing resources automatically. However, we can enforce rules by stopping VMs or deleting resources altogether.

First, for a test, let me tag the other VMSS I use

/content/images/2022/07/custodian-17.png

One issue I encountered was that one of my VMSSes had a Spot Instance limit causing the Update Command to fail

2022-07-13 12:45:37,086: custodian.azure.tagging.Tag:ERROR tag failed for 'myvmsslarge'.
Traceback (most recent call last):
  File "/src/tools/c7n_azure/c7n_azure/actions/base.py", line 69, in _process_resources
    result = self._process_resource(r)
  File "/src/tools/c7n_azure/c7n_azure/actions/tagging.py", line 72, in _process_resource
    return TagHelper.add_tags(self, resource, new_tags)
  File "/src/tools/c7n_azure/c7n_azure/tags.py", line 80, in add_tags
    TagHelper.update_resource_tags(tag_action, resource, tags)
  File "/src/tools/c7n_azure/c7n_azure/tags.py", line 39, in update_resource_tags
    client.resources.begin_update_by_id(resource['id'], api_version, tags_patch)
  File "/usr/local/lib/python3.8/site-packages/azure/mgmt/resource/resources/v2020_10_01/operations/_resources_operations.py", line 1461, in begin_update_by_id
    raw_result = self._update_by_id_initial(
  File "/usr/local/lib/python3.8/site-packages/azure/mgmt/resource/resources/v2020_10_01/operations/_resources_operations.py", line 1412, in _update_by_id_initial
    map_error(status_code=response.status_code, response=response, error_map=error_map)
  File "/usr/local/lib/python3.8/site-packages/azure/core/exceptions.py", line 107, in map_error
    raise error
azure.core.exceptions.ResourceExistsError: (OperationNotAllowed) Unable to perform operation 'Update Virtual Machine Scale Set' since the provided max price '0.1 USD' is lower than the current spot price '0.1536 USD' for Azure Spot VM size 'Standard_D8s_v3'. For more information, see http://aka.ms/AzureSpot/errormessages.
Code: OperationNotAllowed
Message: Unable to perform operation 'Update Virtual Machine Scale Set' since the provided max price '0.1 USD' is lower than the current spot price '0.1536 USD' for Azure Spot VM size 'Standard_D8s_v3'. For more information, see http://aka.ms/AzureSpot/errormessages.

I could see the same error if I used the Portal and tried to Tag it

/content/images/2022/07/custodian-18.png

This is the first time I can ever recall seeing inflation reflected in my cloud configurations in such a direct way

/content/images/2022/07/custodian-19.png

I updated the value by a nickel.

When I verified that indeed, the filter on tag wouldn’t match my known good VMSSes

...
2022-07-13 12:51:12,220: custodian.azure.utils.ServiceClient.send:DEBUG 200
2022-07-13 12:51:12,220: custodian.azure.utils.ServiceClient.send:DEBUG x-ms-ratelimit-remaining-resource:Microsoft.Compute/HighCostGetVMScaleSet3Min;173,Microsoft.Compute/HighCostGetVMScaleSet30Min;866
2022-07-13 12:51:12,221: custodian.azure.utils.ServiceClient.send:DEBUG x-ms-ratelimit-remaining-subscription-reads:11999
2022-07-13 12:51:12,231: custodian.resources.vmscaleset:DEBUG Filtered from 2 to 0 vmscaleset
2022-07-13 12:51:12,231: custodian.policy:INFO policy:my-second-policy resource:azure.vmss region:AzureCloud count:0 time:0.62
2022-07-13 12:51:12,232: custodian.output:DEBUG metric:ResourceCount Count:0 policy:my-second-policy restype:azure.vmss scope:policy
....

I then changed the policy to delete

$ cat vmss.policy2.yml
policies:
    - name: my-second-policy
      description: |
        removes untagged VMSSes
      resource: azure.vmss
      filters:
        - tag:Hello: absent
      actions:
       - type: delete

We can now watch and see it in action:

/content/images/2022/07/custodian-21.gif

Automating with AzDO

Let’s first create a new Repo to store the pipeline

/content/images/2022/07/custodian-22.png

And give it a reaonsable name

/content/images/2022/07/custodian-23.png

Next, I’ll “Set up build”

/content/images/2022/07/custodian-24.png

And choose a starter pipeline

/content/images/2022/07/custodian-25.png

We’ll change the contents to look like this:

trigger:
- main

pool:
  vmImage: ubuntu-latest


steps:
- script: |
    cat > $(Build.ArtifactStagingDirectory)/vmss.tag.policy.yml << "EOF"
    policies:
        - name: my-first-policy
          description: |
            Adds a tag to a virtual machine scale sets
          resource: azure.vmss
          filters:
            - type: value
              key: name
              value: azdovmssexample
          actions:
          - type: tag
            tag: Hello2
            value: World4
    EOF
    cat $(Build.ArtifactStagingDirectory)/vmss.tag.policy.yml
  displayName: 'Inline VMSS Tag Policy'

- script: |
    mkdir $(Build.ArtifactStagingDirectory)/output || true
    chmod 777 $(Build.ArtifactStagingDirectory)/output
  displayName: 'Create output dir'

- script: |
    docker run -v $(Build.ArtifactStagingDirectory)/output:/home/custodian/output \
       -v $(Build.ArtifactStagingDirectory)/vmss.tag.policy.yml:/home/custodian/policy.yml \
       -e AZURE_CLIENT_ID='$(myazureclientid)' \
       -e AZURE_CLIENT_SECRET='$(myazureclientsecret)' \
       -e AZURE_SUBSCRIPTION_ID='$(myazuresubscription)' \
       -e AZURE_TENANT_ID='$(myazuretenantid)' \
       cloudcustodian/c7n run \
        -v -s /home/custodian/output /home/custodian/policy.yml\
  displayName: 'Run Cloud Custodian'

- script: |
    find $(Build.ArtifactStagingDirectory)/output -type f -exec cat {} \; -print
  displayName: 'Show output files'
  condition: always()

We’ll need to set the secrets as well. You can use just build secret variables in this case

/content/images/2022/07/custodian-26.png

When done, you should have set these four

/content/images/2022/07/custodian-27.png

Then save and run

/content/images/2022/07/custodian-28.png

And if all goes well, you should see output in the build log

/content/images/2022/07/custodian-29.png

And a new tag applied

/content/images/2022/07/custodian-30.png

scheduling

Obviously, running this periodically would be ideal so things are automatically updated.

We can set a schedule two ways in AzDO. The first being via Triggers in the UI

/content/images/2022/07/custodian-31.png

and adding a Schedule

/content/images/2022/07/custodian-32.png

Using this approach we have the advantage of not having to update code (the azure-pipelines.yml) to change schedules, but at the consequence of not seeing who and when the schedule changes.

Instead, we’ll go ahead and use the YAML approach. Here we can set a schedule for daily at midnight

trigger:
- main

pool:
  vmImage: ubuntu-latest

schedules:
- cron: "0 0 * * *"
  displayName: Daily midnight build
  branches:
    include:
    - main
  always: true

steps:
- script: |
    cat > $(Build.ArtifactStagingDirectory)/vmss.tag.policy.yml << "EOF"
    policies:
        - name: my-first-policy
          description: |
            Adds a tag to a virtual machine scale sets
          resource: azure.vmss
          filters:
            - type: value
              key: name
              value: azdovmssexample
          actions:
          - type: tag
            tag: Hello2
            value: World4
    EOF
    cat $(Build.ArtifactStagingDirectory)/vmss.tag.policy.yml
  displayName: 'Inline VMSS Tag Policy'

- script: |
    mkdir $(Build.ArtifactStagingDirectory)/output || true
    chmod 777 $(Build.ArtifactStagingDirectory)/output
  displayName: 'Create output dir'

- script: |
    docker run -v $(Build.ArtifactStagingDirectory)/output:/home/custodian/output \
       -v $(Build.ArtifactStagingDirectory)/vmss.tag.policy.yml:/home/custodian/policy.yml \
       -e AZURE_CLIENT_ID='$(myazureclientid)' \
       -e AZURE_CLIENT_SECRET='$(myazureclientsecret)' \
       -e AZURE_SUBSCRIPTION_ID='$(myazuresubscription)' \
       -e AZURE_TENANT_ID='$(myazuretenantid)' \
       cloudcustodian/c7n run \
        -v -s /home/custodian/output /home/custodian/policy.yml\
  displayName: 'Run Cloud Custodian'

- script: |
    find $(Build.ArtifactStagingDirectory)/output -type f -exec cat {} \; -print
  displayName: 'Show output files'
  condition: always()

Note: Azure Pipelines timezone isn’t always your local timezone. It’s usually best to double check what Azure DevOps thinks “midnight” is. At times I’ve seen this really represent UTC. I believe it leverages the Organization timezone:

/content/images/2022/07/custodian-33.png

Summary

We experimented with C7n on the command line and in Docker. We showed how we could identify resources by tag, tag items that are missing required tags and lastly remove items that are untagged in Azure.

We could have automated by running through scheduled Ansible Jobs, scheduled AzDO Pipeline, Jenkins Job or a myriad of other ways to schedule automated workloads.

/content/images/2022/07/custodian-20.png

To demonstrate an example of that, we created an Azure Repo and Pipeline then automated the running of the docker file with an inline policy that tagged a specific VMSS. Lastly, we discussed schedules and how to automatically run that pipeline on a schedule to enforce policy.

Cloud Custodian is a nice addition to our toolset and partnered with tools like Kubecost and Ternary can track cloud costs and mitigate unapproved spend.

cloudcustodian kubernetes

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes