Skip to content

Commit

Permalink
That's one small step for mankind, one giant leap for a company
Browse files Browse the repository at this point in the history
  • Loading branch information
jmfontaine committed Sep 21, 2022
0 parents commit 9f9ab7f
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.terraform
.terraform.lock.hcl
terraform.tfstate*
*.tfvars
!*.example.tfvars
/generator.tftpl
/out
116 changes: 116 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Spacelift Migration Kit

This repository contains scripts to help you move from various vendors to Spacelift.

There is no one-size-fits-all for this kind of migration so this kit aims at doing the heavy lifting and getting you 90% through. You will likely need to slightly tweak the generated Terraform code to fit your specific context.

## Overview

The migration process is as follows:

- Export the definition for your resources at your current vendor.
- Generate the Terraform code to recreate similar resources at Spacelift using the [Terraform provider](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs).
- Review and possibly edit the generated Terraform code.
- Commit the Terraform code to a repository.
- Create a manager Spacelift stack that points to the repository with the Terraform code.

## Supported Source

Currently, only Terraform Cloud/Enterprise is supported as a source.

## Prerequisites

- Terraform


## Instructions

### Preparation

Use the `terraform login spacelift.io` command to ensure that Terraform can interact with your Spacelift account.

Depending on the exporter used, you may need additional steps:

- **Terraform Cloud/Enterprise**: Use the `terraform login` command to ensure that Terraform can interact with your Terraform Cloud/Enterprise account.

### Pre-Migration Cleanup

In order to start fresh, clean up files and folders from previous runs.

```shell
rm -rf ./out ./{exporters/tfc,generator,manager-stack}/.terraform ./{exporters/tfc,generator,manager-stack}/.terraform.lock.hcl ./{exporters/tfc,generator,manager-stack}/terraform.tfstate ./{exporters/tfc,generator,manager-stack}/terraform.tfstate.backup
```

### Export the resource definitions and Terraform state

- Choose an exporter and copy the example `.tfvars` file for it into `exporter.tfvars`.
- Edit that file to match your context.
- Run the following commands:

```shell
cd exporters/<EXPORTER>
terraform init
terraform apply -auto-approve -var-file=../../exporter.tfvars
```

A new `out` folder should have been created. The `data.json` files contains the mapping of your vendor resources to the equivalent Spacelift resources, and the `state-files` folder contains the files for the Terraform state of your stacks, if the state export was enabled.

Please note that once exported the Terraform state files can be imported into Spacelift or to any backend supported by Terraform.

### Generate the Terraform code

- If you want to customize the template that generates the Terraform code, run `cp ../../generator/generator.tftpl ../generator.tftpl`, and edit the `generator.tftpl` file at the root of the repository. If present, it will be used automatically.
- Run the following commands:

```shell
cd ../../generator
terraform init
terraform apply -auto-approve -var-file=../out/data.json
```

### Review and edit the generated Terraform code

A `main.tf` should have been generated in the `out` folder. It contains all the Terraform code for your Spacelift resources.

Mapping resources from a vendor to Spacelift resources is not an exact science. There are gaps in functionality and caveats in the mapping process.

Please carefully review the generated Terraform code and make sure that it looks fine. If it does not, repeat the process with a different configuration or edit the Terraform code.

### Commit the Terraform code

When the Terraform code is ready, commit it to a repository.

### Create a manager Spacelift stack

It is now time to create a Spacelift stack that will point to the commited Terraform code that manages your Spacelift resources.

- Copy the example `manager-stack.example.tfvars` file into `manager-stack.tfvars` .
- Edit that file to match your context.
- Run the following commands:

```shell
cd ../manager-stack
terraform init
terraform apply -auto-approve -var-file=../manager-stack.tfvars
```

After the stack has been created, a tracked run will be triggered automatically. That run will create the defined Spacelift resources.

### Post-Migration Cleanup

Before you can use Spacelift to manage your infrastructure, you may need to make changes to the Terraform code for your infrastructure, depending on the Terraform state is managed.

If the Terraform state is managed by Spacelift,perform the following actions, otherwise you can skip this section:

- Remove any [backend](https://developer.hashicorp.com/terraform/language/settings/backends/configuration#using-a-backend-block)/[cloud](https://developer.hashicorp.com/terraform/language/settings/terraform-cloud) block from the Terraform code that manages your infrastructure to avoid a conflict with Spacelift's backend.
- Delete the `import_state_file` arguments from the Terraform code that manages your Spacelift resources.
- After the manager stack has successfully run, the mounted Terraform state files are not needed anymore and can be deleted by setting the `import_state` argument to `false` in the `manager-stack.tfvars` file and run `terraform apply -auto-approve -var-file=../manager-stack.tfvars` in the `manager-stack` folder.


## Known Limitations

### Terraform Cloud/Enterprise Exporter

- The variable sets are not exposed so they cannot be listed and exported.
- The name of the Version Control System (VCS) provider for a stack is not returned so it has to be set in the exporter configuration file.
- When the branch for the stack is the repository default branch, the value is empty. You can set the value for the default branch in the exporter configuration file, or edit the generated Terraform code.
17 changes: 17 additions & 0 deletions exporter.tfc.example.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Terraform Cloud/Enterprise organization name
tfc_organization = ""

# Export Terraform state to files?
export_state = true

# Terraform Cloud/Enterprise does not return the VCS provider name so we use the value below instead.
vcs_provider = "github"

# The name of the entity containing the repository.
# The value should be empty for GitHub.com, the user/organization for GitHub (custom application),
# the project for Bitbucket, and the namespace for Gitlab.
vcs_namespace = ""

# When the branch for the stack is the repository's default branch,
# the value is empty so we use the value provided below instead
vcs_default_branch = "main"
85 changes: 85 additions & 0 deletions exporters/tfc/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
locals {
stack_ids = [for i, v in data.tfe_workspace_ids.all.ids : v]
stack_names = [for i, v in data.tfe_workspace_ids.all.ids : i]
stacks = [for i, v in data.tfe_workspace_ids.all.ids : {
autodeploy = data.tfe_workspace.all[i].auto_apply
env_vars = [for i, v in data.tfe_variables.all[v].variables : {
name = v.category == "terraform" ? "TF_VAR_${v.name}" : v.name
sensitive = v.sensitive
value = v.value
}]
labels = data.tfe_workspace.all[i].tag_names
manage_state = var.export_state
name = data.tfe_workspace.all[i].name
terraform_version = data.tfe_workspace.all[i].terraform_version
vcs = {
# The "identifier" argument contains the acccount/organization and the respository names, separated by a slash
account = length(data.tfe_workspace.all[i].vcs_repo) > 0 ? split("/", data.tfe_workspace.all[i].vcs_repo[0].identifier)[1] : ""

# When the branch for the stack is the repository's default branch, the value is empty so we use the value provided via the variable
branch = length(data.tfe_workspace.all[i].vcs_repo) > 0 ? data.tfe_workspace.all[i].vcs_repo[0].branch != "" ? data.tfe_workspace.all[i].vcs_repo[0].branch : var.vcs_default_branch : var.vcs_default_branch

namespace = var.vcs_namespace
project_root = data.tfe_workspace.all[i].working_directory

# TFC/TFE does not return the VCS provider name so we use the value provided via the variable
provider = var.vcs_provider

# The "identifier" argument contains the acccount/organization and the respository names, separated by a slash
repository = length(data.tfe_workspace.all[i].vcs_repo) > 0 ? split("/", data.tfe_workspace.all[i].vcs_repo[0].identifier)[1] : ""
}
}]
data = jsonencode({
"stacks" : local.stacks
})
}

data "tfe_workspace_ids" "all" {
names = ["*"]
organization = var.tfc_organization
}

data "tfe_workspace" "all" {
for_each = toset(local.stack_names)

name = each.key
organization = var.tfc_organization
}

data "tfe_variables" "all" {
for_each = toset(local.stack_ids)

workspace_id = each.key
}

resource "local_file" "data" {
content = local.data
filename = "${path.module}/../../out/data.json"
}

resource "local_file" "generate_temp_tf_files" {
for_each = var.export_state ? toset(local.stack_names) : []

content = templatefile("${path.module}/main.tftpl", { tfc_organization = var.tfc_organization, workspace = each.key })
filename = "${path.module}/../../out/tf-files/${each.key}/main.tf"
}

resource "null_resource" "export_state_files" {
depends_on = [local_file.generate_temp_tf_files]
for_each = var.export_state ? toset(local.stack_names) : []

provisioner "local-exec" {
command = "mkdir -p ../../state-files && rm -rf .terraform .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup && terraform init -input=false && terraform state pull > ../../state-files/'${each.key}.tfstate'"
working_dir = "${path.module}/../../out/tf-files/${each.key}"
}
}

resource "null_resource" "delete_temp_tf_files" {
count = var.export_state ? 1 : 0
depends_on = [null_resource.export_state_files]

provisioner "local-exec" {
command = "rm -rf tf-files"
working_dir = "${path.module}/../../out/"
}
}
8 changes: 8 additions & 0 deletions exporters/tfc/main.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
cloud {
organization = "${tfc_organization}"
workspaces {
name = "${workspace}"
}
}
}
28 changes: 28 additions & 0 deletions exporters/tfc/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
variable "export_state" {
default = true
description = "Export Terraform state to files?"
type = bool
}

variable "tfc_organization" {
description = "TFC/TFE organization name"
type = string
}

variable "vcs_default_branch" {
default = "main"
description = "Name of the repositories' default branch"
type = string
}

variable "vcs_namespace" {
default = ""
description = "The name of the entity containing the repository. The value should be empty for GitHub.com, the user/organization for GitHub (custom application), the project for Bitbucket, and the namespace for Gitlab."
type = string
}

variable "vcs_provider" {
default = "github"
description = "Name of the Version Control System (VCS) provider to use"
type = string
}
40 changes: 40 additions & 0 deletions generator/generator.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
terraform {
required_providers {
spacelift = {
source = "spacelift-io/spacelift"
version = "~> 0.1"
}
}
}
%{ for stack in stacks ~}

resource "spacelift_stack" "${replace(lower(stack.name), "-", "_")}" {
%{if stack.vcs.provider != "github" ~}
${stack.vcs.provider} {
namespace = "${stack.vcs.namespace}"
}
%{endif ~}
autodeploy = ${stack.autodeploy}
branch = "${stack.vcs.branch}"
name = "${stack.name}"
project_root = "${stack.vcs.project_root}"
repository = "${stack.vcs.repository}"
terraform_version = "${stack.terraform_version}"

%{if stack.manage_state ~}
# 8< --------------------------------------------------------------
# Delete the following line after the stack has been created
import_state_file = "/mnt/workspace/state-import/${stack.name}.tfstate"
# -------------------------------------------------------------- 8<
%{endif ~}
}

%{ for env_var in stack.env_vars ~}
resource "spacelift_environment_variable" "${replace(lower(env_var.name), "-", "_")}" {
stack_id = spacelift_stack.${replace(lower(stack.name), "-", "_")}.id
name = "${env_var.name}"
value = "${env_var.value}"
write_only = ${env_var.sensitive}
}
%{ endfor ~}
%{ endfor ~}
13 changes: 13 additions & 0 deletions generator/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
locals {
# Use custom template file, if present
template_file = fileexists("${path.module}/../generator.tftpl") ? "${path.module}/../generator.tftpl" : "${path.module}/generator.tftpl"
}

resource "local_file" "terraform" {
content = templatefile(local.template_file, { stacks = var.stacks })
filename = "${path.module}/../out/main.tf"

provisioner "local-exec" {
command = "terraform fmt ${self.filename}"
}
}
22 changes: 22 additions & 0 deletions generator/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
variable "stacks" {
description = "Stacks to import"
type = list(object({
autodeploy = bool
env_vars = list(object({
name = string
sensitive = bool
value = string
}))
manage_state = bool
name = string
terraform_version = string
vcs = object({
account = string
branch = string
namespace = string
project_root = string
provider = string
repository = string
})
}))
}
26 changes: 26 additions & 0 deletions manager-stack.example.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Name of the manager stack
stack_name = "Spacelift Manager"

# Description for the stack
stack_description = "Spacelift resources manager"

# Name of the repository to associate with the stack
repository = "spacelift"

# Name of the branch to associate with the stack
branch = "main"

# Path to the folder containing the Terraform code, in case of a monorepo
project_root = ""

# Import the Terraform state for the managed stacks into Spacelift?
import_state = false

# Spacelift API endpoint
spacelift_api_key_endpoint = "https://example.app.spacelift.io/"

# Spacelift API key ID - Alternatively, you could pass that value via the SPACELIFT_API_KEY_ID env var
spacelift_api_key_id = ""

# Spacelift API key secret - Alternatively, you could pass that value via the SPACELIFT_API_KEY_SECRET env var
spacelift_api_key_secret = ""
Loading

0 comments on commit 9f9ab7f

Please sign in to comment.