Skip to content
184 changes: 92 additions & 92 deletions function-app-v1/ProviderRelay/__init__.py
Original file line number Diff line number Diff line change
@@ -1,92 +1,92 @@
# Copyright 2024 Stacklet
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
import json
import logging
import os
import azure.functions as func
from azure.identity import DefaultAzureCredential
import boto3
import botocore
def get_session(client_id, audience, role_arn):
client = boto3.client("sts")
creds = DefaultAzureCredential(
managed_identity_client=client_id, exclude_environment_credential=True
)
token = creds.get_token(audience)
try:
res = client.assume_role_with_web_identity(
WebIdentityToken=token.token,
RoleArn=role_arn,
RoleSessionName="StackletAzureRelay",
)
except Exception as e:
logging.error(f"unable to assume role:{e}")
raise
session = boto3.session.Session(
aws_access_key_id=res["Credentials"]["AccessKeyId"],
aws_secret_access_key=res["Credentials"]["SecretAccessKey"],
aws_session_token=res["Credentials"]["SessionToken"],
)
logging.info("Got session")
return session
def main(msg: func.QueueMessage):
client_id = os.environ["AZURE_CLIENT_ID"]
audience = os.environ["AZURE_AUDIENCE"]
target_account = os.environ["AWS_TARGET_ACCOUNT"]
region = os.environ["AWS_TARGET_REGION"]
role_name = os.environ["AWS_TARGET_ROLE_NAME"]
partition = os.environ["AWS_TARGET_PARTITION"]
role_arn = f"arn:{partition}:iam::{target_account}:role/{role_name}"
session = get_session(client_id, audience, role_arn)
events_client = session.client("events", region_name=region)
body_string = msg.get_body().decode("utf-8")
body = json.loads(body_string)
source = body["data"]["operationName"].split("/")[0]
try:
logging.info('Forwarding event to Stacklet')
logging.info(body_string)
events_client.put_events(
Entries=[
{
"Time": msg.insertion_time,
"Source": source,
"DetailType": "CloudEvent/Azure System Topic Event",
"Detail": body_string,
"EventBusName": os.environ["AWS_TARGET_EVENT_BUS"],
}
]
)
except botocore.exceptions.ClientError as e:
if e.response["Error"]["Code"] == "AccessDeniedException" and str(e).endswith(
"with an explicit deny in a resource-based policy"
):
logging.warning("skipping event")
return
logging.error(f"failed to put event:{e}")
raise
# Copyright 2024 Stacklet
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

import json
import logging
import os

import azure.functions as func

from azure.identity import DefaultAzureCredential

import boto3
import botocore


def get_session(client_id, audience, role_arn):
client = boto3.client("sts")
creds = DefaultAzureCredential(
managed_identity_client=client_id, exclude_environment_credential=True
)
token = creds.get_token(audience)
try:
res = client.assume_role_with_web_identity(
WebIdentityToken=token.token,
RoleArn=role_arn,
RoleSessionName="StackletAzureRelay",
)
except Exception as e:
logging.error(f"unable to assume role:{e}")
raise

session = boto3.session.Session(
aws_access_key_id=res["Credentials"]["AccessKeyId"],
aws_secret_access_key=res["Credentials"]["SecretAccessKey"],
aws_session_token=res["Credentials"]["SessionToken"],
)
logging.info("Got session")
return session


def main(msg: func.QueueMessage):
client_id = os.environ["AZURE_CLIENT_ID"]
audience = os.environ["AZURE_AUDIENCE"]

target_account = os.environ["AWS_TARGET_ACCOUNT"]
region = os.environ["AWS_TARGET_REGION"]
role_name = os.environ["AWS_TARGET_ROLE_NAME"]
partition = os.environ["AWS_TARGET_PARTITION"]
role_arn = f"arn:{partition}:iam::{target_account}:role/{role_name}"

session = get_session(client_id, audience, role_arn)
events_client = session.client("events", region_name=region)

body_string = msg.get_body().decode("utf-8")
body = json.loads(body_string)
source = body["data"]["operationName"].split("/")[0]

try:
logging.info('Forwarding event to Stacklet')
logging.info(body_string)
events_client.put_events(
Entries=[
{
"Time": msg.insertion_time,
"Source": source,
"DetailType": "CloudEvent/Azure System Topic Event",
"Detail": body_string,
"EventBusName": os.environ["AWS_TARGET_EVENT_BUS"],
}
]
)
except botocore.exceptions.ClientError as e:
if e.response["Error"]["Code"] == "AccessDeniedException" and str(e).endswith(
"with an explicit deny in a resource-based policy"
):
logging.warning("skipping event")
return
logging.error(f"failed to put event:{e}")
raise
9 changes: 7 additions & 2 deletions function.tf
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,14 @@ resource "azurerm_linux_function_app" "stacklet" {
application_stack {
python_version = "3.10"
}
application_insights_key = azurerm_application_insights.stacklet.instrumentation_key
}

