Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions .github/workflows/ministack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: "MiniStack Terraform lifecycle"

on:
push:
branches:
- main
paths:
- ".github/workflows/ministack.yml"
- "tests/ministack/**"
- "*.tf"
- "policies/**"
- "examples/**"
- "modules/**"
pull_request:
paths:
- ".github/workflows/ministack.yml"
- "tests/ministack/**"
- "*.tf"
- "policies/**"
- "examples/**"
- "modules/**"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
AWS_ACCESS_KEY_ID: "000000000000"
AWS_DEFAULT_REGION: eu-west-1
AWS_EC2_METADATA_DISABLED: "true"
AWS_ENDPOINT_URL: http://ministack:4566
AWS_REGION: eu-west-1
AWS_SECRET_ACCESS_KEY: test-only
TF_IN_AUTOMATION: "true"
TF_INPUT: "false"

jobs:
terraform_lifecycle:
name: Apply/destroy ${{ matrix.example }} against MiniStack
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
max-parallel: 4
matrix:
example:
- base
- default
- ephemeral
- external-managed-ssm-secrets
- multi-runner
- permissions-boundary
- prebuilt
- termination-watcher
container:
image: hashicorp/terraform@sha256:1d10ec4073f4ddbdf34a28540a3b9250852ab500cb1c53f68c8bd17d82f474d8 # 1.14
services:
ministack:
image: ghcr.io/ministackorg/ministack:1.5.0@sha256:ba48c20747780605a4287a950e7bb1758ddc3b55ec92409a0c47677cbe26bbb9
env:
MINISTACK_ACCOUNT_ID: "000000000000"
MINISTACK_REGION: eu-west-1
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Mark repository as safe
shell: sh
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"

- id: prepare
name: Prepare isolated fixture
env:
EXAMPLE: ${{ matrix.example }}
run: tests/ministack/run-example.sh prepare "$EXAMPLE"

- id: fixture
name: Apply shared fixture
env:
EXAMPLE: ${{ matrix.example }}
run: tests/ministack/run-example.sh fixture-apply "$EXAMPLE"

- id: setup
name: Apply permissions-boundary prerequisite
if: matrix.example == 'permissions-boundary'
env:
EXAMPLE: ${{ matrix.example }}
run: tests/ministack/run-example.sh setup-apply "$EXAMPLE"

- id: init
name: Terraform init
env:
EXAMPLE: ${{ matrix.example }}
run: tests/ministack/run-example.sh init "$EXAMPLE"

- id: apply
name: Terraform apply
env:
EXAMPLE: ${{ matrix.example }}
run: tests/ministack/run-example.sh apply "$EXAMPLE"

- name: Terraform destroy
if: always() && steps.init.outcome == 'success'
env:
EXAMPLE: ${{ matrix.example }}
run: tests/ministack/run-example.sh destroy "$EXAMPLE"

- name: Destroy permissions-boundary prerequisite
if: always() && matrix.example == 'permissions-boundary' && steps.setup.outcome != 'skipped'
env:
EXAMPLE: ${{ matrix.example }}
run: tests/ministack/run-example.sh setup-destroy "$EXAMPLE"

- name: Destroy shared fixture
if: always() && steps.prepare.outcome == 'success' && steps.fixture.outcome != 'skipped'
env:
EXAMPLE: ${{ matrix.example }}
run: tests/ministack/run-example.sh fixture-destroy "$EXAMPLE"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# lock files
**/.terraform.lock.hcl
!examples/**/.terraform.lock.hcl
!tests/ministack/setup/.terraform.lock.hcl


# keys
Expand Down
30 changes: 30 additions & 0 deletions tests/ministack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# MiniStack example lifecycle tests

The MiniStack workflow runs explicit `terraform apply` and `terraform destroy`
commands against the repository's runnable AWS examples. Every matrix entry gets
an isolated source tree, Terraform state, and MiniStack service.

The fixture creates synthetic SSM values through MiniStack and an inert Lambda
archive under its ignored `.terraform/` directory. Test-only Terraform override
files route each example to that archive and a MiniStack AMI. They also disable
the `webhook-github-app` local-exec module, which would otherwise update a real
GitHub App. The production example configurations are not changed.

