Configure Amazon Forecast for a multi-tenant SaaS application

Amazon Forecast is a fully managed service that is based on the same technology used for forecasting at Amazon.com. Forecast uses machine learning (ML) to combine time series data with additional variables to build highly accurate forecasts. Forecast requires no ML experience to get started. You only need to provide historical data and any additional data that may impact forecasts.

Customers are turning towards using a Software as service (SaaS) model for delivery of multi-tenant solutions. You can build SaaS applications with a variety of different architectural models to meet regulatory and compliance requirements. Depending on the SaaS model, resources like Forecast are shared across tenants. Forecast data access, monitoring, and billing needs to be considered per tenant for deploying SaaS solutions.

This post outlines how to use Forecast within a multi-tenant SaaS application using Attribute Based Access Control (ABAC) in AWS Identity and Access Management (IAM) to provide these capabilities. ABAC is a powerful approach that you can use to isolate resources across tenants.

In this post, we provide guidance on setting up IAM policies for tenants using ABAC principles and Forecast. To demonstrate the configuration, we set up two tenants, TenantA and TenantB, and show a use case in the context of an SaaS application using Forecast. In our use case, TenantB can’t delete TenantA resources, and vice versa. The following diagram illustrates our architecture.

TenantA and TenantB have services running as microservice within Amazon Elastic Kubernetes Service (Amazon EKS). The tenant application uses Forecast as part of its business flow.

Forecast data ingestion

Forecast imports data from the tenant’s Amazon Simple Storage Service (Amazon S3) bucket to the Forecast managed S3 bucket. Data can be encrypted in transit and at rest automatically using Forecast managed keys or tenant-specific keys through AWS Key Management Service (AWS KMS). The tenant-specific key can be created by the SaaS application as part of onboarding, or the tenant can provide their own customer managed key (CMK) using AWS KMS. Revoking permission on the tenant-specific key prevents Forecast from using the tenant’s data. We recommend using a tenant-specific key and an IAM role per tenant in a multi-tenant SaaS environment. This enables securing data on a tenant-by-tenant basis.

Solution overview

You can partition data on Amazon S3 to segregate tenant access in different ways. For this post, we discuss two strategies:

  • Use one S3 bucket per tenant
  • Use a single S3 bucket and separate tenant data with a prefix

For more information about various strategies, see the Storing Multi-Tenant Data on Amazon S3 GitHub repo.

When using one bucket per tenant, you use an IAM policy to restrict access to a given tenant S3 bucket. For example:

s3://tenant_a    [ Tag tenant = tenant_a ]
s3://tenant_b     [ Tag tenant = tenant_b ]

There is a hard limit on the number of S3 buckets per account. A multi-account strategy needs to be considered to overcome this limit.

In our second option, tenant data separated using an S3 prefix in a single S3 bucket. We use an IAM policy  to restrict access within a bucket prefix per tenant. For example:

s3://<bucketname>/tenant_a

For this post, we use the second option of assigning S3 prefixes within a single bucket. We encrypt tenant data using CMKs in AWS KMS.

Tenant onboarding

SaaS applications rely on a frictionless model for introducing new tenants into their environment. This often requires orchestrating several components to successfully provision and configure all the elements needed to create a new tenant. This process, in SaaS architecture, is referred to as tenant onboarding. This can be initiated directly by tenants or as part of a provider-managed process. The following diagram illustrates the flow of configuring Forecast per tenant as part of onboarding process.

Resources are tagged with tenant information. For this post, we tag resources with a value for tenant, for example,  tenant_a.

Create a Forecast role

This IAM role is assumed by Forecast per tenant. You should apply the following policy to allow Forecast to interact with Amazon S3 and AWS KMS in the customer account. The role is tagged with the tag tenant. For example, see the following code:

TenantA create role Forecast_TenantA_Role  [ Tag tenant = tenant_a]
TenantB create role Forecast_TenantB_Role [ Tag tenant = tenant_b]

Create the policies

In this next step, we create policies for our Forecast role. For this post, we split them into two policies for more readability, but you can create them according to your needs.

Policy 1: Forecast read-only access

