terraform/iam: create additional service account for VMs

This service account is used in the following commits and is attached to the VMs
This commit is contained in:
Leonard Cohnen 2025-02-20 20:09:30 +01:00
parent 90b0de8c00
commit bd65ad3697
14 changed files with 241 additions and 129 deletions

View File

@ -110,6 +110,7 @@ runs:
--projectID="${{ inputs.gcpProjectID }}" \
--zone="${{ inputs.gcpZone }}" \
--serviceAccountID="${{ inputs.namePrefix }}-sa" \
--serviceAccountVMID="${{ inputs.namePrefix }}-vm-sa" \
--update-config \
--tf-log=DEBUG \
--yes

View File

@ -87,10 +87,11 @@ type IAMConfigOptions struct {
// GCPIAMConfig holds the necessary values for GCP IAM configuration.
type GCPIAMConfig struct {
Region string
Zone string
ProjectID string
ServiceAccountID string
Region string
Zone string
ProjectID string
ServiceAccountID string
ServiceAccountVMID string
}
// AzureIAMConfig holds the necessary values for Azure IAM configuration.
@ -140,10 +141,11 @@ func (c *IAMCreator) createGCP(ctx context.Context, cl tfIAMClient, opts *IAMCon
defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel)
vars := terraform.GCPIAMVariables{
ServiceAccountID: opts.GCP.ServiceAccountID,
Project: opts.GCP.ProjectID,
Region: opts.GCP.Region,
Zone: opts.GCP.Zone,
IAMServiceAccountVM: opts.GCP.ServiceAccountID,
ServiceAccountID: opts.GCP.ServiceAccountVMID,
Project: opts.GCP.ProjectID,
Region: opts.GCP.Region,
Zone: opts.GCP.Zone,
}
if err := cl.PrepareWorkspace(path.Join(constants.TerraformEmbeddedDir, "iam", strings.ToLower(cloudprovider.GCP.String())), &vars); err != nil {
@ -158,7 +160,8 @@ func (c *IAMCreator) createGCP(ctx context.Context, cl tfIAMClient, opts *IAMCon
return IAMOutput{
CloudProvider: cloudprovider.GCP,
GCPOutput: GCPIAMOutput{
ServiceAccountKey: iamOutput.GCP.SaKey,
ServiceAccountKey: iamOutput.GCP.SaKey,
IAMServiceAccountVM: iamOutput.GCP.ServiceAccountVMMailAddress,
},
}, nil
}
@ -232,7 +235,8 @@ type IAMOutput struct {
// GCPIAMOutput contains the output information of a GCP IAM configuration.
type GCPIAMOutput struct {
ServiceAccountKey string `json:"serviceAccountID,omitempty"`
ServiceAccountKey string `json:"serviceAccountID,omitempty"`
IAMServiceAccountVM string `json:"iamServiceAccountVM,omitempty"`
}
// AzureIAMOutput contains the output information of a Microsoft Azure IAM configuration.

View File

@ -452,113 +452,124 @@ func TestIAMCreateGCP(t *testing.T) {
}
testCases := map[string]struct {
setupFs func(require *require.Assertions, provider cloudprovider.Provider, existingConfigFiles []string, existingDirs []string) afero.Fs
creator *stubIAMCreator
zoneFlag string
serviceAccountIDFlag string
projectIDFlag string
yesFlag bool
updateConfigFlag bool
existingConfigFiles []string
existingDirs []string
stdin string
wantAbort bool
wantErr bool
setupFs func(require *require.Assertions, provider cloudprovider.Provider, existingConfigFiles []string, existingDirs []string) afero.Fs
creator *stubIAMCreator
zoneFlag string
serviceAccountIDFlag string
serviceAccountVMIDFlag string
projectIDFlag string
yesFlag bool
updateConfigFlag bool
existingConfigFiles []string
existingDirs []string
stdin string
wantAbort bool
wantErr bool
}{
"iam create gcp": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
yesFlag: true,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
yesFlag: true,
},
"iam create gcp with existing config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
yesFlag: true,
existingConfigFiles: []string{constants.ConfigFilename},
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
yesFlag: true,
existingConfigFiles: []string{constants.ConfigFilename},
},
"iam create gcp --update-config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
updateConfigFlag: true,
yesFlag: true,
existingConfigFiles: []string{constants.ConfigFilename},
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
updateConfigFlag: true,
yesFlag: true,
existingConfigFiles: []string{constants.ConfigFilename},
},
"iam create gcp existing terraform dir": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
existingDirs: []string{constants.TerraformIAMWorkingDir},
yesFlag: true,
wantErr: true,
},
"iam create gcp invalid b64": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: invalidIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
yesFlag: true,
wantErr: true,
setupFs: defaultFs,
creator: &stubIAMCreator{id: invalidIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
yesFlag: true,
wantErr: true,
},
"interactive": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
stdin: "yes\n",
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
stdin: "yes\n",
},
"interactive update config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
stdin: "yes\n",
updateConfigFlag: true,
existingConfigFiles: []string{constants.ConfigFilename},
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
stdin: "yes\n",
updateConfigFlag: true,
existingConfigFiles: []string{constants.ConfigFilename},
},
"interactive abort": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
stdin: "no\n",
wantAbort: true,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
stdin: "no\n",
wantAbort: true,
},
"interactive abort update config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
stdin: "no\n",
wantAbort: true,
updateConfigFlag: true,
existingConfigFiles: []string{constants.ConfigFilename},
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
stdin: "no\n",
wantAbort: true,
updateConfigFlag: true,
existingConfigFiles: []string{constants.ConfigFilename},
},
"unwritable fs": {
setupFs: readOnlyFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
yesFlag: true,
updateConfigFlag: true,
wantErr: true,
setupFs: readOnlyFs,
creator: &stubIAMCreator{id: validIAMIDFile},
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
serviceAccountVMIDFlag: "constell-test-vm",
projectIDFlag: "constell-1234",
yesFlag: true,
updateConfigFlag: true,
wantErr: true,
},
}
@ -588,9 +599,10 @@ func TestIAMCreateGCP(t *testing.T) {
},
providerCreator: &gcpIAMCreator{
flags: gcpIAMCreateFlags{
zone: tc.zoneFlag,
serviceAccountID: tc.serviceAccountIDFlag,
projectID: tc.projectIDFlag,
zone: tc.zoneFlag,
serviceAccountID: tc.serviceAccountIDFlag,
serviceAccountVMID: tc.serviceAccountVMIDFlag,
projectID: tc.projectIDFlag,
},
},
}