app_settings = {
SCM_DO_BUILD_DURING_DEPLOYMENT = true
APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.stacklet.instrumentation_key
AZURE_CLIENT_ID = azurerm_user_assigned_identity.stacklet_identity.client_id
AZURE_AUDIENCE = local.audience
AZURE_STORAGE_QUEUE_NAME = azurerm_storage_queue.stacklet.name
AZURE_SUBSCRIPTION_ID = data.azurerm_subscription.current.subscription_id
AWS_TARGET_ACCOUNT = var.aws_target_account
AWS_TARGET_REGION = var.aws_target_region
AWS_TARGET_ROLE_NAME = var.aws_target_role_name
Expand All @@ -65,6 +64,12 @@ resource "azurerm_linux_function_app" "stacklet" {
identity_ids = [azurerm_user_assigned_identity.stacklet_identity.id]
}
tags = local.tags

lifecycle {
ignore_changes = [
tags["hidden-link: /app-insights-resource-id"]
]
}
}

resource "local_file" "function_json" {
Expand Down
2 changes: 0 additions & 2 deletions locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
# SPDX-License-Identifier: Apache-2.0

locals {
object_id = azurerm_user_assigned_identity.stacklet_identity.principal_id
app_role_id = var.azuread_application == null ? random_uuid.app_role_uuid.id : data.azuread_application.stacklet_application[0].app_role_ids.AssumeRoleWithWebIdentity
resource_id = local.azuread_service_principal.object_id

audience = "api://stacklet/provider/azure/${var.aws_target_prefix}"

Expand Down
21 changes: 9 additions & 12 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ data "azurerm_role_definition" "builtin" {

resource "random_uuid" "app_role_uuid" {}

locals {
resource_group_name = coalesce(var.resource_group_name, var.prefix)
}

resource "azurerm_resource_group" "stacklet_rg" {
name = var.prefix
name = local.resource_group_name
location = var.resource_group_location
tags = local.tags
}
Expand Down Expand Up @@ -85,15 +89,8 @@ data "azuread_service_principal" "stacklet_sp" {
display_name = var.azuread_application
}

resource "null_resource" "stacklet" {
depends_on = [local.azuread_application, local.azuread_service_principal]
provisioner "local-exec" {
command = <<EOF
az rest \
--method POST \
--uri https://graph.microsoft.com/v1.0/servicePrincipals/${local.object_id}/appRoleAssignments \
--headers 'Content-Type=application/json' \
--body '{"principalId": "${local.object_id}", "resourceId": "${local.resource_id}", "appRoleId": "${local.app_role_id}"}'
EOF
}
resource "azuread_app_role_assignment" "stacklet_app_role_assignment" {
principal_object_id = azurerm_user_assigned_identity.stacklet_identity.principal_id
resource_object_id = local.azuread_service_principal.object_id
app_role_id = local.app_role_id
}
20 changes: 19 additions & 1 deletion provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@
#
# SPDX-License-Identifier: Apache-2.0

# Note: Unlike AWS provider, Azure provider (azurerm) does not support
# default_tags configuration. We use local.tags instead to achieve
# consistent tagging across all resources.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">=4.35.0"
}
}
}

provider "azurerm" {
features {}
features {
resource_group {
prevent_deletion_if_contains_resources = !var.force_delete_resource_group
}
}

subscription_id = var.subscription_id
}
9 changes: 7 additions & 2 deletions storage.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
#
# SPDX-License-Identifier: Apache-2.0

locals {
# Storage account name must be 3-24 characters and cannot contain hyphens.
prefix_no_hyphens = replace(var.prefix, "-", "")
}

resource "random_string" "storage_account_suffix" {
special = false
length = 24
Expand All @@ -22,8 +27,8 @@ resource "random_string" "storage_account_suffix" {
}

resource "azurerm_storage_account" "stacklet" {
# there is a global uniquness constraing on storage account names, as well as a length requirement of 3-24 characters
name = substr("${var.prefix}${random_string.storage_account_suffix.result}", 0, 23)
# there is a global uniqueness constraint on storage account names, as well as a length requirement of 3-24 characters
name = substr("${local.prefix_no_hyphens}${random_string.storage_account_suffix.result}", 0, 23)
resource_group_name = azurerm_resource_group.stacklet_rg.name
location = azurerm_resource_group.stacklet_rg.location
account_tier = "Standard"
Expand Down
25 changes: 22 additions & 3 deletions vars.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,37 @@
#
# SPDX-License-Identifier: Apache-2.0

variable "subscription_id" {
type = string
description = "Azure subscription ID. This could also be set using the ARM_SUBSCRIPTION_ID environment variable."
default = null
}

variable "prefix" {
type = string
description = "A Prefix for all of the generated resources"
validation {
condition = can(regex("^[a-z0-9]+$", var.prefix))
error_message = "Prefix should contain only numbers and lowercase letters"
condition = can(regex("^[a-z](-?[a-z0-9]+)*$", var.prefix))
error_message = "Prefix must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens"
}
}

variable "resource_group_name" {
type = string
description = "Resource Group name for generated resources"
default = null
}

variable "resource_group_location" {
type = string
description = "Resource Group location for generated resoruces"
description = "Resource Group location for generated resources"
default = "East US"
}

variable "force_delete_resource_group" {
type = bool
description = "Force delete the resource group when terraform destroy is run"
default = false
}

variable "event_grid_topic_name" {
Expand Down