The following policy gives privileges to describe, list, and query Forecast resources. This policy restricts Forecast to read-only access. The tenant tag validation condition in the following code makes sure that the tenant tag value matches the principal’s tenant tag. Refer to the bolded code for specifics.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DescribeQuery",
            "Effect": "Allow",
            "Action": [
                "forecast:GetAccuracyMetrics",
                "forecast:ListTagsForResource",
                "forecast:DescribeDataset",
                "forecast:DescribeForecast",
                "forecast:DescribePredictor",
                "forecast:DescribeDatasetImportJob",
                "forecast:DescribePredictorBacktestExportJob",
                "forecast:DescribeDatasetGroup",
                "forecast:DescribeForecastExportJob",
                "forecast:QueryForecast"
            ],
            "Resource": [
                "arn:aws:forecast:*:<accountid>:dataset-import-job/*",
                "arn:aws:forecast:*:<accountid>:dataset-group/*",
                "arn:aws:forecast:*:<accountid>:predictor/*",
                "arn:aws:forecast:*:<accountid>:forecast/*",
                "arn:aws:forecast:*:<accountid>:forecast-export-job/*",
                "arn:aws:forecast:*:<accountid>:dataset/*",
                "arn:aws:forecast:*:<accountid>:predictor-backtest-export-job/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/tenant":"${aws:PrincipalTag/tenant}"
                }
            }
        },
        {
            "Sid": "List",
            "Effect": "Allow",
            "Action": [
                "forecast:ListDatasetImportJobs",
                "forecast:ListDatasetGroups",
                "forecast:ListPredictorBacktestExportJobs",
                "forecast:ListForecastExportJobs",
                "forecast:ListForecasts",
                "forecast:ListPredictors",
                "forecast:ListDatasets"
            ],
            "Resource": "*"
        }
    ]
}

Policy 2: Amazon S3 and AWS KMS access policy

The following policy gives privileges to AWS KMS and access to the S3 tenant prefix. The tenant tag validation condition in the following code makes sure that the tenant tag value matches the principal’s tenant tag. Refer to the bolded code for specifics.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "KMS",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt",
                "kms:Encrypt",
                "kms:RevokeGrant",
                "kms:GenerateDataKey",
                "kms:DescribeKey",
                "kms:RetireGrant",
                "kms:CreateGrant",
                "kms:ListGrants"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/tenant":"${aws:PrincipalTag/tenant}"
                }
            }
        },
        {
            "Sid": "S3Access",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject", 
                "s3:PutObject",
                "s3:GetObjectVersionTagging",
                "s3:GetObjectAcl",
                "s3:GetObjectVersionAcl",
                "s3:GetBucketPolicyStatus",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:ListAccessPoints",
                "s3:GetObjectVersion"
            ],
            "Resource": [
                "arn:aws:s3:::<bucketname>/*",
                "arn:aws:s3:::<bucketname>"
            ],
            "Condition": {
                "StringLike": {
                    "s3:prefix": [
                        "${aws:PrincipalTag/tenant}",
                        "${aws:PrincipalTag/tenant}/*"
                    ]
                }
            }
        }
    ]
}

Create a tenant specific key

We now create a tenant-specific key in AWS KMS per tenant and tag it with the tenant tag value. Alternatively, the tenant can bring their own key to AWS KMS. We give the preceding roles (Forecast_TenantA_Role or Forecast_TenantB_Role) access to the tenant-specific key.

For example, the following screenshot shows the key-value pair of tenant and tenant_a.

The following screenshot shows the IAM roles that can use this key.

Create an application role

The second role we create is assumed by the SaaS application per tenant. You should apply the following policy to allow the application to interact with Forecast, Amazon S3, and AWS KMS. The role is tagged with the tag tenant. See the following code:

TenantA create role TenantA_Application_Role  [ Tag tenant = tenant_a]
TenantB create role TenantB_Application_Role  [ Tag tenant = tenant_b]

Create the policies

We now create policies for the application role. For this post, we split them into two policies for more readability, but you can create them according to your needs.

Policy 1: Forecast access

The following policy gives privileges to create, update, and delete Forecast resources. The policy enforces the tag requirement during creation. In addition, it restricts the list, describe, and delete actions on resources to the respective tenant. This policy has IAM PassRole to allow Forecast to assume the role.