View File

@ -34,6 +34,9 @@ func newIAMCreateGCPCmd() *cobra.Command {
cmd.Flags().String("serviceAccountID", "", "ID for the service account that will be created (required)\n"+
"Must be 6 to 30 lowercase letters, digits, or hyphens.")
must(cobra.MarkFlagRequired(cmd.Flags(), "serviceAccountID"))
cmd.Flags().String("serviceAccountVMID", "", "ID for the VM service account that will be created (required)\n"+
"Must be 6 to 30 lowercase letters, digits, or hyphens.")
must(cobra.MarkFlagRequired(cmd.Flags(), "serviceAccountVMID"))
cmd.Flags().String("projectID", "", "ID of the GCP project the configuration will be created in (required)\n"+
"Find it on the welcome screen of your project: https://console.cloud.google.com/welcome")
must(cobra.MarkFlagRequired(cmd.Flags(), "projectID"))
@ -52,10 +55,11 @@ func runIAMCreateGCP(cmd *cobra.Command, _ []string) error {
// gcpIAMCreateFlags contains the parsed flags of the iam create gcp command.
type gcpIAMCreateFlags struct {
rootFlags
serviceAccountID string
zone string
region string
projectID string
serviceAccountID string
serviceAccountVMID string
zone string
region string
projectID string
}
func (f *gcpIAMCreateFlags) parse(flags *pflag.FlagSet) error {
@ -94,6 +98,14 @@ func (f *gcpIAMCreateFlags) parse(flags *pflag.FlagSet) error {
if !gcpIDRegex.MatchString(f.serviceAccountID) {
return fmt.Errorf("serviceAccountID %q doesn't match %s", f.serviceAccountID, gcpIDRegex)
}
f.serviceAccountVMID, err = flags.GetString("serviceAccountVMID")
if err != nil {
return fmt.Errorf("getting 'serviceAccountVMID' flag: %w", err)
}
if !gcpIDRegex.MatchString(f.serviceAccountVMID) {
return fmt.Errorf("serviceAccountVMID %q doesn't match %s", f.serviceAccountVMID, gcpIDRegex)
}
return nil
}
@ -105,10 +117,11 @@ type gcpIAMCreator struct {
func (c *gcpIAMCreator) getIAMConfigOptions() *cloudcmd.IAMConfigOptions {
return &cloudcmd.IAMConfigOptions{
GCP: cloudcmd.GCPIAMConfig{
Zone: c.flags.zone,
Region: c.flags.region,
ProjectID: c.flags.projectID,
ServiceAccountID: c.flags.serviceAccountID,
Zone: c.flags.zone,
Region: c.flags.region,
ProjectID: c.flags.projectID,
ServiceAccountID: c.flags.serviceAccountID,
ServiceAccountVMID: c.flags.serviceAccountVMID,
},
}
}
@ -116,6 +129,7 @@ func (c *gcpIAMCreator) getIAMConfigOptions() *cloudcmd.IAMConfigOptions {
func (c *gcpIAMCreator) printConfirmValues(cmd *cobra.Command) {
cmd.Printf("Project ID:\t\t%s\n", c.flags.projectID)
cmd.Printf("Service Account ID:\t%s\n", c.flags.serviceAccountID)
cmd.Printf("Service Account VM ID:\t%s\n", c.flags.serviceAccountVMID)
cmd.Printf("Region:\t\t\t%s\n", c.flags.region)
cmd.Printf("Zone:\t\t\t%s\n\n", c.flags.zone)
}

View File

@ -103,9 +103,18 @@ func (c *Client) ShowIAM(ctx context.Context, provider cloudprovider.Provider) (
if !ok {
return IAMOutput{}, errors.New("invalid type in service_account_key output: not a string")
}
IAMServiceAccountVMOutputRaw, ok := tfState.Values.Outputs["service_account_mail_vm"]
if !ok {
return IAMOutput{}, errors.New("no service_account_mail_vm output found")
}
IAMServiceAccountVMOutput, ok := IAMServiceAccountVMOutputRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in service_account_mail_vm output: not a string")
}
return IAMOutput{
GCP: GCPIAMOutput{
SaKey: saKeyOutput,
SaKey: saKeyOutput,
ServiceAccountVMMailAddress: IAMServiceAccountVMOutput,
},
}, nil
case cloudprovider.Azure:
@ -539,7 +548,8 @@ type IAMOutput struct {
// GCPIAMOutput contains the output information of the Terraform IAM operation on GCP.
type GCPIAMOutput struct {
SaKey string
SaKey string
ServiceAccountVMMailAddress string
}
// AzureIAMOutput contains the output information of the Terraform IAM operation on Microsoft Azure.

View File

@ -116,10 +116,11 @@ func TestPrepareCluster(t *testing.T) {
func TestPrepareIAM(t *testing.T) {
gcpVars := &GCPIAMVariables{
Project: "const-1234",
Region: "europe-west1",
Zone: "europe-west1-a",
ServiceAccountID: "const-test-case",
Project: "const-1234",
Region: "europe-west1",
Zone: "europe-west1-a",
ServiceAccountID: "const-test-case",
IAMServiceAccountVM: "test_iam_service_account_vm",
}
azureVars := &AzureIAMVariables{
Location: "westus",
@ -509,6 +510,9 @@ func TestCreateIAM(t *testing.T) {
"service_account_key": {
Value: "12345678_abcdefg",
},
"service_account_mail_vm": {
Value: "test_iam_service_account_vm",
},
"subscription_id": {
Value: "test_subscription_id",
},
@ -581,7 +585,7 @@ func TestCreateIAM(t *testing.T) {
vars: gcpVars,
tf: &stubTerraform{showState: newTestState()},
fs: afero.NewMemMapFs(),
want: IAMOutput{GCP: GCPIAMOutput{SaKey: "12345678_abcdefg"}},
want: IAMOutput{GCP: GCPIAMOutput{SaKey: "12345678_abcdefg", ServiceAccountVMMailAddress: "test_iam_service_account_vm"}},
},
"gcp init fails": {
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
@ -614,7 +618,25 @@ func TestCreateIAM(t *testing.T) {
tf: &stubTerraform{
showState: &tfjson.State{
Values: &tfjson.StateValues{
Outputs: map[string]*tfjson.StateOutput{},
Outputs: map[string]*tfjson.StateOutput{
"service_account_mail_vm": {Value: "test_iam_service_account_vm"},
},
},
},
},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"gcp no service_account_mail_vm": {
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
provider: cloudprovider.GCP,
vars: gcpVars,
tf: &stubTerraform{
showState: &tfjson.State{
Values: &tfjson.StateValues{
Outputs: map[string]*tfjson.StateOutput{
"service_account_key": {Value: "12345678_abcdefg"},
},
},
},
},
@ -1129,7 +1151,8 @@ func TestShowIAM(t *testing.T) {
"GCP success": {
tf: &stubTerraform{
showState: getTfjsonState(map[string]any{
"service_account_key": "key",
"service_account_key": "key",
"service_account_mail_vm": "example@example.com",
}),
},
csp: cloudprovider.GCP,
@ -1137,7 +1160,8 @@ func TestShowIAM(t *testing.T) {
"GCP wrong data type": {
tf: &stubTerraform{
showState: getTfjsonState(map[string]any{
"service_account_key": map[string]any{},
"service_account_key": map[string]any{},
"service_account_mail_vm": "example@example.com",
}),
},
csp: cloudprovider.GCP,
@ -1145,7 +1169,9 @@ func TestShowIAM(t *testing.T) {
},
"GCP missing key": {
tf: &stubTerraform{
showState: getTfjsonState(map[string]any{}),
showState: getTfjsonState(map[string]any{
"service_account_mail_vm": "example@example.com",
}),
},
csp: cloudprovider.GCP,
wantErr: true,

View File

@ -182,6 +182,8 @@ type GCPIAMVariables struct {
Zone string `hcl:"zone" cty:"zone"`
// ServiceAccountID is the ID of the service account to use.
ServiceAccountID string `hcl:"service_account_id" cty:"service_account_id"`
// IAMServiceAccountVM is the ID of the service account to attach to VMs.
IAMServiceAccountVM string `hcl:"service_account_id_vm" cty:"service_account_id_vm"`
}
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.

View File

@ -102,7 +102,7 @@ If you encounter any problem with the following steps, make sure to use the [lat
<TabItem value="gcp" label="GCP">
```bash
constellation iam create gcp --projectID=yourproject-12345 --zone=europe-west2-a --serviceAccountID=constell-test --update-config
constellation iam create gcp --projectID=yourproject-12345 --zone=europe-west2-a --serviceAccountVMID=constell-test-vm --serviceAccountID=constell-test --update-config
```
This command creates IAM configuration in the GCP project `yourproject-12345` on the GCP zone `europe-west2-a` creating a new service account `constell-test`. It also updates the configuration file `constellation-conf.yaml` in your current directory with the IAM values filled in.

View File

@ -685,13 +685,15 @@ constellation iam create gcp [flags]
### Options
```
-h, --help help for gcp
--projectID string ID of the GCP project the configuration will be created in (required)
Find it on the welcome screen of your project: https://console.cloud.google.com/welcome
--serviceAccountID string ID for the service account that will be created (required)
Must be 6 to 30 lowercase letters, digits, or hyphens.
--zone string GCP zone the cluster will be deployed in (required)
Find a list of available zones here: https://cloud.google.com/compute/docs/regions-zones#available
-h, --help help for gcp
--projectID string ID of the GCP project the configuration will be created in (required)
Find it on the welcome screen of your project: https://console.cloud.google.com/welcome
--serviceAccountID string ID for the service account that will be created (required)
Must be 6 to 30 lowercase letters, digits, or hyphens.
--serviceAccountVMID string ID for the VM service account that will be created (required)
Must be 6 to 30 lowercase letters, digits, or hyphens.
--zone string GCP zone the cluster will be deployed in (required)
Find a list of available zones here: https://cloud.google.com/compute/docs/regions-zones#available
```
### Options inherited from parent commands

View File

@ -210,7 +210,7 @@ Paste the output into the corresponding fields of the `constellation-conf.yaml`
You must be authenticated with the [GCP CLI](https://cloud.google.com/sdk/gcloud) in the shell session with a user that has the [required permissions for IAM creation](../getting-started/install.md#set-up-cloud-credentials).
```bash
constellation iam create gcp --projectID=yourproject-12345 --zone=europe-west2-a --serviceAccountID=constell-test
constellation iam create gcp --projectID=yourproject-12345 --zone=europe-west2-a --serviceAccountVMID=constell-test-vm --serviceAccountID=constell-test
```
This command creates IAM configuration in the GCP project `yourproject-12345` on the GCP zone `europe-west2-a` creating a new service account `constell-test`.

View File

@ -45,11 +45,11 @@ resource "random_bytes" "measurement_salt" {
module "gcp_iam" {
// replace $VERSION with the Constellation version you want to use, e.g., v2.14.0
source = "https://github.com/edgelesssys/constellation/releases/download/$VERSION/terraform-module.zip//terraform-module/iam/gcp"
project_id = local.project_id
service_account_id = "${local.name}-sa"
zone = local.zone
region = local.region
project_id = local.project_id
service_account_id = "${local.name}-sa"
service_account_id_vm = "${local.name}-sa-vm"
zone = local.zone
region = local.region
}
module "gcp_infrastructure" {

View File

@ -13,6 +13,12 @@ provider "google" {
zone = var.zone
}
resource "google_service_account" "service_account_vm" {
account_id = var.service_account_id_vm
display_name = "Constellation service account for VMs"
description = "Service account used by the VMs"
}
resource "google_service_account" "service_account" {
account_id = var.service_account_id
display_name = "Constellation service account"
@ -65,6 +71,30 @@ resource "google_project_iam_member" "iam_service_account_user_role" {
depends_on = [null_resource.delay]
}
resource "google_project_iam_custom_role" "iam_custom_role_vm" {
# role_id must not contain dashes
role_id = replace("${var.service_account_id}-role", "-", "_")
title = "Constellation IAM role for VMs"
description = "Constellation IAM role for VMs"
permissions = [
"compute.instances.get",
"compute.instances.list",
"compute.subnetworks.get",
"compute.globalForwardingRules.list",
"compute.zones.list",
]
}
resource "google_project_iam_binding" "iam_binding_custom_role_vm_to_service_account_vm" {
project = var.project_id
role = "projects/${var.project_id}/roles/${google_project_iam_custom_role.iam_custom_role_vm.role_id}"
members = [
"serviceAccount:${google_service_account.service_account_vm.email}",
]
depends_on = [null_resource.delay]
}
resource "google_service_account_key" "service_account_key" {
service_account_id = google_service_account.service_account.name
depends_on = [null_resource.delay]

View File

@ -3,3 +3,9 @@ output "service_account_key" {
description = "Private key of the service account."
sensitive = true
}
output "service_account_mail_vm" {
value = google_service_account.service_account_vm.email
description = "Mail address of the service account to be attached to the VMs"
sensitive = false
}

View File

@ -8,6 +8,11 @@ variable "service_account_id" {
description = "ID for the service account being created. Must match ^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$."
}
variable "service_account_id_vm" {
type = string
description = "ID for the service account being created. Must match ^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$."
}
variable "region" {
type = string
description = "GCP region the cluster should reside in. Needs to have the N2D machine type available."