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.

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
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
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"
# Access.
readers_team = github_team.stack_readers.slug
writers_team = github_team.stack_writers.slug
}

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
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"
# Access.
readers_team = github_team.stack_readers.slug
writers_team = github_team.stack_writers.slug
}
# 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
}

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:

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:

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:

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:

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 between stacks, please consider programmatically creating and attaching contexts.

Programmatically generated stacks and contexts 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.