The tenant tag validation condition in the following code makes sure that the tenant tag value matches the tenant. Refer to the bolded code for specifics.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CreateDataSet",
            "Effect": "Allow",
            "Action": [
                "forecast:CreateDataset",
                "forecast:CreateDatasetGroup",
                "forecast:TagResource"
            ],
            "Resource": [
                "arn:aws:forecast:*:<accountid>:dataset-import-job/*",
                "arn:aws:forecast:*:<accountid>:dataset-group/*",
                "arn:aws:forecast:*:<accountid>:predictor/*",
                "arn:aws:forecast:*:<accountid>:forecast/*",
                "arn:aws:forecast:*:<accountid>:forecast-export-job/*",
                "arn:aws:forecast:*:<accountid>:dataset/*",
                "arn:aws:forecast:*:<accountid>:predictor-backtest-export-job/*"
            ],
            "Condition": {
                "ForAnyValue:StringEquals": {
                    "aws:TagKeys": [ "tenant" ]
                },
                "StringEquals": {
                    "aws:RequestTag/tenant": "${aws:PrincipalTag/tenant}"
                }
            }
        },
        {
            "Sid": "CreateUpdateDescribeQueryDelete",
            "Effect": "Allow",
            "Action": [
                "forecast:CreateDatasetImportJob",
                "forecast:CreatePredictor",
                "forecast:CreateForecast",
                "forecast:CreateForecastExportJob",
                "forecast:CreatePredictorBacktestExportJob",
                "forecast:GetAccuracyMetrics",
                "forecast:ListTagsForResource",
                "forecast:UpdateDatasetGroup",
                "forecast:DescribeDataset",
                "forecast:DescribeForecast",
                "forecast:DescribePredictor",
                "forecast:DescribeDatasetImportJob",
                "forecast:DescribePredictorBacktestExportJob",
                "forecast:DescribeDatasetGroup",
                "forecast:DescribeForecastExportJob",
                "forecast:QueryForecast",
                "forecast:DeletePredictorBacktestExportJob",
                "forecast:DeleteDatasetImportJob",
                "forecast:DeletePredictor",
                "forecast:DeleteDataset",
                "forecast:DeleteDatasetGroup",
                "forecast:DeleteForecastExportJob",
                "forecast:DeleteForecast"
            ],
            "Resource": [
                "arn:aws:forecast:*:<accountid>:dataset-import-job/*",
                "arn:aws:forecast:*:<accountid>:dataset-group/*",
                "arn:aws:forecast:*:<accountid>:predictor/*",
                "arn:aws:forecast:*:<accountid>:forecast/*",
                "arn:aws:forecast:*:<accountid>:forecast-export-job/*",
                "arn:aws:forecast:*:<accountid>:dataset/*",
                "arn:aws:forecast:*:<accountid>:predictor-backtest-export-job/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/tenant": "${aws:PrincipalTag/tenant}"
                }
            }
        },
        {
            "Sid": "IAMPassRole",
            "Effect": "Allow",
            "Action": [
                "iam:GetRole",
                "iam:PassRole"
            ],
            "Resource": "--Provide Resource ARN--"
        },
        {
            "Sid": "ListAccess",
            "Effect": "Allow",
            "Action": [
                "forecast:ListDatasetImportJobs",
                "forecast:ListDatasetGroups",
                "forecast:ListPredictorBacktestExportJobs",
                "forecast:ListForecastExportJobs",
                "forecast:ListForecasts",
                "forecast:ListPredictors",
                "forecast:ListDatasets"
            ],
            "Resource": "*"
        }
    ]
}

Policy 2: Amazon S3, AWS KMS, Amazon CloudWatch, and resource group access

The following policy gives privileges to access Amazon S3 and AWS KMS resources, and also Amazon CloudWatch. It limits access to the tenant-specific S3 prefix and tenant-specific CMK. The tenant validation condition is in bolded code.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "S3Storage",
            "Effect": "Allow",
            "Action": [
                "s3:*" ---> To be modifed based on application needs
            ],
            "Resource": [
                "arn:aws:s3:::<bucketname>",
                "arn:aws:s3:::<bucketname>/*"
            ],
            "Condition": {
                "StringLike": {
                    "s3:prefix": [ "${aws:PrincipalTag/tenant}", "${aws:PrincipalTag/tenant}/*"
                    ]
                }
            }
        },
  {
            "Sid": "ResourceGroup",
            "Effect": "Allow",
            "Action": [
                "resource-groups:SearchResources",
                "tag:GetResources",
                "tag:getTagKeys",
                "tag:getTagValues",
         "resource-explorer:List*",
   "cloudwatch:PutMetricData"
            ],
            "Resource": "*"
        },
        {
            "Sid": "KMS",
            "Effect": "Allow",
            "Action": [
                "kms:Encrypt",
                "kms:Decrypt",
                "kms:CreateGrant",
                "kms:RevokeGrant",
                "kms:RetireGrant",
                "kms:ListGrants",
                "kms:DescribeKey",
                "kms:GenerateDataKey"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/tenant": "${aws:PrincipalTag/tenant}"
                }
            }
        }
    ]
}

Create a resource group

The resource group allows all tagged resources to be queried by the tenant. The following example code uses the AWS Command Line Interface (AWS CLI) to create a resource group for TenantA:

aws resource-groups create-group --name TenantA --tags tenant=tenant_a --resource-query '{"Type":"TAG_FILTERS_1_0", "Query":"{"ResourceTypeFilters":["AWS::AllSupported"],"TagFilters":[{"Key":"tenant", "Values":["tenant_a"]}]}"}'

Forecast application flow

The following diagram illustrates our Forecast application flow. Application service assumes IAM role for the tenant and as part of its business flow invokes Forecast API.

Create a predictor for TenantB

Resources created should be tagged with the tenant tag. The following code uses the Python (Boto3) API to create a predictor for TenantB (refer to the bolded code for specifics):

