Skip to content

Commit

Permalink
Add a Time-To-Live Stacks example (pulumi#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukehoban authored Oct 26, 2022
1 parent e392f56 commit 3f40519
Show file tree
Hide file tree
Showing 7 changed files with 2,906 additions and 0 deletions.
6 changes: 6 additions & 0 deletions pulumi-programs/ttl-stacks/Pulumi.dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
config:
aws:region: us-west-2
ttlstacks:githubToken:
secure: AAABAHtQA1KxZ3dqKUIh11TNyfas+vJxfASDzlpbuwRpP6UCnHONDEvd/P2V8kJu5nAVjRhYyPBOmC06tyW+tL9DcL4lgXjS
ttlstacks:pulumiAccessToken:
secure: AAABALPgH6Vxjw9qEL55mHNqu2QzAhVsGSQZo5bYGtMGBg18Ukth+oq1CQl1ZChTaPdJiGfdHbo8DyAG/SVhYNahUgfdCxKXCZUzsg==
3 changes: 3 additions & 0 deletions pulumi-programs/ttl-stacks/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: ttlstacks
description: Clean up resources that are past their TTL
runtime: nodejs
51 changes: 51 additions & 0 deletions pulumi-programs/ttl-stacks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# TTL Stacks

> Automatically destroy stacks that are older than their TTL.
Deploy automation with Pulumi that uses Pulumi Deploy to automatically trigger stack destroy on any stacks tagged with `pulumi:ttl` stack tag that are past the configured TTL value.

This stack deploys a cron job to AWS that runs every 30 minutes to identify stacks that need to be destroyed.

## Setup

1. Install prerequisites:

```bash
npm install
```

1. Create a new Pulumi stack, which is an isolated deployment target for this example:

```bash
pulumi stack init
```

1. Set required configuration

Using the pulumi deployment API requires a [pulumi access token](https://www.pulumi.com/docs/intro/pulumi-service/accounts/#access-tokens).
If using with private GitHub repos, an optional GitHub access token can also be provided.

```bash
pulumi config set aws:region us-west-2
pulumi config set --secret pulumiAccessToken xxxxxxxxxxxxxxxxx # your Pulumi access token
pulumi config set --secret githubToken xxxxxxxxxxxxxxxxx # (optional) your GitHub access token
```

1. Execute the Pulumi program:

```bash
pulumi up
```

1. Tag a stack.

You can now add the `pulumi:ttl` tag to any stack. This can be done either on the stack page at https://app.pulumi.com/<org>/<project>/<stack>, or via `pulumi stack tag set pulumi:ttl 24`.
The value set for the tag should be the number of hours the stack should live after it was created.

1. When you are done, cleanup:

```bash
pulumi destroy
```

(or just tag the stack with `pulumi:ttl` set to 0 to cause the stack to destroy itself! :-))
92 changes: 92 additions & 0 deletions pulumi-programs/ttl-stacks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import fetch from "node-fetch";

const config = new pulumi.Config();
const pat = config.requireSecret("pulumiAccessToken");
const githubtoken = config.getSecret("githubToken");

async function postDeployment(operation: string, stack: string, repoURL: string, branch: string, preRunCommands?: string[]) {
const res = await fetch(`https://api.pulumi.com/api/preview/${stack}/deployments`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `token ${pat.get()}`,
},
body: JSON.stringify({
sourceContext: {
git: {
repoURL,
branch,
gitAuth: githubtoken ? { accessToken: `${githubtoken.get()}` } : undefined,
},
},
operationContext: {
operation,
environmentVariables: {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN,
},
preRunCommands,
},
}),
});
console.debug(`status=${res.status}`);
return res.json();
}

async function getPulumiAPI(path: string) {
const res = await fetch(`https://api.pulumi.com/api${path}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `token ${pat.get()}`,
},
});
return res.json();
}

const subscription = aws.cloudwatch.onSchedule("get-tags", "rate(30 minutes)", async (ev, ctx) => {
const stacksToDestroy: Record<string, { ttl: string; runtime: string }> = {};
const resp = await getPulumiAPI(`/user/stacks?maxResults=2000&tagName=pulumi:ttl`);
for (const stackid of resp.stacks) {
const stack = await getPulumiAPI(`/stacks/${stackid.orgName}/${stackid.projectName}/${stackid.stackName}`);
const fqsn = `${stack.orgName}/${stack.projectName}/${stack.stackName}`;
for (const tag in stack.tags) {
if (tag == "pulumi:ttl") {
let tagValue = +(stack.tags[tag]);
if (isNaN(tagValue)) {
tagValue = 24;
}
const ttlSeconds = tagValue * 60 * 60;
const timeSinceUpdateSeconds = Math.floor(+new Date() / 1000) - (stackid.lastUpdate ?? 0);
const timeLeft = ttlSeconds - timeSinceUpdateSeconds;
console.log(`stack '${fqsn}' with TTL tag '${stack.tags[tag]}': ${timeLeft} seconds left`);
if (timeLeft <= 0) {
stacksToDestroy[fqsn] = {
ttl: stack.tags[tag],
runtime: stack.tags["pulumi:runtime"],
};
console.log(`registering stack ${fqsn} for deletion`)
}
break;
}
}
}
for (const fqsn in stacksToDestroy) {
console.log(`destroying stack ${fqsn}...`)
const stackDetails = stacksToDestroy[fqsn];
await postDeployment("destroy", fqsn, "https://github.com/lukehoban/blank", "refs/heads/main", [
// Try to remove the stack if it has no resources.
// TODO: Ideally this would be a post-run command or a `--rm` flag on the destroy operation.
`pulumi stack rm -y -s ${fqsn} || true`,
`echo "name: ${fqsn.split("/")[1]}" > Pulumi.yaml`,
`echo "runtime: ${stackDetails.runtime}" >> Pulumi.yaml`,
`pulumi config refresh -s ${fqsn}`,
]);
console.log(`destroyed stack ${fqsn}.`)
}
});

export const functionArn = subscription.func.name;
Loading

0 comments on commit 3f40519

Please sign in to comment.