Terraform and Spotify – Part 2 (how I got George Michael to shut up)

This is a follow-up to my first post on how leveraging Terraform with Spotify.

I’m sure you have someone in your family or in your group of friends with terrible musical taste.

I know I certainly had. When I was a kid, there would always be someone asking the DJ to play some terrible French tune. As an adult and father of 3, I had to endure the Frozen soundtracks on far too many occasions.

So I thought it would be handy to control which songs could be added to my Spotify playlist and in particular, can I find a way to block George Michael’s “Careless Whisper” from being added to my playlist?

The concept is to use Terraform and the Sentinel Policy-As-Code framework to block this track to be added to my playlist.

I first need to find the Spotify track Id of “Careless Whisper”. The Spotify Web API was super easy to use (note the OAuth token is pretty short-lived which is why I left it clear in the video).

Now that I’ve got my track id (5WDLRQ3VCdVrKw0njWe5E5), I know which value I don’t want in my playlist.

This time, I am going to use the Sentinel CLI and mock data to test out how to build my Sentinel policies.

There are dozens of sample policies here, some great learning resources here and even a playground where you can test your Sentinel policies.

The Sentinel CLI is great to test your policies.

The process to build and test a policy is:

  • Download the Sentinel CLI
  • Download the Sentinel mocks
  • Build the Sentinel policies
  • Run the test to verify the policy.
  • If needed, amend the policy to get your expected results. Repeat.

The Sentinel Mocks are generated from plans of Terraform Cloud runs. For example, when I ran a Terraform plan in my previous post, Terraform Cloud generates a Sentinel mock I can download locally.

Download the files and unzip them and you get a set of files.

These files captures different stages in the Terraform run (tfplan gives access to the Terraform plan used by the current run, tfconfig refers to the Terraform configuration described in the tf files, tfstate refers to the state of the workspace of the current run while tfrun refers to the workspace and the cost estimate data of the current run).

This “tfplan” is commonly used to control which resources can or cannot be created.

The “mock-” reflects these files are just captured at a moment in time and can be used to simulate our policies against them.

For example, in my mock-tfplan-v2.sentinel file, I had the following resource Terraform was going to create. I will be manually adding the line 14 with the track ID of the song we want to block to prove my code.

resource_changes = {
	"spotify_playlist.playlist": {
		"address": "spotify_playlist.playlist",
		"change": {
			"actions": [
				"create",
			],
			"after": {
				"description": "This playlist was created by Terraform",
				"name":        "Terraform Summer Playlist",
				"public":      true,
				"tracks": [
					# "5WDLRQ3VCdVrKw0njWe5E5",
					"37BZB0z9T8Xu7U3e65qxFy",
					"0VjIjW4GlUZAMYd2vXMi3b",
					"6OGogr19zPTM4BALXuMQpF",
					"5QO79kh1waicV47BqGRL3g",
					"7fBv7CLKzipRk6EC6TWHOB",
					"7MXVkk9YMctZqd1Srtv4MB",
					"6M3PsepEj5gyJoIi7Xvr7u",
					"22VdIZQfgXJea34mQxlt81",
				],
			},
////
		},
		"deposed":        "",
		"index":          null,
		"mode":           "managed",
		"module_address": "",
		"name":           "playlist",
		"provider_name":  "registry.terraform.io/conradludgate/spotify",
		"type":           "spotify_playlist",
	},
}

Setting up Sentinel Testing

Before I can test my policy, I need to set up a test case for my mock. Here, you have to be careful to create a test case directory with the right syntax:

/test/sentinel_policy_name

For me, my test directory is therefore called:

/test/prevent_careless_whisper

And my policy is called:

prevent_careless_whisper.sentinel

You then have to create a couple of test cases (pass.hcl and fail.hcl).

Here is pass.hcl:

module "tfplan-functions" {
  source = "./tfplan-functions.sentinel"
}

mock "tfplan/v2" {
  module {
    source = "mock-tfplan-pass.sentinel"
  }
}

test {
    rules = {
        main = true
    }
}

And here is fail.hcl:

module "tfplan-functions" {
  source = "./tfplan-functions.sentinel"
}

mock "tfplan/v2" {
  module {
    source = "mock-tfplan-fail.sentinel"
  }
}

test {
    rules = {
        main = false
    }
}

Go and save two copies of the “mock-tfplan-v2.sentinel” file: “mock-tfplan-pass.sentinel” that would pass the policy (without George Michael’s song added to the playlist) and “mock-tfplan-fail.sentinel” with that would fail the policy (with George’s atrocious song).

These are there to validate your logic. You want mock data that you expect to be successful and mock data that you expect to fail the policy.

My Sentinel policy is pretty simple and similar to the one I used in my previous blog post.

We look in the mock-tf-plan files for resources that are either created or updated.

# Imports mock data
import "tfplan/v2" as tfplan

import "tfplan-functions" as plan

# Get all playlists
playlists = filter tfplan.resource_changes as _, rc {
	rc.type is "spotify_playlist" and
		(rc.change.actions contains "create" or rc.change.actions is ["update"])
}

print(playlists)

# Disallowed Track
disallowed_track = [
	"5WDLRQ3VCdVrKw0njWe5E5",
]

print(disallowed_track)

violatingTrack = plan.filter_attribute_contains_items_from_list(playlists,
	"tracks", disallowed_track, true)

print(violatingTrack)

validated = length(violatingTrack["messages"]) is 0

main = rule {
	validated is true
}

When I run sentinel test, I can see that my policies are well written as I get the expected output:

% sentinel test -verbose
Installing test modules for test/prevent_careless_whisper/fail.hcl
  - Module tfplan-functions marked for installation
Installation complete for test/prevent_careless_whisper/fail.hcl
Installing test modules for test/prevent_careless_whisper/pass.hcl
  - Module tfplan-functions marked for installation
Installation complete for test/prevent_careless_whisper/pass.hcl

PASS - prevent_careless_whisper.sentinel
  PASS - test/prevent_careless_whisper/fail.hcl


    logs:
      spotify_playlist.playlist has tracks [5WDLRQ3VCdVrKw0njWe5E5, 37BZB0z9T8Xu7U3e65qxFy, 0VjIjW4GlUZAMYd2vXMi3b, 6OGogr19zPTM4BALXuMQpF, 5QO79kh1waicV47BqGRL3g, 7fBv7CLKzipRk6EC6TWHOB, 7MXVkk9YMctZqd1Srtv4MB, 6M3PsepEj5gyJoIi7Xvr7u, 22VdIZQfgXJea34mQxlt81] that has items [5WDLRQ3VCdVrKw0njWe5E5] from the forbidden list: [5WDLRQ3VCdVrKw0njWe5E5]
      {"messages": {"spotify_playlist.playlist": "spotify_playlist.playlist has tracks [5WDLRQ3VCdVrKw0njWe5E5, 37BZB0z9T8Xu7U3e65qxFy, 0VjIjW4GlUZAMYd2vXMi3b, 6OGogr19zPTM4BALXuMQpF, 5QO79kh1waicV47BqGRL3g, 7fBv7CLKzipRk6EC6TWHOB, 7MXVkk9YMctZqd1Srtv4MB, 6M3PsepEj5gyJoIi7Xvr7u, 22VdIZQfgXJea34mQxlt81] that has items [5WDLRQ3VCdVrKw0njWe5E5] from the forbidden list: [5WDLRQ3VCdVrKw0njWe5E5]"}, "resources": {"spotify_playlist.playlist": {"address": "spotify_playlist.playlist", "change": {"actions": ["create"], "after": {"description": "This playlist was created by Terraform", "name": "Terraform Summer Playlist", "public": true, "tracks": ["5WDLRQ3VCdVrKw0njWe5E5" "37BZB0z9T8Xu7U3e65qxFy" "0VjIjW4GlUZAMYd2vXMi3b" "6OGogr19zPTM4BALXuMQpF" "5QO79kh1waicV47BqGRL3g" "7fBv7CLKzipRk6EC6TWHOB" "7MXVkk9YMctZqd1Srtv4MB" "6M3PsepEj5gyJoIi7Xvr7u" "22VdIZQfgXJea34mQxlt81"]}, "after_unknown": {"id": true, "snapshot_id": true, "tracks": [false false false false false false false false]}, "before": null}, "deposed": "", "index": null, "mode": "managed", "module_address": "", "name": "playlist", "provider_name": "registry.terraform.io/conradludgate/spotify", "type": "spotify_playlist"}}}
    trace:
      prevent_careless_whisper.sentinel:24:1 - Rule "main"
        Value:
          false
  PASS - test/prevent_careless_whisper/pass.hcl


    logs:
      {"messages": {}, "resources": {}}
    trace:
      prevent_careless_whisper.sentinel:24:1 - Rule "main"
        Value:
          true

My testing is complete and I can apply my policies to my Terraform Workspaces. I associated my Sentinel Policy with my TF workspace and tried to create a playlist but it failed as I expected.

Failed policy

No more George Michael for me!

Full repo can be found here.


Thanks for reading my silly post. Obviously you will want to use Terraform and Sentinel for a real business use case so if you want to learn more about Sentinel, Roger is the worldwide expert on it:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s