| Example | Lifecycle coverage |
| --- | --- |
| `base` | VPC and Resource Groups |
| `default` | Full default runner stack |
| `ephemeral` | Ephemeral runner and job-retry stack |
| `external-managed-ssm-secrets` | Runner stack with fixture-owned external SSM parameters |
| `multi-runner` | All runner lanes: public/private SSM wiring for Linux x64/ARM64 and module-managed AMI parameters for three lanes |
| `permissions-boundary` | IAM setup, assumed-role runner stack, and reverse-order teardown |
| `prebuilt` | Prebuilt-runner stack using a MiniStack AMI |
| `termination-watcher` | Standalone termination watcher |

Two examples are intentionally outside the matrix:

- `dedicated-mac-hosts` requires the EC2 Dedicated Hosts and License Manager
APIs, which MiniStack v1.5.0 does not implement.
- `lambdas-download` has no AWS resources. Its apply operation downloads release
archives from GitHub, so it is not a MiniStack lifecycle test. The underlying
`download-lambda` module remains in the existing module validation matrix.
40 changes: 40 additions & 0 deletions tests/ministack/overrides/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.10.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | = 6.35.1 |

## Providers

No providers.

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_runners"></a> [runners](#module\_runners) | | n/a |
| <a name="module_spot_termination_watchter"></a> [spot\_termination\_watchter](#module\_spot\_termination\_watchter) | | n/a |
| <a name="module_webhook_github_app"></a> [webhook\_github\_app](#module\_webhook\_github\_app) | | n/a |

## Resources

No resources.

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_ami_name_filter"></a> [ami\_name\_filter](#input\_ami\_name\_filter) | n/a | `string` | `"amzn2-ami-hvm-2.0.20231116.0-x86_64-gp2"` | no |
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | n/a | `string` | `"eu-west-1"` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | n/a | `string` | `"ministack-prebuilt"` | no |
| <a name="input_github_app"></a> [github\_app](#input\_github\_app) | n/a | `map` | <pre>{<br/> "id": "0",<br/> "key_base64": "ministack-invalid-key"<br/>}</pre> | no |
| <a name="input_github_app_ssm_parameters"></a> [github\_app\_ssm\_parameters](#input\_github\_app\_ssm\_parameters) | n/a | `map` | <pre>{<br/> "id": {<br/> "arn": "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/terraform-aws-github-runner/github-app/id",<br/> "name": "/ministack/terraform-aws-github-runner/github-app/id"<br/> },<br/> "key_base64": {<br/> "arn": "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/terraform-aws-github-runner/github-app/key-base64",<br/> "name": "/ministack/terraform-aws-github-runner/github-app/key-base64"<br/> },<br/> "webhook_secret": {<br/> "arn": "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/terraform-aws-github-runner/github-app/webhook-secret",<br/> "name": "/ministack/terraform-aws-github-runner/github-app/webhook-secret"<br/> }<br/>}</pre> | no |
| <a name="input_ministack_lambda_archive"></a> [ministack\_lambda\_archive](#input\_ministack\_lambda\_archive) | Absolute path to the inert Lambda archive created by the MiniStack fixture. | `string` | n/a | yes |
| <a name="input_prefix"></a> [prefix](#input\_prefix) | n/a | `string` | `"ministack-base"` | no |

## Outputs

No outputs.
<!-- END_TF_DOCS -->
7 changes: 7 additions & 0 deletions tests/ministack/overrides/base_override.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
variable "prefix" {
default = "ministack-base"
}

variable "aws_region" {
default = "eu-west-1"
}
19 changes: 19 additions & 0 deletions tests/ministack/overrides/common.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
variable "ministack_lambda_archive" {
description = "Absolute path to the inert Lambda archive created by the MiniStack fixture."
type = string

validation {
condition = endswith(var.ministack_lambda_archive, "/tests/ministack/setup/.terraform/ministack/lambda.zip")
error_message = "The MiniStack Lambda archive must come from the isolated test fixture."
}
}

locals {
ministack_ami = {
filter = {
name = ["amzn2-ami-hvm-2.0.20231116.0-x86_64-gp2"]
state = ["available"]
}
owners = ["self"]
}
}
29 changes: 29 additions & 0 deletions tests/ministack/overrides/default_override.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
variable "environment" {
default = "ministack-default"
}

variable "github_app" {
default = {
id = "0"
key_base64 = "ministack-invalid-key"
}
}

module "runners" {
ami = local.ministack_ami
ami_housekeeper_lambda_zip = var.ministack_lambda_archive
runner_binaries_syncer_lambda_zip = var.ministack_lambda_archive
runners_lambda_zip = var.ministack_lambda_archive
webhook_lambda_zip = var.ministack_lambda_archive

instance_termination_watcher = {
enable = true
zip = var.ministack_lambda_archive
}
}

# The production example updates GitHub through a local-exec provisioner.
# A zero count structurally prevents any external GitHub API call in this test.
module "webhook_github_app" {
count = 0
}
23 changes: 23 additions & 0 deletions tests/ministack/overrides/ephemeral_override.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
variable "environment" {
default = "ministack-ephemeral"
}

variable "github_app" {
default = {
id = "0"
key_base64 = "ministack-invalid-key"
}
}

module "runners" {
ami = local.ministack_ami
runner_binaries_syncer_lambda_zip = var.ministack_lambda_archive
runners_lambda_zip = var.ministack_lambda_archive
webhook_lambda_zip = var.ministack_lambda_archive
}

# The production example updates GitHub through a local-exec provisioner.
# A zero count structurally prevents any external GitHub API call in this test.
module "webhook_github_app" {
count = 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
variable "environment" {
default = "ministack-external-ssm"
}

variable "github_app_ssm_parameters" {
default = {
id = {
arn = "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/terraform-aws-github-runner/github-app/id"
name = "/ministack/terraform-aws-github-runner/github-app/id"
}
key_base64 = {
arn = "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/terraform-aws-github-runner/github-app/key-base64"
name = "/ministack/terraform-aws-github-runner/github-app/key-base64"
}
webhook_secret = {
arn = "arn:aws:ssm:eu-west-1:000000000000:parameter/ministack/terraform-aws-github-runner/github-app/webhook-secret"
name = "/ministack/terraform-aws-github-runner/github-app/webhook-secret"
}
}
}

module "runners" {
ami = local.ministack_ami
runner_binaries_syncer_lambda_zip = var.ministack_lambda_archive
runners_lambda_zip = var.ministack_lambda_archive
webhook_lambda_zip = var.ministack_lambda_archive
}
39 changes: 39 additions & 0 deletions tests/ministack/overrides/multi-runner_override.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
variable "environment" {
default = "ministack"
}

variable "github_app" {
default = {
id = "0"
key_base64 = "ministack-invalid-key"
}
}

locals {
# The production example consumes this override after it is copied into the
# example root; it is intentionally unused in this standalone fixture folder.
# tflint-ignore: terraform_unused_declarations
multi_runner_config = {
for name, config in local.multi_runner_config_files : name => merge(config, {
runner_config = merge(config.runner_config, {
ami = contains(keys(config.runner_config), "ami") ? merge(local.ministack_ami, {
id_ssm_parameter_arn = lookup(local.ssm_ami_arns, name, null)
}) : null
subnet_ids = lookup(config.runner_config, "subnet_ids", null) != null ? [module.base.vpc.private_subnets[0]] : null
vpc_id = lookup(config.runner_config, "vpc_id", null) != null ? module.base.vpc.vpc_id : null
})
})
}
}

module "runners" {
runner_binaries_syncer_lambda_zip = var.ministack_lambda_archive
runners_lambda_zip = var.ministack_lambda_archive
webhook_lambda_zip = var.ministack_lambda_archive
}

# The production example updates GitHub through a local-exec provisioner.
# A zero count structurally prevents any external GitHub API call in this test.
module "webhook_github_app" {
count = 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
provider "aws" {
alias = "terraform_role"
region = "eu-west-1"

# These values are MiniStack test identifiers and cannot authenticate to AWS.
access_key = "000000000000"
secret_key = "ministack-test-only"

skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
s3_use_path_style = true
}
13 changes: 13 additions & 0 deletions tests/ministack/overrides/permissions-boundary_override.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
variable "github_app" {
default = {
id = "0"
key_base64 = "ministack-invalid-key"
}
}

module "runners" {
ami = local.ministack_ami
runner_binaries_syncer_lambda_zip = var.ministack_lambda_archive
runners_lambda_zip = var.ministack_lambda_archive
webhook_lambda_zip = var.ministack_lambda_archive
}
Loading