-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
9 additions
and
247 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,250 +1,12 @@ | ||
import { Duration, RemovalPolicy, Stack, type StackProps } from 'aws-cdk-lib'; | ||
import { Rule } from 'aws-cdk-lib/aws-events'; | ||
import { SnsTopic } from 'aws-cdk-lib/aws-events-targets'; | ||
import { | ||
AccountPrincipal, | ||
ManagedPolicy, | ||
PolicyStatement, | ||
Role, | ||
ServicePrincipal, | ||
} from 'aws-cdk-lib/aws-iam'; | ||
import { Runtime } from 'aws-cdk-lib/aws-lambda'; | ||
import { S3EventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; | ||
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; | ||
import { | ||
Bucket, | ||
BlockPublicAccess, | ||
EventType, | ||
BucketEncryption, | ||
} from 'aws-cdk-lib/aws-s3'; | ||
import { Topic } from 'aws-cdk-lib/aws-sns'; | ||
import { type Construct } from 'constructs'; | ||
import { | ||
APP_NAME, | ||
AWS_ACCOUNT_ID, | ||
AWS_REGION, | ||
INPUT_BUCKET, | ||
OUTPUT_BUCKET, | ||
MANIFEST_PREFIX, | ||
MANIFEST_SUFFIX, | ||
READY2RUN_WORKFLOW_ID, | ||
} from './constants'; | ||
import { App } from 'aws-cdk-lib'; | ||
import { OmicsQuiltStack } from './omics-quilt'; | ||
|
||
export class OmicsQuiltStack extends Stack { | ||
public readonly inputBucket: Bucket; | ||
public readonly outputBucket: Bucket; | ||
public readonly statusTopic: Topic; | ||
function main() { | ||
const app = new App(); | ||
|
||
public readonly manifest_prefix: string; | ||
public readonly manifest_suffix: string; | ||
|
||
readonly lambdaRole: Role; | ||
readonly omicsRole: Role; | ||
readonly principal: AccountPrincipal; | ||
|
||
constructor(scope: Construct, id: string, props?: StackProps) { | ||
super(scope, id, props); | ||
this.principal = new AccountPrincipal(AWS_ACCOUNT_ID); | ||
this.manifest_prefix = MANIFEST_PREFIX; | ||
this.manifest_suffix = MANIFEST_SUFFIX; | ||
|
||
// Create Input/Output S3 buckets | ||
this.inputBucket = this.makeBucket(INPUT_BUCKET); | ||
this.outputBucket = this.makeBucket(OUTPUT_BUCKET); | ||
|
||
// SNS Topic for failure notifications | ||
const topicName = `${APP_NAME}_workflow_status_topic`; | ||
this.statusTopic = new Topic(this, topicName, { | ||
displayName: topicName, | ||
topicName: topicName, | ||
}); | ||
|
||
// Create an EventBridge rule that sends SNS notification on failure | ||
const ruleWorkflowStatusTopic = new Rule( | ||
this, | ||
`${APP_NAME}_rule_workflow_status_topic`, | ||
{ | ||
eventPattern: { | ||
source: ['aws.omics'], | ||
detailType: ['Run Status Change'], | ||
detail: { | ||
status: ['FAILED', 'COMPLETED', 'CREATED'], | ||
}, | ||
}, | ||
}, | ||
); | ||
|
||
ruleWorkflowStatusTopic.addTarget(new SnsTopic(this.statusTopic)); | ||
|
||
const servicePrincipal = new ServicePrincipal('events.amazonaws.com'); | ||
this.statusTopic.grantPublish(servicePrincipal); | ||
this.statusTopic.grantPublish(this.principal); // for debugging purposes | ||
|
||
// Create an IAM service role for HealthOmics workflows | ||
this.omicsRole = this.makeOmicsRole(); | ||
|
||
// Create an IAM role for the Lambda functions | ||
this.lambdaRole = this.makeLambdaRole(); | ||
|
||
// Create Lambda function to submit initial HealthOmics workflow | ||
const fastqWorkflowLambda = this.makeLambda('wf1_fastq', {}); | ||
// Add S3 event source to Lambda | ||
fastqWorkflowLambda.addEventSource( | ||
new S3EventSource(this.inputBucket, { | ||
events: [EventType.OBJECT_CREATED], | ||
filters: [ | ||
{ prefix: this.manifest_prefix, suffix: this.manifest_suffix }, | ||
], | ||
}), | ||
); | ||
} | ||
|
||
private makeBucket(name: string) { | ||
const bucketOptions = { | ||
autoDeleteObjects: true, | ||
blockPublicAccess: BlockPublicAccess.BLOCK_ALL, | ||
encryption: BucketEncryption.S3_MANAGED, | ||
enforceSSL: true, | ||
removalPolicy: RemovalPolicy.DESTROY, | ||
versioned: true, | ||
}; | ||
const bucket = new Bucket(this, name, bucketOptions); | ||
bucket.grantDelete(this.principal); | ||
bucket.grantReadWrite(this.principal); | ||
return bucket; | ||
} | ||
|
||
private makeLambda(name: string, env: object) { | ||
const default_env = { | ||
OMICS_ROLE: this.omicsRole.roleArn, | ||
OUTPUT_S3_LOCATION: 's3://' + this.outputBucket.bucketName + '/outputs', | ||
WORKFLOW_ID: READY2RUN_WORKFLOW_ID, | ||
ECR_REGISTRY: | ||
AWS_ACCOUNT_ID + '.dkr.ecr.' + AWS_REGION + '.amazonaws.com', | ||
LOG_LEVEL: 'ALL', | ||
}; | ||
// create merged env | ||
const final_env = Object.assign(default_env, env); | ||
return new NodejsFunction(this, name, { | ||
runtime: Runtime.NODEJS_18_X, | ||
role: this.lambdaRole, | ||
timeout: Duration.seconds(60), | ||
retryAttempts: 1, | ||
environment: final_env, | ||
}); | ||
} | ||
|
||
private makeLambdaRole() { | ||
const lambdaRole = new Role(this, `${APP_NAME}-lambda-role`, { | ||
assumedBy: new ServicePrincipal('lambda.amazonaws.com'), | ||
managedPolicies: [ | ||
ManagedPolicy.fromAwsManagedPolicyName( | ||
'service-role/AWSLambdaBasicExecutionRole', | ||
), | ||
], | ||
}); | ||
|
||
// Allow the Lambda functions to pass Omics service role to the Omics service | ||
const lambdaIamPassrolePolicy = new PolicyStatement({ | ||
actions: ['iam:PassRole'], | ||
resources: [this.omicsRole.roleArn], | ||
}); | ||
lambdaRole.addToPolicy(lambdaIamPassrolePolicy); | ||
|
||
const lambdaS3Policy = new PolicyStatement({ | ||
actions: ['s3:ListBucket', 's3:GetObject', 's3:PutObject'], | ||
resources: [ | ||
this.inputBucket.bucketArn, | ||
this.outputBucket.bucketArn, | ||
this.inputBucket.bucketArn + '/*', | ||
this.outputBucket.bucketArn + '/*', | ||
], | ||
}); | ||
lambdaRole.addToPolicy(lambdaS3Policy); | ||
|
||
const lambdaOmicsPolicy = new PolicyStatement({ | ||
actions: ['omics:StartRun', 'omics:TagResource', 'omics:GetRun'], | ||
resources: ['*'], | ||
}); | ||
lambdaRole.addToPolicy(lambdaOmicsPolicy); | ||
return lambdaRole; | ||
} | ||
|
||
private makeOmicsRole() { | ||
const omicsRole = new Role(this, `${APP_NAME}-omics-service-role`, { | ||
assumedBy: new ServicePrincipal('omics.amazonaws.com'), | ||
}); | ||
|
||
// Limit to buckets from where inputs need to be read | ||
const omicsS3ReadPolicy = new PolicyStatement({ | ||
actions: ['s3:ListBucket', 's3:GetObject'], | ||
resources: [ | ||
this.inputBucket.bucketArn, | ||
this.outputBucket.bucketArn, | ||
this.inputBucket.bucketArn + '/*', | ||
this.outputBucket.bucketArn + '/*', | ||
], | ||
}); | ||
omicsRole.addToPolicy(omicsS3ReadPolicy); | ||
|
||
// Limit to buckets where outputs need to be written | ||
const omicsS3WritePolicy = new PolicyStatement({ | ||
actions: ['s3:ListBucket', 's3:PutObject'], | ||
resources: [ | ||
this.outputBucket.bucketArn, | ||
this.outputBucket.bucketArn + '/*', | ||
], | ||
}); | ||
omicsRole.addToPolicy(omicsS3WritePolicy); | ||
|
||
// ECR image access | ||
const omicsEcrPolicy = new PolicyStatement({ | ||
actions: [ | ||
'ecr:BatchGetImage', | ||
'ecr:GetDownloadUrlForLayer', | ||
'ecr:BatchCheckLayerAvailability', | ||
], | ||
resources: [`arn:aws:ecr:${AWS_REGION}:${AWS_ACCOUNT_ID}:repository/*`], | ||
}); | ||
omicsRole.addToPolicy(omicsEcrPolicy); | ||
|
||
// CloudWatch logging access | ||
const omicsLoggingPolicy = new PolicyStatement({ | ||
actions: [ | ||
'logs:CreateLogGroup', | ||
'logs:DescribeLogStreams', | ||
'logs:CreateLogStream', | ||
'logs:PutLogEvents', | ||
], | ||
resources: [ | ||
`arn:aws:logs:${AWS_REGION}:${AWS_ACCOUNT_ID}:log-group:/aws/omics/WorkflowLog:log-stream:*`, | ||
`arn:aws:logs:${AWS_REGION}:${AWS_ACCOUNT_ID}:log-group:/aws/omics/WorkflowLog:*`, | ||
], | ||
}); | ||
omicsRole.addToPolicy(omicsLoggingPolicy); | ||
|
||
// KMS access | ||
const omicsKmsPolicy = new PolicyStatement({ | ||
actions: ['kms:Decrypt', 'kms:GenerateDataKey'], | ||
resources: ['*'], | ||
}); | ||
omicsRole.addToPolicy(omicsKmsPolicy); | ||
|
||
// Allow Omics service role to access some common public AWS S3 buckets with test data | ||
const omicsRoleAdditionalPolicy = new PolicyStatement({ | ||
actions: ['s3:Get*', 's3:List*'], | ||
resources: [ | ||
'arn:aws:s3:::broad-references', | ||
'arn:aws:s3:::broad-references/*', | ||
'arn:aws:s3:::giab', | ||
'arn:aws:s3:::giab/*', | ||
`arn:aws:s3:::aws-genomics-static-${AWS_REGION}`, | ||
`arn:aws:s3:::aws-genomics-static-${AWS_REGION}/*`, | ||
`arn:aws:s3:::omics-${AWS_REGION}`, | ||
`arn:aws:s3:::omics-${AWS_REGION}/*`, | ||
], | ||
}); | ||
omicsRole.addToPolicy(omicsRoleAdditionalPolicy); | ||
return omicsRole; | ||
} | ||
new OmicsQuiltStack(app, 'omics-quilt', { env: { region: 'us-west-2' } }); | ||
// new DiaStack(app, 'vivos-prod', { env: prodEnv }); | ||
app.synth(); | ||
} | ||
|
||
main(); |