amplify-hosting
amplify-hosting copied to clipboard
How to delete Preview environments?
** Please describe which feature you have a question about? **
I made a PR on a private GitHub repo with a successful Amplify CI build and Preview link, I expected that closing the PR would delete the environment automatically. Failing that, I expected to see an 'Delete Preview Environment' option somewhere in the Amplify CI Console.
Possibly related to #277
Just to update, the Preview environment has disappeared by itself, no action on my part. Would be great if the Preview environment setup/teardown process was better documented 😞
@hassankhan the previews are automatically deleted when they are closed without requiring any action on your part. What could be better documented?
On the Preview docs page, there is only one line at the end which describes what happens to Preview environments. It would be nice if it clarified that it does indeed work for both merged AND closed pull requests, since currently it seems a bit vague.
Also, would be nice if the Preview status in the Amplify Console updated the PR's status. For example, in the following screenshot (taken from the docs), when a PR is being removed, it would be helpful for the status field to indicate that.

Hi, I am facing a similar issue, i created and merged a PR with a preview, the entry in pull requests was removed but the environment is still there one day after merging. has any one faced this issue?
Amplify Preview is broken. It often fails to create an environment, then fails the most basic thing, which is resuming its creation, which means you have to resort to deleting the stack manually, which is truly painful, because it won't delete non-empty S3 buckets and whatnot. There's a myriad of cases where it won't delete everything. To deal with this, I wrote a node script that lists all environments and then lets you delete them. However, it's proprietary to my app so it won't work perfectly for others. If anyone is interested, I might post it. Amplify should halt adding more features and fix these basic things that may otherwise draw out users from the platform.
@adi518 please post your script if you get a chance so we have a reference idea of how to delete the preview environment related resources. We are experiencing similar problems with our AWS Amplify Preview environments sometimes hanging around after the PR is closed or merged.
I have to package it and then I can post it.
Also facing this issue, we have some old preview environments from about a year ago that never got deleted. There does not seem to be away from the AWS amplify console to delete them so they are now stuck forever...
I'll post my script, totally forgot about that.
How to use:
- Create a new file
delete_stale_envs.ts. - Place the following script in your app (e.g.,
app-root/scripts). - Install
ts-nodeandenquirerasdevdependencies. If you don't want to use TS, you'll have to migrate the script to JS. - IMPORTANT Edit the
BLACKLISTED_ENVSconstant to reflect your protected environments correctly. - Run the script with
yarn ts-node ./scripts/delete_stale_envsand follow the prompts.
// delete_stale_envs.ts
// https://awscli.amazonaws.com/v2/documentation/api/latest/reference/amplify/index.html#cli-aws-amplify
// https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks?filteringStatus=active&filteringText=&viewNested=true&hideStacks=false
const { Confirm, Select } = require('enquirer');
function execPromise(
command: string,
{ logStdout, logStderr }: { logStdout: boolean; logStderr: boolean } = { logStdout: true, logStderr: true }
) {
return new Promise((resolve, reject) => {
child_process.exec(command, (error, stdout, stderr) => {
if (error) {
logStderr && console.error(error);
reject(error);
} else {
logStdout && console.log(stdout);
logStderr && console.error(stderr);
try {
resolve(JSON.parse(stdout));
} catch (error) {
resolve(stdout);
}
}
});
});
}
export async function execPromiseNoLog(command: string) {
return execPromise(command, { logStdout: false, logStderr: false }).catch(throwError);
}
function throwError(error: Error) {
throw error;
}
function logError(error: Error) {
console.error(error.message);
}
const APP_ID = '<APP_ID>';
const APP_NAME = '<APP_NAME>';
// prettier-ignore
const BLACKLISTED_ENVS = [
'prod',
'production',
'dev',
'development',
'staging',
'test',
];
type BackendEnvironment = { [key: string]: any };
type BackendEnvironments = BackendEnvironment[];
type Branch = { [key: string]: any };
type Branches = Branch[];
function confirmPrompt(message = 'Are you sure?') {
return new Confirm({ name: 'question', message }).run();
}
function SelectPrompt(options: any) {
return new Select(options).run();
}
async function deleteEnvironment(environmentName: string, stackName: string, bucketNames: string[]) {
// https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudformation/delete-stack.html
const deleteStackPromise = execPromiseNoLog(`aws cloudformation delete-stack --stack-name ${stackName}`);
// https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3/rb.html
const deleteS3BucketsPromises = bucketNames.map((bucketName) =>
execPromiseNoLog(`aws s3 rb s3://${bucketName} --force`).catch((error) => {
if (error.message.includes('NoSuchBucket')) {
// continue as normal; we don't care about non-existing buckets
} else {
throw error;
}
})
);
// https://awscli.amazonaws.com/v2/documentation/api/latest/reference/amplify/delete-backend-environment.html
const deleteFromConsolePromise = execPromiseNoLog(`aws amplify delete-backend-environment --app-id ${APP_ID} --environment-name ${environmentName}`);
return Promise.all([deleteFromConsolePromise, deleteStackPromise, ...deleteS3BucketsPromises]);
}
function getEnvironmentNames(environments: BackendEnvironments) {
return environments.map((env) => env.environmentName);
}
enum Actions {
DeleteAllStaleEnvironments = 'delete all stale environments',
DeleteSingleStaleEnvironment = 'delete single stale environment',
DeleteSingleEnvironment = 'delete single environment',
}
async function main() {
// should we delete a single or all environment?
const selectedAction = await SelectPrompt({
name: 'action',
message: 'Select action',
// prettier-ignore
choices: [
Actions.DeleteAllStaleEnvironments,
Actions.DeleteSingleStaleEnvironment,
Actions.DeleteSingleEnvironment
],
});
// prompt for blacklisted environments; we do not want to delete
// newly user-created environments (e.g., prod, dev, etc').
// https://stackoverflow.com/a/63338302/4106263
// https://www.npmjs.com/package/endent
console.log(`The following environments will *NOT* be deleted.\n\
If you created a new environment that does not appear in this list,\n\
please choose NO and add it to the blacklist.`);
console.table(BLACKLISTED_ENVS);
const confirmBlacklist = await confirmPrompt();
if (confirmBlacklist === false) {
return;
}
// https://awscli.amazonaws.com/v2/documentation/api/latest/reference/amplify/list-branches.html
const branchesPromise = getBranches().catch(throwError);
const backendEnvironmentsPromise = getBackendEnvironments().catch(throwError);
const [backendEnvironments = [], branches = []] = await Promise.all([backendEnvironmentsPromise, branchesPromise]);
// get all environments that don't have a branch;
// those are the ones we want to delete.
let backendEnvironmentsToDelete = backendEnvironments.filter((env) => branches.find((branch) => branch.backendEnvironmentArn === env.backendEnvironmentArn) === undefined);
if (backendEnvironments.length === 0) {
return console.log('No environments found.');
}
if (selectedAction === Actions.DeleteAllStaleEnvironments || selectedAction === Actions.DeleteSingleStaleEnvironment) {
if (backendEnvironmentsToDelete.length === 0) return console.log('No stale environments found.');
}
if (selectedAction === Actions.DeleteSingleStaleEnvironment) {
const staleEnvironmentNames = getEnvironmentNames(backendEnvironmentsToDelete);
const selectedEnvironment = await SelectPrompt({
name: 'Select stale environment',
choices: staleEnvironmentNames,
});
backendEnvironmentsToDelete = [backendEnvironmentsToDelete.find((env) => env.environmentName === selectedEnvironment)];
} else if (selectedAction === Actions.DeleteSingleEnvironment) {
console.warn('\nThe following environments are currently in use! proceed with caution.');
const environmentNames = getEnvironmentNames(backendEnvironments);
const selectedEnvironment = await SelectPrompt({ name: 'Select environment', choices: environmentNames });
backendEnvironmentsToDelete = [backendEnvironments.find((env) => env.environmentName === selectedEnvironment)];
}
// prompt to confirm deletion of stale environments;
// in order to delete an environment, we have to delete the following:
// * delete cloudformation stack
// * delete S3 buckets
// * delete environment from amplify console
//
// we have to manually delete s3 buckets, because
// when deleting a cloudformation stack, it does *NOT*
// delete non-empty buckets; they are not empty because
// amplify populates them when creating a new environment.
// every environment consists of two buckets, e.g:
// `amplify-<APP_NAME>-<ENV>-<BUCKET_ID>-deployment` (where amplify stores all its internals)
// `<APP_NAME><BUCKET_ID>-<ENV>` (where app content is stored)
// https://s3.console.aws.amazon.com/s3/home?region=us-east-1#
//
console.table(getEnvironmentNames(backendEnvironmentsToDelete));
const shouldDelete = await confirmPrompt('The following environments will be deleted, please confirm:');
if (shouldDelete) {
try {
const deleteEnvironmentsPromises = backendEnvironmentsToDelete.map((env) => {
const stackName = env.stackName;
const environmentName = env.environmentName;
const environmentId = env.stackName.split('-').pop();
const deploymentBucket = env.deploymentArtifacts;
const appContentBucket = `${APP_NAME}${environmentId}-${environmentName}`;
const bucketNames = [deploymentBucket, appContentBucket];
return deleteEnvironment(environmentName, stackName, bucketNames);
});
await Promise.all(deleteEnvironmentsPromises).catch(throwError);
if (selectedAction === Actions.DeleteSingleStaleEnvironment || selectedAction === Actions.DeleteSingleEnvironment) {
console.log('Deleted environment successfully ✓');
} else {
console.log('Deleted all environments successfully ✓');
}
} catch (error) {
console.error('Failed to delete environment(s) with the following error:', error);
}
}
}
async function getBranches(): Promise<Branches> {
const response: any = await execPromiseNoLog(`aws amplify list-branches --app-id ${APP_ID}`).catch(throwError);
return response.branches;
}
async function getBackendEnvironments(): Promise<BackendEnvironments> {
return new Promise((resolve, reject) => {
(async () => {
console.log('\nFetching Amplify backend environments...');
let nextToken = null;
let backendEnvironments = [];
do {
// https://awscli.amazonaws.com/v2/documentation/api/latest/reference/amplify/list-backend-environments.html
let command = `aws amplify list-backend-environments --app-id ${APP_ID}`;
if (nextToken) command = `${command} --next-token ${nextToken}`;
let response: any = await execPromiseNoLog(command).catch(reject);
backendEnvironments.push(...response.backendEnvironments);
nextToken = response.nextToken;
if (nextToken === null) break;
} while (nextToken);
backendEnvironments = backendEnvironments.filter((env) => !BLACKLISTED_ENVS.includes(env.environmentName));
resolve(backendEnvironments);
})();
});
}
main().catch(logError);
For anyone that stumbles on this and wants to delete orphaned amplify previews, you can use the amplify CLI to delete connected branches. Be careful to only delete your preview branches...
# list branches for app
aws amplify list-branches --app-id <amplify_app_id>
# delete branch for app
aws amplify delete-branch --app-id <amplify_app_id> --branch-name <your_branch_name>
# example
aws amplify delete-branch --app-id <amplify_app_id> --branch-name pr-1000
Your amplify app ID is in the URL of the amplify console and is the last part of the ARN.