//Run under TenantB role TenantB_Application_Role
session = boto3.Session() 
forecast = session.client(service_name='forecast') 
...
response=forecast.create_dataset(
                    Domain="CUSTOM",
                    DatasetType='TARGET_TIME_SERIES',
                    DatasetName=datasetName,
                    DataFrequency=DATASET_FREQUENCY, 
                    Schema = schema,
                    Tags = [{'Key':'tenant','Value':'tenant_b'}],
                    EncryptionConfig={'KMSKeyArn':'KMS_TenantB_ARN', 'RoleArn':Forecast_TenantB_Role}
)
...
create_predictor_response=forecast.create_predictor(
                    ...
                    EncryptionConfig={ 'KMSKeyArn':'KMS_TenantB _ARN', 'RoleArn':Forecast_TenantB_Role}, Tags = [{'Key':'tenant','Value':'tenant_b'}],
                      ...
                      }) 
predictor_arn=create_predictor_response['PredictorArn']

Create a forecast on the predictor for TenantB

The following code uses the Python (Boto3) API to create a forecast on the predictor you just created:

//Run under TenantB role TenantB_Application_Role
session = boto3.Session() 
forecast = session.client(service_name='forecast') 
...
create_forecast_response=create_forecast_response=forecast.create_forecast(
ForecastName=forecastName,
             PredictorArn=predictor_arn,
             Tags = [{'Key':'tenant','Value':'tenant_b'}])
tenant_b_forecast_arn = create_forecast_response['ForecastArn']

Validate access to Forecast resources

In this section, we confirm that only the respective tenant can access Forecast resources. Access, modifying, or deleting Forecast resources belonging to a different tenant throws an error. The following code uses the Python (Boto3) API to demonstrate TenantA attempting to delete a TenantB Forecast resource:

//Run under TenantA role TenantA_Application_Role
session = boto3.Session() 
forecast = session.client(service_name='forecast') 
..
forecast.delete_forecast(ForecastArn= tenant_b_forecast_arn)

ClientError: An error occurred (AccessDeniedException) when calling the DeleteForecast operation: User: arn:aws:sts::<accountid>:assumed-role/TenantA_Application_Role/tenant-a-role is not authorized to perform: forecast:DeleteForecast on resource: arn:aws:forecast:<region>:<accountid>:forecast/tenantb_deeparp_algo_forecast

List and monitor predictors

The following example code uses the Python (Boto3) API to query Forecast predictors for TenantA using resource groups:

//Run under TenantA role TenantA_Application_Role
session = boto3.Session() 
resourcegroup = session.client(service_name='resource-groups')

query="{"ResourceTypeFilters":["AWS::Forecast::Predictor"],"TagFilters":[{"Key":"tenant", "Values":["tenant_a"]}]}"  Tenant Tag needs to be specified.

response = resourcegroup.search_resources(
    ResourceQuery={
        'Type': 'TAG_FILTERS_1_0',
        'Query': query
    },
    MaxResults=20
)

predictor_count=0
for resource in response['ResourceIdentifiers']:
    print(resource['ResourceArn'])
    predictor_count=predictor_count+1

As the AWS Well-Architected Framework explains, it’s important to monitor service quotas (which are also referred to as service limits). Forecast has limits per accounts; for more information, see Guidelines and Quotas.

The following code is an example of populating a CloudWatch metric with the total number of predictors:

cloudwatch = session.client(service_name='cloudwatch')
cwresponse = cloudwatch.put_metric_data(Namespace='TenantA_PredictorCount',MetricData=[
 {
 'MetricName': 'TotalPredictors',
 'Value': predictor_count
 }]
)

Other considerations

Resource limits and throttling need to be managed by the application across tenants. If you can’t accommodate the Forecast limits, you should consider a multi-account configuration.

The Forecast List APIs or resource group response need to be filtered by application based on the tenant tag value.

Conclusion

In this post, we demonstrated how to isolate Forecast access using the ABAC technique in a multi-tenant SaaS application. We showed how to limit access to Forecast by tenant using the tenant tag. You can further customize policies by applying more tags, or apply this strategy to other AWS services.

For more information about using ABAC as an authorization strategy, see What is ABAC for AWS?


About the Authors

Gunjan Garg is a Sr. Software Development Engineer in the AWS Vertical AI team. In her current role at Amazon Forecast, she focuses on engineering problems and enjoys building scalable systems that provide the most value to end users. In her free time, she enjoys playing Sudoku and Minesweeper.

 

 

 

Matias Battaglia is a Technical Account Manager at Amazon Web Services. In his current role, he enjoys helping customers at all the stages of their cloud journey. On his free time, he enjoys building AI/ML projects.

 

 

 

Rakesh Ramadas is an ISV Solution Architect at Amazon Web Services. His focus areas include AI/ML and Big Data.

 

Read More