Skip to content
This repository has been archived by the owner on Dec 6, 2024. It is now read-only.

feat: integrating appstream with rstudio-alb #783

Merged
merged 13 commits into from
Nov 15, 2021
Prev Previous commit
Next Next commit
feat: appstream integration for RStudio ALB
  • Loading branch information
SanketD92 committed Nov 3, 2021
commit c2d6fe9983d9e91ebb832b92e2f010a860adb2c3
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ Outputs:
ALBDNSName:
Description: DNS Name of Application Load Balancer
Value: !GetAtt ApplicationLoadBalancer.DNSName
ALBHostedZoneId:
Description: Hosted Zone ID of Application Load Balancer
Value: !GetAtt ApplicationLoadBalancer.HostedZoneId
ListenerArn:
Description: ARN of Application Load Balancer Listener
Value: !Ref ALBListener
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Conditions:
- !Ref EnableAppStream
- true
isNotAppStream: !Not [Condition: isAppStream]
hasCustomDomain: !Not [!Equals [!Ref "DomainName", ""]]
isAppStreamAndCustomDomain: !And
- !Not [!Equals [!Ref "DomainName", ""]]
- !Condition isAppStream
Expand Down Expand Up @@ -190,8 +191,8 @@ Resources:
- ec2:DescribeImages
- ec2:DescribeInstances
- ec2:DescribeSecurityGroups
- ec2:RevokeSecurityGroupIngress
- ec2:AuthorizeSecurityGroupIngress
- ec2:RevokeSecurityGroup*
- ec2:AuthorizeSecurityGroup*
- ec2-instance-connect:SendSSHPublicKey
Resource: '*'
- PolicyName: cfn-access
Expand Down Expand Up @@ -453,8 +454,8 @@ Resources:
Action:
- ec2:CreateSecurityGroup
- ec2:DeleteSecurityGroup
- ec2:AuthorizeSecurityGroupIngress
- ec2:RevokeSecurityGroupIngress
- ec2:AuthorizeSecurityGroup*
- ec2:RevokeSecurityGroup*
Resource:
- !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:vpc/*'
- !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:security-group/*'
Expand Down Expand Up @@ -992,7 +993,7 @@ Resources:
IpProtocol: '-1'
- DestinationSecurityGroupId: !Ref WorkspaceSecurityGroup
IpProtocol: '-1'

AppStreamSecurityGroupEgress:
Type: AWS::EC2::SecurityGroupEgress
Condition: isAppStream
Expand Down Expand Up @@ -1115,7 +1116,7 @@ Outputs:
Description: SageMaker Security Group
Condition: isAppStream
Value: !Ref SageMakerSecurityGroup

AppStreamFleet:
Description: AppStream Fleet
Condition: isAppStream
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ describe('ALBService', () => {
};
service.getAlbSdk = jest.fn().mockResolvedValue(albClient);
service.getEc2Sdk = jest.fn().mockResolvedValue(ec2Client);
service.checkIfAppStreamEnabled = jest.fn(() => {
return false;
});
});

afterEach(() => {
Expand Down Expand Up @@ -557,7 +560,6 @@ describe('ALBService', () => {
});
service.getAlbSdk = jest.fn().mockResolvedValue(albClient);
const response = await service.modifyRule({}, resolvedVars);
expect(albClient.modifyRule).toHaveBeenCalledWith(params);
expect(response).toEqual({});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ class ALBService extends Service {
return deploymentItem;
}

async checkIfAppStreamEnabled() {
return this.settings.get(settingKeys.isAppStreamEnabled);
}

/**
* Method to create listener rule. The method creates rule using the ALB SDK client.
* Tags are read form the resolvedVars so the billing will happen properly
Expand All @@ -202,7 +206,7 @@ class ALBService extends Service {
* @returns {Promise<string>}
*/
async createListenerRule(prefix, requestContext, resolvedVars, targetGroupArn) {
const isAppStreamEnabled = this.settings.get(settingKeys.isAppStreamEnabled);
const isAppStreamEnabled = this.checkIfAppStreamEnabled();
const deploymentItem = await this.getAlbDetails(requestContext, resolvedVars.projectId);
const albRecord = JSON.parse(deploymentItem.value);
const listenerArn = albRecord.listenerArn;
Expand Down Expand Up @@ -465,7 +469,7 @@ class ALBService extends Service {
*/
async modifyRule(requestContext, resolvedVars) {
const subdomain = this.getHostname(resolvedVars.prefix, resolvedVars.envId);
const isAppStreamEnabled = this.settings.get(settingKeys.isAppStreamEnabled);
const isAppStreamEnabled = this.checkIfAppStreamEnabled();
try {
const params = {
Conditions: isAppStreamEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ class EnvironmentDnsService extends Service {
await this.changeResourceRecordSets(route53Client, hostedZoneId, action, subdomain, 'A', privateIp);
}

async changePrivateRecordSetALB(requestContext, action, prefix, id, hostedZoneId, albHostedZoneId, recordValue) {
const environmentScService = await this.service('environmentScService');
const route53Client = await environmentScService.getClientSdkWithEnvMgmtRole(
requestContext,
{ id },
{ clientName: 'Route53', options: { apiVersion: '2017-07-24' } },
);
const subdomain = this.getHostname(prefix, id);
await this.changeResourceRecordSetsPrivateALB(
route53Client,
hostedZoneId,
action,
subdomain,
albHostedZoneId,
recordValue,
);
}

async changeRecordSet(action, prefix, id, publicDnsName) {
const aws = await this.service('aws');
const route53Client = new aws.sdk.Route53();
Expand Down Expand Up @@ -70,6 +88,60 @@ class EnvironmentDnsService extends Service {
await route53Client.changeResourceRecordSets(params).promise();
}

async changeResourceRecordSetsPrivateALB(
SanketD92 marked this conversation as resolved.
Show resolved Hide resolved
route53Client,
hostedZoneId,
action,
subdomain,
albHostedZoneId,
recordValue,
) {
const params = {
HostedZoneId: hostedZoneId,
ChangeBatch: {
Changes: [
{
Action: action,
ResourceRecordSet: {
Name: subdomain,
Type: 'A',
AliasTarget: {
HostedZoneId: albHostedZoneId,
DNSName: recordValue,
EvaluateTargetHealth: false,
},
},
},
],
},
};
await route53Client.changeResourceRecordSets(params).promise();
}

async createPrivateRecordForDNS(requestContext, prefix, id, albHostedZoneId, albDnsName, hostedZoneId) {
await this.changePrivateRecordSetALB(
requestContext,
'CREATE',
prefix,
id,
hostedZoneId,
albHostedZoneId,
albDnsName,
);
}

async deletePrivateRecordForDNS(requestContext, prefix, id, albHostedZoneId, albDnsName, hostedZoneId) {
await this.changePrivateRecordSetALB(
requestContext,
'DELETE',
prefix,
id,
hostedZoneId,
albHostedZoneId,
albDnsName,
);
}

async createPrivateRecord(requestContext, prefix, id, privateIp, hostedZoneId) {
await this.changePrivateRecordSet(requestContext, 'CREATE', prefix, id, privateIp, hostedZoneId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,22 @@ async function updateEnvOnProvisioningSuccess({
if (!deploymentItem) {
throw new Error(`Error provisioning environment. Reason: No ALB found for this AWS account`);
}
const dnsName = JSON.parse(deploymentItem.value).albDnsName;
const deploymentValue = JSON.parse(deploymentItem.value);
const dnsName = deploymentValue.albDnsName;
const targetGroupArn = _.find(outputs, o => o.OutputKey === 'TargetGroupARN').OutputValue;
// Create DNS record for RStudio workspaces
const environmentDnsService = await container.find('environmentDnsService');
const settings = await container.find('settings');
if (settings.getBoolean(settingKeys.isAppStreamEnabled)) {
const hostedZoneId = await getHostedZone(requestContext, environmentScService, existingEnvRecord);
await environmentDnsService.createPrivateRecord(requestContext, 'rstudio', envId, dnsName, hostedZoneId);
await environmentDnsService.createPrivateRecordForDNS(
requestContext,
'rstudio',
envId,
deploymentValue.albHostedZoneId,
dnsName,
hostedZoneId,
);
} else {
await environmentDnsService.createRecord('rstudio', envId, dnsName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const outPayloadKeys = {

const settingKeys = {
envMgmtRoleArn: 'envMgmtRoleArn',
isAppStreamEnabled: 'isAppStreamEnabled',
};

const pluginConstants = {
Expand Down Expand Up @@ -215,10 +216,16 @@ class CheckLaunchDependency extends StepBase {
listenerArn: _.get(stackOutputs, 'ListenerArn', null),
albDnsName: _.get(stackOutputs, 'ALBDNSName', null),
albSecurityGroup: _.get(stackOutputs, 'ALBSecurityGroupId', null),
albHostedZoneId: _.get(stackOutputs, 'ALBHostedZoneId', null),
albDependentWorkspacesCount: 0,
};
if (albLock) {
await albService.saveAlbDetails(awsAccountId, albDetails);
if (this.settings.getBoolean(settingKeys.isAppStreamEnabled)) {
// Allow ALB and AppStream security groups to interact with each other
await this.authorizeAppStreamAlbEgress(requestContext, resolvedVars, albDetails);
await this.authorizeAlbAppStreamIngress(requestContext, resolvedVars, albDetails);
}
} else {
throw new Error(`Error provisioning environment. Reason: ALB lock does not exist or expired`);
}
Expand All @@ -227,6 +234,70 @@ class CheckLaunchDependency extends StepBase {
});
}

/**
* Method to allow ingress access from AppStream to ALB
*
* @param requestContext
* @param resolvedVars
* @param albDetails
*/
async authorizeAlbAppStreamIngress(requestContext, resolvedVars, albDetails) {
SanketD92 marked this conversation as resolved.
Show resolved Hide resolved
try {
// Assign AppStream security group to ALB security group ingress
const appStreamSecurityGroupId = await this.getAppStreamSecurityGroupId(requestContext, resolvedVars);
const params = {
GroupId: albDetails.albSecurityGroup,
IpPermissions: [
{
IpProtocol: '-1',
UserIdGroupPairs: [
{
GroupId: appStreamSecurityGroupId,
},
],
},
],
};
const [albService] = await this.mustFindServices(['albService']);
const ec2Client = await albService.getEc2Sdk(requestContext, resolvedVars);
await ec2Client.authorizeSecurityGroupEgress(params).promise();
} catch (e) {
throw new Error(`Assigning AppStream security group to ALB security group failed with error - ${e.message}`);
}
}

/**
* Method to allow egress access from AppStream to ALB
*
* @param requestContext
* @param resolvedVars
* @param albDetails
*/
async authorizeAppStreamAlbEgress(requestContext, resolvedVars, albDetails) {
try {
// Assign ALB security group to AppStream security group engress (allow ALB egress from appstream)
const appStreamSecurityGroupId = await this.getAppStreamSecurityGroupId(requestContext, resolvedVars);
const params = {
GroupId: appStreamSecurityGroupId,
IpPermissions: [
{
IpProtocol: '-1',
UserIdGroupPairs: [
{
GroupId: albDetails.albSecurityGroup,
},
],
},
],
};
const [albService] = await this.mustFindServices(['albService']);
const ec2Client = await albService.getEc2Sdk(requestContext, resolvedVars);
await ec2Client.authorizeSecurityGroupEgress(params).promise();
} catch (e) {
throw new Error(`Assigning ALB security group to AppStream security group failed with error - ${e.message}`);
}
}

/**
* Method to get CFT template output. The method gets CFT URL, read the CFT from S3
* and parses the tmplate using js-yaml library
Expand Down Expand Up @@ -336,6 +407,19 @@ class CheckLaunchDependency extends StepBase {
return cfnClient;
}

/**
* Method to get appstream security group ID for the target aws account
*
* @param requestContext
* @param resolvedVars
* @returns {Promise<string>}
*/
async getAppStreamSecurityGroupId(requestContext, resolvedVars) {
const [albService] = await this.mustFindServices(['albService']);
const { appStreamSecurityGroupId } = await albService.findAwsAccountDetails(requestContext, resolvedVars.projectId);
return appStreamSecurityGroupId;
}

/**
* Method to get role arn for the target aws account
*
Expand Down
Loading