Desktop and Application Streaming

Cross-account resources and Amazon AppStream 2.0

Some of our customers using Amazon AppStream 2.0 leverage multi-account setups to separate their AppStream 2.0 resources. For these customers, there are many reasons they choose to separate their AppStream 2.0 resources into multiple accounts. However, the most common reasons our customers do this, is for resource and billing isolation and enhanced security. For example, each business unit, external customer or deployment environment may need to have its own AWS account, each with their own, uniquely configured, AppStream 2.0 environments.

In this blog post, I show you how to create a basic multi-account AppStream 2.0 deployment. We will create an AWS Lambda function in one AWS account that generates an AppStream 2.0 streaming URL from a stack and fleet in one of two AWS accounts, depending on the email address submitted.

Prerequisites

  • Three AWS accounts, in this blog referred to as account A, account B, and account C
  • An AppStream 2.0 stack and fleet in accounts B and C

Create and configure IAM Roles in accounts B and C

In account B open the IAM console. This should be an account that contains one of your AppStream 2.0 stacks and fleets.

  • Open the IAM console, and choose Policies, Create policy.
  • Choose the JSON tab. Copy and paste the following JSON policy into the policy document box:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "appstream:CreateStreamingURL",
            "Resource": [
                "arn:aws:appstream:<region>:<Account_B-AWS-Account-ID>:fleet/<Fleet-Name>",
                "arn:aws:appstream:<region>:<Account_B-AWS-Account-ID>:stack/<Stack-Name>"
            ]
        }
    ]
}
  • In the JSON policy replace:
    • The placeholder <Account_B-AWS-Account-ID> with the AWS Account ID for account B
    • The placeholders <Fleet-Name> and <Stack-Name> with your AppStream 2.0 fleet and stack
    • The placeholder <region> where your AppStream 2.0 resources are located.
  • Choose Review policy.
  • Name your policy, and choose Create policy.
  • In the navigation pane, choose Roles, Create Role, and configure the following boxes:
    • For Select type of trusted entity, choose another AWS Account.
    • Enter the AWS Account ID for account A
  • Choose Next: Permissions.
  • In the Filter policies search box, search for the policy that you previously created. When the policy appears in the list, select the check box next to the policy name.
  • Choose Next: Tags. Although you can specify a tag for the policy, a tag is optional.
  • Choose Next: Review. Name your role, and choose Create role.
  • Repeat these steps in account C, update the IAM policy with the AWS Region and AppStream 2.0 stack and fleet ARNs from account C

Create and configure IAM Role in account A

In account A, open up the IAM Console, this is the account that contains the Lambda function.

  • Open the IAM console, and choose Policies, Create policy.
  • Choose the JSON tab. Copy and paste the following JSON policy into the policy document box:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::<Account_B-AWS-Account-ID>:role/<Role-Name>"
        },
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::<Account_C-AWS-Account-ID>:role/<Role-Name>"
        }
    ]
}
  • Update the policy with the the ARNs of the roles you created in account’s B and C.
  • After creating the policy, in the navigation pane choose Roles, Create Role, and configure the following boxes:
    • For Select type of trusted entity, choose AWS Service.
    • For the service that will use this role, choose Lambda.
  • Choose Next: Permissions.
  • In the Filter policies search box, search for the policy that you previously created. When the policy appears in the list, select the check box next to the policy name.
  • Choose Next: Tags. Although you can specify a tag for the policy, a tag is optional.
  • Choose Next: Review. Name your role, and choose Create role.

Create a Lambda Function in account A

  • In account A, open the Lambda console.
  • Choose Create function.
  • On the Create function page, keep Author from scratch selected.
  • Under Basic information, do the following:
    • Name your function.
    • For Runtime, choose Node.js 12.X.
  • Under Permissions, choose the icon next to Choose or create an execution role. Then do the following:
    • For Execution role, choose Use an existing role.
    • For Existing role, choose the Lambda execution role you previously created.
  • Choose Create function.
  •  In the Function code section, copy and paste the following code onto the tab:
const AWS = require('aws-sdk');
const crypto = require('crypto');

const sts = new AWS.STS();

