Skip to content

Instantly share code, notes, and snippets.

@outofcoffee
Created November 13, 2018 01:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save outofcoffee/ebb82e13809554b5a1dd2d123c6abd45 to your computer and use it in GitHub Desktop.
Save outofcoffee/ebb82e13809554b5a1dd2d123c6abd45 to your computer and use it in GitHub Desktop.
Migrates Terraform resources between modules.
#!/usr/bin/env groovy
import groovy.json.JsonSlurper
// Migrates Terraform resources between modules.
//
// Resources are read from the source Terraform module, imported into the target module,
// then removed from the source module. The resource names can differ between source and target
// modules.
//
// Configure the source and target mappings in a JSON file, e.g.
// [
// {
// "source": {
// "module": "api_gw",
// "resource": "module.alb_api_gw.aws_route53_record.r"
// },
// "target": {
// "module": "endpoints",
// "resource": "aws_route53_record.api_origin"
// }
// },
// // ...other mappings here
// ]
//
OptionAccessor parseArguments() {
def cli = new CliBuilder(usage: 'migrateTerraformResources.groovy [options]')
cli.h(required: false, longOpt: 'help', 'help/usage')
cli.e(required: true, longOpt: 'env', args: 1, 'environment')
cli.f(required: true, longOpt: 'mappings', args: 1, 'migration mapping file')
cli.stopAtNonOption = false
def options = cli.parse(args)
if (!options) {
System.exit(1)
} else if (options.h) {
cli.usage()
System.exit(0)
}
options
}
class Migrator {
def initialisedDirs = []
def migratedCount = 0
String exec(File workingDir, String command, boolean followOutput = false) {
println "\n> ${command}"
def commandElements = command.replaceAll("[\\r\\n]*", "").split("\\s+")
def procBuilder = new ProcessBuilder(commandElements)
.redirectErrorStream(true)
.directory(workingDir)
if (followOutput) procBuilder.inheritIO()
def proc = procBuilder.start()
if (0 != proc.waitFor()) throw new RuntimeException(proc.text) else proc.text
}
void initForEnvironment(File moduleDir, String environmentName) {
if (initialisedDirs.contains(moduleDir.toString())) {
return
}
exec(moduleDir, "terraform init", true)
try {
exec(moduleDir, "terraform workspace select ${environmentName}", true)
} catch (ignored) {
exec(moduleDir, "terraform workspace new ${environmentName}", true)
}
initialisedDirs += moduleDir.toString()
}
void migrateResources(List<Map<String, Map<String, String>>> mappings, String environmentName) {
for (mapping in mappings) {
println "\nChecking resource ${mapping.source.resource} in ${mapping.source.module}..."
File srcDir = new File(mapping.source.module)
initForEnvironment(srcDir, environmentName)
String resourceInfo = exec(srcDir, "terraform state show ${mapping.source.resource}")
if (null == resourceInfo || resourceInfo.trim().isEmpty()) {
println "Source resource not found: ${mapping.source.resource} - skipping"
continue
}
String resourceId = findResourceId(resourceInfo, mapping.source.resource)
File targetDir = new File(mapping.target.module)
initForEnvironment(targetDir, environmentName)
println "Importing resource into ${targetDir} state with ID: ${resourceId}..."
exec(targetDir, "terraform import ${mapping.target.resource} ${resourceId}", true)
migratedCount++
println "Removing resource from ${srcDir} state: ${mapping.source.resource}..."
exec(srcDir, "terraform state rm ${mapping.source.resource}", true)
}
}
String findResourceId(String resourceInfo, sourceResource) {
String resourceId
for (line in resourceInfo.split("\\n")) {
String[] parts = line.trim().split("\\s+")
if (parts.length < 3) {
println "Resource details not found: ${sourceResource} - skipping"
break
}
if (parts[0] == 'id') {
resourceId = parts[2]
break
}
}
resourceId
}
}
def options = parseArguments()
def mappings = new JsonSlurper().parseText(new File(options.f).text)
println "Attempting to migrate ${mappings.size()} resources using mappings in ${options.f}..."
def migrator = new Migrator()
migrator.migrateResources(mappings, options.e)
println "\n${migrator.migratedCount} resources migrated."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment