Skip to content

Using generic CI/CD tools for your IaC automation? 🤖⚙️

Download the Build vs Buy Guide→

Provider»

What would you say if you could manage Spacelift resources - that is stacks, contexts, integrations, and configuration - using Spacelift? We hate ClickOps as much as anyone, so we designed everything from the ground up to be easily managed using a Terraform provider. We hope that advanced users will define most of their resources programmatically.

Self-Hosted Version Compatibility»

The Terraform provider uses our GraphQL API to manage Spacelift, and relies on certain features being available in the API in order to work. What this can sometimes mean is that a new feature is added to the Terraform provider which hasn't yet been made available in the GraphQL API for Self-Hosted versions of Spacelift.

Because of this, it's not always possible to use the latest version of the Terraform provider with Self-Hosted, and we recommend that you pin to a known-compatible version. You can do this using a required_providers block like in the following example:

1
2
3
4
5
6
7
8
terraform {
    required_providers {
        spacelift = {
            source = "spacelift-io/spacelift"
            version = "1.14.0"
        }
    }
}

The following table shows the latest version of the Terraform provider known to work with our Self-Hosted versions:

Self-Hosted Version Max Provider Version
2.1.0 1.15.0
2.0.0 1.14.0
1.3.0 1.14.0
1.2.1 1.13.1
1.2.0 1.13.0
1.1.0-hotfix.1 1.10.0
1.1.0 1.10.0
1.0.0 1.8.1
0.0.12 1.8.0
0.0.11 1.8.0
0.0.10 1.4.0
0.0.9 1.3.1
0.0.8-hotfix.1 1.3.1
0.0.8 1.3.1

Taking it for a spin»

Our Terraform provider is open source and its README always contains the latest available documentation. It's also distributed as part of our Docker runner image and available through our own provider registry. The purpose of this article isn't as much to document the provider itself but to show how it can be used to incorporate Spacelift resources into your infra-as-code.

So, without further ado, let's define a stack:

stack.tf
1
2
3
4
5
6
7
resource "spacelift_stack" "managed-stack" {
  name = "Stack managed by Spacelift"

  # Source code.
  repository = "testing-spacelift"
  branch     = "master"
}

That's awesome. But can we put Terraform to good use and integrate it with resources from a completely different provider? Sure we can, and we have a good excuse, too. Stacks accessibility can be managed by GitHub teams, so why don't we define some?

stack-and-teams.tf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
resource "github_team" "stack-readers" {
  name = "managed-stack-readers"
}

resource "github_team" "stack-writers" {
  name = "managed-stack-writers"
}

resource "spacelift_stack" "managed-stack" {
  name = "Stack managed by Spacelift"

  # Source code.
  repository = "testing-spacelift"
  branch     = "master"

}

Now that we programmatically combine Spacelift and GitHub resources, let's add AWS to the mix and give our new stack a dedicated IAM role:

stack-teams-and-iam.tf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
resource "github_team" "stack-readers" {
  name = "managed-stack-readers"
}

resource "github_team" "stack-writers" {
  name = "managed-stack-writers"
}

resource "spacelift_stack" "managed-stack" {
  name = "Stack managed by Spacelift"

  # Source code.
  repository = "testing-spacelift"
  branch     = "master"

}

# IAM role.
resource "aws_iam_role" "managed-stack" {
  name = "spacelift-managed-stack"

  assume_role_policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [
      jsondecode(
        spacelift_stack.managed-stack.aws_assume_role_policy_statement
      )
    ]
  })
}

# Attaching a nice, powerful policy to it.
resource "aws_iam_role_policy_attachment" "managed-stack" {
  role       = aws_iam_role.managed-stack.name
  policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
}

# Telling Spacelift stack to assume it.
resource "spacelift_stack_aws_role" "managed-stack" {
  stack_id = spacelift_stack.managed-stack.id
  role_arn = aws_iam_role.managed-stack.arn
}

Success

OK, so who wants to go back to clicking on things in the web GUI? Because you will likely need to do some clicking, too, at least with your first stack.

How it works»

Depending on whether you're using Terraform 0.12.x or higher, the Spacelift provider is distributed slightly differently. For 0.12.x users, the provider is distributed as a binary available in the runner Docker image in the same folder we put the Terraform binary. If you're using Terraform 0.13 and above, you can benefit from pulling our provider directly from our own provider registry. In order to do that, just point Terraform to the right location:

1
2
3
4
5
6
7
8
9
provider "spacelift" {}

terraform {
  required_providers {
    spacelift = {
      source = "spacelift-io/spacelift"
    }
  }
}

Using inside Spacelift»

Within Spacelift, the provider is configured by an environment variable SPACELIFT_API_TOKEN injected into each run and task belonging to stacks marked as administrative. This value is a bearer token that contains all the details necessary for the provider to work, including the full address of the API endpoint to talk to. It's technically valid for 3 hours but only when the run responsible for generating it is in Planning, Applying or Performing (for tasks) state and throughout that time it provides full administrative access to Spacelift entities that can be managed by Terraform within the same Spacelift account.

Using outside of Spacelift»

If you want to run the Spacelift provider outside of Spacelift, or you need to manage resources across multiple Spacelift accounts from the same Terraform project, the preferred method is to generate and use dedicated API keys. Note that unless you're just accessing whitelisted data resources, the Terraform use case will normally require marking the API key as administrative.

In order to set up the provider to use an API key, you will need the key ID, secret, and the API key endpoint:

1
2
3
4
5
6
7
8
variable "spacelift_key_id" {}
variable "spacelift_key_secret" {}

provider "spacelift" {
  api_key_endpoint = "https://your-account.app.spacelift.io"
  api_key_id       = var.spacelift_key_id
  api_key_secret   = var.spacelift_key_secret
}

These values can also be passed using environment variables, though this will only work to set up the provider for a single Spacelift account:

  • SPACELIFT_API_KEY_ENDPOINT for api_key_endpoint;
  • SPACELIFT_API_KEY_ID for api_key_id;
  • SPACELIFT_API_KEY_SECRET for api_key_secret;

If you want to talk to multiple Spacelift accounts, you just need to set up provider aliases like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
variable "spacelift_first_key_id" {}
variable "spacelift_first_key_secret" {}

variable "spacelift_second_key_id" {}
variable "spacelift_second_key_secret" {}

provider "spacelift" {
  alias = "first"

  api_key_endpoint = "https://first.app.spacelift.io"
  api_key_id       = var.spacelift_first_key_id
  api_key_secret   = var.spacelift_first_key_secret
}

provider "spacelift" {
  alias = "second"

  api_key_endpoint = "https://second.app.spacelift.io"
  api_key_id       = var.spacelift_second_key_id
  api_key_secret   = var.spacelift_second_key_secret
}

If you're running from inside Spacelift, you can still use the default, zero-setup provider for the current account with providers for accounts set up through API keys:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
variable "spacelift_that_key_id" {}
variable "spacelift_that_key_secret" {}

provider "spacelift" {
  alias = "this"
}

provider "spacelift" {
  alias = "that"

  api_key_endpoint = "https://that.app.spacelift.io"
  api_key_id       = var.spacelift_that_key_id
  api_key_secret   = var.spacelift_that_key_secret
}

Proposed workflow»

We suggest to first manually create a single administrative stack, and then use it to programmatically define other stacks as necessary. If you're using an integration like AWS, you should probably give the role associated with this stack full IAM access too, allowing it to create separate roles and policies for individual stacks.

If you want to share data or outputs between stacks, please consider programmatically creating Stack Dependencies.

Info

Programmatically generated stacks can still be manually augmented, for example by setting extra elements of the environment. Thanks to the magic of Terraform, these will simply be invisible to (and thus not disturbed by) your resource definitions.

Boundaries of programmatic management»

Spacelift administrative tokens are not like user tokens. Specifically, they allow access to a much smaller subset of the API. They allow managing the lifecycles of stacks, contexts, integrations, and configuration, but they won't allow you to create or even access Terraform state, runs or tasks, or their associated logs.

Administrative tokens have no superpowers either. They can't read write-only configuration elements any more than you can as a user. Unlike human users with user tokens, administrative tokens won't allow you to run env in a task and read back the logs.

In general, we believe that things like runs or tasks do not fit the (relatively static) Terraform resource lifecycle model and that hiding those parts of the API from Terraform helps us ensure the integrity of potentially sensitive data - just see the example above.