exports.handler = (event, context, callback) => {

    var eventdata = JSON.parse(event.body);
    var email = eventdata.email;

    if (email.includes("example.com")) { //replace with the domain you want to route to Account B's AS2 stack and fleet
        var sts_params = {
            RoleArn: "arn:aws:iam::<AWS-Account-ID-For-Account-B>:role/<Role-Name-From-Account-B>", //ARN from role created in Account B
            RoleSessionName: "<Any-String>", //Replace with any string
            DurationSeconds: 900
    
        };
        var as2region = "<Region>"; //Region where AS2 Stack and Fleet are located in Account B
        var as2stack = "<Stack-Name-From-Account-B>"; //Stack Name from account B
        var as2fleet = "<Fleet-Name-From-Account-B>"; //Fleet Name from account B
        
    }
    else if (email.includes("example-1.com")) { //replace with the domain you want to route to Account C's AS2 stack and fleet
        var sts_params = {
            RoleArn: "arn:aws:iam::<AWS-Account-ID-For-Account-C>:role/<Role-Name-From-Account-C>", //ARN from role created in Account C
            RoleSessionName: "<Any-String>", //Replace with any string
            DurationSeconds: 900
    
        };
        var as2region = "<Region>"; //Region where AS2 Stack and Fleet are located in Account C
        var as2stack = "<Stack-Name-From-Account-C>"; //Stack Name from account C
        var as2fleet = "<Fleet-Name-From-Account-C>"; //Fleet Name from account C
        
    }
    else{
        console.log("No matching domain. Email address is " + email);
        errorResponse('Email domain does not match!', context.awsRequestId, callback);
        process.exit(0)

    }

    sts.assumeRole(sts_params, function (err, data) {
        if (err) {
            console.log(err, err.stack);
            errorResponse('Error assuming role!', context.awsRequestId, callback);
        } else {
            console.log(data);

            var unlength = 16;
            var username = crypto.randomBytes(Math.ceil(unlength / 2)).toString('hex').slice(0, unlength);

            console.log("username: " + username);

            var as2_params = {
                FleetName: as2fleet,
                StackName: as2stack,
                UserId: username,
                Validity: 5
            };

            AWS.config.credentials = new AWS.TemporaryCredentials({ RoleArn: sts_params.RoleArn });
            AWS.config.update({ region: as2region }); 

            const appstream = new AWS.AppStream;

            var request = appstream.createStreamingURL(as2_params);
            request.
                on('success', function (response) {
                    console.log("Success! AS2 Streaming URL created.");
                    var output = response.data;
                    var url = output.StreamingURL;
                    callback(null, {
                        statusCode: 201,
                        body: JSON.stringify({
                            Message: url,
                            Reference: context.awsRequestId,
                        }),
                        headers: {
                            'Access-Control-Allow-Origin': '*',
                        },
                    });
                }).
                on('error', function (response) {
                    console.log("error: " + JSON.stringify(response.message));
                    errorResponse('Error creating AS2 streaming URL.', context.awsRequestId, callback);

                }).
                send();

        }
    });

};

function errorResponse(errorMessage, awsRequestId, callback) {
    callback(null, {
        statusCode: 500,
        body: JSON.stringify({
            Error: errorMessage,
            Reference: awsRequestId,
        }),
        headers: {
            'Access-Control-Allow-Origin': '*',
        },
    });
}
  • Replace all the placeholder values as instructed in the code comments.
  • Save the function.

Testing your setup

  • In account A, open the Lambda console.
  • Select the Lambda function you previously created
  • In the Select a test event drop-down menu, select Configure test events.
  • In the Configure test event window, configure the event with the following settings:
    • Event Template should be “Hello World”
    • Name your Event
    • Replace the JSON block with the following: {"body": "{\"email\":\"test@example.com\"}"}
    • Replace the example email address with one that contains one of the domains you are filtering for
  • Create the Event
  • With the event created, choose the Test button, to begin the test
  • If the test was successful, you should get an AppStream 2.0 streaming URL returned
  • Copy and paste that URL in a web browser to verify that it functions
  • Reconfigure the test event with a different email domain and repeat the testing process

Clean up

  • Delete the Lambda function you created in account A
  • Delete the IAM role you created in Account A
  • Delete the IAM roles you created in Account’s B and C
  • Optional: Delete the AppStream 2.0 stacks and fleets in account’s B and C

Conclusion

And that’s it! You now have multi-account setup of AppStream 2.0. Our ISV Workshop Series provides tutorials on how you can leverage Amazon S3, API Gateway, and Lambda to create an AppStream 2.0 streaming web portal. You may want to consider modifying one of those workshops with your new Lambda function so your users can start streaming from this multi-account deployment.